1use crate::error::{DataSpoolError, Result};
7use std::fs::{File, OpenOptions};
8use std::io::{Read, Seek, SeekFrom, Write};
9use std::path::Path;
10
11const MAGIC: &[u8; 4] = b"SP01";
13
14const VERSION: u8 = 1;
16
17#[derive(Debug, Clone)]
19pub struct SpoolEntry {
20 pub offset: u64,
22 pub length: u32,
24}
25
26pub struct SpoolBuilder {
28 output: File,
30 current_offset: u64,
32 entries: Vec<SpoolEntry>,
34}
35
36impl SpoolBuilder {
37 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
39 let mut output = OpenOptions::new()
40 .write(true)
41 .create(true)
42 .truncate(true)
43 .open(path)?;
44
45 output.write_all(MAGIC)?;
47 output.write_all(&[VERSION])?;
48 output.write_all(&0u32.to_le_bytes())?; output.write_all(&0u64.to_le_bytes())?; let current_offset = output.stream_position()?;
52
53 Ok(Self {
54 output,
55 current_offset,
56 entries: Vec::new(),
57 })
58 }
59
60 pub fn add_card(&mut self, card_data: &[u8]) -> Result<SpoolEntry> {
64 let offset = self.current_offset;
65 let length = card_data.len() as u32;
66
67 self.output.write_all(card_data)?;
69 self.current_offset += card_data.len() as u64;
70
71 let entry = SpoolEntry { offset, length };
72 self.entries.push(entry.clone());
73
74 Ok(entry)
75 }
76
77 pub fn finalize(mut self) -> Result<()> {
79 let index_offset = self.current_offset;
80 let card_count = self.entries.len() as u32;
81
82 for entry in &self.entries {
84 self.output.write_all(&entry.offset.to_le_bytes())?;
85 self.output.write_all(&entry.length.to_le_bytes())?;
86 }
87
88 self.output.seek(SeekFrom::Start(5))?; self.output.write_all(&card_count.to_le_bytes())?;
91 self.output.write_all(&index_offset.to_le_bytes())?;
92
93 self.output.sync_all()?;
94
95 Ok(())
96 }
97}
98
99pub struct SpoolReader {
101 file: File,
103 entries: Vec<SpoolEntry>,
105}
106
107impl SpoolReader {
108 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
110 let mut file = File::open(path)?;
111
112 let mut magic = [0u8; 4];
114 file.read_exact(&mut magic)?;
115 if &magic != MAGIC {
116 return Err(DataSpoolError::Decompression(
117 "Invalid spool magic bytes".into(),
118 ));
119 }
120
121 let mut version = [0u8; 1];
122 file.read_exact(&mut version)?;
123 if version[0] != VERSION {
124 return Err(DataSpoolError::InvalidFormat);
125 }
126
127 let mut card_count_bytes = [0u8; 4];
128 file.read_exact(&mut card_count_bytes)?;
129 let card_count = u32::from_le_bytes(card_count_bytes);
130
131 let mut index_offset_bytes = [0u8; 8];
132 file.read_exact(&mut index_offset_bytes)?;
133 let index_offset = u64::from_le_bytes(index_offset_bytes);
134
135 file.seek(SeekFrom::Start(index_offset))?;
137 let mut entries = Vec::with_capacity(card_count as usize);
138
139 for _ in 0..card_count {
140 let mut offset_bytes = [0u8; 8];
141 file.read_exact(&mut offset_bytes)?;
142 let offset = u64::from_le_bytes(offset_bytes);
143
144 let mut length_bytes = [0u8; 4];
145 file.read_exact(&mut length_bytes)?;
146 let length = u32::from_le_bytes(length_bytes);
147
148 entries.push(SpoolEntry { offset, length });
149 }
150
151 Ok(Self { file, entries })
152 }
153
154 pub fn open_embedded<P: AsRef<Path>>(path: P, base_offset: u64) -> Result<Self> {
161 let mut file = File::open(path)?;
162
163 file.seek(SeekFrom::Start(base_offset))?;
165
166 let mut magic = [0u8; 4];
168 file.read_exact(&mut magic)?;
169 if &magic != MAGIC {
170 return Err(DataSpoolError::Decompression(
171 "Invalid spool magic bytes".into(),
172 ));
173 }
174
175 let mut version = [0u8; 1];
176 file.read_exact(&mut version)?;
177 if version[0] != VERSION {
178 return Err(DataSpoolError::InvalidFormat);
179 }
180
181 let mut card_count_bytes = [0u8; 4];
182 file.read_exact(&mut card_count_bytes)?;
183 let card_count = u32::from_le_bytes(card_count_bytes);
184
185 let mut index_offset_bytes = [0u8; 8];
186 file.read_exact(&mut index_offset_bytes)?;
187 let index_offset = u64::from_le_bytes(index_offset_bytes);
188
189 file.seek(SeekFrom::Start(base_offset + index_offset))?;
191 let mut entries = Vec::with_capacity(card_count as usize);
192
193 for _ in 0..card_count {
194 let mut offset_bytes = [0u8; 8];
195 file.read_exact(&mut offset_bytes)?;
196 let offset = u64::from_le_bytes(offset_bytes);
197
198 let mut length_bytes = [0u8; 4];
199 file.read_exact(&mut length_bytes)?;
200 let length = u32::from_le_bytes(length_bytes);
201
202 entries.push(SpoolEntry {
204 offset: base_offset + offset,
205 length,
206 });
207 }
208
209 Ok(Self { file, entries })
210 }
211
212 pub fn card_count(&self) -> usize {
214 self.entries.len()
215 }
216
217 pub fn read_card(&mut self, index: usize) -> Result<Vec<u8>> {
219 if index >= self.entries.len() {
220 return Err(DataSpoolError::Decompression(format!(
221 "Card index {} out of bounds (max: {})",
222 index,
223 self.entries.len() - 1
224 )));
225 }
226
227 let entry = &self.entries[index];
228 self.read_card_at(entry.offset, entry.length as usize)
229 }
230
231 pub fn read_card_at(&mut self, offset: u64, length: usize) -> Result<Vec<u8>> {
233 self.file.seek(SeekFrom::Start(offset))?;
234
235 let mut buffer = vec![0u8; length];
236 self.file.read_exact(&mut buffer)?;
237
238 Ok(buffer)
239 }
240
241 pub fn get_entry(&self, index: usize) -> Option<&SpoolEntry> {
243 self.entries.get(index)
244 }
245
246 pub fn entries(&self) -> &[SpoolEntry] {
248 &self.entries
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use std::fs;
256
257 #[test]
258 fn test_spool_roundtrip() {
259 let temp_path = "test_spool.spool";
260
261 let card1 = b"BP01\x01\x08code:apisome data 1";
263 let card2 = b"BP01\x01\x08code:apisome data 2 longer";
264 let card3 = b"BP01\x01\x08code:apidata 3";
265
266 {
268 let mut builder = SpoolBuilder::new(temp_path).unwrap();
269 let entry1 = builder.add_card(card1).unwrap();
270 let entry2 = builder.add_card(card2).unwrap();
271 let entry3 = builder.add_card(card3).unwrap();
272
273 assert_eq!(entry1.length, card1.len() as u32);
274 assert_eq!(entry2.length, card2.len() as u32);
275 assert_eq!(entry3.length, card3.len() as u32);
276
277 builder.finalize().unwrap();
278 }
279
280 {
282 let mut reader = SpoolReader::open(temp_path).unwrap();
283 assert_eq!(reader.card_count(), 3);
284
285 let read1 = reader.read_card(0).unwrap();
286 let read2 = reader.read_card(1).unwrap();
287 let read3 = reader.read_card(2).unwrap();
288
289 assert_eq!(&read1, card1);
290 assert_eq!(&read2, card2);
291 assert_eq!(&read3, card3);
292 }
293
294 fs::remove_file(temp_path).unwrap();
296 }
297
298 #[test]
299 fn test_spool_direct_access() {
300 let temp_path = "test_spool_direct.spool";
301
302 let card = b"BP01\x01\x08code:apitest data";
303
304 {
305 let mut builder = SpoolBuilder::new(temp_path).unwrap();
306 let entry = builder.add_card(card).unwrap();
307
308 let offset = entry.offset;
310 let length = entry.length;
311
312 builder.finalize().unwrap();
313
314 let mut reader = SpoolReader::open(temp_path).unwrap();
316 let read = reader.read_card_at(offset, length as usize).unwrap();
317 assert_eq!(&read, card);
318 }
319
320 fs::remove_file(temp_path).unwrap();
321 }
322
323 #[test]
324 fn test_spool_open_embedded() {
325 let temp_spool = "test_embedded_source.spool";
327 let temp_host = "test_embedded_host.bin";
328
329 let card1 = b"BP01\x01\x08code:apicard one data";
330 let card2 = b"BP01\x01\x08code:apicard two longer data here";
331
332 {
334 let mut builder = SpoolBuilder::new(temp_spool).unwrap();
335 builder.add_card(card1).unwrap();
336 builder.add_card(card2).unwrap();
337 builder.finalize().unwrap();
338 }
339
340 let spool_bytes = fs::read(temp_spool).unwrap();
342 let prefix = vec![0xAA; 64];
343 let suffix = vec![0xBB; 32];
344 let mut host = Vec::new();
345 host.extend_from_slice(&prefix);
346 host.extend_from_slice(&spool_bytes);
347 host.extend_from_slice(&suffix);
348 fs::write(temp_host, &host).unwrap();
349
350 {
352 let mut reader = SpoolReader::open_embedded(temp_host, 64).unwrap();
353 assert_eq!(reader.card_count(), 2);
354
355 let read1 = reader.read_card(0).unwrap();
356 let read2 = reader.read_card(1).unwrap();
357 assert_eq!(&read1, card1);
358 assert_eq!(&read2, card2);
359 }
360
361 fs::remove_file(temp_spool).unwrap();
362 fs::remove_file(temp_host).unwrap();
363 }
364}