diff --git a/NEWS.md b/NEWS.md index 26d5157c4aa..6392e1ba479 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,6 +29,9 @@ Note: We're only listing outstanding class updates. * `Array#pack` accepts new formats `R` and `r` for unsigned and signed LEB128 encoded integers. [[Feature #21785]] + * `Array#pack` accepts new formats `x!` and `@!` to align the current + offset to a byte boundary or to the ABI alignment of another + directive. [[Feature #22185]] * ENV @@ -79,6 +82,9 @@ Note: We're only listing outstanding class updates. * `String#unpack` and `String#unpack1` accept a new format `^` that returns the current offset. Useful when combined with variable width formats like LEB128. [[Feature #21796]] + * `String#unpack` and `String#unpack1` accept new formats `x!` and + `@!` to align the current offset to a byte boundary or to the ABI + alignment of another directive. [[Feature #22185]] * Symbol diff --git a/doc/language/packed_data.md b/doc/language/packed_data.md index 1b133367d68..ce4b85ba0c4 100644 --- a/doc/language/packed_data.md +++ b/doc/language/packed_data.md @@ -879,6 +879,32 @@ for one byte in the input or output string. "\x00\x00\x02".unpack("x4C") # Raises ArgumentError ``` +- `'x!'` - Align to the given byte boundary or directive alignment; + for packing, null fill if necessary. With `buffer:` or `offset:`, + alignment is relative to the starting position: + + ```ruby + [1, 2].pack("C x!4 C") # => "\x01\x00\x00\x00\x02" + [1, 2].pack("C x!i i") # Aligns to the native int alignment. + ``` + + For unpacking, skip forward to the aligned offset: + + ```ruby + "\x01\x00\x00\x00\x02".unpack("C x!4 C") # => [1, 2] + ``` + +- `'@!'` - Align to the given byte boundary or directive alignment + from the beginning of the output or input string: + + ```ruby + buffer = +"z" + [1, 2].pack("C @!4 C", buffer: buffer) + buffer # => "z\x01\x00\x00\x02" + + "z\x01\x00\x00\x02".unpack("C @!4 C", offset: 1) # => [1, 2] + ``` + - `'^'` - Only for unpacking; the current position: ```ruby diff --git a/pack.c b/pack.c index 56af4ea099c..4b6595b45a9 100644 --- a/pack.c +++ b/pack.c @@ -72,8 +72,12 @@ is_bigendian(void) #ifdef NATINT_PACK # define NATINT_LEN(type,len) (natint?(int)sizeof(type):(int)(len)) +# define NATINT_ALIGN(type,len) (natint?(int)RUBY_ALIGNOF(type):(int)(len)) +# define USING_NATINT(expr) expr #else # define NATINT_LEN(type,len) ((int)sizeof(type)) +# define NATINT_ALIGN(type,len) ((int)RUBY_ALIGNOF(type)) +# define USING_NATINT(expr) /* void */ #endif typedef union { @@ -218,9 +222,7 @@ pack_modifiers(const char *p, const char *pend, char type, int *natint, int *exp case '_': case '!': if (strchr(natstr, type)) { -#ifdef NATINT_PACK - *natint = 1; -#endif + USING_NATINT(*natint = 1); p++; } else { @@ -245,6 +247,91 @@ pack_modifiers(const char *p, const char *pend, char type, int *natint, int *exp return p; } +#ifndef NATINT_PACK +# define pack_alignof(t, n) pack_alignof(t) +#endif +static size_t +pack_alignof(char type, int natint) +{ + switch (type) { + case 'c': case 'C': + return RUBY_ALIGNOF(char); + case 's': case 'S': + return NATINT_ALIGN(short, 2); + case 'i': case 'I': + return RUBY_ALIGNOF(int); + case 'l': case 'L': + return NATINT_ALIGN(long, 4); + case 'q': case 'Q': + return RUBY_ALIGNOF(int64_t); + case 'j': + return RUBY_ALIGNOF(intptr_t); + case 'J': + return RUBY_ALIGNOF(uintptr_t); + case 'n': case 'v': + return RUBY_ALIGNOF(uint16_t); + case 'N': case 'V': + return RUBY_ALIGNOF(uint32_t); + case 'f': case 'F': case 'e': case 'g': + return RUBY_ALIGNOF(float); + case 'd': case 'D': case 'E': case 'G': + return RUBY_ALIGNOF(double); + case 'p': case 'P': + return RUBY_ALIGNOF(char *); + default: + return 0; + } +} + +static long +pack_align_pad(long pos, long base, size_t alignment) +{ + long offset, mod; + + if (alignment <= 1) return 0; + if (alignment > LONG_MAX) rb_raise(rb_eRangeError, "alignment too big"); + offset = pos - base; + mod = offset % (long)alignment; + if (mod < 0) mod += alignment; + return mod ? (long)alignment - mod : 0; +} + +static char * +pack_alignment(const char *p, const char *pend, VALUE fmt, size_t *alignment) +{ + char type; + int explicit_endian = 0; + USING_NATINT(int natint = 0); + + if (p >= pend) { + rb_raise(rb_eArgError, "missing alignment"); + } + type = *p++; + p = pack_modifiers(p, pend, type, &natint, &explicit_endian); + if (explicit_endian) { + rb_raise(rb_eArgError, "endian modifier is not allowed for alignment"); + } + *alignment = pack_alignof(type, natint); + if (!*alignment) { + unknown_directive("alignment", type, fmt); + } + return (char *)p; +} + +static char * +pack_alignment_size(const char *p, const char *pend, VALUE fmt, size_t *alignment) +{ + if (p < pend && ISDIGIT(*p)) { + errno = 0; + *alignment = STRTOUL(p, (char**)&p, 10); + if (*alignment <= 0 || errno) { + rb_raise(rb_eRangeError, "invalid alignment"); + } + return (char *)p; + } + return pack_alignment(p, pend, fmt, alignment); +} + static VALUE pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) { @@ -254,6 +341,7 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) const char *ptr; int enc_info = 1; /* 0 - BINARY, 1 - US-ASCII, 2 - UTF-8 */ int integer_size, bigendian_p; + long align_base; StringValue(fmt); rb_must_asciicompat(fmt); @@ -271,6 +359,7 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) } idx = 0; + align_base = RSTRING_LEN(res); #define TOO_FEW (rb_raise(rb_eArgError, toofew), 0) #define MORE_ITEM (idx < RARRAY_LEN(ary)) @@ -281,15 +370,25 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) while (p < pend) { int explicit_endian = 0; + size_t align = 0; if (RSTRING_END(fmt) != pend) { rb_raise(rb_eRuntimeError, "format string modified"); } const char type = *p++; /* get data type */ -#ifdef NATINT_PACK - int natint = 0; /* native integer */ -#endif + USING_NATINT(int natint = 0); /* native integer */ if (skip_blank(p, type)) continue; + + /* Directives that do not take modifiers. */ + if ((type == 'x' || type == '@') && p < pend && *p == '!') { + p++; + p = pack_alignment_size(p, pend, fmt, &align); + len = pack_align_pad(RSTRING_LEN(res), type == '@' ? 0 : align_base, align); + rb_str_modify_expand(res, len); + str_expand_fill(res, '\0', len); + continue; + } + p = pack_modifiers(p, pend, type, &natint, &explicit_endian); if (*p == '*') { /* set data length */ @@ -1005,6 +1104,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) long len; AVOID_CC_BUG long tmp_len; int signed_p, integer_size, bigendian_p; + long align_base; #define UNPACK_PUSH(item) do {\ VALUE item_val = (item);\ if ((mode) == UNPACK_BLOCK) {\ @@ -1031,6 +1131,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) s = RSTRING_PTR(str); send = s + len; s += offset; + align_base = offset; p = RSTRING_PTR(fmt); pend = p + RSTRING_LEN(fmt); @@ -1041,12 +1142,24 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) while (p < pend) { int explicit_endian = 0; const char type = *p++; -#ifdef NATINT_PACK - int natint = 0; /* native integer */ -#endif + size_t align = 0; int star = 0; + USING_NATINT(int natint = 0); /* native integer */ if (skip_blank(p, type)) continue; + + /* Directives that do not take modifiers. */ + if ((type == 'x' || type == '@') && p < pend && *p == '!') { + p++; + p = pack_alignment_size(p, pend, fmt, &align); + len = pack_align_pad(s - RSTRING_PTR(str), type == '@' ? 0 : align_base, align); + if (len > send - s) { + rb_raise(rb_eArgError, type == '@' ? "@ outside of string" : "x outside of string"); + } + s += len; + continue; + } + p = pack_modifiers(p, pend, type, &natint, &explicit_endian); if (p >= pend) diff --git a/spec/ruby/core/array/pack/x_spec.rb b/spec/ruby/core/array/pack/x_spec.rb index 7ff587a01ed..1d26de6c51e 100644 --- a/spec/ruby/core/array/pack/x_spec.rb +++ b/spec/ruby/core/array/pack/x_spec.rb @@ -32,6 +32,22 @@ describe "Array#pack with format 'x'" do [].pack("x*").should == "" [1, 2].pack("Cx*C").should == "\x01\x02" end + + ruby_version_is "4.1" do + it "aligns to the given byte boundary with the '!' modifier" do + [1, 2].pack("C x!4 C").should == "\x01\x00\x00\x00\x02" + end + + it "aligns from the beginning of the output with '@!'" do + buffer = +"z" + [1, 2].pack("C @!4 C", buffer: buffer) + buffer.should == "z\x01\x00\x00\x02" + end + + it "aligns to a directive's alignment with the '!' modifier" do + [1, 2].pack("C x!i i").should == [1, 2].pack("i< i") + end + end end describe "Array#pack with format 'X'" do diff --git a/spec/ruby/core/string/unpack/x_spec.rb b/spec/ruby/core/string/unpack/x_spec.rb index fb2e79fc1f7..a93a4dba995 100644 --- a/spec/ruby/core/string/unpack/x_spec.rb +++ b/spec/ruby/core/string/unpack/x_spec.rb @@ -59,4 +59,18 @@ describe "String#unpack with format 'x'" do it "raises an ArgumentError if the count exceeds the size of the String" do -> { "\x01\x02\x03\x04".unpack("C2x3C") }.should.raise(ArgumentError) end + + ruby_version_is "4.1" do + it "aligns to the given byte boundary with the '!' modifier" do + "\x01\x00\x00\x00\x02".unpack("C x!4 C").should == [1, 2] + end + + it "aligns from the beginning of the String with '@!'" do + "z\x01\x00\x00\x02".unpack("C @!4 C", offset: 1).should == [1, 2] + end + + it "aligns to a directive's alignment with the '!' modifier" do + [1, 2].pack("i< i").unpack("C x!i i").should == [1, 2] + end + end end diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index 6e5f0fe7ff9..e1dd3968378 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -939,6 +939,21 @@ EXPECTED assert_equal [nil], "a".unpack("C", offset: 1) end + def test_pack_alignment + assert_equal "\x01\x00\x00\x00\x02".b, [1, 2].pack("C x!4 C") + assert_equal [1, 2], "\x01\x00\x00\x00\x02".b.unpack("C x!4 C") + + buffer = +"z" + [1, 2].pack("C x!4 C", buffer: buffer) + assert_equal "z\x01\x00\x00\x00\x02".b, buffer + assert_equal [1, 2], buffer.unpack("C x!4 C", offset: 1) + + buffer = +"z" + [1, 2].pack("C @!4 C", buffer: buffer) + assert_equal "z\x01\x00\x00\x02".b, buffer + assert_equal [1, 2], buffer.unpack("C @!4 C", offset: 1) + end + def test_monkey_pack assert_separately([], <<-'end;') $-w = false