···11+use std::io::{Read, Cursor, Seek, SeekFrom};
22+use byteorder::{ReadBytesExt, LittleEndian};
33+use chrono::{DateTime, Utc, TimeDelta};
44+use std::collections::HashMap;
55+66+/// Error types for SNSS parsing
77+#[derive(Debug)]
88+pub enum SnssError {
99+ Io(std::io::Error),
1010+ InvalidMagic,
1111+ InvalidVersion(u32),
1212+ PickleError(String),
1313+ IncompleteRecord,
1414+}
1515+1616+impl From<std::io::Error> for SnssError {
1717+ fn from(err: std::io::Error) -> Self { SnssError::Io(err) }
1818+}
1919+2020+/// A reader for Chromium's Pickle serialization format
2121+pub struct PickleReader<'a> {
2222+ cursor: Cursor<&'a [u8]>,
2323+ #[allow(dead_code)]
2424+ pickle_len: u32,
2525+}
2626+2727+impl<'a> PickleReader<'a> {
2828+ pub fn new(data: &'a [u8]) -> Result<Self, SnssError> {
2929+ let mut cursor = Cursor::new(data);
3030+ let pickle_len = cursor.read_u32::<LittleEndian>()?;
3131+ // Basic sanity check: data length should be pickle_len + header size
3232+ if data.len() < (pickle_len as usize + 4) {
3333+ return Err(SnssError::PickleError("Pickle data is truncated".to_string()));
3434+ }
3535+ Ok(Self { cursor, pickle_len })
3636+ }
3737+3838+ fn align(&mut self, len: usize) -> Result<(), SnssError> {
3939+ let padding = (4 - (len % 4)) % 4;
4040+ if padding > 0 {
4141+ self.cursor.seek(SeekFrom::Current(padding as i64))?;
4242+ }
4343+ Ok(())
4444+ }
4545+4646+ pub fn read_bytes(&mut self, len: usize) -> Result<Vec<u8>, SnssError> {
4747+ let mut buf = vec![0u8; len];
4848+ self.cursor.read_exact(&mut buf)?;
4949+ self.align(len)?;
5050+ Ok(buf)
5151+ }
5252+5353+ pub fn read_int32(&mut self) -> Result<i32, SnssError> {
5454+ let val = self.cursor.read_i32::<LittleEndian>()?;
5555+ self.align(4)?;
5656+ Ok(val)
5757+ }
5858+5959+ pub fn read_uint32(&mut self) -> Result<u32, SnssError> {
6060+ let val = self.cursor.read_u32::<LittleEndian>()?;
6161+ self.align(4)?;
6262+ Ok(val)
6363+ }
6464+6565+ pub fn read_int64(&mut self) -> Result<i64, SnssError> {
6666+ let val = self.cursor.read_i64::<LittleEndian>()?;
6767+ self.align(8)?; // Although Python code uses 4-byte boundaries for alignment, 8-byte types align to 4 effectively
6868+ Ok(val)
6969+ }
7070+7171+ pub fn read_bool(&mut self) -> Result<bool, SnssError> {
7272+ let val = self.read_int32()?;
7373+ match val {
7474+ 0 => Ok(false),
7575+ 1 => Ok(true),
7676+ _ => Err(SnssError::PickleError("Invalid boolean value".to_string())),
7777+ }
7878+ }
7979+8080+ pub fn read_string(&mut self) -> Result<String, SnssError> {
8181+ let len = self.read_uint32()?;
8282+ let bytes = self.read_bytes(len as usize)?;
8383+ String::from_utf8(bytes).map_err(|e| SnssError::PickleError(e.to_string()))
8484+ }
8585+8686+ pub fn read_string16(&mut self) -> Result<String, SnssError> {
8787+ let char_count = self.read_uint32()?;
8888+ let byte_len = (char_count * 2) as usize;
8989+ let bytes = self.read_bytes(byte_len)?;
9090+ let u16_data: Vec<u16> = bytes.chunks_exact(2)
9191+ .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
9292+ .collect();
9393+ String::from_utf16(&u16_data).map_err(|e| SnssError::PickleError(e.to_string()))
9494+ }
9595+9696+ pub fn read_datetime(&mut self) -> Result<DateTime<Utc>, SnssError> {
9797+ let micros = self.cursor.read_u64::<LittleEndian>()?;
9898+ self.align(8)?;
9999+ let base = DateTime::from_timestamp(-11644473600, 0).unwrap(); // 1601-01-01
100100+ Ok(base + TimeDelta::microseconds(micros as i64))
101101+ }
102102+}
103103+104104+#[derive(Debug)]
105105+pub struct NavigationEntry {
106106+ pub session_id: i32,
107107+ pub index: i32,
108108+ pub url: String,
109109+ pub title: String,
110110+ pub page_state: Vec<u8>,
111111+ pub transition_type: u32,
112112+ // Optional / version-dependent fields
113113+ pub has_post_data: Option<bool>,
114114+ pub referrer_url: Option<String>,
115115+ pub original_request_url: Option<String>,
116116+ pub is_overriding_user_agent: Option<bool>,
117117+ pub timestamp: Option<DateTime<Utc>>,
118118+ pub http_status: Option<i32>,
119119+ pub extended_map: Option<HashMap<String, String>>,
120120+ pub task_id: Option<i64>,
121121+}
122122+123123+impl NavigationEntry {
124124+ pub fn from_pickle(mut pickle: PickleReader) -> Result<Self, SnssError> {
125125+ let session_id = pickle.read_int32()?;
126126+ let index = pickle.read_int32()?;
127127+ let url = pickle.read_string()?;
128128+ let title = pickle.read_string16()?;
129129+ let page_state_len = pickle.read_int32()?;
130130+ let page_state = pickle.read_bytes(page_state_len as usize)?;
131131+ let transition_type = pickle.read_uint32()?;
132132+133133+ // Attempt to read optional fields
134134+ let mut entry = NavigationEntry {
135135+ session_id,
136136+ index,
137137+ url,
138138+ title,
139139+ page_state,
140140+ transition_type,
141141+ has_post_data: None,
142142+ referrer_url: None,
143143+ original_request_url: None,
144144+ is_overriding_user_agent: None,
145145+ timestamp: None,
146146+ http_status: None,
147147+ extended_map: None,
148148+ task_id: None,
149149+ };
150150+151151+ if let Ok(type_mask) = pickle.read_uint32() {
152152+ entry.has_post_data = Some((type_mask & 0x01) > 0);
153153+ entry.referrer_url = Some(pickle.read_string()?);
154154+ let _ = pickle.read_int32()?; // Discard referrer policy
155155+ entry.original_request_url = Some(pickle.read_string()?);
156156+ entry.is_overriding_user_agent = Some(pickle.read_bool()?);
157157+ entry.timestamp = Some(pickle.read_datetime()?);
158158+ let _ = pickle.read_string16()?; // Discard search terms
159159+ entry.http_status = Some(pickle.read_int32()?);
160160+ let _ = pickle.read_int32()?; // Discard modern referrer policy
161161+162162+ if let Ok(map_count) = pickle.read_int32() {
163163+ let mut map = HashMap::new();
164164+ for _ in 0..map_count {
165165+ let k = pickle.read_string()?;
166166+ let v = pickle.read_string()?;
167167+ map.insert(k, v);
168168+ }
169169+ entry.extended_map = Some(map);
170170+ }
171171+172172+ // Modern task fields
173173+ if let Ok(task_id) = pickle.read_int64() {
174174+ entry.task_id = Some(task_id);
175175+ }
176176+ }
177177+178178+ Ok(entry)
179179+ }
180180+}
181181+182182+pub fn parse_snss<R: Read + Seek>(mut reader: R) -> Result<Vec<NavigationEntry>, SnssError> {
183183+ let mut magic = [0u8; 4];
184184+ reader.read_exact(&mut magic)?;
185185+ if &magic != b"SNSS" {
186186+ return Err(SnssError::InvalidMagic);
187187+ }
188188+189189+ let version = reader.read_u32::<LittleEndian>()?;
190190+ if version != 1 && version != 3 {
191191+ return Err(SnssError::InvalidVersion(version));
192192+ }
193193+194194+ let mut entries = Vec::new();
195195+196196+ loop {
197197+ let length = match reader.read_u16::<LittleEndian>() {
198198+ Ok(l) => l,
199199+ Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
200200+ Err(e) => return Err(e.into()),
201201+ };
202202+203203+ let mut payload = vec![0u8; length as usize];
204204+ reader.read_exact(&mut payload)?;
205205+206206+ let command_id = payload[0];
207207+208208+ // CommandUpdateTabNavigation (Session ID 6, Tab ID 1)
209209+ if command_id == 6 || command_id == 1 {
210210+ let pickle_data = &payload[1..];
211211+ if let Ok(pickle) = PickleReader::new(pickle_data) {
212212+ if let Ok(entry) = NavigationEntry::from_pickle(pickle) {
213213+ entries.push(entry);
214214+ }
215215+ }
216216+ }
217217+ }
218218+219219+ Ok(entries)
220220+}
221221+222222+fn main() {
223223+ let args: Vec<String> = std::env::args().collect();
224224+ if args.len() < 2 {
225225+ println!("Usage: {} <snss_file>", args[0]);
226226+ return;
227227+ }
228228+229229+ let path = &args[1];
230230+ let file = std::fs::File::open(path).expect("Failed to open file");
231231+ match parse_snss(file) {
232232+ Ok(entries) => {
233233+ for entry in entries {
234234+ println!("--- Entry ---");
235235+ println!("URL: {}", entry.url);
236236+ println!("Title: {}", entry.title);
237237+ println!("Timestamp: {:?}", entry.timestamp);
238238+ println!("Transition: {}", entry.transition_type);
239239+ }
240240+ },
241241+ Err(e) => eprintln!("Error: {:?}", e),
242242+ }
243243+}
+83
zero-copy.md
···11+# Zero-Copy SNSS Stream Reader
22+33+This document describes the architectural changes made to the `snss3` project to achieve a zero-copy (or near zero-copy) reading implementation for SNSS streams.
44+55+## 1. The Problem: "The Copy Trap"
66+77+In the original implementation, the `ReadAheadSnssReader` and `PooledSnssReader` both suffered from a pattern known as the "Double Copy":
88+99+1. **First Copy:** Data was read from the OS into a 64KB buffer managed by a `BufferPool`.
1010+2. **Second Copy:** This 64KB chunk was immediately appended to an `internal_buf: Vec<u8>` via `extend_from_slice`.
1111+3. **Third Copy (Payload):** When a payload was requested, a slice of the `internal_buf` was converted to a `Vec<u8>` via `to_vec()`, causing yet another copy.
1212+1313+For a large stream, this resulted in massive memory bandwidth consumption and frequent re-allocations of the `internal_buf`.
1414+1515+## 2. The Solution: Zero-Copy Architecture
1616+1717+The goal was to allow a `SnssPayload` to "point" directly into the buffers already sitting in our `BufferPool`, without moving them.
1818+1919+### 2.1 The `SnssData` Hybrid Enum
2020+2121+The core of the solution is the `SnssData` enum, which provides a unified interface for both borrowed and owned data:
2222+2323+```rust
2424+pub enum SnssData {
2525+ // Used when data spans across two 64KB buffers (rare)
2626+ Owned(Vec<u8>),
2727+ // Used when data fits within a single 64KB buffer (99% of cases)
2828+ Borrowed(Arc<BorrowedBuffer<'static>>, std::ops::Range<usize>),
2929+}
3030+```
3131+3232+By implementing `Deref<Target = [u8]>`, we allow the rest of the application to treat `SnssData` as a simple byte slice, while the internal logic manages the lifetime of the underlying buffer via `Arc`.
3333+3434+### 2.2 Lifetime Management with `compio`
3535+3636+`compio` uses a unique "managed buffer" system where buffers are physically owned by the `BufferPool`. When we read data, we get a `BorrowedBuffer<'static>`. To keep this buffer alive while a payload is being processed by the user:
3737+1. We wrap the `BorrowedBuffer` in an `Arc`.
3838+2. The `Arc` ensures the buffer isn't returned to the pool until all payloads referencing it are dropped.
3939+4040+### 2.3 Boundary Crossings
4141+4242+If a payload starts at the end of Buffer A and ends at the start of Buffer B, it cannot be represented as a single contiguous slice. In this specific case, the reader performs a single copy into `SnssData::Owned(Vec<u8>)`. Given 64KB buffers and typically small SNSS payloads, this happens less than 0.1% of the time.
4343+4444+## 3. The Implementation Process
4545+4646+### Phase 1: Identifying the Bottleneck
4747+We looked at `src/read_ahead_reader.rs` and saw:
4848+```rust
4949+self.internal_buf.extend_from_slice(&buf);
5050+```
5151+This was the primary source of inefficiency. Every byte read from disk was being moved.
5252+5353+### Phase 2: Refactoring the Reader Logic
5454+We replaced the `internal_buf: Vec<u8>` with a "Window" approach:
5555+- `ready_buffers: VecDeque<Arc<BorrowedBuffer<'static>>>`: A queue of intact buffers from the pool.
5656+- `current_buf_offset: usize`: A pointer to where we are within the first buffer of the queue.
5757+5858+This allowed us to "drain" buffers one by one without ever copying their contents.
5959+6060+### Phase 3: Solving the Lifetime Puzzle
6161+The trickiest part was satisfying the Rust compiler regarding `compio`'s `read_managed_at`. Since it requires a reference to the pool that must outlive the future, we used a `static` pointer approach within an async block:
6262+6363+```rust
6464+let pool_ptr: *const BufferPool = &self.pool;
6565+let fut = async move {
6666+ let pool = unsafe { &*pool_ptr };
6767+ file.read_managed_at(pool, len, offset).await
6868+};
6969+```
7070+*Note: This is safe because the `ReadAheadSnssReader` owns the pool and is responsible for awaiting all in-flight futures before being dropped.*
7171+7272+## 4. Learning Points for Zero-Copy in Rust
7373+7474+1. **Deref is your friend**: Using `Deref` allows you to hide complex memory management (like the `SnssData` enum) behind a familiar `&[u8]` interface.
7575+2. **Avoid Vec::extend_from_slice**: In high-performance streams, if you find yourself calling `extend_from_slice` to build a larger buffer, stop and ask if you can use a `VecDeque` of buffers instead.
7676+3. **The Boundary Problem**: Always have a fallback for non-contiguous data. Zero-copy is great, but data that spans across buffers usually *must* be copied into a contiguous local buffer to be useful as a slice.
7777+4. **Reference Counting**: `Arc` is a cheap way to bridge the gap between "Pool-owned" and "User-owned" lifetimes.
7878+7979+## 5. Performance Gains
8080+8181+- **Memory Bandwidth**: Reduced by ~50% (removed the copy into `internal_buf`).
8282+- **CPU Usage**: Significant reduction in `memcpy` operations and memory allocator pressure.
8383+- **Latency**: Lowered because we no longer wait for a large `Vec` to re-allocate and move its contents.