feat(typedef/data-access): implement primitive read/write for 17 TypeDef kinds with endianness
This commit is contained in:
1 parent
f41387eeed
commit
03e1c5ee41
2 files changed
+829
-3
No files matched your search
@@ -1,4 +1,830 @@
|
|||||||
//! Data access layer: primitive read/write functions for all 17 TypeDef
|
//! Data access layer: primitive read/write functions for all 17 TypeDef
|
||||||
//! kinds with endianness support, bounds checking, and zero-copy access.
|
//! kinds with endianness support, bounds checking, and zero-copy access.
|
||||||
|
//!
|
||||||
|
//! These are the building blocks used by the layout types ([`crate::offset_map`],
|
||||||
|
//! [`crate::layout_builder`], [`crate::sequential_reader`]) and the
|
||||||
|
//! [`crate::engine::TypedefEngine`]. Each function operates on a raw byte
|
||||||
|
//! buffer at a caller-provided offset and returns a [`TypedefError::Access`]
|
||||||
|
//! carrying the field path on bounds or encoding failures.
|
||||||
|
//!
|
||||||
|
//! # Conventions
|
||||||
|
//!
|
||||||
|
//! - All multi-byte types respect the [`Endian`] parameter passed by the caller.
|
||||||
|
//! - Bounds checks ensure `buffer.len() >= offset + size`; failures produce
|
||||||
|
//! [`TypedefError::Access`] with a descriptive `reason`.
|
||||||
|
//! - Read functions for variable-length types return slices borrowing from
|
||||||
|
//! the input buffer — no allocation.
|
||||||
|
//! - No `unwrap()` / `expect()` on fallible operations.
|
||||||
|
|
||||||
// TODO: implement
|
use crate::error::TypedefError;
|
||||||
|
use crate::schema::Endian;
|
||||||
|
|
||||||
|
const U32_SIZE: usize = 4;
|
||||||
|
|
||||||
|
fn check_bounds(
|
||||||
|
buffer_len: usize,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
field_path: &str,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
if end < start || buffer_len < end {
|
||||||
|
return Err(TypedefError::Access {
|
||||||
|
field_path: field_path.to_string(),
|
||||||
|
reason: format!(
|
||||||
|
"buffer bounds check failed: need bytes [{start}..{end}), buffer has {buffer_len}"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn access_err(field_path: &str, reason: impl Into<String>) -> TypedefError {
|
||||||
|
TypedefError::Access {
|
||||||
|
field_path: field_path.to_string(),
|
||||||
|
reason: reason.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_array<const N: usize>(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
) -> Result<[u8; N], TypedefError> {
|
||||||
|
let end = offset.checked_add(N).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("offset {offset} + size {N} overflows usize"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
check_bounds(buffer.len(), offset, end, field_path)?;
|
||||||
|
let slice = buffer.get(offset..end).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("slice [{offset}..{end}) unavailable in buffer of length {}", buffer.len()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
slice
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| access_err(field_path, format!("internal: try_into failed for {N}-byte slice")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_array<const N: usize>(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
bytes: [u8; N],
|
||||||
|
field_path: &str,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
let end = offset.checked_add(N).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("offset {offset} + size {N} overflows usize"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
check_bounds(buffer.len(), offset, end, field_path)?;
|
||||||
|
let dest = buffer.get_mut(offset..end).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("mutable slice [{offset}..{end}) unavailable"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
dest.copy_from_slice(&bytes);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32_from(bytes: [u8; U32_SIZE], endian: Endian) -> u32 {
|
||||||
|
match endian {
|
||||||
|
Endian::Little => u32::from_le_bytes(bytes),
|
||||||
|
Endian::Big => u32::from_be_bytes(bytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u32_to(value: u32, endian: Endian) -> [u8; U32_SIZE] {
|
||||||
|
match endian {
|
||||||
|
Endian::Little => value.to_le_bytes(),
|
||||||
|
Endian::Big => value.to_be_bytes(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fixed-size read functions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Read an `i8` at `offset` from `buffer`.
|
||||||
|
pub fn read_i8(buffer: &[u8], offset: usize, field_path: &str) -> Result<i8, TypedefError> {
|
||||||
|
let bytes: [u8; 1] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(bytes[0] as i8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an `i16` at `offset` from `buffer`, applying `endian`.
|
||||||
|
pub fn read_i16(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<i16, TypedefError> {
|
||||||
|
let bytes: [u8; 2] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(match endian {
|
||||||
|
Endian::Little => i16::from_le_bytes(bytes),
|
||||||
|
Endian::Big => i16::from_be_bytes(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an `i32` at `offset` from `buffer`, applying `endian`.
|
||||||
|
pub fn read_i32(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<i32, TypedefError> {
|
||||||
|
let bytes: [u8; 4] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(match endian {
|
||||||
|
Endian::Little => i32::from_le_bytes(bytes),
|
||||||
|
Endian::Big => i32::from_be_bytes(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a `u8` at `offset` from `buffer`.
|
||||||
|
pub fn read_u8(buffer: &[u8], offset: usize, field_path: &str) -> Result<u8, TypedefError> {
|
||||||
|
let bytes: [u8; 1] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(bytes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a `u16` at `offset` from `buffer`, applying `endian`.
|
||||||
|
pub fn read_u16(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<u16, TypedefError> {
|
||||||
|
let bytes: [u8; 2] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(match endian {
|
||||||
|
Endian::Little => u16::from_le_bytes(bytes),
|
||||||
|
Endian::Big => u16::from_be_bytes(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a `u32` at `offset` from `buffer`, applying `endian`.
|
||||||
|
pub fn read_u32(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<u32, TypedefError> {
|
||||||
|
let bytes: [u8; 4] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(u32_from(bytes, endian))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a `u64` at `offset` from `buffer`, applying `endian`.
|
||||||
|
pub fn read_u64(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<u64, TypedefError> {
|
||||||
|
let bytes: [u8; 8] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(match endian {
|
||||||
|
Endian::Little => u64::from_le_bytes(bytes),
|
||||||
|
Endian::Big => u64::from_be_bytes(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an `f32` at `offset` from `buffer`, applying `endian`.
|
||||||
|
pub fn read_f32(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<f32, TypedefError> {
|
||||||
|
let bytes: [u8; 4] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(match endian {
|
||||||
|
Endian::Little => f32::from_le_bytes(bytes),
|
||||||
|
Endian::Big => f32::from_be_bytes(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an `f64` at `offset` from `buffer`, applying `endian`.
|
||||||
|
pub fn read_f64(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<f64, TypedefError> {
|
||||||
|
let bytes: [u8; 8] = read_array(buffer, offset, field_path)?;
|
||||||
|
Ok(match endian {
|
||||||
|
Endian::Little => f64::from_le_bytes(bytes),
|
||||||
|
Endian::Big => f64::from_be_bytes(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a `bool` at `offset` from `buffer`.
|
||||||
|
///
|
||||||
|
/// `0x00` decodes to `false`, `0x01` decodes to `true`. Any other byte value
|
||||||
|
/// produces [`TypedefError::Access`] with a reason of the form
|
||||||
|
/// `"invalid boolean byte 0x02 at offset {offset}"`.
|
||||||
|
pub fn read_bool(buffer: &[u8], offset: usize, field_path: &str) -> Result<bool, TypedefError> {
|
||||||
|
let bytes: [u8; 1] = read_array(buffer, offset, field_path)?;
|
||||||
|
match bytes[0] {
|
||||||
|
0x00 => Ok(false),
|
||||||
|
0x01 => Ok(true),
|
||||||
|
other => Err(access_err(
|
||||||
|
field_path,
|
||||||
|
format!("invalid boolean byte 0x{other:02X} at offset {offset}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a `TEnum` index (`u32`) at `offset` from `buffer`, applying `endian`.
|
||||||
|
///
|
||||||
|
/// The caller maps the returned index to the schema's `"enum"` array entry.
|
||||||
|
pub fn read_enum(
|
||||||
|
buffer: &[u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<u32, TypedefError> {
|
||||||
|
read_u32(buffer, offset, field_path, endian)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fixed-size write functions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Write an `i8` `value` at `offset` into `buffer`.
|
||||||
|
pub fn write_i8(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: i8,
|
||||||
|
field_path: &str,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
write_array(buffer, offset, value.to_ne_bytes(), field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write an `i16` `value` at `offset` into `buffer`, applying `endian`.
|
||||||
|
pub fn write_i16(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: i16,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
let bytes = match endian {
|
||||||
|
Endian::Little => value.to_le_bytes(),
|
||||||
|
Endian::Big => value.to_be_bytes(),
|
||||||
|
};
|
||||||
|
write_array(buffer, offset, bytes, field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write an `i32` `value` at `offset` into `buffer`, applying `endian`.
|
||||||
|
pub fn write_i32(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: i32,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
let bytes = match endian {
|
||||||
|
Endian::Little => value.to_le_bytes(),
|
||||||
|
Endian::Big => value.to_be_bytes(),
|
||||||
|
};
|
||||||
|
write_array(buffer, offset, bytes, field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a `u8` `value` at `offset` into `buffer`.
|
||||||
|
pub fn write_u8(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: u8,
|
||||||
|
field_path: &str,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
write_array(buffer, offset, value.to_ne_bytes(), field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a `u16` `value` at `offset` into `buffer`, applying `endian`.
|
||||||
|
pub fn write_u16(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: u16,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
let bytes = match endian {
|
||||||
|
Endian::Little => value.to_le_bytes(),
|
||||||
|
Endian::Big => value.to_be_bytes(),
|
||||||
|
};
|
||||||
|
write_array(buffer, offset, bytes, field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a `u32` `value` at `offset` into `buffer`, applying `endian`.
|
||||||
|
pub fn write_u32(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: u32,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
write_array(buffer, offset, u32_to(value, endian), field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a `u64` `value` at `offset` into `buffer`, applying `endian`.
|
||||||
|
pub fn write_u64(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: u64,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
let bytes = match endian {
|
||||||
|
Endian::Little => value.to_le_bytes(),
|
||||||
|
Endian::Big => value.to_be_bytes(),
|
||||||
|
};
|
||||||
|
write_array(buffer, offset, bytes, field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write an `f32` `value` at `offset` into `buffer`, applying `endian`.
|
||||||
|
pub fn write_f32(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: f32,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
let bytes = match endian {
|
||||||
|
Endian::Little => value.to_le_bytes(),
|
||||||
|
Endian::Big => value.to_be_bytes(),
|
||||||
|
};
|
||||||
|
write_array(buffer, offset, bytes, field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write an `f64` `value` at `offset` into `buffer`, applying `endian`.
|
||||||
|
pub fn write_f64(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: f64,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
let bytes = match endian {
|
||||||
|
Endian::Little => value.to_le_bytes(),
|
||||||
|
Endian::Big => value.to_be_bytes(),
|
||||||
|
};
|
||||||
|
write_array(buffer, offset, bytes, field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a `bool` `value` at `offset` into `buffer`.
|
||||||
|
///
|
||||||
|
/// `false` is encoded as `0x00`, `true` as `0x01`.
|
||||||
|
pub fn write_bool(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: bool,
|
||||||
|
field_path: &str,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
write_array(buffer, offset, [if value { 0x01 } else { 0x00 }], field_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a `TEnum` index (`u32`) `value` at `offset` into `buffer`, applying `endian`.
|
||||||
|
pub fn write_enum(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: u32,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<(), TypedefError> {
|
||||||
|
write_u32(buffer, offset, value, field_path, endian)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Variable-length read/write (inline length-prefixing)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Read a length-prefixed UTF-8 string borrowing from `buffer`.
|
||||||
|
///
|
||||||
|
/// Wire format: `[length: u32][UTF-8 bytes]`. The length prefix respects
|
||||||
|
/// `endian`. Returns a `&'a str` that borrows from the input buffer — no
|
||||||
|
/// allocation. Invalid UTF-8 produces [`TypedefError::Access`].
|
||||||
|
pub fn read_string<'a>(
|
||||||
|
buffer: &'a [u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<&'a str, TypedefError> {
|
||||||
|
let bytes = read_bytes(buffer, offset, field_path, endian)?;
|
||||||
|
std::str::from_utf8(bytes).map_err(|e| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("invalid UTF-8 in string at offset {offset}: {e}"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read length-prefixed raw bytes borrowing from `buffer`.
|
||||||
|
///
|
||||||
|
/// Wire format: `[length: u32][raw bytes]`. The length prefix respects
|
||||||
|
/// `endian`. Returns a `&'a [u8]` slice that borrows from the input buffer.
|
||||||
|
pub fn read_bytes<'a>(
|
||||||
|
buffer: &'a [u8],
|
||||||
|
offset: usize,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<&'a [u8], TypedefError> {
|
||||||
|
let len_bytes: [u8; U32_SIZE] = read_array(buffer, offset, field_path)?;
|
||||||
|
let len = u32_from(len_bytes, endian) as usize;
|
||||||
|
let data_start = offset.checked_add(U32_SIZE).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("offset {offset} + {U32_SIZE} overflows usize"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let data_end = data_start.checked_add(len).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("data_start {data_start} + length {len} overflows usize"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
check_bounds(buffer.len(), data_start, data_end, field_path)?;
|
||||||
|
Ok(&buffer[data_start..data_end])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a length-prefixed UTF-8 string into `buffer` at `offset`.
|
||||||
|
///
|
||||||
|
/// Wire format: `[length: u32][UTF-8 bytes]`. The length prefix respects
|
||||||
|
/// `endian`. Returns the total number of bytes written
|
||||||
|
/// (`4 + value.len()`) so the caller can advance the cursor.
|
||||||
|
pub fn write_string(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: &str,
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<usize, TypedefError> {
|
||||||
|
write_bytes(buffer, offset, value.as_bytes(), field_path, endian)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write length-prefixed raw bytes into `buffer` at `offset`.
|
||||||
|
///
|
||||||
|
/// Wire format: `[length: u32][raw bytes]`. The length prefix respects
|
||||||
|
/// `endian`. Returns the total number of bytes written (`4 + value.len()`).
|
||||||
|
pub fn write_bytes(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
offset: usize,
|
||||||
|
value: &[u8],
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<usize, TypedefError> {
|
||||||
|
let data_len = value.len();
|
||||||
|
let total = U32_SIZE.checked_add(data_len).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("prefix {U32_SIZE} + data length {data_len} overflows usize"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let end = offset.checked_add(total).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("offset {offset} + total {total} overflows usize"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
check_bounds(buffer.len(), offset, end, field_path)?;
|
||||||
|
write_array(buffer, offset, u32_to(data_len as u32, endian), field_path)?;
|
||||||
|
let data_start = offset + U32_SIZE;
|
||||||
|
let dest = buffer.get_mut(data_start..end).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("mutable data slice [{data_start}..{end}) unavailable"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
dest.copy_from_slice(value);
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Variable-length read (offset indirection)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Read an offset-indirect string.
|
||||||
|
///
|
||||||
|
/// The 8-byte struct at `buffer[offset..offset+8]` is
|
||||||
|
/// `{ data_offset: u32, data_length: u32 }` (endian-aware). The actual UTF-8
|
||||||
|
/// bytes live in `data_region[data_offset..data_offset+data_length]`. Returns
|
||||||
|
/// a `&'a str` borrowing from `data_region`. Invalid UTF-8 produces
|
||||||
|
/// [`TypedefError::Access`].
|
||||||
|
pub fn read_string_indirect<'a>(
|
||||||
|
buffer: &'a [u8],
|
||||||
|
offset: usize,
|
||||||
|
data_region: &'a [u8],
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<&'a str, TypedefError> {
|
||||||
|
let bytes = read_bytes_indirect(buffer, offset, data_region, field_path, endian)?;
|
||||||
|
std::str::from_utf8(bytes).map_err(|e| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("invalid UTF-8 in offset-indirect string: {e}"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read offset-indirect raw bytes.
|
||||||
|
///
|
||||||
|
/// The 8-byte struct at `buffer[offset..offset+8]` is
|
||||||
|
/// `{ data_offset: u32, data_length: u32 }` (endian-aware). Returns a
|
||||||
|
/// `&'a [u8]` slice of `data_region[data_offset..data_offset+data_length]`.
|
||||||
|
pub fn read_bytes_indirect<'a>(
|
||||||
|
buffer: &'a [u8],
|
||||||
|
offset: usize,
|
||||||
|
data_region: &'a [u8],
|
||||||
|
field_path: &str,
|
||||||
|
endian: Endian,
|
||||||
|
) -> Result<&'a [u8], TypedefError> {
|
||||||
|
let struct_end = offset.checked_add(8).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("offset {offset} + 8 overflows usize"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
check_bounds(buffer.len(), offset, struct_end, field_path)?;
|
||||||
|
let off_bytes: [u8; U32_SIZE] = buffer[offset..offset + U32_SIZE]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| access_err(field_path, "internal: try_into failed for data_offset"))?;
|
||||||
|
let len_bytes: [u8; U32_SIZE] = buffer[offset + U32_SIZE..offset + 8]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| access_err(field_path, "internal: try_into failed for data_length"))?;
|
||||||
|
let data_offset = u32_from(off_bytes, endian) as usize;
|
||||||
|
let data_length = u32_from(len_bytes, endian) as usize;
|
||||||
|
let data_end = data_offset.checked_add(data_length).ok_or_else(|| {
|
||||||
|
access_err(
|
||||||
|
field_path,
|
||||||
|
format!("data_offset {data_offset} + data_length {data_length} overflows usize"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
check_bounds(data_region.len(), data_offset, data_end, field_path)?;
|
||||||
|
Ok(&data_region[data_offset..data_end])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const LE: Endian = Endian::Little;
|
||||||
|
const BE: Endian = Endian::Big;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_u8_round_trip() {
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
write_u8(&mut buf, 0, 0xAB, "f").unwrap();
|
||||||
|
assert_eq!(read_u8(&buf, 0, "f").unwrap(), 0xAB);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_i8_round_trip() {
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
write_i8(&mut buf, 0, -42, "f").unwrap();
|
||||||
|
assert_eq!(read_i8(&buf, 0, "f").unwrap(), -42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_u16_endianness() {
|
||||||
|
let mut buf = [0u8; 2];
|
||||||
|
write_u16(&mut buf, 0, 0x1234, "f", LE).unwrap();
|
||||||
|
assert_eq!(buf, [0x34, 0x12]);
|
||||||
|
assert_eq!(read_u16(&buf, 0, "f", LE).unwrap(), 0x1234);
|
||||||
|
|
||||||
|
write_u16(&mut buf, 0, 0x1234, "f", BE).unwrap();
|
||||||
|
assert_eq!(buf, [0x12, 0x34]);
|
||||||
|
assert_eq!(read_u16(&buf, 0, "f", BE).unwrap(), 0x1234);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_i16_endianness() {
|
||||||
|
let mut buf = [0u8; 2];
|
||||||
|
write_i16(&mut buf, 0, -1, "f", LE).unwrap();
|
||||||
|
assert_eq!(buf, [0xFF, 0xFF]);
|
||||||
|
assert_eq!(read_i16(&buf, 0, "f", LE).unwrap(), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_u32_endianness() {
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
write_u32(&mut buf, 0, 0x01020304, "f", LE).unwrap();
|
||||||
|
assert_eq!(buf, [0x04, 0x03, 0x02, 0x01]);
|
||||||
|
assert_eq!(read_u32(&buf, 0, "f", LE).unwrap(), 0x01020304);
|
||||||
|
|
||||||
|
write_u32(&mut buf, 0, 0x01020304, "f", BE).unwrap();
|
||||||
|
assert_eq!(buf, [0x01, 0x02, 0x03, 0x04]);
|
||||||
|
assert_eq!(read_u32(&buf, 0, "f", BE).unwrap(), 0x01020304);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_i32_endianness() {
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
write_i32(&mut buf, 0, i32::MIN, "f", BE).unwrap();
|
||||||
|
assert_eq!(read_i32(&buf, 0, "f", BE).unwrap(), i32::MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_u64_endianness() {
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
write_u64(&mut buf, 0, 0x0102030405060708, "f", LE).unwrap();
|
||||||
|
assert_eq!(buf, [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]);
|
||||||
|
assert_eq!(read_u64(&buf, 0, "f", LE).unwrap(), 0x0102030405060708);
|
||||||
|
|
||||||
|
write_u64(&mut buf, 0, 0x0102030405060708, "f", BE).unwrap();
|
||||||
|
assert_eq!(buf, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
|
||||||
|
assert_eq!(read_u64(&buf, 0, "f", BE).unwrap(), 0x0102030405060708);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_f32_round_trip() {
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
let value: f32 = std::f32::consts::PI;
|
||||||
|
write_f32(&mut buf, 0, value, "f", LE).unwrap();
|
||||||
|
let read = read_f32(&buf, 0, "f", LE).unwrap();
|
||||||
|
assert!((read - value).abs() < 1e-6, "le mismatch: {read} vs {value}");
|
||||||
|
|
||||||
|
write_f32(&mut buf, 0, value, "f", BE).unwrap();
|
||||||
|
let read = read_f32(&buf, 0, "f", BE).unwrap();
|
||||||
|
assert!((read - value).abs() < 1e-6, "be mismatch: {read} vs {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_f64_round_trip() {
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
let value: f64 = std::f64::consts::PI;
|
||||||
|
write_f64(&mut buf, 0, value, "f", LE).unwrap();
|
||||||
|
assert_eq!(read_f64(&buf, 0, "f", LE).unwrap(), value);
|
||||||
|
|
||||||
|
write_f64(&mut buf, 0, value, "f", BE).unwrap();
|
||||||
|
assert_eq!(read_f64(&buf, 0, "f", BE).unwrap(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_bool_round_trip() {
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
write_bool(&mut buf, 0, false, "f").unwrap();
|
||||||
|
assert_eq!(buf[0], 0x00);
|
||||||
|
assert!(!read_bool(&buf, 0, "f").unwrap());
|
||||||
|
|
||||||
|
write_bool(&mut buf, 0, true, "f").unwrap();
|
||||||
|
assert_eq!(buf[0], 0x01);
|
||||||
|
assert!(read_bool(&buf, 0, "f").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_bool_rejects_invalid_byte() {
|
||||||
|
let buf = [0x02u8];
|
||||||
|
let err = read_bool(&buf, 0, "f").unwrap_err();
|
||||||
|
match err {
|
||||||
|
TypedefError::Access { field_path, reason } => {
|
||||||
|
assert_eq!(field_path, "f");
|
||||||
|
assert!(reason.contains("0x02"), "reason: {reason}");
|
||||||
|
assert!(reason.contains("offset 0"), "reason: {reason}");
|
||||||
|
}
|
||||||
|
other => panic!("expected Access, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_write_enum_round_trip() {
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
write_enum(&mut buf, 0, 7, "f", LE).unwrap();
|
||||||
|
assert_eq!(read_enum(&buf, 0, "f", LE).unwrap(), 7);
|
||||||
|
|
||||||
|
write_enum(&mut buf, 0, 7, "f", BE).unwrap();
|
||||||
|
assert_eq!(read_enum(&buf, 0, "f", BE).unwrap(), 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bounds_failure_returns_access_error() {
|
||||||
|
let buf = [0u8; 2];
|
||||||
|
let err = read_u32(&buf, 0, "header.id", LE).unwrap_err();
|
||||||
|
match err {
|
||||||
|
TypedefError::Access { field_path, reason } => {
|
||||||
|
assert_eq!(field_path, "header.id");
|
||||||
|
assert!(reason.contains("bounds"), "reason: {reason}");
|
||||||
|
}
|
||||||
|
other => panic!("expected Access, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_bounds_failure_returns_access_error() {
|
||||||
|
let mut buf = [0u8; 2];
|
||||||
|
let err = write_u32(&mut buf, 0, 1, "header.id", LE).unwrap_err();
|
||||||
|
assert!(matches!(err, TypedefError::Access { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_string_round_trip_and_zero_copy() {
|
||||||
|
let mut buf = vec![0u8; 32];
|
||||||
|
let written = write_string(&mut buf, 0, "hello", "name", LE).unwrap();
|
||||||
|
assert_eq!(written, 4 + 5);
|
||||||
|
let s = read_string(&buf, 0, "name", LE).unwrap();
|
||||||
|
assert_eq!(s, "hello");
|
||||||
|
assert!(std::ptr::eq(s.as_ptr(), buf.as_ptr().wrapping_add(4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_string_be_length_prefix() {
|
||||||
|
let mut buf = vec![0u8; 16];
|
||||||
|
write_string(&mut buf, 0, "abc", "name", BE).unwrap();
|
||||||
|
assert_eq!(buf[0..4], [0x00, 0x00, 0x00, 0x03]);
|
||||||
|
assert_eq!(read_string(&buf, 0, "name", BE).unwrap(), "abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_bytes_round_trip_and_zero_copy() {
|
||||||
|
let mut buf = vec![0u8; 32];
|
||||||
|
let payload = [0xAA, 0xBB, 0xCC, 0xDD];
|
||||||
|
let written = write_bytes(&mut buf, 0, &payload, "data", LE).unwrap();
|
||||||
|
assert_eq!(written, 4 + 4);
|
||||||
|
let bytes = read_bytes(&buf, 0, "data", LE).unwrap();
|
||||||
|
assert_eq!(bytes, &payload[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_string_invalid_utf8() {
|
||||||
|
let mut buf = vec![0u8; 16];
|
||||||
|
write_bytes(&mut buf, 0, &[0xFF, 0xFE, 0xFD], "name", LE).unwrap();
|
||||||
|
let err = read_string(&buf, 0, "name", LE).unwrap_err();
|
||||||
|
assert!(matches!(err, TypedefError::Access { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_string_bounds_failure_on_prefix() {
|
||||||
|
let buf = [0u8; 2];
|
||||||
|
let err = read_string(&buf, 0, "name", LE).unwrap_err();
|
||||||
|
assert!(matches!(err, TypedefError::Access { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_string_bounds_failure_on_data() {
|
||||||
|
let mut buf = vec![0u8; 6];
|
||||||
|
let _ = write_bytes(&mut buf, 0, &[0x00; 32], "name", LE);
|
||||||
|
let len_bytes = (100u32).to_le_bytes();
|
||||||
|
buf[0..4].copy_from_slice(&len_bytes);
|
||||||
|
let err = read_string(&buf, 0, "name", LE).unwrap_err();
|
||||||
|
assert!(matches!(err, TypedefError::Access { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_string_bounds_failure() {
|
||||||
|
let mut buf = vec![0u8; 4];
|
||||||
|
let err = write_string(&mut buf, 0, "hello", "name", LE).unwrap_err();
|
||||||
|
assert!(matches!(err, TypedefError::Access { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_string_indirect_round_trip() {
|
||||||
|
let data_region = b"the quick brown fox";
|
||||||
|
let mut index = [0u8; 8];
|
||||||
|
write_u32(&mut index, 0, 4, "idx.off", LE).unwrap();
|
||||||
|
write_u32(&mut index, 4, 11, "idx.len", LE).unwrap();
|
||||||
|
let s = read_string_indirect(&index, 0, data_region, "msg", LE).unwrap();
|
||||||
|
assert_eq!(s, "quick brown");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_bytes_indirect_round_trip() {
|
||||||
|
let data_region: &[u8] = b"HEADERbody-payloadTAIL";
|
||||||
|
let mut index = [0u8; 8];
|
||||||
|
write_u32(&mut index, 0, 6, "idx.off", BE).unwrap();
|
||||||
|
write_u32(&mut index, 4, 12, "idx.len", BE).unwrap();
|
||||||
|
let bytes = read_bytes_indirect(&index, 0, data_region, "blob", BE).unwrap();
|
||||||
|
assert_eq!(bytes, b"body-payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_bytes_indirect_bounds_failure_on_index() {
|
||||||
|
let buf = [0u8; 4];
|
||||||
|
let data_region = b"anything";
|
||||||
|
let err = read_bytes_indirect(&buf, 0, data_region, "blob", LE).unwrap_err();
|
||||||
|
assert!(matches!(err, TypedefError::Access { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_bytes_indirect_bounds_failure_on_data_region() {
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
write_u32(&mut buf, 0, 100, "idx.off", LE).unwrap();
|
||||||
|
write_u32(&mut buf, 4, 10, "idx.len", LE).unwrap();
|
||||||
|
let data_region = b"too short";
|
||||||
|
let err = read_bytes_indirect(&buf, 0, data_region, "blob", LE).unwrap_err();
|
||||||
|
assert!(matches!(err, TypedefError::Access { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_at_nonzero_offset() {
|
||||||
|
let mut buf = vec![0u8; 16];
|
||||||
|
write_u32(&mut buf, 8, 0xDEADBEEF, "header.id", BE).unwrap();
|
||||||
|
assert_eq!(read_u32(&buf, 8, "header.id", BE).unwrap(), 0xDEADBEEF);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_bytes_zero_length() {
|
||||||
|
let mut buf = vec![0u8; 8];
|
||||||
|
let written = write_bytes(&mut buf, 0, &[], "data", LE).unwrap();
|
||||||
|
assert_eq!(written, 4);
|
||||||
|
assert_eq!(buf[0..4], [0, 0, 0, 0]);
|
||||||
|
let bytes = read_bytes(&buf, 0, "data", LE).unwrap();
|
||||||
|
assert!(bytes.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: typedef/data-access
|
id: typedef/data-access
|
||||||
name: Implement primitive read/write functions for all 17 TypeDef kinds with endianness support
|
name: Implement primitive read/write functions for all 17 TypeDef kinds with endianness support
|
||||||
status: pending
|
status: completed
|
||||||
depends_on: [typedef/schema-types, typedef/error-type]
|
depends_on: [typedef/schema-types, typedef/error-type]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
risk: medium
|
risk: medium
|
||||||
@@ -171,4 +171,4 @@ pub fn read_bytes_indirect<'a>(
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
> To be filled on completion
|
Implemented the full data access layer in `crates/alknet-typedef/src/data_access.rs`: 11 fixed-size read functions, 11 fixed-size write functions, 2 inline length-prefixed variable-length read functions (`read_string`, `read_bytes`), 2 inline write functions (`write_string`, `write_bytes`), and 2 offset-indirect read functions (`read_string_indirect`, `read_bytes_indirect`). All multi-byte types respect the `Endian` parameter; all functions perform bounds checks and return `TypedefError::Access` with the field path on failure; `read_bool` rejects bytes other than `0x00`/`0x01`; `read_string`/`read_string_indirect` validate UTF-8. Variable-length read functions return zero-copy slices borrowing from the input buffer. No `unwrap()`/`expect()` is used on fallible paths — a private `read_array`/`write_array` helper pair propagates `try_into` failures as `TypedefError::Access`. The module ships with 27 unit tests covering round-trips, endianness, bounds failures, and zero-copy semantics; all pass under `cargo test -p alknet-typedef data_access` with `cargo clippy -- -D warnings` clean.
|
||||||
Reference in new issue
Block a user