SMB Server Phase 2: VFS backend build fix + integration test
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled

- 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:
Warren
2026-06-20 19:42:29 +08:00
parent 45d050c0b3
commit 7eb528d35f
167 changed files with 59897 additions and 12 deletions

45
vendor/smb2/src/pack/CLAUDE.md vendored Normal file
View 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
View 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
View 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
View 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);
}
}
}