Skip to main content

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}