#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Endianness {
Little,
Big,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FormatOptions {
pub endianness: Endianness,
pub align: usize,
}
const DEFAULT_ALIGN: usize = 8;
const MAX_INT_SIZE: usize = 32;
pub const fn default_options() -> FormatOptions {
FormatOptions {
endianness: Endianness::Little,
align: 1,
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum StructValue {
Number(f64),
Bytes(Vec<u8>),
}
impl StructValue {
fn as_number(&self) -> f64 {
match self {
Self::Number(n) => *n,
Self::Bytes(b) => str::from_utf8(b)
.ok()
.and_then(|t| t.parse().ok())
.unwrap_or(0.0),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StructUnpackOut {
pub values: Vec<StructValue>,
pub consumed: usize,
}
fn try_get_num(format: &[u8], pos: &mut usize, default: usize) -> Option<usize> {
let Some(&first) = format.get(*pos) else {
return Some(default);
};
if !first.is_ascii_digit() {
return Some(default);
}
let mut result = 0usize;
while let Some(&b) = format.get(*pos) {
if !b.is_ascii_digit() {
break;
}
let digit = usize::from(b - b'0');
result = result.checked_mul(10)?.checked_add(digit)?;
*pos += 1;
}
Some(result)
}
fn try_get_opt_size(format: &[u8], pos: &mut usize) -> Option<(u8, usize)> {
let token = *format.get(*pos)?;
*pos += 1;
let size = match token {
b'b' | b'B' => 1,
b'h' | b'H' => 2,
b'l' | b'L' | b'T' => 8,
b'f' => 4,
b'd' => 8,
b'x' => 1,
b'c' => try_get_num(format, pos, 1)?,
b'i' | b'I' => {
let n = try_get_num(format, pos, 4)?;
if n > MAX_INT_SIZE {
return None;
}
n
}
_ => 0,
};
Some((token, size))
}
fn try_parse_control_options(
token: u8,
format: &[u8],
pos: &mut usize,
header: &mut FormatOptions,
) -> Option<()> {
match token {
b' ' => {}
b'<' => header.endianness = Endianness::Little,
b'>' => header.endianness = Endianness::Big,
b'!' => {
let a = try_get_num(format, pos, DEFAULT_ALIGN)?;
if !a.is_power_of_two() {
return None;
}
header.align = a;
}
_ => return None,
}
Some(())
}
fn get_to_align(len: usize, alignment: usize, token: u8, size: usize) -> usize {
if size == 0 || token == b'c' {
return 0;
}
let size = size.min(alignment);
(size - (len & (size.wrapping_sub(1)))) & size.wrapping_sub(1)
}
fn try_encode_integer(out: &mut Vec<u8>, value: i64, width: usize, endianness: Endianness) {
let bytes = value.to_le_bytes();
let width = width.clamp(1, 8);
if endianness == Endianness::Little {
out.extend_from_slice(&bytes[..width]);
} else {
out.extend(bytes[..width].iter().rev());
}
}
fn try_encode_single(out: &mut Vec<u8>, value: f32, endianness: Endianness) {
let bytes = value.to_le_bytes();
if endianness == Endianness::Little {
out.extend_from_slice(&bytes);
} else {
out.extend(bytes.iter().rev());
}
}
fn try_encode_double(out: &mut Vec<u8>, value: f64, endianness: Endianness) {
let bytes = value.to_le_bytes();
if endianness == Endianness::Little {
out.extend_from_slice(&bytes);
} else {
out.extend(bytes.iter().rev());
}
}
fn try_decode_integer(
input: &[u8],
offset: usize,
width: usize,
endianness: Endianness,
signed: bool,
) -> Option<f64> {
debug_assert!((1..=8).contains(&width));
let slot = input.get(offset..offset + width)?;
let mut l = 0u64;
match endianness {
Endianness::Little => {
for &b in slot.iter().rev() {
l = (l << 8) | u64::from(b);
}
}
Endianness::Big => {
for &b in slot {
l = (l << 8) | u64::from(b);
}
}
}
let bits = width * 8;
if signed && bits < 64 && l & (1 << (bits - 1)) != 0 {
l |= u64::MAX << bits;
}
Some(if signed { l as i64 as f64 } else { l as f64 })
}
fn try_decode_single(input: &[u8], offset: usize, endianness: Endianness) -> Option<f32> {
let slot: [u8; 4] = input.get(offset..offset + 4)?.try_into().ok()?;
Some(match endianness {
Endianness::Little => f32::from_le_bytes(slot),
Endianness::Big => f32::from_be_bytes(slot),
})
}
fn try_decode_double(input: &[u8], offset: usize, endianness: Endianness) -> Option<f64> {
let slot: [u8; 8] = input.get(offset..offset + 8)?.try_into().ok()?;
Some(match endianness {
Endianness::Little => f64::from_le_bytes(slot),
Endianness::Big => f64::from_be_bytes(slot),
})
}
pub fn struct_pack(format: &[u8], values: &[StructValue]) -> Option<Vec<u8>> {
let mut header = default_options();
let mut out = Vec::new();
let mut pos = 0usize;
let mut next = values.iter();
while pos < format.len() {
let (token, size) = try_get_opt_size(format, &mut pos)?;
let pad = get_to_align(out.len(), header.align, token, size);
out.resize(out.len() + pad, 0);
match token {
b'b' | b'B' => out.push(next.next()?.as_number() as i64 as u8),
b'h' | b'H' => try_encode_integer(
&mut out,
next.next()?.as_number() as i64,
2,
header.endianness,
),
b'l' | b'L' | b'T' => {
try_encode_integer(
&mut out,
next.next()?.as_number() as i64,
8,
header.endianness,
);
}
b'i' | b'I' => {
if size == 0 || size > 8 {
return None;
}
try_encode_integer(
&mut out,
next.next()?.as_number() as i64,
size,
header.endianness,
);
}
b'f' => try_encode_single(&mut out, next.next()?.as_number() as f32, header.endianness),
b'd' => try_encode_double(&mut out, next.next()?.as_number(), header.endianness),
b'x' => out.push(0),
b'c' | b's' => {
let StructValue::Bytes(data) = next.next()? else {
return None;
};
let n = if size == 0 { data.len() } else { size };
if data.len() < n {
return None;
}
out.extend_from_slice(&data[..n]);
if token == b's' {
out.push(0);
}
}
_ => try_parse_control_options(token, format, &mut pos, &mut header)?,
}
}
Some(out)
}
pub fn struct_unpack(format: &[u8], data: &[u8]) -> Option<StructUnpackOut> {
let mut header = default_options();
let mut pos = 0usize;
let mut offset = 0usize;
let mut values = Vec::new();
while pos < format.len() {
let (token, mut size) = try_get_opt_size(format, &mut pos)?;
offset += get_to_align(offset, header.align, token, size);
if size > data.len() || offset > data.len() - size {
return None;
}
match token {
b'b' | b'B' | b'h' | b'H' | b'l' | b'L' | b'T' | b'i' | b'I' => {
if size == 0 || size > 8 {
return None;
}
let signed = token.is_ascii_lowercase();
let value = try_decode_integer(data, offset, size, header.endianness, signed)?;
values.push(StructValue::Number(value));
}
b'x' => {}
b'f' => values.push(StructValue::Number(f64::from(try_decode_single(
data,
offset,
header.endianness,
)?))),
b'd' => values.push(StructValue::Number(try_decode_double(
data,
offset,
header.endianness,
)?)),
b'c' => {
if size == 0 {
match values.pop()? {
StructValue::Number(n) if n >= 0.0 => size = n as usize,
_ => return None,
}
if size > data.len() - offset {
return None;
}
}
values.push(StructValue::Bytes(data[offset..offset + size].to_vec()));
}
b's' => {
let nul = data[offset..].iter().position(|&b| b == 0)?;
values.push(StructValue::Bytes(data[offset..offset + nul].to_vec()));
size = nul + 1;
}
_ => try_parse_control_options(token, format, &mut pos, &mut header)?,
}
offset += size;
}
Some(StructUnpackOut {
values,
consumed: offset,
})
}
pub fn struct_size(format: &[u8]) -> Option<usize> {
let mut header = default_options();
let mut pos = 0usize;
let mut total = 0usize;
while pos < format.len() {
let (token, size) = try_get_opt_size(format, &mut pos)?;
total += get_to_align(total, header.align, token, size);
if token == b's' {
return None;
}
if token == b'c' && size == 0 {
return None;
}
if !token.is_ascii_alphanumeric() {
try_parse_control_options(token, format, &mut pos, &mut header)?;
}
total += size;
}
Some(total)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_unpack_roundtrip() {
let packed = struct_pack(
b"<ib",
&[
StructValue::Number(305_419_896.0),
StructValue::Number(-1.0),
],
)
.unwrap();
assert_eq!(packed.len(), 5);
assert_eq!(packed[..4], 0x1234_5678i32.to_le_bytes());
assert_eq!(packed[4], 0xff);
let out = struct_unpack(b"<ib", &packed).unwrap();
assert_eq!(
out.values,
vec![
StructValue::Number(305_419_896.0),
StructValue::Number(-1.0)
]
);
assert_eq!(out.consumed, 5);
}
#[test]
fn sized_integer_suffix_is_byte_width() {
assert_eq!(struct_size(b"<i8"), Some(8));
assert_eq!(struct_size(b"i"), Some(4));
assert_eq!(struct_size(b"i33"), None, "超过 MAXINTSIZE=32 报错");
let packed = struct_pack(b">I2", &[StructValue::Number(4660.0)]).unwrap();
assert_eq!(packed, vec![0x12, 0x34]);
let out = struct_unpack(b">I2", &packed).unwrap();
assert_eq!(out.values, vec![StructValue::Number(4660.0)]);
}
#[test]
fn sign_extension_by_token_case() {
let out = struct_unpack(b"B", &[0xff]).unwrap();
assert_eq!(out.values, vec![StructValue::Number(255.0)]);
let out = struct_unpack(b"h", &[0xff, 0xff]).unwrap();
assert_eq!(out.values, vec![StructValue::Number(-1.0)]);
let out = struct_unpack(b"H", &[0xff, 0xff]).unwrap();
assert_eq!(out.values, vec![StructValue::Number(65535.0)]);
}
#[test]
fn double_preserves_fraction() {
let packed = struct_pack(b"d", &[StructValue::Number(1.5)]).unwrap();
let out = struct_unpack(b"d", &packed).unwrap();
assert_eq!(out.values, vec![StructValue::Number(1.5)]);
}
#[test]
fn string_family() {
let packed = struct_pack(b"c4", &[StructValue::Bytes(b"abcd".to_vec())]).unwrap();
assert_eq!(packed, b"abcd");
assert!(struct_pack(b"c4", &[StructValue::Bytes(b"ab".to_vec())]).is_none());
let out = struct_unpack(b"Ic0", &[4, 0, 0, 0, b'a', b'b', b'c', b'd']).unwrap();
assert_eq!(out.values, vec![StructValue::Bytes(b"abcd".to_vec())]);
assert_eq!(out.consumed, 8);
let out = struct_unpack(b"s", b"abc\0rest").unwrap();
assert_eq!(out.values, vec![StructValue::Bytes(b"abc".to_vec())]);
assert_eq!(out.consumed, 4);
let packed = struct_pack(b"s", &[StructValue::Bytes(b"ab".to_vec())]).unwrap();
assert_eq!(packed, b"ab\0");
assert_eq!(struct_size(b"s"), None);
assert_eq!(struct_size(b"c0"), None);
}
#[test]
fn struct_size_and_align() {
assert_eq!(struct_size(b"<ibB"), Some(6));
assert_eq!(struct_size(b"x"), Some(1));
assert_eq!(struct_size(b"c10"), Some(10));
assert_eq!(struct_size(b"!4 bi"), Some(8));
assert_eq!(struct_size(b">"), Some(0));
assert_eq!(struct_size(b"z"), Some(0));
}
#[test]
fn unpack_reports_short_data() {
assert!(struct_unpack(b"i", &[1, 2, 3]).is_none());
assert!(struct_unpack(b"s", b"no-null").is_none());
assert!(struct_unpack(b"q", &[0]).is_none(), "未知字母报错");
}
#[test]
fn big_endian_doubles() {
let mut out = Vec::new();
try_encode_double(&mut out, 1.5, Endianness::Big);
assert_eq!(out, 1.5f64.to_be_bytes());
assert_eq!(try_decode_double(&out, 0, Endianness::Big), Some(1.5));
}
#[test]
fn alignment_padding_positions() {
let format = b"!4 icd";
assert_eq!(struct_size(format), Some(16));
let packed = struct_pack(
format,
&[
StructValue::Number(1.0),
StructValue::Bytes(b"ab".to_vec()),
StructValue::Number(1.5),
],
)
.unwrap();
assert_eq!(packed.len(), 16);
let out = struct_unpack(format, &packed).unwrap();
assert_eq!(out.consumed, 16);
assert_eq!(out.values[2], StructValue::Number(1.5));
}
#[test]
fn get_to_align_matches_csharp_bit_form() {
assert_eq!(get_to_align(0, 4, b'i', 4), 0);
assert_eq!(get_to_align(5, 4, b'i', 4), 3);
assert_eq!(get_to_align(5, 4, b'c', 3), 0, "c 不对齐");
assert_eq!(get_to_align(5, 1, b'i', 4), 0, "默认不对齐");
}
#[test]
fn array_decimal_reads() {
let packed = struct_pack(b"i4", &[StructValue::Number(-2.0)]).unwrap();
assert_eq!(packed, (-2i32).to_le_bytes());
let out = struct_unpack(b"i4", &packed).unwrap();
assert_eq!(out.values, vec![StructValue::Number(-2.0)]);
assert_eq!(out.consumed, 4);
}
}