Skip to main content

musefs_format/ogg/
page.rs

1use super::art_source::ArtSource;
2use super::crc::{crc_shift_zeros, crc32, crc32_update};
3use crate::error::{FormatError, Result};
4
5pub const CAPTURE: &[u8; 4] = b"OggS";
6
7/// Header-type flag bits.
8pub const FLAG_CONTINUED: u8 = 0x01;
9pub const FLAG_BOS: u8 = 0x02;
10#[allow(dead_code)]
11pub const FLAG_EOS: u8 = 0x04;
12
13/// A parsed Ogg page header (the 27 fixed bytes + the segment table) plus the
14/// derived payload length. Multi-byte fields are little-endian on disk.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct PageHeader {
17    pub header_type: u8,
18    pub granule: u64,
19    pub serial: u32,
20    pub seq: u32,
21    pub crc: u32,
22    pub seg_count: u8,
23    pub header_len: usize,
24    pub payload_len: usize,
25}
26
27impl PageHeader {
28    pub fn total_len(&self) -> usize {
29        self.header_len + self.payload_len
30    }
31}
32
33/// Parse the page starting at `pos`. Errors if the capture pattern is missing or
34/// the buffer is too short for the header + segment table.
35pub fn parse_page(buf: &[u8], pos: usize) -> Result<PageHeader> {
36    if pos + 27 > buf.len() || &buf[pos..pos + 4] != CAPTURE {
37        return Err(FormatError::Malformed);
38    }
39    if buf[pos + 4] != 0 {
40        return Err(FormatError::Malformed);
41    }
42    let header_type = buf[pos + 5];
43    let granule = u64::from_le_bytes(buf[pos + 6..pos + 14].try_into().unwrap());
44    let serial = u32::from_le_bytes(buf[pos + 14..pos + 18].try_into().unwrap());
45    let seq = u32::from_le_bytes(buf[pos + 18..pos + 22].try_into().unwrap());
46    let crc = u32::from_le_bytes(buf[pos + 22..pos + 26].try_into().unwrap());
47    let seg_count = buf[pos + 26];
48    let table_start = pos + 27;
49    let table_end = table_start + seg_count as usize;
50    if table_end > buf.len() {
51        return Err(FormatError::Malformed);
52    }
53    let payload_len: usize = buf[table_start..table_end]
54        .iter()
55        .map(|&b| b as usize)
56        .sum();
57    Ok(PageHeader {
58        header_type,
59        granule,
60        serial,
61        seq,
62        crc,
63        seg_count,
64        header_len: 27 + seg_count as usize,
65        payload_len,
66    })
67}
68
69/// Encode `payload_len` as Ogg lacing values: ⌊L/255⌋ values of 255 followed by
70/// one value of L mod 255. When L is a multiple of 255 this appends a terminating
71/// 0, which is required to signal the packet's end.
72pub(crate) fn lacing_values(payload_len: usize) -> Vec<u8> {
73    let mut v = vec![255u8; payload_len / 255];
74    v.push(u8::try_from(payload_len % 255).expect("x % 255 < 256"));
75    v
76}
77
78/// Lace one packet into one or more pages starting at sequence number `seq_start`.
79/// Each page carries up to 255 lacing values (≤ 65 025 payload bytes). `bos` sets
80/// the BOS flag on the packet's first page; continuation pages get FLAG_CONTINUED.
81/// All pages use the given `granule`. Returns `(bytes, pages_used)`.
82pub fn lace_packet(
83    serial: u32,
84    seq_start: u32,
85    bos: bool,
86    granule: u64,
87    packet: &[u8],
88) -> (Vec<u8>, u32) {
89    let laces = lacing_values(packet.len());
90    let mut out = Vec::new();
91    let mut seq = seq_start;
92    let mut lace_pos = 0usize;
93    let mut payload_pos = 0usize;
94    let mut first = true;
95    // Always emit at least one page (handles a zero-length packet: laces == [0]).
96    while first || lace_pos < laces.len() {
97        let chunk = (laces.len() - lace_pos).min(255);
98        let table = &laces[lace_pos..lace_pos + chunk];
99        let page_payload: usize = table.iter().map(|&b| b as usize).sum();
100
101        let mut header_type = 0u8;
102        if bos && first {
103            header_type |= FLAG_BOS;
104        }
105        if !first {
106            header_type |= FLAG_CONTINUED;
107        }
108
109        let page_start = out.len();
110        out.extend_from_slice(CAPTURE);
111        out.push(0);
112        out.push(header_type);
113        out.extend_from_slice(&granule.to_le_bytes());
114        out.extend_from_slice(&serial.to_le_bytes());
115        out.extend_from_slice(&seq.to_le_bytes());
116        out.extend_from_slice(&0u32.to_le_bytes()); // CRC placeholder
117        out.push(u8::try_from(chunk).expect("chunk is .min(255) so fits in u8"));
118        out.extend_from_slice(table);
119        out.extend_from_slice(&packet[payload_pos..payload_pos + page_payload]);
120
121        let crc = crc32(&out[page_start..]);
122        out[page_start + 22..page_start + 26].copy_from_slice(&crc.to_le_bytes());
123
124        lace_pos += chunk;
125        payload_pos += page_payload;
126        seq = seq.wrapping_add(1);
127        first = false;
128    }
129    (out, seq.wrapping_sub(seq_start))
130}
131
132/// Lace a sequence of header packets onto fresh pages starting at sequence 0, with
133/// BOS on the very first page and granule 0 throughout (header pages carry no
134/// audio). Each packet begins a new page. Returns `(bytes, page_count)`.
135pub fn build_header(serial: u32, packets: &[&[u8]]) -> (Vec<u8>, u32) {
136    let mut out = Vec::new();
137    let mut seq = 0u32;
138    for (i, pkt) in packets.iter().enumerate() {
139        let (bytes, used) = lace_packet(serial, seq, i == 0, 0, pkt);
140        out.extend_from_slice(&bytes);
141        seq += used;
142    }
143    (out, seq)
144}
145
146/// A packet reassembled from one or more pages, plus the byte offset just past
147/// the page on which it completed (used to locate where audio begins).
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct ReadPacket {
150    pub data: Vec<u8>,
151    pub end_offset: usize,
152    pub pages_through_end: u32,
153}
154
155/// Reassemble up to `want` packets from the pages starting at `data[0]`. Stops as
156/// soon as `want` packets have completed (audio for Opus/Vorbis/OggFLAC begins on
157/// a fresh page after the header packets). A packet ends at the first lacing value
158/// < 255.
159pub fn read_packets(data: &[u8], want: usize) -> Result<Vec<ReadPacket>> {
160    let mut out: Vec<ReadPacket> = Vec::new();
161    let mut pos = 0usize;
162    let mut pages = 0u32;
163    let mut cur: Vec<u8> = Vec::new();
164    while out.len() < want {
165        let h = parse_page(data, pos)?;
166        pages += 1;
167        let table_start = pos + 27;
168        let mut payload_pos = h.header_len;
169        for i in 0..h.seg_count as usize {
170            let lace = data[table_start + i] as usize;
171            let seg_start = pos + payload_pos;
172            let seg_end = seg_start + lace;
173            if seg_end > data.len() {
174                return Err(FormatError::Malformed);
175            }
176            cur.extend_from_slice(&data[seg_start..seg_end]);
177            payload_pos += lace;
178            if lace < 255 {
179                out.push(ReadPacket {
180                    data: std::mem::take(&mut cur),
181                    end_offset: pos + h.total_len(),
182                    pages_through_end: pages,
183                });
184                if out.len() == want {
185                    break;
186                }
187            }
188        }
189        pos += h.total_len();
190    }
191    Ok(out)
192}
193
194/// Given the full bytes of one page, return just its header bytes (length
195/// `header_len`) with the sequence number set to `new_seq` and the CRC recomputed
196/// over the patched page. The payload is read (to recompute the CRC) but not
197/// returned — callers splice it verbatim from the backing file.
198pub fn patch_page_header(page: &[u8], new_seq: u32) -> Result<Vec<u8>> {
199    let h = parse_page(page, 0)?;
200    if page.len() < h.total_len() {
201        return Err(FormatError::Malformed);
202    }
203    let mut full = page[..h.total_len()].to_vec();
204    full[18..22].copy_from_slice(&new_seq.to_le_bytes());
205    full[22..26].copy_from_slice(&0u32.to_le_bytes());
206    let crc = crc32(&full);
207    full[22..26].copy_from_slice(&crc.to_le_bytes());
208    full.truncate(h.header_len);
209    Ok(full)
210}
211
212/// Patch a page header algebraically — no payload read needed.
213///
214/// `header` must be exactly `27 + seg_count` bytes (the fixed Ogg page header
215/// plus segment table; seg_count is read from byte 26). Returns the patched
216/// header bytes with `new_seq` written and the CRC updated via:
217///
218///   new_crc = old_crc XOR crc32(DELTA)
219///
220/// where DELTA is the all-zero message of length page_len, except bytes 18–21
221/// hold `old_seq XOR new_seq`. The payload cancels out of the XOR because the
222/// Ogg CRC is linear (init=0, no xorout). `payload_len` is derived from the
223/// segment table (no payload I/O required).
224pub fn patch_page_header_algebraic(header: &[u8], new_seq: u32) -> Result<Vec<u8>> {
225    if header.len() < 27 {
226        return Err(FormatError::Malformed);
227    }
228    let seg_count = header[26] as usize;
229    let header_len = 27 + seg_count;
230    if header.len() < header_len {
231        return Err(FormatError::Malformed);
232    }
233    let payload_len: usize = header[27..header_len].iter().map(|&b| b as usize).sum();
234    let old_seq = u32::from_le_bytes(header[18..22].try_into().unwrap());
235    let old_crc = u32::from_le_bytes(header[22..26].try_into().unwrap());
236    // 18 leading zeros leave the CRC state at 0 (TABLE[0]=0), so we start
237    // directly from the 4-byte seq delta, then shift by the trailing zero count.
238    let delta_bytes = (old_seq ^ new_seq).to_le_bytes();
239    let trailing = 5 + seg_count + payload_len; // bytes 22..page_end are zero in DELTA
240    let delta_crc = crc_shift_zeros(crc32(&delta_bytes), trailing);
241    let new_crc = old_crc ^ delta_crc;
242    let mut out = header[..header_len].to_vec();
243    out[18..22].copy_from_slice(&new_seq.to_le_bytes());
244    out[22..26].copy_from_slice(&new_crc.to_le_bytes());
245    Ok(out)
246}
247
248/// Verify that the page at the start of `page` carries a stored CRC matching a
249/// fresh computation. `page` must hold at least the full page (`total_len()`
250/// bytes). Used by the backward-scan entry-page guard to reject a coincidental
251/// `OggS` match in audio payload (a false page start fails this check).
252pub fn verify_page_crc(page: &[u8]) -> Result<bool> {
253    let h = parse_page(page, 0)?;
254    if page.len() < h.total_len() {
255        return Err(FormatError::Malformed);
256    }
257    let mut buf = page[..h.total_len()].to_vec();
258    buf[22..26].copy_from_slice(&0u32.to_le_bytes());
259    Ok(crc32(&buf) == h.crc)
260}
261
262use crate::layout::Segment;
263
264/// One span of a packet's payload during chunk-aware lacing.
265pub(crate) enum PayloadChunk {
266    /// Literal bytes copied verbatim into the layout as `Inline`.
267    Bytes(Vec<u8>),
268    /// An art run. Carries no bytes: its OUTPUT length is derived from `art_total`
269    /// (base64-expanded when `base64`), and its bytes are streamed from an
270    /// `ArtSource` to compute page CRCs, then never stored — the layout keeps an
271    /// `OggArtSlice` referencing `art_id`. `art_total` is the raw image length.
272    Art {
273        art_id: i64,
274        base64: bool,
275        art_total: u64,
276    },
277}
278
279impl PayloadChunk {
280    fn out_len(&self) -> usize {
281        match self {
282            PayloadChunk::Bytes(b) => b.len(),
283            PayloadChunk::Art {
284                base64, art_total, ..
285            } => {
286                let n = if *base64 {
287                    crate::ogg::b64::b64_len(*art_total)
288                } else {
289                    *art_total
290                };
291                crate::convert::usize_from(n)
292            }
293        }
294    }
295}
296
297/// Lace one packet (a chunk list) into pages from sequence `seq_start`, emitting
298/// layout segments: page headers + literal payload as `Inline` (CRCs baked in),
299/// art runs as `OggArtSlice` (no bytes stored). Art bytes are streamed from `src`
300/// only to compute page CRCs. Returns `(segments, pages_used)`.
301pub(crate) fn lace_chunks_to_segments(
302    serial: u32,
303    seq_start: u32,
304    bos: bool,
305    chunks: &[PayloadChunk],
306    src: &dyn ArtSource,
307) -> crate::error::Result<(Vec<Segment>, u32)> {
308    let total: usize = chunks.iter().map(PayloadChunk::out_len).sum();
309    let laces = lacing_values(total);
310
311    let mut segments: Vec<Segment> = Vec::new();
312    let mut seq = seq_start;
313    let mut lace_pos = 0usize;
314    let mut payload_pos = 0usize;
315    let mut first = true;
316
317    while first || lace_pos < laces.len() {
318        let seg_count = (laces.len() - lace_pos).min(255);
319        let table = &laces[lace_pos..lace_pos + seg_count];
320        let page_payload: usize = table.iter().map(|&b| b as usize).sum();
321
322        let mut header_type = 0u8;
323        if bos && first {
324            header_type |= FLAG_BOS;
325        }
326        if !first {
327            header_type |= FLAG_CONTINUED;
328        }
329
330        // Build the page header + lacing table only (CRC field zeroed), then stream
331        // the page CRC over header+payload without materializing the payload.
332        let header_len = 27 + seg_count;
333        let mut header = Vec::with_capacity(header_len);
334        header.extend_from_slice(CAPTURE);
335        header.push(0);
336        header.push(header_type);
337        header.extend_from_slice(&0u64.to_le_bytes()); // granule 0 (header page)
338        header.extend_from_slice(&serial.to_le_bytes());
339        header.extend_from_slice(&seq.to_le_bytes());
340        header.extend_from_slice(&0u32.to_le_bytes()); // CRC placeholder
341        header.push(u8::try_from(seg_count).expect("seg_count is .min(255) so fits in u8"));
342        header.extend_from_slice(table);
343
344        let mut crc = crc32_update(0, &header);
345        crc_feed_payload(&mut crc, chunks, src, payload_pos, page_payload)?;
346        header[22..26].copy_from_slice(&crc.to_le_bytes());
347
348        emit_segments(&mut segments, &header, chunks, payload_pos, page_payload);
349
350        payload_pos += page_payload;
351        lace_pos += seg_count;
352        seq += 1;
353        first = false;
354    }
355    Ok((segments, seq - seq_start))
356}
357
358/// Fold payload bytes `[p0, p0+plen)` (packet-payload coordinates) into `crc`,
359/// reading art runs from `src` instead of materializing them.
360fn crc_feed_payload(
361    crc: &mut u32,
362    chunks: &[PayloadChunk],
363    src: &dyn ArtSource,
364    p0: usize,
365    plen: usize,
366) -> crate::error::Result<()> {
367    let end = p0 + plen;
368    let mut cs = 0usize;
369    for c in chunks {
370        let ce = cs + c.out_len();
371        let os = p0.max(cs);
372        let oe = end.min(ce);
373        if os < oe {
374            match c {
375                PayloadChunk::Bytes(b) => {
376                    *crc = crc32_update(*crc, &b[os - cs..oe - cs]);
377                }
378                PayloadChunk::Art {
379                    art_id,
380                    base64,
381                    art_total,
382                } => {
383                    crc_feed_art(
384                        crc,
385                        src,
386                        *art_id,
387                        *base64,
388                        *art_total,
389                        (os - cs) as u64,
390                        oe - os,
391                    )?;
392                }
393            }
394        }
395        cs = ce;
396    }
397    Ok(())
398}
399
400/// Fold one art window — output bytes `[out_off, out_off+out_len)` of the run —
401/// into `crc`, base64-encoding on the fly when `base64`. `out_len` is page-bounded.
402fn crc_feed_art(
403    crc: &mut u32,
404    src: &dyn ArtSource,
405    art_id: i64,
406    base64: bool,
407    art_total: u64,
408    out_off: u64,
409    out_len: usize,
410) -> crate::error::Result<()> {
411    if base64 {
412        let w = crate::ogg::b64::b64_window(out_off, out_len as u64, art_total);
413        let mut raw = vec![0u8; crate::convert::usize_from(w.in_len)];
414        src.read_window(art_id, w.in_start, &mut raw)?;
415        let enc = crate::ogg::b64::encode_b64_slice(&raw, w.skip, out_len)
416            .ok_or(crate::error::FormatError::ArtRead { art_id })?;
417        *crc = crc32_update(*crc, &enc);
418    } else {
419        let mut raw = vec![0u8; out_len];
420        src.read_window(art_id, out_off, &mut raw)?;
421        *crc = crc32_update(*crc, &raw);
422    }
423    Ok(())
424}
425
426/// Emit the page header + payload `[p0, p0+plen)` as layout segments: `Inline` for
427/// the header and literal byte spans, `OggArtSlice` for art spans.
428fn emit_segments(
429    segments: &mut Vec<Segment>,
430    header: &[u8],
431    chunks: &[PayloadChunk],
432    p0: usize,
433    plen: usize,
434) {
435    let end = p0 + plen;
436    let mut buf: Vec<u8> = header.to_vec();
437    let mut cs = 0usize;
438    for c in chunks {
439        let ce = cs + c.out_len();
440        let os = p0.max(cs);
441        let oe = end.min(ce);
442        if os < oe {
443            match c {
444                PayloadChunk::Bytes(b) => buf.extend_from_slice(&b[os - cs..oe - cs]),
445                PayloadChunk::Art {
446                    art_id,
447                    base64,
448                    art_total,
449                } => {
450                    if !buf.is_empty() {
451                        segments.push(Segment::Inline(std::mem::take(&mut buf)));
452                    }
453                    segments.push(Segment::OggArtSlice {
454                        art_id: *art_id,
455                        offset: (os - cs) as u64,
456                        len: crate::BlobLen::new((oe - os) as u64)
457                            .expect("ogg art slice span is non-empty"),
458                        base64: *base64,
459                        art_total: *art_total,
460                    });
461                }
462            }
463        }
464        cs = ce;
465    }
466    if !buf.is_empty() {
467        segments.push(Segment::Inline(buf));
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    fn hand_page() -> Vec<u8> {
476        let mut p = Vec::new();
477        p.extend_from_slice(CAPTURE);
478        p.push(0); // version
479        p.push(FLAG_BOS); // header_type
480        p.extend_from_slice(&0u64.to_le_bytes()); // granule
481        p.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); // serial
482        p.extend_from_slice(&7u32.to_le_bytes()); // seq
483        p.extend_from_slice(&0x1122_3344u32.to_le_bytes()); // crc field (as stored)
484        p.push(2); // seg_count
485        p.push(0x10);
486        p.push(0x20); // segment table => payload 0x30
487        p.extend(std::iter::repeat_n(0xAB, 0x30));
488        p
489    }
490
491    #[test]
492    fn parses_fields_and_lengths() {
493        let p = hand_page();
494        let h = parse_page(&p, 0).unwrap();
495        assert_eq!(h.header_type, FLAG_BOS);
496        assert_eq!(h.serial, 0xDEAD_BEEF);
497        assert_eq!(h.seq, 7);
498        assert_eq!(h.crc, 0x1122_3344);
499        assert_eq!(h.seg_count, 2);
500        assert_eq!(h.payload_len, 0x30);
501        assert_eq!(h.header_len, 29);
502        assert_eq!(h.total_len(), 0x30 + 29);
503    }
504
505    #[test]
506    fn rejects_bad_capture() {
507        let mut p = hand_page();
508        p[0] = b'X';
509        assert_eq!(parse_page(&p, 0), Err(FormatError::Malformed));
510    }
511
512    #[test]
513    fn rejects_nonzero_version() {
514        let mut p = hand_page();
515        p[4] = 1; // bad version
516        assert_eq!(parse_page(&p, 0), Err(FormatError::Malformed));
517    }
518
519    #[test]
520    fn single_page_packet_round_trips_and_crc_valid() {
521        let packet: Vec<u8> = (0..200u8).collect();
522        let (bytes, pages) = lace_packet(0xABCD, 0, true, 0, &packet);
523        assert_eq!(pages, 1);
524        let h = parse_page(&bytes, 0).unwrap();
525        assert_eq!(h.header_type, FLAG_BOS);
526        assert_eq!(h.payload_len, 200);
527        // CRC self-check: zero the field, recompute, compare to stored.
528        let mut z = bytes.clone();
529        z[22..26].copy_from_slice(&0u32.to_le_bytes());
530        assert_eq!(crc32(&z), h.crc);
531    }
532
533    #[test]
534    fn exact_multiple_of_255_appends_terminating_zero() {
535        let packet = vec![0u8; 255];
536        let (bytes, pages) = lace_packet(1, 0, false, 0, &packet);
537        assert_eq!(pages, 1);
538        let h = parse_page(&bytes, 0).unwrap();
539        // 255 + terminating 0 => two lacing values, both summing to 255 payload.
540        assert_eq!(h.seg_count, 2);
541        assert_eq!(h.payload_len, 255);
542    }
543
544    #[test]
545    fn large_packet_spans_multiple_pages_with_continuation() {
546        let packet = vec![0x5Au8; 70_000]; // > 65 025 => 2 pages
547        let (bytes, pages) = lace_packet(2, 5, false, 0, &packet);
548        assert_eq!(pages, 2);
549        let p0 = parse_page(&bytes, 0).unwrap();
550        assert_eq!(p0.header_type & FLAG_CONTINUED, 0);
551        assert_eq!(p0.payload_len, 65_025);
552        let p1 = parse_page(&bytes, p0.total_len()).unwrap();
553        assert_eq!(p1.header_type & FLAG_CONTINUED, FLAG_CONTINUED);
554        assert_eq!(p1.seq, 6);
555        assert_eq!(p0.payload_len + p1.payload_len, 70_000);
556    }
557
558    #[test]
559    fn build_header_numbers_pages_and_sets_bos_once() {
560        let a = vec![1u8; 10];
561        let b = vec![2u8; 10];
562        let (bytes, count) = build_header(9, &[&a, &b]);
563        assert_eq!(count, 2);
564        let p0 = parse_page(&bytes, 0).unwrap();
565        let p1 = parse_page(&bytes, p0.total_len()).unwrap();
566        assert_eq!(p0.header_type & FLAG_BOS, FLAG_BOS);
567        assert_eq!(p1.header_type & FLAG_BOS, 0);
568        assert_eq!(p0.seq, 0);
569        assert_eq!(p1.seq, 1);
570    }
571
572    #[test]
573    fn read_packets_reassembles_multipage_packet() {
574        // One small packet, then one packet that spans two pages.
575        let small = vec![7u8; 5];
576        let big = vec![9u8; 70_000];
577        let (mut bytes, _) = lace_packet(3, 0, true, 0, &small);
578        let (b2, _) = lace_packet(3, 1, false, 0, &big);
579        bytes.extend_from_slice(&b2);
580
581        let pkts = read_packets(&bytes, 2).unwrap();
582        assert_eq!(pkts.len(), 2);
583        assert_eq!(pkts[0].data, small);
584        assert_eq!(pkts[1].data, big);
585        assert_eq!(pkts[1].pages_through_end, 3);
586        assert_eq!(pkts[1].end_offset, bytes.len());
587    }
588
589    #[test]
590    fn multipage_payload_cursor_advances_across_pages() {
591        // A >65 025-byte packet with DISTINCT bytes at every position: the second
592        // page must carry the *tail* of the packet, not a re-read from offset 0.
593        // Pins `payload_pos += page_payload` against a cursor stuck at 0 (a uniform
594        // payload, as in the other multi-page tests, can't observe this).
595        let packet: Vec<u8> = (0..70_000u32).map(|i| (i % 251) as u8).collect();
596        let (bytes, pages) = lace_packet(2, 0, false, 0, &packet);
597        assert_eq!(pages, 2);
598        let pkts = read_packets(&bytes, 1).unwrap();
599        assert_eq!(pkts[0].data, packet);
600    }
601
602    #[test]
603    fn patch_page_header_updates_seq_and_crc() {
604        let packet = vec![0x42u8; 300];
605        let (page, _) = lace_packet(0xCAFE, 10, false, 7, &packet);
606        let patched = patch_page_header(&page, 12).unwrap();
607        let h0 = parse_page(&page, 0).unwrap();
608        assert_eq!(patched.len(), h0.header_len);
609        // Reassemble a full page from the patched header + original payload and
610        // verify the parsed seq and a self-consistent CRC.
611        let mut full = patched.clone();
612        full.extend_from_slice(&page[h0.header_len..h0.total_len()]);
613        let h1 = parse_page(&full, 0).unwrap();
614        assert_eq!(h1.seq, 12);
615        let mut z = full.clone();
616        z[22..26].copy_from_slice(&0u32.to_le_bytes());
617        assert_eq!(crc32(&z), h1.crc);
618    }
619
620    use crate::layout::Segment;
621
622    // Reconstruct the laced byte stream from segments, expanding OggArtSlice from a
623    // provided art output map, so we can validate framing/CRCs end to end.
624    fn flatten(segments: &[Segment], art_out: &[u8]) -> Vec<u8> {
625        let mut v = Vec::new();
626        for s in segments {
627            match s {
628                Segment::Inline(b) => v.extend_from_slice(b),
629                Segment::OggArtSlice {
630                    offset,
631                    len,
632                    base64,
633                    ..
634                } => {
635                    assert!(*base64);
636                    v.extend_from_slice(
637                        &art_out[usize::try_from(*offset).unwrap()
638                            ..usize::try_from(*offset + len.get()).unwrap()],
639                    );
640                }
641                other => panic!("unexpected segment {other:?}"),
642            }
643        }
644        v
645    }
646
647    #[test]
648    fn chunk_lacer_splits_art_across_pages_and_crcs_validate() {
649        use super::super::art_source::MapArtSource;
650        // A packet: 50 literal bytes, then a 60_000-byte art run (spans pages), then
651        // 10 trailing literal bytes.
652        let head = vec![0xA0u8; 50];
653        // 60_000 raw bytes -> b64 output ~80_000 > one page (65025), so it spans pages.
654        let image: Vec<u8> = (0..60_000u32).map(|i| (i % 251) as u8).collect();
655        let art_out = crate::ogg::b64::encode_b64_slice(
656            &image,
657            0,
658            crate::convert::usize_from(crate::ogg::b64::b64_len(image.len() as u64)),
659        )
660        .expect("full-length window lies within the encoded output");
661        let tail = vec![0xB0u8; 10];
662        let chunks = vec![
663            PayloadChunk::Bytes(head.clone()),
664            PayloadChunk::Art {
665                art_id: 42,
666                base64: true,
667                art_total: image.len() as u64,
668            },
669            PayloadChunk::Bytes(tail.clone()),
670        ];
671        let src = MapArtSource::new([(42i64, image.clone())]);
672        let (segments, pages) = lace_chunks_to_segments(0x1234, 0, true, &chunks, &src).unwrap();
673        assert!(pages >= 2, "art run should span multiple pages");
674
675        // Reassemble the packet payload and confirm it equals head ++ art_out ++ tail.
676        let flat = flatten(&segments, &art_out);
677        // Walk pages: validate CRC + collect payloads.
678        let mut pos = 0usize;
679        let mut payload = Vec::new();
680        let mut seq_expected = 0u32;
681        while pos < flat.len() {
682            let h = parse_page(&flat, pos).unwrap();
683            // Flag-bit kills: BOS only on the first page, FLAG_CONTINUED on every
684            // later page (kills :263 |=->&= on BOS, :266 on CONTINUED, :265 delete !).
685            let is_first = seq_expected == 0;
686            if is_first {
687                assert_eq!(h.header_type & FLAG_BOS, FLAG_BOS);
688                assert_eq!(h.header_type & FLAG_CONTINUED, 0);
689            } else {
690                assert_eq!(h.header_type & FLAG_BOS, 0);
691                assert_eq!(h.header_type & FLAG_CONTINUED, FLAG_CONTINUED);
692            }
693            assert_eq!(h.seq, seq_expected);
694            seq_expected += 1;
695            // CRC self-check.
696            let mut z = flat[pos..pos + h.total_len()].to_vec();
697            z[22..26].copy_from_slice(&0u32.to_le_bytes());
698            assert_eq!(crc32(&z), h.crc);
699            payload.extend_from_slice(&flat[pos + h.header_len..pos + h.total_len()]);
700            pos += h.total_len();
701        }
702        let mut expected = head.clone();
703        expected.extend_from_slice(&art_out);
704        expected.extend_from_slice(&tail);
705        assert_eq!(payload, expected);
706
707        // The art bytes must be carried by OggArtSlice segments (not Inline).
708        let art_served: u64 = segments
709            .iter()
710            .filter_map(|s| match s {
711                Segment::OggArtSlice { len, .. } => Some(len.get()),
712                _ => None,
713            })
714            .sum();
715        assert_eq!(art_served, art_out.len() as u64);
716    }
717
718    #[test]
719    fn eos_bit_is_preserved_through_renumber() {
720        // Build a one-page packet, set its EOS bit, repatch the CRC, then renumber
721        // via patch_page_header and confirm header_type (incl. EOS) is unchanged.
722        let (mut page, _) = lace_packet(0xEE, 3, false, 9, &[0x11u8; 120]);
723        page[5] |= FLAG_EOS; // header_type byte
724        // Recompute the CRC over the EOS-modified page (CRC field zeroed first).
725        let mut z = page.clone();
726        z[22..26].copy_from_slice(&0u32.to_le_bytes());
727        let crc = crc32(&z);
728        page[22..26].copy_from_slice(&crc.to_le_bytes());
729
730        let patched = patch_page_header(&page, 99).unwrap();
731        let h0 = parse_page(&page, 0).unwrap();
732        let mut full = patched.clone();
733        full.extend_from_slice(&page[h0.header_len..h0.total_len()]);
734        let h1 = parse_page(&full, 0).unwrap();
735        assert_eq!(h1.seq, 99);
736        assert_eq!(h1.header_type & FLAG_EOS, FLAG_EOS, "EOS bit dropped");
737        assert_eq!(h1.header_type, h0.header_type, "header_type changed");
738    }
739
740    #[test]
741    fn parse_page_rejects_truncated_header_and_table() {
742        // Truncated 27-byte header (kills :33 `> -> ==`/`>=`).
743        let p = hand_page();
744        assert_eq!(parse_page(&p[..26], 0), Err(FormatError::Malformed));
745        assert!(parse_page(&p[..27], 0).is_err()); // header present but table missing
746        // Header present, segment table truncated (kills :47).
747        assert_eq!(parse_page(&p[..28], 0), Err(FormatError::Malformed));
748        // Exactly full header+table+payload parses.
749        assert!(parse_page(&p, 0).is_ok());
750    }
751
752    #[test]
753    fn parse_page_accepts_27_byte_zero_segment_page() {
754        // A zero-segment page is exactly the 27-byte fixed header (no lacing table,
755        // no payload). `pos + 27 == buf.len()` is the valid boundary; `>=` would
756        // wrongly reject a page that fills the buffer exactly.
757        let mut page = vec![0u8; 27];
758        page[0..4].copy_from_slice(CAPTURE); // "OggS"
759        page[26] = 0; // seg_count = 0
760        let h = parse_page(&page, 0).unwrap();
761        assert_eq!(h.seg_count, 0);
762        assert_eq!(h.total_len(), 27);
763    }
764
765    #[test]
766    fn patch_page_header_rejects_truncated_page() {
767        let (page, _) = lace_packet(0xCAFE, 1, false, 0, &vec![0x42u8; 300]);
768        let h = parse_page(&page, 0).unwrap();
769        // Hand a buffer shorter than total_len: original returns Err; the `>` mutant
770        // would proceed and panic slicing page[..total_len].
771        assert_eq!(
772            patch_page_header(&page[..h.total_len() - 10], 2),
773            Err(FormatError::Malformed)
774        );
775    }
776
777    #[test]
778    fn read_packets_stops_exactly_at_want_within_a_page() {
779        // One page carrying two complete packets (two lacing values < 255).
780        // want=1 must return after the first, ignoring the second on the same page.
781        let mut page = Vec::new();
782        page.extend_from_slice(CAPTURE);
783        page.push(0);
784        page.push(FLAG_BOS);
785        page.extend_from_slice(&0u64.to_le_bytes());
786        page.extend_from_slice(&7u32.to_le_bytes());
787        page.extend_from_slice(&0u32.to_le_bytes());
788        page.extend_from_slice(&0u32.to_le_bytes()); // crc placeholder
789        page.push(2); // 2 segments
790        page.push(3); // packet A: 3 bytes
791        page.push(4); // packet B: 4 bytes
792        page.extend_from_slice(&[1, 2, 3, 9, 9, 9, 9]);
793        let mut z = page.clone();
794        z[22..26].copy_from_slice(&0u32.to_le_bytes());
795        let crc = crc32(&z);
796        page[22..26].copy_from_slice(&crc.to_le_bytes());
797
798        let pkts = read_packets(&page, 1).unwrap();
799        assert_eq!(pkts.len(), 1);
800        assert_eq!(pkts[0].data, vec![1, 2, 3]);
801    }
802
803    #[test]
804    fn patch_algebraic_matches_full_page() {
805        // For each combination of payload size and seq values, the algebraic
806        // patch must produce the same header bytes as the full-page oracle.
807        for &payload_len in &[0usize, 1, 255, 3000, 65025] {
808            for &old_seq in &[0u32, 1, 42, u32::MAX - 5] {
809                for &new_seq in &[old_seq, old_seq.wrapping_add(1), old_seq.wrapping_add(10)] {
810                    let payload = vec![0xA5u8; payload_len];
811                    let (page_bytes, _) = lace_packet(0x1234, old_seq, false, 0, &payload);
812                    // Full-page oracle (existing function).
813                    let want = patch_page_header(&page_bytes, new_seq).unwrap();
814                    // Header-only algebraic version.
815                    let h = parse_page(&page_bytes, 0).unwrap();
816                    let got =
817                        patch_page_header_algebraic(&page_bytes[..h.header_len], new_seq).unwrap();
818                    assert_eq!(
819                        got, want,
820                        "payload_len={payload_len} old_seq={old_seq} new_seq={new_seq}"
821                    );
822                }
823            }
824        }
825    }
826
827    #[test]
828    fn verify_page_crc_accepts_valid_rejects_tampered() {
829        // A freshly laced page has a correct CRC.
830        let (page, _) = lace_packet(0x55, 9, false, 42, &vec![0x7Eu8; 500]);
831        assert!(
832            verify_page_crc(&page).unwrap(),
833            "valid page must verify true"
834        );
835        // Flip one payload byte → CRC no longer matches.
836        let mut tampered = page.clone();
837        let h = parse_page(&page, 0).unwrap();
838        tampered[h.header_len] ^= 0xFF; // first payload byte
839        assert!(
840            !verify_page_crc(&tampered).unwrap(),
841            "tampered payload must verify false"
842        );
843        // Corrupt the stored CRC field directly → also false.
844        let mut bad_crc = page.clone();
845        bad_crc[22] ^= 0x01;
846        assert!(
847            !verify_page_crc(&bad_crc).unwrap(),
848            "corrupt stored CRC must verify false"
849        );
850    }
851
852    #[test]
853    fn patch_algebraic_accepts_zero_segment_header() {
854        // A valid 0-segment page header is exactly 27 bytes (header_len == 27), so it
855        // exercises the `header.len() < 27` guard at the boundary: it must be
856        // accepted. `<`->`==`/`<=` would reject this valid header.
857        let mut hdr = vec![0u8; 27];
858        hdr[..4].copy_from_slice(b"OggS");
859        hdr[18..22].copy_from_slice(&7u32.to_le_bytes()); // old_seq
860        // byte 26 (seg_count) == 0 → header_len 27, payload_len 0.
861        let out = patch_page_header_algebraic(&hdr, 9).unwrap();
862        assert_eq!(out.len(), 27);
863        assert_eq!(u32::from_le_bytes(out[18..22].try_into().unwrap()), 9);
864    }
865
866    #[test]
867    fn patch_algebraic_rejects_truncated_segment_table() {
868        // A 27-byte header whose seg_count byte claims 5 segments → header_len 32 >
869        // 27 bytes provided. The `header.len() < header_len` guard must reject it;
870        // `<`->`>` would proceed and read past the slice.
871        let mut hdr = vec![0u8; 27];
872        hdr[..4].copy_from_slice(b"OggS");
873        hdr[26] = 5; // seg_count 5 → header_len 32
874        assert!(patch_page_header_algebraic(&hdr, 1).is_err());
875    }
876
877    #[test]
878    fn verify_page_crc_rejects_truncated_page() {
879        // A page buffer shorter than its declared total_len. The `page.len() <
880        // total_len` guard must reject it; `<`->`>` would slice past the buffer.
881        let (page, _) = lace_packet(0x55, 1, false, 0, &vec![0u8; 300]);
882        let h = parse_page(&page, 0).unwrap();
883        let truncated = &page[..h.total_len() - 10];
884        assert!(verify_page_crc(truncated).is_err());
885    }
886}