aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-binary/src/lib.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 12:12:27 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 12:13:32 +0300
commitd0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch)
treea0bd35c3940be62a5b5de1acc2366af377ffd181 /crates/fparkan-binary/src/lib.rs
parent7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff)
downloadfparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz
fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip
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.
Diffstat (limited to 'crates/fparkan-binary/src/lib.rs')
-rw-r--r--crates/fparkan-binary/src/lib.rs308
1 files changed, 308 insertions, 0 deletions
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<u16, DecodeError> {
+ 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<u32, DecodeError> {
+ 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<i32, DecodeError> {
+ 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<f32, DecodeError> {
+ 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<usize, DecodeError> {
+ 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<usize, DecodeError> {
+ 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<Vec<u8>, 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);
+ }
+}