rustpix_io/scanner.rs
1//! Section scanner for TPX3 files.
2//!
3//! Identifies logical sections in the file based on TPX3 headers.
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8/// A section of the TPX3 file belonging to a specific chip.
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11pub struct Section {
12 /// Start offset in bytes (inclusive).
13 pub start_offset: usize,
14 /// End offset in bytes (exclusive).
15 pub end_offset: usize,
16 /// Chip ID for this section.
17 pub chip_id: u8,
18}
19
20/// Scanner for discovering sections in TPX3 data.
21pub struct PacketScanner;
22
23impl PacketScanner {
24 /// Scans the provided data for TPX3 sections.
25 ///
26 /// The data should be 8-byte aligned.
27 ///
28 /// # Arguments
29 /// * `data` - The byte slice to scan.
30 /// * `is_eof` - Whether this is the final chunk of data.
31 ///
32 /// # Returns
33 /// A tuple `(sections, consumed_bytes)`.
34 /// `consumed_bytes` indicates how many bytes can be safely advanced.
35 /// Bytes after `consumed_bytes` belong to an incomplete section (unless `is_eof`).
36 ///
37 /// # Panics
38 /// Panics if a chunk is not exactly 8 bytes. This should be unreachable because
39 /// `chunks_exact(8)` guarantees each chunk length.
40 #[must_use]
41 pub fn scan_sections(data: &[u8], is_eof: bool) -> (Vec<Section>, usize) {
42 let mut sections = Vec::new();
43 let mut current_section_start = 0;
44 let mut current_chip_id = 0;
45 let mut in_section = false;
46
47 // Track the end of the last fully completed section
48 let mut consumed_bytes = 0;
49
50 // Iterate over data in 8-byte chunks
51 for (offset, chunk) in data.chunks_exact(8).enumerate() {
52 let offset_bytes = offset * 8;
53 let packet = u64::from_le_bytes(chunk.try_into().unwrap());
54
55 // Check if this is a TPX3 header: magic "TPX3" (0x33585054) in lower 32 bits
56 if (packet & 0xFFFF_FFFF) == 0x3358_5054 {
57 let chip_id = ((packet >> 32) & 0xFF) as u8;
58
59 if in_section {
60 // Close previous section
61 sections.push(Section {
62 start_offset: current_section_start,
63 end_offset: offset_bytes,
64 chip_id: current_chip_id,
65 });
66 // Previous section ended here, so we have consumed up to here
67 consumed_bytes = offset_bytes;
68 }
69
70 // Start new section
71 current_section_start = offset_bytes;
72 current_chip_id = chip_id;
73 in_section = true;
74 }
75 }
76
77 if is_eof {
78 // fast forward consumed to end
79 consumed_bytes = data.len();
80
81 if in_section && data.len() > current_section_start {
82 sections.push(Section {
83 start_offset: current_section_start,
84 end_offset: data.len(),
85 chip_id: current_chip_id,
86 });
87 }
88 } else {
89 // If we are not at EOF, and we are inside a section,
90 // that section is incomplete.
91 // Special case: If we haven't found ANY closed sections (consumed_bytes == 0)
92 // but we found a start header?
93 // Then we return 0 consumed, meaning "need more data".
94 // If the buffer is huge and we still return 0, the section is huge.
95 }
96
97 (sections, consumed_bytes)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::reader::MappedFileReader;
105 use std::path::PathBuf;
106
107 #[test]
108 fn test_scan_tiny_tpx3() {
109 let path = PathBuf::from("../tests/data/tiny.tpx3");
110 if !path.exists() {
111 eprintln!("Skipping test: tiny.tpx3 not found");
112 return;
113 }
114
115 let reader = MappedFileReader::open(path.to_str().unwrap()).unwrap();
116 let (sections, consumed) = PacketScanner::scan_sections(reader.as_bytes(), true);
117
118 let sections_len = sections.len();
119 println!("Found {sections_len} sections");
120 println!("Consumed {consumed} bytes");
121
122 assert_eq!(consumed, reader.len()); // Should consume everything at EOF
123
124 for (i, section) in sections.iter().enumerate() {
125 let chip_id = section.chip_id;
126 let byte_len = section.end_offset - section.start_offset;
127 println!("Section {i}: Chip {chip_id}, {byte_len} bytes");
128 }
129
130 assert!(!sections.is_empty(), "Should find at least one section");
131 // Verify contiguous sections (optional, but good sanity check)
132 for i in 0..sections.len() - 1 {
133 assert_eq!(
134 sections[i].end_offset,
135 sections[i + 1].start_offset,
136 "Sections should be contiguous"
137 );
138 }
139 }
140}