SMB Server Phase 2: VFS backend build fix + integration test
- Add VfsFile: Send supertrait for Mutex compatibility - Fix SmbServerCommand: struct → Subcommand enum with Start variant - Fix tracing_subscriber::init() → try_init() to avoid panic when logger already initialized - Fix CLI subcommand name: smb-server → smb-start (flatten naming) - Add #[command(name = "smb-start")] for CLI disambiguation - Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs) - Remove unused VfsFile imports (webdav.rs, scp_handler.rs) - Integration test: Docker smbclient verified (list, upload, read)
This commit is contained in:
45
vendor/smb2/src/pack/CLAUDE.md
vendored
Normal file
45
vendor/smb2/src/pack/CLAUDE.md
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Pack -- binary serialization primitives
|
||||
|
||||
Cursor-based binary reader/writer for SMB2 wire format. Hand-rolled, no proc macros.
|
||||
|
||||
## Key files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `mod.rs` | `ReadCursor`, `WriteCursor`, `Pack`/`Unpack` traits, primitive read/write methods |
|
||||
| `guid.rs` | GUID pack/unpack with mixed-endian layout |
|
||||
| `filetime.rs` | Windows FILETIME (100ns ticks since 1601-01-01) to/from `SystemTime` |
|
||||
|
||||
## Core types
|
||||
|
||||
- **`ReadCursor<'a>`**: Reads from `&[u8]` with position tracking. Returns `Error` on buffer overrun (no panics). All reads are little-endian.
|
||||
- **`WriteCursor`**: Writes into a growable `Vec<u8>`. Supports backpatching (`set_u16_le_at`, `set_u32_le_at`) for length fields written before their values are known. `align_to(n)` pads with zeros to n-byte boundary.
|
||||
- **`Pack` trait**: `fn pack(&self, cursor: &mut WriteCursor)` -- serialize to binary.
|
||||
- **`Unpack` trait**: `fn unpack(cursor: &mut ReadCursor) -> Result<Self>` -- deserialize from binary.
|
||||
|
||||
## GUID mixed-endian layout
|
||||
|
||||
Windows GUIDs have a mixed-endian wire format:
|
||||
- `data1` (u32): little-endian
|
||||
- `data2` (u16): little-endian
|
||||
- `data3` (u16): little-endian
|
||||
- `data4` ([u8; 8]): raw bytes (no endian conversion)
|
||||
|
||||
This matches the COM/DCOM convention. Not the same as RFC 4122 UUID byte order.
|
||||
|
||||
## FileTime conversion
|
||||
|
||||
Windows FILETIME: 100-nanosecond intervals since 1601-01-01 00:00:00 UTC.
|
||||
Unix epoch: 1970-01-01 00:00:00 UTC.
|
||||
Offset: 11,644,473,600 seconds (116,444,736,000,000,000 ticks).
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Hand-rolled instead of proc macros**: Full control over wire format details (offsets, alignment, backpatching). Easier to debug. No build-time dependency.
|
||||
- **`MAX_UNPACK_BUFFER` (16 MB)**: `read_bytes_bounded` refuses allocations larger than 16 MB. Prevents OOM from malicious packets claiming huge lengths.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Everything is little-endian**: Except TCP framing (see transport module). ReadCursor/WriteCursor only do LE.
|
||||
- **UTF-16LE byte length must be even**: `read_utf16_le` returns an error on odd byte counts.
|
||||
- **Backpatching requires placeholder**: Write a zero first, then `set_u32_le_at` to overwrite once the real value is known. Common pattern for length-prefixed fields.
|
||||
175
vendor/smb2/src/pack/filetime.rs
vendored
Normal file
175
vendor/smb2/src/pack/filetime.rs
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
//! Windows FILETIME type for SMB2.
|
||||
//!
|
||||
//! A FILETIME is a 64-bit value representing 100-nanosecond intervals
|
||||
//! since 1601-01-01 00:00:00 UTC.
|
||||
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use super::{Pack, ReadCursor, Unpack, WriteCursor};
|
||||
use crate::error::Result;
|
||||
|
||||
/// Difference between the Windows epoch (1601-01-01) and Unix epoch (1970-01-01)
|
||||
/// in 100-nanosecond intervals.
|
||||
const EPOCH_DIFF_100NS: u64 = 116_444_736_000_000_000;
|
||||
|
||||
/// Windows FILETIME: 100-nanosecond intervals since 1601-01-01 00:00:00 UTC.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct FileTime(
|
||||
/// The raw 100-nanosecond tick count.
|
||||
pub u64,
|
||||
);
|
||||
|
||||
impl FileTime {
|
||||
/// A zero filetime, meaning "not set" or "unknown".
|
||||
pub const ZERO: Self = Self(0);
|
||||
|
||||
/// Convert a [`SystemTime`] to a `FileTime`.
|
||||
///
|
||||
/// Uses the Unix epoch offset (116,444,736,000,000,000 intervals of
|
||||
/// 100 ns) to translate between the two epoch origins.
|
||||
pub fn from_system_time(t: SystemTime) -> Self {
|
||||
match t.duration_since(UNIX_EPOCH) {
|
||||
Ok(dur) => {
|
||||
let intervals = dur.as_nanos() / 100;
|
||||
Self(intervals as u64 + EPOCH_DIFF_100NS)
|
||||
}
|
||||
Err(e) => {
|
||||
// Time is before Unix epoch. The duration tells us how far before.
|
||||
let before = e.duration();
|
||||
let intervals = before.as_nanos() / 100;
|
||||
// If the pre-Unix time is still after the Windows epoch, compute it.
|
||||
Self(EPOCH_DIFF_100NS.saturating_sub(intervals as u64))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this `FileTime` to a [`SystemTime`].
|
||||
///
|
||||
/// Returns `None` if the filetime represents a date before the Unix epoch,
|
||||
/// since [`SystemTime`] cannot represent dates before that.
|
||||
pub fn to_system_time(self) -> Option<SystemTime> {
|
||||
if self.0 < EPOCH_DIFF_100NS {
|
||||
return None;
|
||||
}
|
||||
let intervals_since_unix = self.0 - EPOCH_DIFF_100NS;
|
||||
let nanos = (intervals_since_unix as u128) * 100;
|
||||
let dur = Duration::new(
|
||||
(nanos / 1_000_000_000) as u64,
|
||||
(nanos % 1_000_000_000) as u32,
|
||||
);
|
||||
Some(UNIX_EPOCH + dur)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pack for FileTime {
|
||||
fn pack(&self, cursor: &mut WriteCursor) {
|
||||
cursor.write_u64_le(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Unpack for FileTime {
|
||||
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
|
||||
let val = cursor.read_u64_le()?;
|
||||
Ok(Self(val))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zero_filetime() {
|
||||
assert_eq!(FileTime::ZERO, FileTime(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_zero() {
|
||||
let mut w = WriteCursor::new();
|
||||
FileTime::ZERO.pack(&mut w);
|
||||
assert_eq!(w.as_bytes(), &[0u8; 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unpack_zero() {
|
||||
let bytes = [0u8; 8];
|
||||
let mut r = ReadCursor::new(&bytes);
|
||||
let ft = FileTime::unpack(&mut r).unwrap();
|
||||
assert_eq!(ft, FileTime::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value_2024_01_01() {
|
||||
// 2024-01-01 00:00:00 UTC = FileTime(133_485_408_000_000_000)
|
||||
// (Unix timestamp 1_704_067_200 * 10_000_000 + 116_444_736_000_000_000)
|
||||
let expected_raw: u64 = 133_485_408_000_000_000;
|
||||
let ft = FileTime(expected_raw);
|
||||
|
||||
// Pack and verify roundtrip
|
||||
let mut w = WriteCursor::new();
|
||||
ft.pack(&mut w);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
let unpacked = FileTime::unpack(&mut r).unwrap();
|
||||
assert_eq!(unpacked, ft);
|
||||
|
||||
// Verify SystemTime conversion
|
||||
// 2024-01-01 00:00:00 UTC = Unix timestamp 1_704_067_200
|
||||
let st = ft.to_system_time().unwrap();
|
||||
let unix_dur = st.duration_since(UNIX_EPOCH).unwrap();
|
||||
assert_eq!(unix_dur.as_secs(), 1_704_067_200);
|
||||
assert_eq!(unix_dur.subsec_nanos(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_system_time_roundtrip() {
|
||||
// Use a known Unix timestamp: 2024-01-01 00:00:00 UTC
|
||||
let unix_secs = 1_704_067_200u64;
|
||||
let st = UNIX_EPOCH + Duration::from_secs(unix_secs);
|
||||
let ft = FileTime::from_system_time(st);
|
||||
assert_eq!(ft.0, 133_485_408_000_000_000);
|
||||
|
||||
let st2 = ft.to_system_time().unwrap();
|
||||
let dur = st2.duration_since(UNIX_EPOCH).unwrap();
|
||||
assert_eq!(dur.as_secs(), unix_secs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_unix_epoch_returns_none() {
|
||||
// A FILETIME value that represents a date before 1970-01-01
|
||||
let ft = FileTime(EPOCH_DIFF_100NS - 1);
|
||||
assert!(ft.to_system_time().is_none());
|
||||
|
||||
// Zero is also before Unix epoch
|
||||
assert!(FileTime::ZERO.to_system_time().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unix_epoch_exactly() {
|
||||
let ft = FileTime(EPOCH_DIFF_100NS);
|
||||
let st = ft.to_system_time().unwrap();
|
||||
assert_eq!(st, UNIX_EPOCH);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_system_time_unix_epoch() {
|
||||
let ft = FileTime::from_system_time(UNIX_EPOCH);
|
||||
assert_eq!(ft.0, EPOCH_DIFF_100NS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_unpack_roundtrip() {
|
||||
let ft = FileTime(133_476_576_000_000_000);
|
||||
let mut w = WriteCursor::new();
|
||||
ft.pack(&mut w);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
let unpacked = FileTime::unpack(&mut r).unwrap();
|
||||
assert_eq!(unpacked, ft);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unpack_insufficient_bytes() {
|
||||
let bytes = [0u8; 4]; // need 8
|
||||
let mut r = ReadCursor::new(&bytes);
|
||||
assert!(FileTime::unpack(&mut r).is_err());
|
||||
}
|
||||
}
|
||||
176
vendor/smb2/src/pack/guid.rs
vendored
Normal file
176
vendor/smb2/src/pack/guid.rs
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
//! GUID (Globally Unique Identifier) type for SMB2.
|
||||
//!
|
||||
//! GUIDs follow the mixed-endian layout defined in MS-DTYP section 2.3.4:
|
||||
//! - Bytes 0-3: `data1` (`u32`, little-endian)
|
||||
//! - Bytes 4-5: `data2` (`u16`, little-endian)
|
||||
//! - Bytes 6-7: `data3` (`u16`, little-endian)
|
||||
//! - Bytes 8-15: `data4` (8 raw bytes, big-endian order)
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use super::{Pack, ReadCursor, Unpack, WriteCursor};
|
||||
use crate::error::Result;
|
||||
|
||||
/// A 128-bit GUID in mixed-endian wire format (MS-DTYP 2.3.4).
|
||||
///
|
||||
/// With the `serde` feature on, the JSON form mirrors the in-memory
|
||||
/// field shape (`{data1, data2, data3, data4}`), **not** the wire byte
|
||||
/// order — the wire layout is mixed-endian and round-tripping it through
|
||||
/// JSON would just be confusing.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct Guid {
|
||||
/// First component (bytes 0-3, little-endian on wire).
|
||||
pub data1: u32,
|
||||
/// Second component (bytes 4-5, little-endian on wire).
|
||||
pub data2: u16,
|
||||
/// Third component (bytes 6-7, little-endian on wire).
|
||||
pub data3: u16,
|
||||
/// Fourth component (bytes 8-15, raw byte order on wire).
|
||||
pub data4: [u8; 8],
|
||||
}
|
||||
|
||||
impl Guid {
|
||||
/// The NULL GUID: `{00000000-0000-0000-0000-000000000000}`.
|
||||
pub const ZERO: Self = Self {
|
||||
data1: 0,
|
||||
data2: 0,
|
||||
data3: 0,
|
||||
data4: [0; 8],
|
||||
};
|
||||
}
|
||||
|
||||
impl Pack for Guid {
|
||||
fn pack(&self, cursor: &mut WriteCursor) {
|
||||
cursor.write_u32_le(self.data1);
|
||||
cursor.write_u16_le(self.data2);
|
||||
cursor.write_u16_le(self.data3);
|
||||
cursor.write_bytes(&self.data4);
|
||||
}
|
||||
}
|
||||
|
||||
impl Unpack for Guid {
|
||||
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
|
||||
let data1 = cursor.read_u32_le()?;
|
||||
let data2 = cursor.read_u16_le()?;
|
||||
let data3 = cursor.read_u16_le()?;
|
||||
let raw = cursor.read_bytes(8)?;
|
||||
let mut data4 = [0u8; 8];
|
||||
data4.copy_from_slice(raw);
|
||||
Ok(Self {
|
||||
data1,
|
||||
data2,
|
||||
data3,
|
||||
data4,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Guid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{{{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}}}",
|
||||
self.data1,
|
||||
self.data2,
|
||||
self.data3,
|
||||
self.data4[0],
|
||||
self.data4[1],
|
||||
self.data4[2],
|
||||
self.data4[3],
|
||||
self.data4[4],
|
||||
self.data4[5],
|
||||
self.data4[6],
|
||||
self.data4[7],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unpack_null_guid() {
|
||||
let bytes = [0u8; 16];
|
||||
let mut cursor = ReadCursor::new(&bytes);
|
||||
let guid = Guid::unpack(&mut cursor).unwrap();
|
||||
assert_eq!(guid, Guid::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_null_guid() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
Guid::ZERO.pack(&mut cursor);
|
||||
assert_eq!(cursor.as_bytes(), &[0u8; 16]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_known_guid() {
|
||||
let guid = Guid {
|
||||
data1: 0x6BA7B810,
|
||||
data2: 0x9DAD,
|
||||
data3: 0x11D1,
|
||||
data4: [0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8],
|
||||
};
|
||||
|
||||
let mut w = WriteCursor::new();
|
||||
guid.pack(&mut w);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
let unpacked = Guid::unpack(&mut r).unwrap();
|
||||
assert_eq!(unpacked, guid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_format() {
|
||||
let guid = Guid {
|
||||
data1: 0x6BA7B810,
|
||||
data2: 0x9DAD,
|
||||
data3: 0x11D1,
|
||||
data4: [0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8],
|
||||
};
|
||||
assert_eq!(guid.to_string(), "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_null_guid() {
|
||||
assert_eq!(
|
||||
Guid::ZERO.to_string(),
|
||||
"{00000000-0000-0000-0000-000000000000}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_endian_byte_ordering() {
|
||||
// Build a GUID with known values and verify the wire bytes directly.
|
||||
let guid = Guid {
|
||||
data1: 0x04030201,
|
||||
data2: 0x0605,
|
||||
data3: 0x0807,
|
||||
data4: [0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10],
|
||||
};
|
||||
|
||||
let mut w = WriteCursor::new();
|
||||
guid.pack(&mut w);
|
||||
let bytes = w.as_bytes();
|
||||
|
||||
// data1: u32 LE -> 01 02 03 04
|
||||
assert_eq!(&bytes[0..4], &[0x01, 0x02, 0x03, 0x04]);
|
||||
// data2: u16 LE -> 05 06
|
||||
assert_eq!(&bytes[4..6], &[0x05, 0x06]);
|
||||
// data3: u16 LE -> 07 08
|
||||
assert_eq!(&bytes[6..8], &[0x07, 0x08]);
|
||||
// data4: raw bytes -> 09 0A 0B 0C 0D 0E 0F 10
|
||||
assert_eq!(
|
||||
&bytes[8..16],
|
||||
&[0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unpack_insufficient_bytes() {
|
||||
let bytes = [0u8; 10]; // need 16
|
||||
let mut cursor = ReadCursor::new(&bytes);
|
||||
assert!(Guid::unpack(&mut cursor).is_err());
|
||||
}
|
||||
}
|
||||
649
vendor/smb2/src/pack/mod.rs
vendored
Normal file
649
vendor/smb2/src/pack/mod.rs
vendored
Normal file
@@ -0,0 +1,649 @@
|
||||
//! Binary serialization/deserialization primitives for SMB2.
|
||||
//!
|
||||
//! Provides [`ReadCursor`] and [`WriteCursor`] for reading and writing
|
||||
//! little-endian binary data, plus [`Pack`] and [`Unpack`] traits for
|
||||
//! structured types.
|
||||
//!
|
||||
//! Most users don't need this module directly -- use [`SmbClient`](crate::SmbClient)
|
||||
//! for high-level file operations.
|
||||
|
||||
pub mod filetime;
|
||||
pub mod guid;
|
||||
|
||||
pub use filetime::FileTime;
|
||||
pub use guid::Guid;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::Error;
|
||||
|
||||
/// Trait for types that can serialize themselves into binary format.
|
||||
pub trait Pack: Send + Sync {
|
||||
/// Write this value into the cursor.
|
||||
fn pack(&self, cursor: &mut WriteCursor);
|
||||
}
|
||||
|
||||
/// Trait for types that can deserialize themselves from binary format.
|
||||
pub trait Unpack: Sized {
|
||||
/// Read a value from the cursor, advancing its position.
|
||||
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ReadCursor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A cursor for reading little-endian binary data from a byte slice.
|
||||
///
|
||||
/// Tracks the current read position and returns errors on buffer overruns
|
||||
/// rather than panicking.
|
||||
pub struct ReadCursor<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> ReadCursor<'a> {
|
||||
/// Create a new read cursor starting at position 0.
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, pos: 0 }
|
||||
}
|
||||
|
||||
/// Read a single byte.
|
||||
pub fn read_u8(&mut self) -> Result<u8> {
|
||||
self.ensure(1)?;
|
||||
let val = self.data[self.pos];
|
||||
self.pos += 1;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Read a little-endian `u16`.
|
||||
pub fn read_u16_le(&mut self) -> Result<u16> {
|
||||
let bytes = self.read_array::<2>()?;
|
||||
Ok(u16::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Read a little-endian `u32`.
|
||||
pub fn read_u32_le(&mut self) -> Result<u32> {
|
||||
let bytes = self.read_array::<4>()?;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Read a little-endian `u64`.
|
||||
pub fn read_u64_le(&mut self) -> Result<u64> {
|
||||
let bytes = self.read_array::<8>()?;
|
||||
Ok(u64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Read a little-endian `u128`.
|
||||
pub fn read_u128_le(&mut self) -> Result<u128> {
|
||||
let bytes = self.read_array::<16>()?;
|
||||
Ok(u128::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Read exactly `n` bytes, returning a sub-slice.
|
||||
pub fn read_bytes(&mut self, n: usize) -> Result<&'a [u8]> {
|
||||
self.ensure(n)?;
|
||||
let slice = &self.data[self.pos..self.pos + n];
|
||||
self.pos += n;
|
||||
Ok(slice)
|
||||
}
|
||||
|
||||
/// Read `byte_len` bytes of UTF-16LE data and decode to a [`String`].
|
||||
///
|
||||
/// `byte_len` must be even (each code unit is 2 bytes).
|
||||
pub fn read_utf16_le(&mut self, byte_len: usize) -> Result<String> {
|
||||
if byte_len % 2 != 0 {
|
||||
return Err(Error::invalid_data(format!(
|
||||
"UTF-16LE byte length must be even, got {}",
|
||||
byte_len
|
||||
)));
|
||||
}
|
||||
let raw = self.read_bytes(byte_len)?;
|
||||
let code_units: Vec<u16> = raw
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||
.collect();
|
||||
String::from_utf16(&code_units)
|
||||
.map_err(|_| Error::invalid_data("invalid UTF-16LE encoding"))
|
||||
}
|
||||
|
||||
/// Skip `n` bytes without reading them.
|
||||
pub fn skip(&mut self, n: usize) -> Result<()> {
|
||||
self.ensure(n)?;
|
||||
self.pos += n;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the number of bytes remaining.
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.data.len() - self.pos
|
||||
}
|
||||
|
||||
/// Return the current byte position.
|
||||
pub fn position(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
/// Return `true` if no bytes remain.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.remaining() == 0
|
||||
}
|
||||
|
||||
/// Maximum buffer size we'll allocate from untrusted data (16 MB).
|
||||
pub const MAX_UNPACK_BUFFER: usize = 16 * 1024 * 1024;
|
||||
|
||||
/// Read `n` bytes, but refuse if `n` exceeds [`Self::MAX_UNPACK_BUFFER`].
|
||||
pub fn read_bytes_bounded(&mut self, n: usize) -> Result<&'a [u8]> {
|
||||
if n > Self::MAX_UNPACK_BUFFER {
|
||||
return Err(Error::invalid_data(format!(
|
||||
"buffer size {} exceeds maximum {} bytes",
|
||||
n,
|
||||
Self::MAX_UNPACK_BUFFER
|
||||
)));
|
||||
}
|
||||
self.read_bytes(n)
|
||||
}
|
||||
|
||||
// -- private helpers --
|
||||
|
||||
fn ensure(&self, n: usize) -> Result<()> {
|
||||
if self.remaining() < n {
|
||||
Err(Error::invalid_data(format!(
|
||||
"need {} bytes but only {} remain at offset {}",
|
||||
n,
|
||||
self.remaining(),
|
||||
self.pos
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn read_array<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||
self.ensure(N)?;
|
||||
let mut arr = [0u8; N];
|
||||
arr.copy_from_slice(&self.data[self.pos..self.pos + N]);
|
||||
self.pos += N;
|
||||
Ok(arr)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WriteCursor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A cursor for writing little-endian binary data into a growable buffer.
|
||||
pub struct WriteCursor {
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl WriteCursor {
|
||||
/// Create an empty write cursor.
|
||||
pub fn new() -> Self {
|
||||
Self { buf: Vec::new() }
|
||||
}
|
||||
|
||||
/// Create a write cursor with pre-allocated capacity.
|
||||
pub fn with_capacity(cap: usize) -> Self {
|
||||
Self {
|
||||
buf: Vec::with_capacity(cap),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a single byte.
|
||||
pub fn write_u8(&mut self, val: u8) {
|
||||
self.buf.push(val);
|
||||
}
|
||||
|
||||
/// Write a little-endian `u16`.
|
||||
pub fn write_u16_le(&mut self, val: u16) {
|
||||
self.buf.extend_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Write a little-endian `u32`.
|
||||
pub fn write_u32_le(&mut self, val: u32) {
|
||||
self.buf.extend_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Write a little-endian `u64`.
|
||||
pub fn write_u64_le(&mut self, val: u64) {
|
||||
self.buf.extend_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Write a little-endian `u128`.
|
||||
pub fn write_u128_le(&mut self, val: u128) {
|
||||
self.buf.extend_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Write a raw byte slice.
|
||||
pub fn write_bytes(&mut self, data: &[u8]) {
|
||||
self.buf.extend_from_slice(data);
|
||||
}
|
||||
|
||||
/// Encode a string as UTF-16LE and write the bytes.
|
||||
pub fn write_utf16_le(&mut self, s: &str) {
|
||||
for code_unit in s.encode_utf16() {
|
||||
self.buf.extend_from_slice(&code_unit.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `n` zero bytes.
|
||||
pub fn write_zeros(&mut self, n: usize) {
|
||||
self.buf.resize(self.buf.len() + n, 0);
|
||||
}
|
||||
|
||||
/// Pad with zero bytes until the position is a multiple of `alignment`.
|
||||
///
|
||||
/// Does nothing if `alignment` is 0 or 1, or if already aligned.
|
||||
pub fn align_to(&mut self, alignment: usize) {
|
||||
if alignment <= 1 {
|
||||
return;
|
||||
}
|
||||
let remainder = self.buf.len() % alignment;
|
||||
if remainder != 0 {
|
||||
self.write_zeros(alignment - remainder);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the current write position (number of bytes written so far).
|
||||
pub fn position(&self) -> usize {
|
||||
self.buf.len()
|
||||
}
|
||||
|
||||
/// Overwrite a `u16` at a previous position (little-endian).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `pos + 2 > self.position()`.
|
||||
pub fn set_u16_le_at(&mut self, pos: usize, val: u16) {
|
||||
self.buf[pos..pos + 2].copy_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Overwrite a `u32` at a previous position (little-endian).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `pos + 4 > self.position()`.
|
||||
pub fn set_u32_le_at(&mut self, pos: usize, val: u32) {
|
||||
self.buf[pos..pos + 4].copy_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
|
||||
/// Consume the cursor and return the underlying buffer.
|
||||
pub fn into_inner(self) -> Vec<u8> {
|
||||
self.buf
|
||||
}
|
||||
|
||||
/// Return a reference to the bytes written so far.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WriteCursor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
// -- ReadCursor tests --
|
||||
|
||||
#[test]
|
||||
fn read_u8_from_known_bytes() {
|
||||
let data = [0x42];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
assert_eq!(cursor.read_u8().unwrap(), 0x42);
|
||||
assert!(cursor.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_u16_le_from_known_bytes() {
|
||||
let data = [0x34, 0x12];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
assert_eq!(cursor.read_u16_le().unwrap(), 0x1234);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_u32_le_from_known_bytes() {
|
||||
let data = [0x78, 0x56, 0x34, 0x12];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
assert_eq!(cursor.read_u32_le().unwrap(), 0x12345678);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_u64_le_from_known_bytes() {
|
||||
let data = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
assert_eq!(cursor.read_u64_le().unwrap(), 0x0102030405060708);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_u128_le_from_known_bytes() {
|
||||
let mut data = [0u8; 16];
|
||||
data[0] = 0x01;
|
||||
data[15] = 0x80;
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
let val = cursor.read_u128_le().unwrap();
|
||||
assert_eq!(val, 0x80000000_00000000_00000000_00000001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_past_end_returns_error() {
|
||||
let data = [0x00];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
assert!(cursor.read_u16_le().is_err());
|
||||
|
||||
let empty: &[u8] = &[];
|
||||
let mut cursor = ReadCursor::new(empty);
|
||||
assert!(cursor.read_u8().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaining_and_position_track_correctly() {
|
||||
let data = [0x01, 0x02, 0x03, 0x04, 0x05];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
assert_eq!(cursor.position(), 0);
|
||||
assert_eq!(cursor.remaining(), 5);
|
||||
|
||||
cursor.read_u8().unwrap();
|
||||
assert_eq!(cursor.position(), 1);
|
||||
assert_eq!(cursor.remaining(), 4);
|
||||
|
||||
cursor.read_u16_le().unwrap();
|
||||
assert_eq!(cursor.position(), 3);
|
||||
assert_eq!(cursor.remaining(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_advances_position() {
|
||||
let data = [0x01, 0x02, 0x03, 0x04];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
cursor.skip(2).unwrap();
|
||||
assert_eq!(cursor.position(), 2);
|
||||
assert_eq!(cursor.read_u8().unwrap(), 0x03);
|
||||
|
||||
// Skip past end is error
|
||||
assert!(cursor.skip(10).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_bytes_returns_correct_slice() {
|
||||
let data = [0x0A, 0x0B, 0x0C, 0x0D];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
cursor.skip(1).unwrap();
|
||||
let slice = cursor.read_bytes(2).unwrap();
|
||||
assert_eq!(slice, &[0x0B, 0x0C]);
|
||||
assert_eq!(cursor.position(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_utf16_le_decodes_hello() {
|
||||
// "hello" in UTF-16LE
|
||||
let data = [0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F, 0x00];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
let s = cursor.read_utf16_le(10).unwrap();
|
||||
assert_eq!(s, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_utf16_le_odd_byte_len_is_error() {
|
||||
let data = [0x68, 0x00, 0x65];
|
||||
let mut cursor = ReadCursor::new(&data);
|
||||
assert!(cursor.read_utf16_le(3).is_err());
|
||||
}
|
||||
|
||||
// -- WriteCursor tests --
|
||||
|
||||
#[test]
|
||||
fn write_u8_produces_correct_byte() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u8(0xFF);
|
||||
assert_eq!(cursor.as_bytes(), &[0xFF]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_u16_le_produces_correct_bytes() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u16_le(0x1234);
|
||||
assert_eq!(cursor.as_bytes(), &[0x34, 0x12]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_u32_le_produces_correct_bytes() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u32_le(0x12345678);
|
||||
assert_eq!(cursor.as_bytes(), &[0x78, 0x56, 0x34, 0x12]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_u64_le_produces_correct_bytes() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u64_le(0x0102030405060708);
|
||||
assert_eq!(
|
||||
cursor.as_bytes(),
|
||||
&[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_u128_le_produces_correct_bytes() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u128_le(0x01);
|
||||
let bytes = cursor.as_bytes();
|
||||
assert_eq!(bytes.len(), 16);
|
||||
assert_eq!(bytes[0], 0x01);
|
||||
assert!(bytes[1..].iter().all(|&b| b == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn align_to_pads_correctly() {
|
||||
// From position 0 -> already aligned
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.align_to(8);
|
||||
assert_eq!(cursor.position(), 0);
|
||||
|
||||
// From position 3 -> pad to 8
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_bytes(&[0x01, 0x02, 0x03]);
|
||||
cursor.align_to(8);
|
||||
assert_eq!(cursor.position(), 8);
|
||||
// Padding bytes should be zeros
|
||||
assert_eq!(&cursor.as_bytes()[3..8], &[0, 0, 0, 0, 0]);
|
||||
|
||||
// From position 8 -> already aligned
|
||||
cursor.align_to(8);
|
||||
assert_eq!(cursor.position(), 8);
|
||||
|
||||
// From position 1 -> pad to 4
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u8(0xAA);
|
||||
cursor.align_to(4);
|
||||
assert_eq!(cursor.position(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_u32_le_at_backpatches_correctly() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u32_le(0); // placeholder
|
||||
cursor.write_u32_le(0xDEADBEEF);
|
||||
cursor.set_u32_le_at(0, 0x12345678);
|
||||
assert_eq!(
|
||||
cursor.as_bytes(),
|
||||
&[0x78, 0x56, 0x34, 0x12, 0xEF, 0xBE, 0xAD, 0xDE]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_u16_le_at_backpatches_correctly() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u16_le(0);
|
||||
cursor.write_u16_le(0xBEEF);
|
||||
cursor.set_u16_le_at(0, 0x1234);
|
||||
assert_eq!(cursor.as_bytes(), &[0x34, 0x12, 0xEF, 0xBE]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_utf16_le_encodes_correctly() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_utf16_le("hello");
|
||||
assert_eq!(
|
||||
cursor.as_bytes(),
|
||||
&[0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F, 0x00]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_zeros_produces_correct_count() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_zeros(5);
|
||||
assert_eq!(cursor.as_bytes(), &[0, 0, 0, 0, 0]);
|
||||
assert_eq!(cursor.position(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_inner_returns_buffer() {
|
||||
let mut cursor = WriteCursor::new();
|
||||
cursor.write_u8(0x42);
|
||||
let buf = cursor.into_inner();
|
||||
assert_eq!(buf, vec![0x42]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_works() {
|
||||
let cursor = WriteCursor::with_capacity(1024);
|
||||
assert_eq!(cursor.position(), 0);
|
||||
}
|
||||
|
||||
// -- Roundtrip tests --
|
||||
|
||||
#[test]
|
||||
fn roundtrip_u8() {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u8(0xAB);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
assert_eq!(r.read_u8().unwrap(), 0xAB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_u16() {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u16_le(0xCAFE);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
assert_eq!(r.read_u16_le().unwrap(), 0xCAFE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_u32() {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u32_le(0xDEADBEEF);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
assert_eq!(r.read_u32_le().unwrap(), 0xDEADBEEF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_u64() {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u64_le(0x0102030405060708);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
assert_eq!(r.read_u64_le().unwrap(), 0x0102030405060708);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_u128() {
|
||||
let val: u128 = 0x0102030405060708090A0B0C0D0E0F10;
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u128_le(val);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
assert_eq!(r.read_u128_le().unwrap(), val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_utf16_le() {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_utf16_le("Hello, world!");
|
||||
let bytes = w.into_inner();
|
||||
let mut r = ReadCursor::new(&bytes);
|
||||
let s = r.read_utf16_le(bytes.len()).unwrap();
|
||||
assert_eq!(s, "Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_utf16_le_emoji() {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_utf16_le("\u{1F600}");
|
||||
let bytes = w.into_inner();
|
||||
let mut r = ReadCursor::new(&bytes);
|
||||
let s = r.read_utf16_le(bytes.len()).unwrap();
|
||||
assert_eq!(s, "\u{1F600}");
|
||||
}
|
||||
|
||||
// -- Property-based tests --
|
||||
|
||||
fn valid_utf16_string() -> impl Strategy<Value = String> {
|
||||
prop::collection::vec(
|
||||
prop::char::range('\u{0000}', '\u{D7FF}')
|
||||
.prop_union(prop::char::range('\u{E000}', '\u{FFFF}')),
|
||||
0..100,
|
||||
)
|
||||
.prop_map(|chars| chars.into_iter().collect())
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_roundtrip_u8(val: u8) {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u8(val);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
prop_assert_eq!(r.read_u8().unwrap(), val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_roundtrip_u16(val: u16) {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u16_le(val);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
prop_assert_eq!(r.read_u16_le().unwrap(), val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_roundtrip_u32(val: u32) {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u32_le(val);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
prop_assert_eq!(r.read_u32_le().unwrap(), val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_roundtrip_u64(val: u64) {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u64_le(val);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
prop_assert_eq!(r.read_u64_le().unwrap(), val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_roundtrip_u128(val: u128) {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_u128_le(val);
|
||||
let mut r = ReadCursor::new(w.as_bytes());
|
||||
prop_assert_eq!(r.read_u128_le().unwrap(), val);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_roundtrip_utf16_le(s in valid_utf16_string()) {
|
||||
let mut w = WriteCursor::new();
|
||||
w.write_utf16_le(&s);
|
||||
let bytes = w.into_inner();
|
||||
let mut r = ReadCursor::new(&bytes);
|
||||
let decoded = r.read_utf16_le(bytes.len()).unwrap();
|
||||
prop_assert_eq!(decoded, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user