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
|
||||
//! 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
|
||||
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]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
@@ -171,4 +171,4 @@ pub fn read_bytes_indirect<'a>(
|
||||
|
||||
## 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