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
7pub const FLAG_CONTINUED: u8 = 0x01;
9pub const FLAG_BOS: u8 = 0x02;
10#[allow(dead_code)]
11pub const FLAG_EOS: u8 = 0x04;
12
13#[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
33pub 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
69pub(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
78pub 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 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()); 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
132pub 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#[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
155pub 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
194pub 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
212pub 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 let delta_bytes = (old_seq ^ new_seq).to_le_bytes();
239 let trailing = 5 + seg_count + payload_len; 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
248pub 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
264pub(crate) enum PayloadChunk {
266 Bytes(Vec<u8>),
268 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
297pub(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 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()); 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()); 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
358fn 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
400fn 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
426fn 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); p.push(FLAG_BOS); p.extend_from_slice(&0u64.to_le_bytes()); p.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); p.extend_from_slice(&7u32.to_le_bytes()); p.extend_from_slice(&0x1122_3344u32.to_le_bytes()); p.push(2); p.push(0x10);
486 p.push(0x20); 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; 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 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 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]; 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 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 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 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 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 let head = vec![0xA0u8; 50];
653 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 let flat = flatten(&segments, &art_out);
677 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 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 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 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 let (mut page, _) = lace_packet(0xEE, 3, false, 9, &[0x11u8; 120]);
723 page[5] |= FLAG_EOS; 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 let p = hand_page();
744 assert_eq!(parse_page(&p[..26], 0), Err(FormatError::Malformed));
745 assert!(parse_page(&p[..27], 0).is_err()); assert_eq!(parse_page(&p[..28], 0), Err(FormatError::Malformed));
748 assert!(parse_page(&p, 0).is_ok());
750 }
751
752 #[test]
753 fn parse_page_accepts_27_byte_zero_segment_page() {
754 let mut page = vec![0u8; 27];
758 page[0..4].copy_from_slice(CAPTURE); page[26] = 0; 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 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 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()); page.push(2); page.push(3); page.push(4); 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 &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 let want = patch_page_header(&page_bytes, new_seq).unwrap();
814 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 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 let mut tampered = page.clone();
837 let h = parse_page(&page, 0).unwrap();
838 tampered[h.header_len] ^= 0xFF; assert!(
840 !verify_page_crc(&tampered).unwrap(),
841 "tampered payload must verify false"
842 );
843 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 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()); 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 let mut hdr = vec![0u8; 27];
872 hdr[..4].copy_from_slice(b"OggS");
873 hdr[26] = 5; assert!(patch_page_header_algebraic(&hdr, 1).is_err());
875 }
876
877 #[test]
878 fn verify_page_crc_rejects_truncated_page() {
879 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}