From b7e5ef421caaf12007edeb57b182c4657ed9e91f Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:26:41 +1000 Subject: [PATCH 1/4] add boundary and edge case tests for bytes and bytesmin --- src/bytes.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++ src/bytesmin.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) diff --git a/src/bytes.rs b/src/bytes.rs index 796707a..51945f6 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -171,6 +171,127 @@ 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::(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::(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::(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)] = &[ diff --git a/src/bytesmin.rs b/src/bytesmin.rs index 9c2f9be..25b2a21 100644 --- a/src/bytesmin.rs +++ b/src/bytesmin.rs @@ -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::(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::(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::(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)] = &[ From 72d314f075bac330a6a1ac84f0af249e198da6c0 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:01:15 +1000 Subject: [PATCH 2/4] permit literal `0` while rejecting other leading zeros --- src/bytes.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/bytes.rs b/src/bytes.rs index 51945f6..1fdf2c8 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -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. return Error::new( lit.span(), format!( @@ -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 = vec![0; total_len]; total_bytes[total_len - int_len..].copy_from_slice(&int_bytes); @@ -299,11 +301,13 @@ mod test { (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() { From 8072051a68df691e3f3270d3141cf4c818ed8933 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:14:38 +1000 Subject: [PATCH 3/4] restrict bytes/bytesmin to hex and binary only --- src/bytes.rs | 139 ++++++++++++++++-------------------------------- src/bytesmin.rs | 87 ++++++++++++++++-------------- src/lib.rs | 47 ++++++---------- 3 files changed, 109 insertions(+), 164 deletions(-) diff --git a/src/bytes.rs b/src/bytes.rs index 1fdf2c8..f24bcad 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -28,38 +28,25 @@ pub fn bytes(input: TokenStream2) -> TokenStream2 { // Remove any leading prefix that indicates the base, and use the base to // determine how many bits per leading zero needs to be prefilled into the - // bytes generated. If bits_per_digit is None, leading zero digits are - // unsupported. - let (form, bits_per_zero_digit, remainder) = match normalized.as_bytes() { - [b'0', b'x', r @ ..] => ("hex", Some(4), r), - [b'0', b'b', r @ ..] => ("binary", Some(1), r), - [b'0', b'o', r @ ..] => ("octal", None, r), - [r @ ..] => ("decimal", None, r), + // bytes generated. + let (bits_per_zero_digit, remainder) = match normalized.as_bytes() { + [b'0', b'x', r @ ..] => (4, r), + [b'0', b'b', r @ ..] => (1, r), + _ => { + return Error::new( + lit.span(), + "only hex (0x) and binary (0b) integer literals are supported", + ) + .to_compile_error(); + } }; // Count the leading zero bits by counting the number of leading zeros and // multiplying by the bits per digit. let leading_zero_count = remainder.iter().take_while(|d| **d == b'0').count(); - let leading_zero_bits = if let Some(bits_per_digit) = bits_per_zero_digit { - leading_zero_count - .checked_mul(bits_per_digit) - .expect("overflow") - } 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. A single digit - // `0` is permitted as it is unambiguously the value zero. - return Error::new( - lit.span(), - format!( - "leading zeros are not preserved or supported on integer literals in {} form", - form, - ), - ) - .to_compile_error(); - } else { - 0 - }; + let leading_zero_bits = leading_zero_count + .checked_mul(bits_per_zero_digit) + .expect("overflow"); // Create the final byte slice, which has length of the leading zero bytes, // followed by the big integer bytes. @@ -68,7 +55,6 @@ 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 = vec![0; total_len]; total_bytes[total_len - int_len..].copy_from_slice(&int_bytes); @@ -116,21 +102,38 @@ mod test { } #[test] - fn base10() { - let tokens = bytes(quote! {340_282_366_920_938_463_463_374_607_431_768_211_455u128}); - let parsed = syn::parse2::(tokens).unwrap(); - let expect = syn::parse_quote!([ - 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, - 255u8, 255u8, 255u8, 255u8 - ]); - assert_eq!(parsed, expect); - - let tokens = bytes(quote! {340_282_366_920_938_463_463_374_607_431_768_211_456}); - let parsed = syn::parse2::(tokens).unwrap(); - let expect = syn::parse_quote!([ - 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8 - ]); - assert_eq!(parsed, expect); + fn decimal_and_octal_unsupported() { + let table: &[_] = &[ + // Decimal. + quote!(0), + quote!(1), + quote!(9), + quote!(255), + quote!(256), + quote!(0255), + quote!(00255), + quote!(00), + quote!(340_282_366_920_938_463_463_374_607_431_768_211_455u128), + quote!(340_282_366_920_938_463_463_374_607_431_768_211_456), + // Octal. + quote!(0o0), + quote!(0o1), + quote!(0o377), + quote!(0o0377), + quote!(0o00377), + quote!(0o00), + quote!(0o400), + ]; + let expect = Error::new( + Span::call_site(), + "only hex (0x) and binary (0b) integer literals are supported", + ) + .to_compile_error() + .to_string(); + for (i, input) in table.iter().enumerate() { + let tokens = bytes(input.clone()); + assert_eq!(tokens.to_string(), expect, "table entry: {}", i); + } } #[test] @@ -176,12 +179,10 @@ 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); @@ -197,31 +198,20 @@ mod test { // 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), @@ -260,10 +250,8 @@ mod test { #[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); @@ -293,39 +281,4 @@ mod test { .to_string(); assert_eq!(tokens.to_string(), expect); } - - #[test] - fn leading_zeros_prohibited() { - let table: &[(_, Result)] = &[ - // 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() { - let tokens = bytes(t.0.clone()); - match t.1.clone() { - Ok(expect) => { - let parsed = syn::parse2::(tokens); - assert_eq!(parsed.unwrap(), expect, "table entry: {}", i); - } - Err(e) => { - assert_eq!( - tokens.to_string(), - e.to_compile_error().to_string(), - "table entry: {}", - i - ); - } - }; - } - } } diff --git a/src/bytesmin.rs b/src/bytesmin.rs index 25b2a21..4c8ebca 100644 --- a/src/bytesmin.rs +++ b/src/bytesmin.rs @@ -14,6 +14,21 @@ pub fn bytesmin(input: TokenStream2) -> TokenStream2 { Ok(int) => int, Err(_) => return Error::new(lit.span(), "negative values unsupported").to_compile_error(), }; + + // Reject unsupported literal forms. + let raw = lit.to_string(); + let normalized = raw.replace('_', ""); + match normalized.as_bytes() { + [b'0', b'x', ..] | [b'0', b'b', ..] => {} + _ => { + return Error::new( + lit.span(), + "only hex (0x) and binary (0b) integer literals are supported", + ) + .to_compile_error(); + } + } + let bytes = int.to_bytes_be(); quote! { [#(#bytes),*] } } @@ -59,32 +74,47 @@ mod test { } #[test] - fn base10() { - let tokens = bytesmin(quote! {340_282_366_920_938_463_463_374_607_431_768_211_455u128}); - let parsed = syn::parse2::(tokens).unwrap(); - let expect = syn::parse_quote!([ - 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, 255u8, - 255u8, 255u8, 255u8, 255u8 - ]); - assert_eq!(parsed, expect); - - let tokens = bytesmin(quote! {340_282_366_920_938_463_463_374_607_431_768_211_456}); - let parsed = syn::parse2::(tokens).unwrap(); - let expect = syn::parse_quote!([ - 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8 - ]); - assert_eq!(parsed, expect); + fn decimal_and_octal_unsupported() { + let table: &[_] = &[ + // Decimal. + quote!(0), + quote!(1), + quote!(9), + quote!(255), + quote!(256), + quote!(0255), + quote!(00255), + quote!(00), + quote!(340_282_366_920_938_463_463_374_607_431_768_211_455u128), + quote!(340_282_366_920_938_463_463_374_607_431_768_211_456), + // Octal. + quote!(0o0), + quote!(0o1), + quote!(0o377), + quote!(0o0377), + quote!(0o00377), + quote!(0o00), + quote!(0o400), + ]; + let expect = Error::new( + Span::call_site(), + "only hex (0x) and binary (0b) integer literals are supported", + ) + .to_compile_error() + .to_string(); + for (i, input) in table.iter().enumerate() { + let tokens = bytesmin(input.clone()); + assert_eq!(tokens.to_string(), expect, "table entry: {}", i); + } } #[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); @@ -100,31 +130,20 @@ mod test { // 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), @@ -163,10 +182,8 @@ mod test { #[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); @@ -228,16 +245,6 @@ mod test { (quote!(0b0000001), parse_quote!([1u8])), (quote!(0b00000001), parse_quote!([1u8])), (quote!(0b000000001), parse_quote!([1u8])), - // Base 8. - (quote!(0o377), parse_quote!([255u8])), - (quote!(0o0377), parse_quote!([255u8])), - (quote!(0o00377), parse_quote!([255u8])), - (quote!(0o400), parse_quote!([1u8, 0u8])), - // Base 10. - (quote!(255), parse_quote!([255u8])), - (quote!(0255), parse_quote!([255u8])), - (quote!(00255), parse_quote!([255u8])), - (quote!(256), parse_quote!([1u8, 0u8])), ]; for (i, t) in table.iter().cloned().enumerate() { let tokens = bytesmin(t.0); diff --git a/src/lib.rs b/src/lib.rs index 00dcfe1..7d166e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,39 +13,23 @@ extern crate proc_macro; /// /// Currently supports only integer literals of unbounded size. /// -/// The following integer literal forms are supported and preserve leading -/// zeros. The final byte representation always returns a consistent number of -/// bytes given the number of digits inputed. -/// - Base 16 (hex) -/// - Base 2 (binary) -/// -/// For integer literal forms that preserve leading zeros, zeros on the front of -/// the number are preserved as zeros in the final bytes. For example: `0x0001` -/// will produce `[0, 1]`. -/// -/// The following integer literal forms are supported and prohibit leading -/// zeros. The number of bytes returned is not based off the number of digits -/// entered. -/// - Base 10 (decimal) -/// - Base 8 (octal) -/// -/// For integer literal forms that do not have consistent digit to byte lengths, -/// the number of bytes returned is the minimum number of bytes required to -/// represent the integer. +/// Supported literal forms: +/// - Base 16 (hex), e.g. `0xff` +/// - Base 2 (binary), e.g. `0b11111111` +/// +/// Leading zeros are preserved. Zeros on the front of the number are preserved +/// as zeros in the final bytes. For example: `0x0001` will produce `[0, 1]`. +/// +/// Decimal and octal literal forms are not supported. /// /// ### Examples /// /// ``` -/// let bytes = bytes_lit::bytes!(1); +/// let bytes = bytes_lit::bytes!(0x1); /// assert_eq!(bytes, [1]); /// ``` /// /// ``` -/// let bytes = bytes_lit::bytes!(9); -/// assert_eq!(bytes, [9]); -/// ``` -/// -/// ``` /// let bytes = bytes_lit::bytes!(0xfded3f55dec47250a52a8c0bb7038e72fa6ffaae33562f77cd2b629ef7fd424d); /// assert_eq!(bytes, [ /// 253, 237, 63, 85, 222, 196, 114, 80, 165, 42, 140, 11, 183, 3, 142, 114, @@ -70,25 +54,26 @@ pub fn bytes(input: TokenStream) -> TokenStream { /// /// Currently supports only integer literals of unbounded size. /// +/// Supported literal forms: +/// - Base 16 (hex), e.g. `0xff` +/// - Base 2 (binary), e.g. `0b11111111` +/// /// Leading zeroes on integer literals are discarded and not preserved. The /// generated byte slice is the minimal bytes required to capture the literal /// provided. /// +/// Decimal and octal literal forms are not supported. +/// /// To preserve leading zeros, use [`bytes!`]. /// /// ### Examples /// /// ``` -/// let bytes = bytes_lit::bytesmin!(1); +/// let bytes = bytes_lit::bytesmin!(0x1); /// assert_eq!(bytes, [1]); /// ``` /// /// ``` -/// let bytes = bytes_lit::bytesmin!(9); -/// assert_eq!(bytes, [9]); -/// ``` -/// -/// ``` /// let bytes = bytes_lit::bytesmin!(0xfded3f55dec47250a52a8c0bb7038e72fa6ffaae33562f77cd2b629ef7fd424d); /// assert_eq!(bytes, [ /// 253, 237, 63, 85, 222, 196, 114, 80, 165, 42, 140, 11, 183, 3, 142, 114, From aa32a64f0a1a62024378ff6c69dbadfbe826452d Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:34:53 +1000 Subject: [PATCH 4/4] improve negative literal error message to use unified form check --- src/bytes.rs | 28 ++++++++++++++-------------- src/bytesmin.rs | 21 ++++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/bytes.rs b/src/bytes.rs index f24bcad..6813380 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -11,15 +11,6 @@ pub fn bytes(input: TokenStream2) -> TokenStream2 { Err(e) => return e.to_compile_error(), }; - // Convert the integer literal into a base10 string, and into a slice of - // bytes, via a big integer. The conversion should never fail because - // syn::LitInt already validated the integer, unless the value is negative. - // Any leading zeroes are discarded. - let int = match BigUint::from_str(lit.base10_digits()) { - Ok(int) => int, - Err(_) => return Error::new(lit.span(), "negative values unsupported").to_compile_error(), - }; - // Get the raw integer literal as it appears in the token stream. let raw = lit.to_string(); @@ -35,12 +26,18 @@ pub fn bytes(input: TokenStream2) -> TokenStream2 { _ => { return Error::new( lit.span(), - "only hex (0x) and binary (0b) integer literals are supported", + "only positive hex (0x) and binary (0b) integer literals are supported", ) .to_compile_error(); } }; + // Convert the integer literal into a base10 string, and into a slice of + // bytes, via a big integer. The conversion should never fail because + // syn::LitInt already validated the integer, and the form check above + // ensures only non-negative hex/binary literals reach here. + let int = BigUint::from_str(lit.base10_digits()).expect("valid hex or binary literal"); + // Count the leading zero bits by counting the number of leading zeros and // multiplying by the bits per digit. let leading_zero_count = remainder.iter().take_while(|d| **d == b'0').count(); @@ -72,9 +69,12 @@ mod test { #[test] fn neg() { let tokens = bytes(quote! {-0x1}); - let expect = Error::new(Span::call_site(), "negative values unsupported") - .to_compile_error() - .to_string(); + let expect = Error::new( + Span::call_site(), + "only positive hex (0x) and binary (0b) integer literals are supported", + ) + .to_compile_error() + .to_string(); assert_eq!(tokens.to_string(), expect); } @@ -126,7 +126,7 @@ mod test { ]; let expect = Error::new( Span::call_site(), - "only hex (0x) and binary (0b) integer literals are supported", + "only positive hex (0x) and binary (0b) integer literals are supported", ) .to_compile_error() .to_string(); diff --git a/src/bytesmin.rs b/src/bytesmin.rs index 4c8ebca..762c222 100644 --- a/src/bytesmin.rs +++ b/src/bytesmin.rs @@ -10,10 +10,6 @@ pub fn bytesmin(input: TokenStream2) -> TokenStream2 { Ok(lit) => lit, Err(e) => return e.to_compile_error(), }; - let int = match BigUint::from_str(lit.base10_digits()) { - Ok(int) => int, - Err(_) => return Error::new(lit.span(), "negative values unsupported").to_compile_error(), - }; // Reject unsupported literal forms. let raw = lit.to_string(); @@ -23,12 +19,16 @@ pub fn bytesmin(input: TokenStream2) -> TokenStream2 { _ => { return Error::new( lit.span(), - "only hex (0x) and binary (0b) integer literals are supported", + "only positive hex (0x) and binary (0b) integer literals are supported", ) .to_compile_error(); } } + // The conversion should never fail because syn::LitInt already validated + // the integer, and the form check above ensures only non-negative hex/binary + // literals reach here. + let int = BigUint::from_str(lit.base10_digits()).expect("valid hex or binary literal"); let bytes = int.to_bytes_be(); quote! { [#(#bytes),*] } } @@ -44,9 +44,12 @@ mod test { #[test] fn neg() { let tokens = bytesmin(quote! {-0x1}); - let expect = Error::new(Span::call_site(), "negative values unsupported") - .to_compile_error() - .to_string(); + let expect = Error::new( + Span::call_site(), + "only positive hex (0x) and binary (0b) integer literals are supported", + ) + .to_compile_error() + .to_string(); assert_eq!(tokens.to_string(), expect); } @@ -98,7 +101,7 @@ mod test { ]; let expect = Error::new( Span::call_site(), - "only hex (0x) and binary (0b) integer literals are supported", + "only positive hex (0x) and binary (0b) integer literals are supported", ) .to_compile_error() .to_string();