Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 127 additions & 2 deletions src/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ pub fn bytes(input: TokenStream2) -> TokenStream2 {
leading_zero_count
.checked_mul(bits_per_digit)
.expect("overflow")
} else if leading_zero_count > 0 {
} else if leading_zero_count > 0 && remainder.len() > 1 {
// If there are leading zeros without a bits per digit error, since a
// caller may expect the zeros to be preserved, and so it is better for
// us to error. They can proceed by removing the zeros.
// us to error. They can proceed by removing the zeros. A single digit
// `0` is permitted as it is unambiguously the value zero.
Comment on lines +47 to +51
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new leading_zero_count > 0 && remainder.len() > 1 check uses remainder derived from lit.to_string(), which includes any integer suffix (e.g. 0u8, 0usize). That means bytes!(0u8) (and bytes!(0o0u8)) will be rejected as “leading zeros” even though the digit portion is a single 0. Consider stripping the suffix before computing remainder.len() / leading-zero conditions (e.g., use LitInt APIs to access the digit part without the suffix, or split normalized into digits vs suffix based on the selected base) and add a regression test for 0u8/0o0u8.

Copilot uses AI. Check for mistakes.
return Error::new(
lit.span(),
format!(
Expand All @@ -67,6 +68,7 @@ pub fn bytes(input: TokenStream2) -> TokenStream2 {
let int_len = int_bytes.len();
let total_bits = leading_zero_bits.checked_add(int_bits).expect("overflow");
let total_len = (total_bits.checked_add(7).expect("overflow")) / 8;
let total_len = total_len.max(int_len);
let mut total_bytes: Vec<u8> = vec![0; total_len];
total_bytes[total_len - int_len..].copy_from_slice(&int_bytes);

Expand Down Expand Up @@ -171,18 +173,141 @@ mod test {
}
}

#[test]
fn zero() {
let table: &[(_, ExprArray)] = &[
(quote!(0), parse_quote!([0u8])),
(quote!(0x0), parse_quote!([0u8])),
(quote!(0x00), parse_quote!([0u8])),
(quote!(0b0), parse_quote!([0u8])),
(quote!(0b00000000), parse_quote!([0u8])),
(quote!(0o0), parse_quote!([0u8])),
];
for (i, t) in table.iter().cloned().enumerate() {
let tokens = bytes(t.0);
let parsed = syn::parse2::<ExprArray>(tokens).unwrap();
let expect = t.1;
assert_eq!(parsed, expect, "table entry: {}", i);
}
}

#[test]
fn byte_boundaries() {
let table: &[(_, ExprArray)] = &[
// u8 max
(quote!(0xff), parse_quote!([255u8])),
(quote!(0b11111111), parse_quote!([255u8])),
(quote!(0o377), parse_quote!([255u8])),
(quote!(255), parse_quote!([255u8])),
// u8 max + 1
(quote!(0x100), parse_quote!([1u8, 0u8])),
(quote!(0b100000000), parse_quote!([1u8, 0u8])),
(quote!(0o400), parse_quote!([1u8, 0u8])),
(quote!(256), parse_quote!([1u8, 0u8])),
// u16 max
(quote!(0xffff), parse_quote!([255u8, 255u8])),
(quote!(65535), parse_quote!([255u8, 255u8])),
// u16 max + 1
(quote!(0x10000), parse_quote!([1u8, 0u8, 0u8])),
(quote!(65536), parse_quote!([1u8, 0u8, 0u8])),
// u32 max
(
quote!(0xffffffff),
parse_quote!([255u8, 255u8, 255u8, 255u8]),
),
(
quote!(4294967295),
parse_quote!([255u8, 255u8, 255u8, 255u8]),
),
// u32 max + 1
(quote!(0x100000000), parse_quote!([1u8, 0u8, 0u8, 0u8, 0u8])),
(quote!(4294967296), parse_quote!([1u8, 0u8, 0u8, 0u8, 0u8])),
// u64 max
(
quote!(0xffffffffffffffff),
parse_quote!([255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8]),
),
// u64 max + 1
(
quote!(0x10000000000000000),
parse_quote!([1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8]),
),
// u128 max
(
quote!(0xffffffffffffffffffffffffffffffff),
parse_quote!([
255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8,
255u8, 255u8, 255u8, 255u8, 255u8
]),
),
// u128 max + 1
(
quote!(0x100000000000000000000000000000000),
parse_quote!([
1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8
]),
),
];
for (i, t) in table.iter().cloned().enumerate() {
let tokens = bytes(t.0);
let parsed = syn::parse2::<ExprArray>(tokens).unwrap();
let expect = t.1;
assert_eq!(parsed, expect, "table entry: {}", i);
}
}

#[test]
fn one() {
let table: &[(_, ExprArray)] = &[
(quote!(1), parse_quote!([1u8])),
(quote!(0x1), parse_quote!([1u8])),
(quote!(0b1), parse_quote!([1u8])),
(quote!(0o1), parse_quote!([1u8])),
];
for (i, t) in table.iter().cloned().enumerate() {
let tokens = bytes(t.0);
let parsed = syn::parse2::<ExprArray>(tokens).unwrap();
let expect = t.1;
assert_eq!(parsed, expect, "table entry: {}", i);
}
}

#[test]
fn empty_input() {
let tokens = bytes(quote! {});
let expect = Error::new(
Span::call_site(),
"unexpected end of input, expected integer literal",
)
.to_compile_error()
.to_string();
assert_eq!(tokens.to_string(), expect);
}

#[test]
fn non_integer_input() {
let tokens = bytes(quote! {"hello"});
let expect = Error::new(Span::call_site(), "expected integer literal")
.to_compile_error()
.to_string();
assert_eq!(tokens.to_string(), expect);
}

#[test]
fn leading_zeros_prohibited() {
let table: &[(_, Result<ExprArray, Error>)] = &[
// Base 8.
(quote!(0o377), Ok(parse_quote!([255u8]))),
(quote!(0o0377), Err(Error::new(Span::call_site(), "leading zeros are not preserved or supported on integer literals in octal form"))),
(quote!(0o00377), Err(Error::new(Span::call_site(), "leading zeros are not preserved or supported on integer literals in octal form"))),
(quote!(0o00), Err(Error::new(Span::call_site(), "leading zeros are not preserved or supported on integer literals in octal form"))),
(quote!(0o400), Ok(parse_quote!([1u8, 0u8]))),
// Base 10.
(quote!(255), Ok(parse_quote!([255u8]))),
(quote!(0255), Err(Error::new(Span::call_site(), "leading zeros are not preserved or supported on integer literals in decimal form"))),
(quote!(00255), Err(Error::new(Span::call_site(), "leading zeros are not preserved or supported on integer literals in decimal form"))),
(quote!(00), Err(Error::new(Span::call_site(), "leading zeros are not preserved or supported on integer literals in decimal form"))),
(quote!(256), Ok(parse_quote!([1u8, 0u8]))),
];
for (i, t) in table.iter().enumerate() {
Expand Down
121 changes: 121 additions & 0 deletions src/bytesmin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,127 @@ mod test {
assert_eq!(parsed, expect);
}

#[test]
fn zero() {
let table: &[(_, ExprArray)] = &[
(quote!(0), parse_quote!([0u8])),
(quote!(0x0), parse_quote!([0u8])),
(quote!(0x00), parse_quote!([0u8])),
(quote!(0b0), parse_quote!([0u8])),
(quote!(0b00000000), parse_quote!([0u8])),
(quote!(0o0), parse_quote!([0u8])),
];
for (i, t) in table.iter().cloned().enumerate() {
let tokens = bytesmin(t.0);
let parsed = syn::parse2::<ExprArray>(tokens).unwrap();
let expect = t.1;
assert_eq!(parsed, expect, "table entry: {}", i);
}
}

#[test]
fn byte_boundaries() {
let table: &[(_, ExprArray)] = &[
// u8 max
(quote!(0xff), parse_quote!([255u8])),
(quote!(0b11111111), parse_quote!([255u8])),
(quote!(0o377), parse_quote!([255u8])),
(quote!(255), parse_quote!([255u8])),
// u8 max + 1
(quote!(0x100), parse_quote!([1u8, 0u8])),
(quote!(0b100000000), parse_quote!([1u8, 0u8])),
(quote!(0o400), parse_quote!([1u8, 0u8])),
(quote!(256), parse_quote!([1u8, 0u8])),
// u16 max
(quote!(0xffff), parse_quote!([255u8, 255u8])),
(quote!(65535), parse_quote!([255u8, 255u8])),
// u16 max + 1
(quote!(0x10000), parse_quote!([1u8, 0u8, 0u8])),
(quote!(65536), parse_quote!([1u8, 0u8, 0u8])),
// u32 max
(
quote!(0xffffffff),
parse_quote!([255u8, 255u8, 255u8, 255u8]),
),
(
quote!(4294967295),
parse_quote!([255u8, 255u8, 255u8, 255u8]),
),
// u32 max + 1
(quote!(0x100000000), parse_quote!([1u8, 0u8, 0u8, 0u8, 0u8])),
(quote!(4294967296), parse_quote!([1u8, 0u8, 0u8, 0u8, 0u8])),
// u64 max
(
quote!(0xffffffffffffffff),
parse_quote!([255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8]),
),
// u64 max + 1
(
quote!(0x10000000000000000),
parse_quote!([1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8]),
),
// u128 max
(
quote!(0xffffffffffffffffffffffffffffffff),
parse_quote!([
255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8,
255u8, 255u8, 255u8, 255u8, 255u8
]),
),
// u128 max + 1
(
quote!(0x100000000000000000000000000000000),
parse_quote!([
1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8,
0u8
]),
),
];
for (i, t) in table.iter().cloned().enumerate() {
let tokens = bytesmin(t.0);
let parsed = syn::parse2::<ExprArray>(tokens).unwrap();
let expect = t.1;
assert_eq!(parsed, expect, "table entry: {}", i);
}
}

#[test]
fn one() {
let table: &[(_, ExprArray)] = &[
(quote!(1), parse_quote!([1u8])),
(quote!(0x1), parse_quote!([1u8])),
(quote!(0b1), parse_quote!([1u8])),
(quote!(0o1), parse_quote!([1u8])),
];
for (i, t) in table.iter().cloned().enumerate() {
let tokens = bytesmin(t.0);
let parsed = syn::parse2::<ExprArray>(tokens).unwrap();
let expect = t.1;
assert_eq!(parsed, expect, "table entry: {}", i);
}
}

#[test]
fn empty_input() {
let tokens = bytesmin(quote! {});
let expect = Error::new(
Span::call_site(),
"unexpected end of input, expected integer literal",
)
.to_compile_error()
.to_string();
assert_eq!(tokens.to_string(), expect);
}

#[test]
fn non_integer_input() {
let tokens = bytesmin(quote! {"hello"});
let expect = Error::new(Span::call_site(), "expected integer literal")
.to_compile_error()
.to_string();
assert_eq!(tokens.to_string(), expect);
}

#[test]
fn leading_zeros_discarded() {
let table: &[(_, ExprArray)] = &[
Expand Down