[Feature #22185] Add alignment directives to pack templates

This commit is contained in:
Nobuyoshi Nakada authored and Nobuyoshi Nakada committed 2026-07-15 22:38:32 +09:00
1 parent bb472c530a
commit e9444aa0da
6 files changed
+199 -9
Notes: git 2026-07-15 13:39:04 +00:00

No files matched your search

+6
View File
@@ -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
+26
View File
@@ -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
+122 -9
View File
@@ -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)
+16
View File
@@ -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
+14
View File
@@ -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
+15
View File
@@ -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