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        *crc = crc32_update(*crc, &enc);
417    } else {
418        let mut raw = vec![0u8; out_len];
419        src.read_window(art_id, out_off, &mut raw)?;
420        *crc = crc32_update(*crc, &raw);
421    }
422    Ok(())
423}
424
425/// Emit the page header + payload `[p0, p0+plen)` as layout segments: `Inline` for
426/// the header and literal byte spans, `OggArtSlice` for art spans.
427fn emit_segments(
428    segments: &mut Vec<Segment>,
429    header: &[u8],
430    chunks: &[PayloadChunk],
431    p0: usize,
432    plen: usize,
433) {
434    let end = p0 + plen;
435    let mut buf: Vec<u8> = header.to_vec();
436    let mut cs = 0usize;
437    for c in chunks {
438        let ce = cs + c.out_len();
439        let os = p0.max(cs);
440        let oe = end.min(ce);
441        if os < oe {
442            match c {
443                PayloadChunk::Bytes(b) => buf.extend_from_slice(&b[os - cs..oe - cs]),
444                PayloadChunk::Art {
445                    art_id,
446                    base64,
447                    art_total,
448                } => {
449                    if !buf.is_empty() {
450                        segments.push(Segment::Inline(std::mem::take(&mut buf)));
451                    }
452                    segments.push(Segment::OggArtSlice {
453                        art_id: *art_id,
454                        offset: (os - cs) as u64,
455                        len: crate::BlobLen::new((oe - os) as u64)
456                            .expect("ogg art slice span is non-empty"),
457                        base64: *base64,
458                        art_total: *art_total,
459                    });
460                }
461            }
462        }
463        cs = ce;
464    }
465    if !buf.is_empty() {
466        segments.push(Segment::Inline(buf));
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    fn hand_page() -> Vec<u8> {
475        let mut p = Vec::new();
476        p.extend_from_slice(CAPTURE);
477        p.push(0); // version
478        p.push(FLAG_BOS); // header_type
479        p.extend_from_slice(&0u64.to_le_bytes()); // granule
480        p.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); // serial
481        p.extend_from_slice(&7u32.to_le_bytes()); // seq
482        p.extend_from_slice(&0x1122_3344u32.to_le_bytes()); // crc field (as stored)
483        p.push(2); // seg_count
484        p.push(0x10);
485        p.push(0x20); // segment table => payload 0x30
486        p.extend(std::iter::repeat_n(0xAB, 0x30));
487        p
488    }
489
490    #[test]
491    fn parses_fields_and_lengths() {
492        let p = hand_page();
493        let h = parse_page(&p, 0).unwrap();
494        assert_eq!(h.header_type, FLAG_BOS);
495        assert_eq!(h.serial, 0xDEAD_BEEF);
496        assert_eq!(h.seq, 7);
497        assert_eq!(h.crc, 0x1122_3344);
498        assert_eq!(h.seg_count, 2);
499        assert_eq!(h.payload_len, 0x30);
500        assert_eq!(h.header_len, 29);
501        assert_eq!(h.total_len(), 0x30 + 29);
502    }
503
504    #[test]
505    fn rejects_bad_capture() {
506        let mut p = hand_page();
507        p[0] = b'X';
508        assert_eq!(parse_page(&p, 0), Err(FormatError::Malformed));
509    }
510
511    #[test]
512    fn rejects_nonzero_version() {
513        let mut p = hand_page();
514        p[4] = 1; // bad version
515        assert_eq!(parse_page(&p, 0), Err(FormatError::Malformed));
516    }
517
518    #[test]
519    fn single_page_packet_round_trips_and_crc_valid() {
520        let packet: Vec<u8> = (0..200u8).collect();
521        let (bytes, pages) = lace_packet(0xABCD, 0, true, 0, &packet);
522        assert_eq!(pages, 1);
523        let h = parse_page(&bytes, 0).unwrap();
524        assert_eq!(h.header_type, FLAG_BOS);
525        assert_eq!(h.payload_len, 200);
526        // CRC self-check: zero the field, recompute, compare to stored.
527        let mut z = bytes.clone();
528        z[22..26].copy_from_slice(&0u32.to_le_bytes());
529        assert_eq!(crc32(&z), h.crc);
530    }
531
532    #[test]
533    fn exact_multiple_of_255_appends_terminating_zero() {
534        let packet = vec![0u8; 255];
535        let (bytes, pages) = lace_packet(1, 0, false, 0, &packet);
536        assert_eq!(pages, 1);
537        let h = parse_page(&bytes, 0).unwrap();
538        // 255 + terminating 0 => two lacing values, both summing to 255 payload.
539        assert_eq!(h.seg_count, 2);
540        assert_eq!(h.payload_len, 255);
541    }
542
543    #[test]
544    fn large_packet_spans_multiple_pages_with_continuation() {
545        let packet = vec![0x5Au8; 70_000]; // > 65 025 => 2 pages
546        let (bytes, pages) = lace_packet(2, 5, false, 0, &packet);
547        assert_eq!(pages, 2);
548        let p0 = parse_page(&bytes, 0).unwrap();
549        assert_eq!(p0.header_type & FLAG_CONTINUED, 0);
550        assert_eq!(p0.payload_len, 65_025);
551        let p1 = parse_page(&bytes, p0.total_len()).unwrap();
552        assert_eq!(p1.header_type & FLAG_CONTINUED, FLAG_CONTINUED);
553        assert_eq!(p1.seq, 6);
554        assert_eq!(p0.payload_len + p1.payload_len, 70_000);
555    }
556
557    #[test]
558    fn build_header_numbers_pages_and_sets_bos_once() {
559        let a = vec![1u8; 10];
560        let b = vec![2u8; 10];
561        let (bytes, count) = build_header(9, &[&a, &b]);
562        assert_eq!(count, 2);
563        let p0 = parse_page(&bytes, 0).unwrap();
564        let p1 = parse_page(&bytes, p0.total_len()).unwrap();
565        assert_eq!(p0.header_type & FLAG_BOS, FLAG_BOS);
566        assert_eq!(p1.header_type & FLAG_BOS, 0);
567        assert_eq!(p0.seq, 0);
568        assert_eq!(p1.seq, 1);
569    }
570
571    #[test]
572    fn read_packets_reassembles_multipage_packet() {
573        // One small packet, then one packet that spans two pages.
574        let small = vec![7u8; 5];
575        let big = vec![9u8; 70_000];
576        let (mut bytes, _) = lace_packet(3, 0, true, 0, &small);
577        let (b2, _) = lace_packet(3, 1, false, 0, &big);
578        bytes.extend_from_slice(&b2);
579
580        let pkts = read_packets(&bytes, 2).unwrap();
581        assert_eq!(pkts.len(), 2);
582        assert_eq!(pkts[0].data, small);
583        assert_eq!(pkts[1].data, big);
584        assert_eq!(pkts[1].pages_through_end, 3);
585        assert_eq!(pkts[1].end_offset, bytes.len());
586    }
587
588    #[test]
589    fn multipage_payload_cursor_advances_across_pages() {
590        // A >65 025-byte packet with DISTINCT bytes at every position: the second
591        // page must carry the *tail* of the packet, not a re-read from offset 0.
592        // Pins `payload_pos += page_payload` against a cursor stuck at 0 (a uniform
593        // payload, as in the other multi-page tests, can't observe this).
594        let packet: Vec<u8> = (0..70_000u32).map(|i| (i % 251) as u8).collect();
595        let (bytes, pages) = lace_packet(2, 0, false, 0, &packet);
596        assert_eq!(pages, 2);
597        let pkts = read_packets(&bytes, 1).unwrap();
598        assert_eq!(pkts[0].data, packet);
599    }
600
601    #[test]
602    fn patch_page_header_updates_seq_and_crc() {
603        let packet = vec![0x42u8; 300];
604        let (page, _) = lace_packet(0xCAFE, 10, false, 7, &packet);
605        let patched = patch_page_header(&page, 12).unwrap();
606        let h0 = parse_page(&page, 0).unwrap();
607        assert_eq!(patched.len(), h0.header_len);
608        // Reassemble a full page from the patched header + original payload and
609        // verify the parsed seq and a self-consistent CRC.
610        let mut full = patched.clone();
611        full.extend_from_slice(&page[h0.header_len..h0.total_len()]);
612        let h1 = parse_page(&full, 0).unwrap();
613        assert_eq!(h1.seq, 12);
614        let mut z = full.clone();
615        z[22..26].copy_from_slice(&0u32.to_le_bytes());
616        assert_eq!(crc32(&z), h1.crc);
617    }
618
619    use crate::layout::Segment;
620
621    // Reconstruct the laced byte stream from segments, expanding OggArtSlice from a
622    // provided art output map, so we can validate framing/CRCs end to end.
623    fn flatten(segments: &[Segment], art_out: &[u8]) -> Vec<u8> {
624        let mut v = Vec::new();
625        for s in segments {
626            match s {
627                Segment::Inline(b) => v.extend_from_slice(b),
628                Segment::OggArtSlice {
629                    offset,
630                    len,
631                    base64,
632                    ..
633                } => {
634                    assert!(*base64);
635                    v.extend_from_slice(
636                        &art_out[usize::try_from(*offset).unwrap()
637                            ..usize::try_from(*offset + len.get()).unwrap()],
638                    );
639                }
640                other => panic!("unexpected segment {other:?}"),
641            }
642        }
643        v
644    }
645
646    #[test]
647    fn chunk_lacer_splits_art_across_pages_and_crcs_validate() {
648        use super::super::art_source::MapArtSource;
649        // A packet: 50 literal bytes, then a 60_000-byte art run (spans pages), then
650        // 10 trailing literal bytes.
651        let head = vec![0xA0u8; 50];
652        // 60_000 raw bytes -> b64 output ~80_000 > one page (65025), so it spans pages.
653        let image: Vec<u8> = (0..60_000u32).map(|i| (i % 251) as u8).collect();
654        let art_out = crate::ogg::b64::encode_b64_slice(
655            &image,
656            0,
657            crate::convert::usize_from(crate::ogg::b64::b64_len(image.len() as u64)),
658        );
659        let tail = vec![0xB0u8; 10];
660        let chunks = vec![
661            PayloadChunk::Bytes(head.clone()),
662            PayloadChunk::Art {
663                art_id: 42,
664                base64: true,
665                art_total: image.len() as u64,
666            },
667            PayloadChunk::Bytes(tail.clone()),
668        ];
669        let src = MapArtSource::new([(42i64, image.clone())]);
670        let (segments, pages) = lace_chunks_to_segments(0x1234, 0, true, &chunks, &src).unwrap();
671        assert!(pages >= 2, "art run should span multiple pages");
672
673        // Reassemble the packet payload and confirm it equals head ++ art_out ++ tail.
674        let flat = flatten(&segments, &art_out);
675        // Walk pages: validate CRC + collect payloads.
676        let mut pos = 0usize;
677        let mut payload = Vec::new();
678        let mut seq_expected = 0u32;
679        while pos < flat.len() {
680            let h = parse_page(&flat, pos).unwrap();
681            // Flag-bit kills: BOS only on the first page, FLAG_CONTINUED on every
682            // later page (kills :263 |=->&= on BOS, :266 on CONTINUED, :265 delete !).
683            let is_first = seq_expected == 0;
684            if is_first {
685                assert_eq!(h.header_type & FLAG_BOS, FLAG_BOS);
686                assert_eq!(h.header_type & FLAG_CONTINUED, 0);
687            } else {
688                assert_eq!(h.header_type & FLAG_BOS, 0);
689                assert_eq!(h.header_type & FLAG_CONTINUED, FLAG_CONTINUED);
690            }
691            assert_eq!(h.seq, seq_expected);
692            seq_expected += 1;
693            // CRC self-check.
694            let mut z = flat[pos..pos + h.total_len()].to_vec();
695            z[22..26].copy_from_slice(&0u32.to_le_bytes());
696            assert_eq!(crc32(&z), h.crc);
697            payload.extend_from_slice(&flat[pos + h.header_len..pos + h.total_len()]);
698            pos += h.total_len();
699        }
700        let mut expected = head.clone();
701        expected.extend_from_slice(&art_out);
702        expected.extend_from_slice(&tail);
703        assert_eq!(payload, expected);
704
705        // The art bytes must be carried by OggArtSlice segments (not Inline).
706        let art_served: u64 = segments
707            .iter()
708            .filter_map(|s| match s {
709                Segment::OggArtSlice { len, .. } => Some(len.get()),
710                _ => None,
711            })
712            .sum();
713        assert_eq!(art_served, art_out.len() as u64);
714    }
715
716    #[test]
717    fn eos_bit_is_preserved_through_renumber() {
718        // Build a one-page packet, set its EOS bit, repatch the CRC, then renumber
719        // via patch_page_header and confirm header_type (incl. EOS) is unchanged.
720        let (mut page, _) = lace_packet(0xEE, 3, false, 9, &[0x11u8; 120]);
721        page[5] |= FLAG_EOS; // header_type byte
722        // Recompute the CRC over the EOS-modified page (CRC field zeroed first).
723        let mut z = page.clone();
724        z[22..26].copy_from_slice(&0u32.to_le_bytes());
725        let crc = crc32(&z);
726        page[22..26].copy_from_slice(&crc.to_le_bytes());
727
728        let patched = patch_page_header(&page, 99).unwrap();
729        let h0 = parse_page(&page, 0).unwrap();
730        let mut full = patched.clone();
731        full.extend_from_slice(&page[h0.header_len..h0.total_len()]);
732        let h1 = parse_page(&full, 0).unwrap();
733        assert_eq!(h1.seq, 99);
734        assert_eq!(h1.header_type & FLAG_EOS, FLAG_EOS, "EOS bit dropped");
735        assert_eq!(h1.header_type, h0.header_type, "header_type changed");
736    }
737
738    #[test]
739    fn parse_page_rejects_truncated_header_and_table() {
740        // Truncated 27-byte header (kills :33 `> -> ==`/`>=`).
741        let p = hand_page();
742        assert_eq!(parse_page(&p[..26], 0), Err(FormatError::Malformed));
743        assert!(parse_page(&p[..27], 0).is_err()); // header present but table missing
744        // Header present, segment table truncated (kills :47).
745        assert_eq!(parse_page(&p[..28], 0), Err(FormatError::Malformed));
746        // Exactly full header+table+payload parses.
747        assert!(parse_page(&p, 0).is_ok());
748    }
749
750    #[test]
751    fn patch_page_header_rejects_truncated_page() {
752        let (page, _) = lace_packet(0xCAFE, 1, false, 0, &vec![0x42u8; 300]);
753        let h = parse_page(&page, 0).unwrap();
754        // Hand a buffer shorter than total_len: original returns Err; the `>` mutant
755        // would proceed and panic slicing page[..total_len].
756        assert_eq!(
757            patch_page_header(&page[..h.total_len() - 10], 2),
758            Err(FormatError::Malformed)
759        );
760    }
761
762    #[test]
763    fn read_packets_stops_exactly_at_want_within_a_page() {
764        // One page carrying two complete packets (two lacing values < 255).
765        // want=1 must return after the first, ignoring the second on the same page.
766        let mut page = Vec::new();
767        page.extend_from_slice(CAPTURE);
768        page.push(0);
769        page.push(FLAG_BOS);
770        page.extend_from_slice(&0u64.to_le_bytes());
771        page.extend_from_slice(&7u32.to_le_bytes());
772        page.extend_from_slice(&0u32.to_le_bytes());
773        page.extend_from_slice(&0u32.to_le_bytes()); // crc placeholder
774        page.push(2); // 2 segments
775        page.push(3); // packet A: 3 bytes
776        page.push(4); // packet B: 4 bytes
777        page.extend_from_slice(&[1, 2, 3, 9, 9, 9, 9]);
778        let mut z = page.clone();
779        z[22..26].copy_from_slice(&0u32.to_le_bytes());
780        let crc = crc32(&z);
781        page[22..26].copy_from_slice(&crc.to_le_bytes());
782
783        let pkts = read_packets(&page, 1).unwrap();
784        assert_eq!(pkts.len(), 1);
785        assert_eq!(pkts[0].data, vec![1, 2, 3]);
786    }
787
788    #[test]
789    fn patch_algebraic_matches_full_page() {
790        // For each combination of payload size and seq values, the algebraic
791        // patch must produce the same header bytes as the full-page oracle.
792        for &payload_len in &[0usize, 1, 255, 3000, 65025] {
793            for &old_seq in &[0u32, 1, 42, u32::MAX - 5] {
794                for &new_seq in &[old_seq, old_seq.wrapping_add(1), old_seq.wrapping_add(10)] {
795                    let payload = vec![0xA5u8; payload_len];
796                    let (page_bytes, _) = lace_packet(0x1234, old_seq, false, 0, &payload);
797                    // Full-page oracle (existing function).
798                    let want = patch_page_header(&page_bytes, new_seq).unwrap();
799                    // Header-only algebraic version.
800                    let h = parse_page(&page_bytes, 0).unwrap();
801                    let got =
802                        patch_page_header_algebraic(&page_bytes[..h.header_len], new_seq).unwrap();
803                    assert_eq!(
804                        got, want,
805                        "payload_len={payload_len} old_seq={old_seq} new_seq={new_seq}"
806                    );
807                }
808            }
809        }
810    }
811
812    #[test]
813    fn verify_page_crc_accepts_valid_rejects_tampered() {
814        // A freshly laced page has a correct CRC.
815        let (page, _) = lace_packet(0x55, 9, false, 42, &vec![0x7Eu8; 500]);
816        assert!(
817            verify_page_crc(&page).unwrap(),
818            "valid page must verify true"
819        );
820        // Flip one payload byte → CRC no longer matches.
821        let mut tampered = page.clone();
822        let h = parse_page(&page, 0).unwrap();
823        tampered[h.header_len] ^= 0xFF; // first payload byte
824        assert!(
825            !verify_page_crc(&tampered).unwrap(),
826            "tampered payload must verify false"
827        );
828        // Corrupt the stored CRC field directly → also false.
829        let mut bad_crc = page.clone();
830        bad_crc[22] ^= 0x01;
831        assert!(
832            !verify_page_crc(&bad_crc).unwrap(),
833            "corrupt stored CRC must verify false"
834        );
835    }
836
837    #[test]
838    fn patch_algebraic_accepts_zero_segment_header() {
839        // A valid 0-segment page header is exactly 27 bytes (header_len == 27), so it
840        // exercises the `header.len() < 27` guard at the boundary: it must be
841        // accepted. `<`->`==`/`<=` would reject this valid header.
842        let mut hdr = vec![0u8; 27];
843        hdr[..4].copy_from_slice(b"OggS");
844        hdr[18..22].copy_from_slice(&7u32.to_le_bytes()); // old_seq
845        // byte 26 (seg_count) == 0 → header_len 27, payload_len 0.
846        let out = patch_page_header_algebraic(&hdr, 9).unwrap();
847        assert_eq!(out.len(), 27);
848        assert_eq!(u32::from_le_bytes(out[18..22].try_into().unwrap()), 9);
849    }
850
851    #[test]
852    fn patch_algebraic_rejects_truncated_segment_table() {
853        // A 27-byte header whose seg_count byte claims 5 segments → header_len 32 >
854        // 27 bytes provided. The `header.len() < header_len` guard must reject it;
855        // `<`->`>` would proceed and read past the slice.
856        let mut hdr = vec![0u8; 27];
857        hdr[..4].copy_from_slice(b"OggS");
858        hdr[26] = 5; // seg_count 5 → header_len 32
859        assert!(patch_page_header_algebraic(&hdr, 1).is_err());
860    }
861
862    #[test]
863    fn verify_page_crc_rejects_truncated_page() {
864        // A page buffer shorter than its declared total_len. The `page.len() <
865        // total_len` guard must reject it; `<`->`>` would slice past the buffer.
866        let (page, _) = lace_packet(0x55, 1, false, 0, &vec![0u8; 300]);
867        let h = parse_page(&page, 0).unwrap();
868        let truncated = &page[..h.total_len() - 10];
869        assert!(verify_page_crc(truncated).is_err());
870    }
871}