From d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Mon, 22 Jun 2026 13:12:27 +0400 Subject: feat: implement FParkan architecture foundation Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation. --- crates/fparkan-binary/src/lib.rs | 308 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 crates/fparkan-binary/src/lib.rs (limited to 'crates/fparkan-binary/src/lib.rs') diff --git a/crates/fparkan-binary/src/lib.rs b/crates/fparkan-binary/src/lib.rs new file mode 100644 index 0000000..ef5a0e4 --- /dev/null +++ b/crates/fparkan-binary/src/lib.rs @@ -0,0 +1,308 @@ +#![forbid(unsafe_code)] +//! Bounded little-endian binary cursor and checked layout helpers. + +use std::fmt; + +/// Parser limits shared by binary formats. +#[derive(Clone, Copy, Debug)] +pub struct Limits { + /// Maximum file bytes. + pub max_file_bytes: u64, + /// Maximum entries. + pub max_entries: u32, + /// Maximum string bytes. + pub max_string_bytes: u32, + /// Maximum array items. + pub max_array_items: u32, + /// Maximum recursion depth. + pub max_recursion_depth: u16, + /// Maximum decoded bytes. + pub max_decoded_bytes: u64, +} + +impl Default for Limits { + fn default() -> Self { + Self { + max_file_bytes: 256 * 1024 * 1024, + max_entries: 1_000_000, + max_string_bytes: 64 * 1024, + max_array_items: 1_000_000, + max_recursion_depth: 64, + max_decoded_bytes: 512 * 1024 * 1024, + } + } +} + +/// Decode error. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DecodeError { + /// Input ended before requested bytes. + UnexpectedEof { + /// Offset where read was attempted. + offset: u64, + /// Required byte count. + needed: u64, + /// Remaining byte count. + remaining: u64, + }, + /// Arithmetic overflow. + IntegerOverflow, + /// Count exceeds limit. + LimitExceeded { + /// Declared count. + count: u64, + /// Configured limit. + limit: u64, + }, + /// Cursor did not end at EOF. + TrailingBytes { + /// Offset where EOF was expected. + offset: u64, + /// Remaining byte count. + remaining: u64, + }, + /// Invalid data. + Invalid(&'static str), +} + +impl fmt::Display for DecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnexpectedEof { + offset, + needed, + remaining, + } => write!( + f, + "unexpected EOF at {offset}: need {needed}, have {remaining}" + ), + Self::IntegerOverflow => write!(f, "integer overflow"), + Self::LimitExceeded { count, limit } => { + write!(f, "count {count} exceeds limit {limit}") + } + Self::TrailingBytes { offset, remaining } => { + write!(f, "trailing bytes at {offset}: {remaining}") + } + Self::Invalid(reason) => write!(f, "invalid data: {reason}"), + } + } +} + +impl std::error::Error for DecodeError {} + +/// Cursor checkpoint. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Checkpoint(pub u64); + +/// Bounded cursor. +#[derive(Clone, Debug)] +pub struct Cursor<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Cursor<'a> { + /// Creates a cursor. + #[must_use] + pub fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + /// Current offset. + #[must_use] + pub fn offset(&self) -> u64 { + self.offset as u64 + } + + /// Remaining bytes. + #[must_use] + pub fn remaining(&self) -> usize { + self.bytes.len().saturating_sub(self.offset) + } + + /// Creates a checkpoint. + #[must_use] + pub fn checkpoint(&self) -> Checkpoint { + Checkpoint(self.offset()) + } + + /// Reads exact bytes. + /// + /// # Errors + /// + /// Returns [`DecodeError::IntegerOverflow`] if the requested end offset + /// overflows, or [`DecodeError::UnexpectedEof`] if there are not enough + /// bytes remaining. + pub fn read_exact(&mut self, len: usize) -> Result<&'a [u8], DecodeError> { + let end = self + .offset + .checked_add(len) + .ok_or(DecodeError::IntegerOverflow)?; + if end > self.bytes.len() { + return Err(DecodeError::UnexpectedEof { + offset: self.offset(), + needed: len as u64, + remaining: self.remaining() as u64, + }); + } + let out = &self.bytes[self.offset..end]; + self.offset = end; + Ok(out) + } + + /// Reads a little-endian u16. + /// + /// # Errors + /// + /// Returns [`DecodeError`] if two bytes cannot be read. + pub fn read_u16_le(&mut self) -> Result { + let b = self.read_exact(2)?; + Ok(u16::from_le_bytes([b[0], b[1]])) + } + + /// Reads a little-endian u32. + /// + /// # Errors + /// + /// Returns [`DecodeError`] if four bytes cannot be read. + pub fn read_u32_le(&mut self) -> Result { + let b = self.read_exact(4)?; + Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) + } + + /// Reads a little-endian i32. + /// + /// # Errors + /// + /// Returns [`DecodeError`] if four bytes cannot be read. + pub fn read_i32_le(&mut self) -> Result { + let b = self.read_exact(4)?; + Ok(i32::from_le_bytes([b[0], b[1], b[2], b[3]])) + } + + /// Reads a little-endian f32. + /// + /// # Errors + /// + /// Returns [`DecodeError`] if four bytes cannot be read. + pub fn read_f32_le(&mut self) -> Result { + Ok(f32::from_bits(self.read_u32_le()?)) + } + + /// Requires exact EOF. + /// + /// # Errors + /// + /// Returns [`DecodeError::TrailingBytes`] when unread bytes remain. + pub fn require_eof(&self) -> Result<(), DecodeError> { + if self.remaining() == 0 { + Ok(()) + } else { + Err(DecodeError::TrailingBytes { + offset: self.offset(), + remaining: self.remaining() as u64, + }) + } + } +} + +/// Validates `count * stride <= remaining` and returns bytes as usize. +/// +/// # Errors +/// +/// Returns [`DecodeError::IntegerOverflow`] on arithmetic or conversion +/// overflow, or [`DecodeError::UnexpectedEof`] when the declared byte count is +/// larger than the remaining bounded input. +pub fn checked_count_bytes(count: u64, stride: u64, remaining: u64) -> Result { + let bytes = count + .checked_mul(stride) + .ok_or(DecodeError::IntegerOverflow)?; + if bytes > remaining { + return Err(DecodeError::UnexpectedEof { + offset: 0, + needed: bytes, + remaining, + }); + } + usize::try_from(bytes).map_err(|_| DecodeError::IntegerOverflow) +} + +/// Validates a declared allocation size before constructing the allocation. +/// +/// # Errors +/// +/// Returns [`DecodeError::LimitExceeded`] when `declared` is larger than +/// `limit`, or [`DecodeError::IntegerOverflow`] when the accepted size cannot +/// be represented by the host `usize`. +pub fn checked_allocation_len(declared: u64, limit: u64) -> Result { + if declared > limit { + return Err(DecodeError::LimitExceeded { + count: declared, + limit, + }); + } + usize::try_from(declared).map_err(|_| DecodeError::IntegerOverflow) +} + +/// Reads length-prefixed bytes. +/// +/// # Errors +/// +/// Returns [`DecodeError`] if the length cannot be read, exceeds `max`, or the +/// declared payload is truncated. +pub fn read_lp_bytes(cursor: &mut Cursor<'_>, max: u32) -> Result, DecodeError> { + let len = cursor.read_u32_le()?; + if len > max { + return Err(DecodeError::LimitExceeded { + count: u64::from(len), + limit: u64::from(max), + }); + } + let len = checked_allocation_len(u64::from(len), u64::from(max))?; + Ok(cursor.read_exact(len)?.to_vec()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_count_stride_overflow() { + assert_eq!( + checked_count_bytes(u64::MAX, 2, u64::MAX), + Err(DecodeError::IntegerOverflow) + ); + } + + #[test] + fn exact_eof_reports_trailing() { + let mut cursor = Cursor::new(&[1, 2]); + assert_eq!(cursor.read_exact(1).expect("byte"), &[1]); + assert!(matches!( + cursor.require_eof(), + Err(DecodeError::TrailingBytes { .. }) + )); + } + + #[test] + fn rejects_oversized_declared_allocation_before_read() { + assert_eq!( + checked_allocation_len(1025, 1024), + Err(DecodeError::LimitExceeded { + count: 1025, + limit: 1024 + }) + ); + + let bytes = 2048u32.to_le_bytes(); + let mut cursor = Cursor::new(&bytes); + assert_eq!( + read_lp_bytes(&mut cursor, 1024), + Err(DecodeError::LimitExceeded { + count: 2048, + limit: 1024 + }) + ); + assert_eq!(cursor.offset(), 4); + } +} -- cgit v1.2.3