From e060d05873d5acecef3804c34a378158d42b5267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 2 Nov 2026 15:55:50 +0100 Subject: [PATCH 1/2] Fix `u32` codec and add `i32`, `i64`, and `bool` --- CHANGELOG.md | 6 ++++ src/lib.rs | 78 ++++++++++++++++++++++++++++++++++++++++------ tests/encodable.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d067d9a..621614c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Implement `CompactEncoding>` for `&[T]` where `T: VecEncodable`, enabling encoding of slices like `&[&[u8]]` * Implement `VecEncodable` for `Vec` to support `Vec>` * Implement `VecEncodable>` for `&[u8]` to support `&[&[u8]]` +* Implement `CompactEncoding` for `bool`, encoded in one byte, where any byte other than 1 decodes as `false` +* Implement `CompactEncoding` for `i32` and `i64`, zig-zag encoded as a variable width uint like `int` of the JavaScript implementation + +### Fixed + +* `u32` decodes a uint with the 8 byte prefix instead of reading only 4 of its bytes, and fails with an overflow error when the value doesn't fit ### Changed diff --git a/src/lib.rs b/src/lib.rs index 62e0264..4c480f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -689,15 +689,9 @@ fn decode_u64(buffer: &[u8]) -> Result<(u64, &[u8]), EncodingError> { } fn decode_u32_var(buffer: &[u8]) -> Result<(u32, &[u8]), EncodingError> { - let ([first], rest) = take_array::<1>(buffer)?; - Ok(match first { - x if x < U16_SIGNIFIER => (x.into(), rest), - U16_SIGNIFIER => { - let (val, rest) = decode_u16(rest)?; - (val.into(), rest) - } - _ => decode_u32(rest)?, - }) + Ok(map_first_result!(decode_u64_var(buffer)?, |val| { + u32::try_from(val).map_err(|_| EncodingError::overflow("Could not convert u64 to u32")) + })) } fn decode_u64_var(buffer: &[u8]) -> Result<(u64, &[u8]), EncodingError> { @@ -876,6 +870,72 @@ impl CompactEncoding for usize { } } +// Signed integers are zig-zag encoded as a variable width uint, like `int` of the JavaScript +// implementation. +fn zig_zag_encode(value: i64) -> u64 { + ((value << 1) ^ (value >> 63)) as u64 +} + +fn zig_zag_decode(value: u64) -> i64 { + ((value >> 1) as i64) ^ -((value & 1) as i64) +} + +impl CompactEncoding for i32 { + fn encoded_size(&self) -> Result { + Ok(encoded_size_var_u64(zig_zag_encode((*self).into()))) + } + + fn encode<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], EncodingError> { + encode_var_u64(zig_zag_encode((*self).into()), buffer) + } + + fn decode(buffer: &[u8]) -> Result<(Self, &[u8]), EncodingError> + where + Self: Sized, + { + Ok(map_first_result!(decode_u64_var(buffer)?, |val| { + i32::try_from(zig_zag_decode(val)) + .map_err(|_| EncodingError::overflow("Could not convert i64 to i32")) + })) + } +} + +impl CompactEncoding for i64 { + fn encoded_size(&self) -> Result { + Ok(encoded_size_var_u64(zig_zag_encode(*self))) + } + + fn encode<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], EncodingError> { + encode_var_u64(zig_zag_encode(*self), buffer) + } + + fn decode(buffer: &[u8]) -> Result<(Self, &[u8]), EncodingError> + where + Self: Sized, + { + Ok(map_first!(decode_u64_var(buffer)?, zig_zag_decode)) + } +} + +// Like the JavaScript implementation, any byte other than 1 decodes as false. +impl CompactEncoding for bool { + fn encoded_size(&self) -> Result { + Ok(1) + } + + fn encode<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], EncodingError> { + write_array(&[u8::from(*self)], buffer) + } + + fn decode(buffer: &[u8]) -> Result<(Self, &[u8]), EncodingError> + where + Self: Sized, + { + let ([byte], rest) = take_array::<1>(buffer)?; + Ok((byte == 1, rest)) + } +} + impl CompactEncoding for String { fn encoded_size(&self) -> Result { encoded_size_str(self) diff --git a/tests/encodable.rs b/tests/encodable.rs index caefa88..4aeba39 100644 --- a/tests/encodable.rs +++ b/tests/encodable.rs @@ -202,3 +202,80 @@ fn cenc_32_byte_array() -> Result<(), EncodingError> { assert_eq!(result.2, many_array); Ok(()) } + +// Encoded by the JavaScript implementation with `int`. +const INT_VECTORS: [(i64, &[u8]); 10] = [ + (0, &[0x00]), + (-1, &[0x01]), + (1, &[0x02]), + (-63, &[0x7d]), + (126, &[0xfc]), + (-127, &[0xfd, 0xfd, 0x00]), + (2147483647, &[0xfe, 0xfe, 0xff, 0xff, 0xff]), + (-2147483648, &[0xfe, 0xff, 0xff, 0xff, 0xff]), + ( + 1 << 40, + &[0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00], + ), + ( + -(1 << 40), + &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00], + ), +]; + +#[test] +fn cenc_i64_like_javascript() -> Result<(), EncodingError> { + for (value, encoded) in INT_VECTORS { + assert_eq!(&*value.to_encoded_bytes()?, encoded); + + let (result, rest) = i64::decode(encoded)?; + assert!(rest.is_empty()); + assert_eq!(result, value); + } + Ok(()) +} + +#[test] +fn cenc_i32_like_javascript() -> Result<(), EncodingError> { + for (value, encoded) in INT_VECTORS { + match i32::try_from(value) { + Ok(value) => { + assert_eq!(&*value.to_encoded_bytes()?, encoded); + + let (result, rest) = i32::decode(encoded)?; + assert!(rest.is_empty()); + assert_eq!(result, value); + } + Err(_) => assert!(i32::decode(encoded).is_err()), + } + } + Ok(()) +} + +#[test] +fn cenc_bool() -> Result<(), EncodingError> { + assert_eq!(&*true.to_encoded_bytes()?, &[1]); + assert_eq!(&*false.to_encoded_bytes()?, &[0]); + + let ((a, b, c), rest) = map_decode!(&[1, 0, 2], [bool, bool, bool]); + assert!(rest.is_empty()); + assert!(a); + assert!(!b); + assert!(!c); + + assert!(bool::decode(&[]).is_err()); + Ok(()) +} + +#[test] +fn cenc_u32_from_eight_byte_uint() -> Result<(), EncodingError> { + let encoded = [0xff, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07]; + let ((value, next), rest) = map_decode!(&encoded, [u32, u8]); + assert!(rest.is_empty()); + assert_eq!(value, 42); + assert_eq!(next, 7); + + let too_big = [0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]; + assert!(u32::decode(&too_big).is_err()); + Ok(()) +} From f3d4d8006cc34baa58b68382e87df483758651b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 4 Nov 2026 19:39:19 +0100 Subject: [PATCH 2/2] Adjust error message and simplify some bits --- src/lib.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4c480f7..4938a08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -522,8 +522,7 @@ pub fn write_array<'a, const N: usize>( let blen = buffer.len(); let Some((dest, rest)) = buffer.split_first_chunk_mut::() else { return Err(EncodingError::out_of_bounds(&format!( - "Could not write [{}] bytes to buffer of length [{}]", - N, blen + "Could not write [{N}] bytes to buffer of length [{blen}]" ))); }; dest.copy_from_slice(source); @@ -550,8 +549,7 @@ pub fn take_array_mut( let blen = buffer.len(); let Some((out, rest)) = buffer.split_first_chunk_mut::() else { return Err(EncodingError::out_of_bounds(&format!( - "Could not write [{}] bytes to buffer of length [{blen}]", - N, + "Could not write [{N}] bytes to buffer of length [{blen}]" ))); }; Ok((out, rest)) @@ -690,17 +688,18 @@ fn decode_u64(buffer: &[u8]) -> Result<(u64, &[u8]), EncodingError> { fn decode_u32_var(buffer: &[u8]) -> Result<(u32, &[u8]), EncodingError> { Ok(map_first_result!(decode_u64_var(buffer)?, |val| { - u32::try_from(val).map_err(|_| EncodingError::overflow("Could not convert u64 to u32")) + u32::try_from(val) + .map_err(|_| EncodingError::overflow("uint too large to decode into a u32")) })) } fn decode_u64_var(buffer: &[u8]) -> Result<(u64, &[u8]), EncodingError> { let ([first], rest) = take_array::<1>(buffer)?; Ok(match first { - x if x < U16_SIGNIFIER => (x.into(), rest), + x @ 0..U16_SIGNIFIER => (x.into(), rest), U16_SIGNIFIER => map_first!(decode_u16(rest)?, |x: u16| x.into()), U32_SIGNIFIER => map_first!(decode_u32(rest)?, |x: u32| x.into()), - _ => decode_u64(rest)?, + U64_SIGNIFIER => decode_u64(rest)?, }) }