Skip to content

Commit 49697e2

Browse files
committed
support sparse simg from a-i-b
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
1 parent 0df405c commit 49697e2

9 files changed

Lines changed: 1832 additions & 264 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/fls/block_writer.rs

Lines changed: 198 additions & 124 deletions
Large diffs are not rendered by default.

src/fls/format_detector.rs

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
//! Automatic file format detection for streaming data
2+
//!
3+
//! This module provides format detection for incoming data streams, allowing
4+
//! automatic identification of file formats without explicit user specification.
5+
6+
use crate::fls::simg;
7+
8+
/// Detected file formats supported by the format detector
9+
#[derive(Debug, Clone, PartialEq)]
10+
pub enum FileFormat {
11+
/// Android sparse image format
12+
SparseImage,
13+
/// Regular (non-sparse) file format
14+
Regular,
15+
}
16+
17+
/// Result of format detection process
18+
#[derive(Debug)]
19+
pub enum DetectionResult {
20+
/// More data needed to make a format determination
21+
NeedMoreData,
22+
/// Format successfully detected with consumed buffer data
23+
Detected {
24+
format: FileFormat,
25+
consumed_bytes: Vec<u8>,
26+
consumed_from_input: usize,
27+
},
28+
}
29+
30+
/// Streaming format detector that buffers initial bytes to identify file formats
31+
pub struct FormatDetector {
32+
/// Buffer accumulating initial bytes for detection
33+
buffer: Vec<u8>,
34+
/// Whether format detection has completed
35+
detection_complete: bool,
36+
/// Detected format (if any)
37+
detected_format: Option<FileFormat>,
38+
}
39+
40+
impl FormatDetector {
41+
/// Minimum bytes required to detect sparse image format (simg header size)
42+
const DETECTION_THRESHOLD: usize = 28;
43+
44+
/// Maximum buffer size to prevent excessive memory usage during detection
45+
const MAX_BUFFER_SIZE: usize = 64;
46+
47+
/// Create a new format detector
48+
pub fn new() -> Self {
49+
Self {
50+
buffer: Vec::new(),
51+
detection_complete: false,
52+
detected_format: None,
53+
}
54+
}
55+
56+
/// Process incoming data for format detection
57+
///
58+
/// # Arguments
59+
/// * `data` - Incoming data bytes to analyze
60+
///
61+
/// # Returns
62+
/// * `DetectionResult::NeedMoreData` if more data is required
63+
/// * `DetectionResult::Detected` with format and buffered bytes when detection completes
64+
pub fn process(&mut self, data: &[u8]) -> DetectionResult {
65+
// If detection already completed, return the cached result
66+
if self.detection_complete {
67+
return DetectionResult::Detected {
68+
format: self.detected_format.clone().unwrap(),
69+
consumed_bytes: Vec::new(), // No additional bytes to consume
70+
consumed_from_input: 0, // No additional bytes consumed from current input
71+
};
72+
}
73+
74+
// Determine how much data we can safely buffer
75+
let space_left = Self::MAX_BUFFER_SIZE - self.buffer.len();
76+
let take = data.len().min(space_left);
77+
78+
// Add new data to buffer
79+
self.buffer.extend_from_slice(&data[..take]);
80+
81+
// Attempt detection if we have enough data or reached buffer limit
82+
if self.buffer.len() >= Self::DETECTION_THRESHOLD
83+
|| self.buffer.len() >= Self::MAX_BUFFER_SIZE
84+
{
85+
return self.attempt_detection_with_consumed(take);
86+
}
87+
88+
DetectionResult::NeedMoreData
89+
}
90+
91+
/// Attempt to detect the file format from buffered data, tracking consumption from current input
92+
fn attempt_detection_with_consumed(&mut self, consumed_from_input: usize) -> DetectionResult {
93+
// Try to detect sparse image format first
94+
match simg::parse_header(&self.buffer) {
95+
Ok(_header) => {
96+
// Valid sparse image header detected
97+
self.detected_format = Some(FileFormat::SparseImage);
98+
self.detection_complete = true;
99+
100+
DetectionResult::Detected {
101+
format: FileFormat::SparseImage,
102+
consumed_bytes: self.buffer.clone(),
103+
consumed_from_input,
104+
}
105+
}
106+
Err(_) => {
107+
// Not a sparse image, assume regular file format
108+
self.detected_format = Some(FileFormat::Regular);
109+
self.detection_complete = true;
110+
111+
DetectionResult::Detected {
112+
format: FileFormat::Regular,
113+
consumed_bytes: self.buffer.clone(),
114+
consumed_from_input,
115+
}
116+
}
117+
}
118+
}
119+
120+
/// Check if format detection has completed
121+
#[allow(dead_code)]
122+
pub fn is_complete(&self) -> bool {
123+
self.detection_complete
124+
}
125+
126+
/// Get the detected format (if detection is complete)
127+
#[allow(dead_code)]
128+
pub fn detected_format(&self) -> Option<&FileFormat> {
129+
self.detected_format.as_ref()
130+
}
131+
}
132+
133+
impl Default for FormatDetector {
134+
fn default() -> Self {
135+
Self::new()
136+
}
137+
}
138+
139+
#[cfg(test)]
140+
mod tests {
141+
use super::*;
142+
143+
/// Create a valid sparse image header for testing
144+
fn create_test_sparse_header() -> Vec<u8> {
145+
let mut header = Vec::with_capacity(28);
146+
// Magic number: 0xED26FF3A (little endian)
147+
header.extend_from_slice(&0xED26FF3Au32.to_le_bytes());
148+
// Major version: 1
149+
header.extend_from_slice(&1u16.to_le_bytes());
150+
// Minor version: 0
151+
header.extend_from_slice(&0u16.to_le_bytes());
152+
// File header size: 28
153+
header.extend_from_slice(&28u16.to_le_bytes());
154+
// Chunk header size: 12
155+
header.extend_from_slice(&12u16.to_le_bytes());
156+
// Block size: 4096
157+
header.extend_from_slice(&4096u32.to_le_bytes());
158+
// Total blocks: 1000
159+
header.extend_from_slice(&1000u32.to_le_bytes());
160+
// Total chunks: 1
161+
header.extend_from_slice(&1u32.to_le_bytes());
162+
// Checksum: 0
163+
header.extend_from_slice(&0u32.to_le_bytes());
164+
header
165+
}
166+
167+
#[test]
168+
fn test_detect_sparse_image_format() {
169+
let mut detector = FormatDetector::new();
170+
171+
// Create a complete valid sparse image header
172+
let header = create_test_sparse_header();
173+
174+
match detector.process(&header) {
175+
DetectionResult::Detected {
176+
format,
177+
consumed_bytes,
178+
consumed_from_input,
179+
} => {
180+
assert_eq!(format, FileFormat::SparseImage);
181+
assert_eq!(consumed_bytes.len(), 28);
182+
assert_eq!(consumed_from_input, 28);
183+
assert!(detector.is_complete());
184+
}
185+
_ => panic!("Expected detection to complete with sparse image format"),
186+
}
187+
}
188+
189+
#[test]
190+
fn test_detect_regular_file_format() {
191+
let mut detector = FormatDetector::new();
192+
193+
// Use non-sparse data (random bytes)
194+
let data = vec![0x12, 0x34, 0x56, 0x78]; // Not a sparse image magic
195+
let mut full_data = data;
196+
full_data.extend(vec![0u8; 24]); // Pad to 28 bytes
197+
198+
match detector.process(&full_data) {
199+
DetectionResult::Detected {
200+
format,
201+
consumed_bytes,
202+
consumed_from_input,
203+
} => {
204+
assert_eq!(format, FileFormat::Regular);
205+
assert_eq!(consumed_bytes.len(), 28);
206+
assert_eq!(consumed_from_input, 28);
207+
assert!(detector.is_complete());
208+
}
209+
_ => panic!("Expected detection to complete with regular format"),
210+
}
211+
}
212+
213+
#[test]
214+
fn test_need_more_data() {
215+
let mut detector = FormatDetector::new();
216+
217+
// Provide insufficient data (less than 28 bytes)
218+
let partial_data = vec![0x3A, 0xFF, 0x26]; // Only 3 bytes
219+
220+
match detector.process(&partial_data) {
221+
DetectionResult::NeedMoreData => {
222+
assert!(!detector.is_complete());
223+
assert_eq!(detector.detected_format(), None);
224+
}
225+
_ => panic!("Expected to need more data"),
226+
}
227+
}
228+
229+
#[test]
230+
fn test_incremental_detection() {
231+
let mut detector = FormatDetector::new();
232+
233+
// Create sparse image header in parts
234+
let full_header = create_test_sparse_header();
235+
let part1 = full_header[0..4].to_vec(); // Magic number
236+
let part2 = full_header[4..].to_vec(); // Rest of header
237+
238+
// First part - should need more data
239+
match detector.process(&part1) {
240+
DetectionResult::NeedMoreData => assert!(!detector.is_complete()),
241+
_ => panic!("Expected to need more data after first part"),
242+
}
243+
244+
// Second part - should complete detection
245+
match detector.process(&part2) {
246+
DetectionResult::Detected {
247+
format,
248+
consumed_bytes,
249+
consumed_from_input,
250+
} => {
251+
assert_eq!(format, FileFormat::SparseImage);
252+
assert_eq!(consumed_bytes.len(), 28);
253+
assert_eq!(consumed_from_input, part2.len());
254+
assert!(detector.is_complete());
255+
}
256+
_ => panic!("Expected detection to complete after second part"),
257+
}
258+
}
259+
260+
#[test]
261+
fn test_buffer_limit() {
262+
let mut detector = FormatDetector::new();
263+
264+
// Provide more than MAX_BUFFER_SIZE bytes of non-sparse data
265+
let large_data = vec![0x12; FormatDetector::MAX_BUFFER_SIZE + 10];
266+
267+
match detector.process(&large_data) {
268+
DetectionResult::Detected {
269+
format,
270+
consumed_bytes,
271+
consumed_from_input,
272+
} => {
273+
assert_eq!(format, FileFormat::Regular);
274+
assert_eq!(consumed_bytes.len(), FormatDetector::MAX_BUFFER_SIZE);
275+
assert_eq!(consumed_from_input, FormatDetector::MAX_BUFFER_SIZE);
276+
assert!(detector.is_complete());
277+
}
278+
_ => panic!("Expected detection to complete with buffer limit reached"),
279+
}
280+
}
281+
282+
#[test]
283+
fn test_detection_idempotent() {
284+
let mut detector = FormatDetector::new();
285+
286+
// Complete detection with sparse image
287+
let header = create_test_sparse_header();
288+
289+
// First call completes detection
290+
let result1 = detector.process(&header);
291+
assert!(matches!(result1, DetectionResult::Detected { .. }));
292+
293+
// Subsequent calls should return cached result without consuming more data
294+
let result2 = detector.process(&[0x99, 0x88, 0x77]);
295+
match result2 {
296+
DetectionResult::Detected {
297+
format,
298+
consumed_bytes,
299+
consumed_from_input,
300+
} => {
301+
assert_eq!(format, FileFormat::SparseImage);
302+
assert_eq!(consumed_bytes.len(), 0); // No new bytes consumed
303+
assert_eq!(consumed_from_input, 0); // No new bytes consumed from input
304+
}
305+
_ => panic!("Expected cached detection result"),
306+
}
307+
}
308+
}

0 commit comments

Comments
 (0)