use crate::error::{DataSpoolError, Result};
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
const MAGIC: &[u8; 4] = b"SP01";
const VERSION: u8 = 1;
#[derive(Debug, Clone)]
pub struct SpoolEntry {
pub offset: u64,
pub length: u32,
}
pub struct SpoolBuilder {
output: File,
current_offset: u64,
entries: Vec<SpoolEntry>,
}
impl SpoolBuilder {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut output = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
output.write_all(MAGIC)?;
output.write_all(&[VERSION])?;
output.write_all(&0u32.to_le_bytes())?; output.write_all(&0u64.to_le_bytes())?;
let current_offset = output.stream_position()?;
Ok(Self {
output,
current_offset,
entries: Vec::new(),
})
}
pub fn add_card(&mut self, card_data: &[u8]) -> Result<SpoolEntry> {
let offset = self.current_offset;
let length = card_data.len() as u32;
self.output.write_all(card_data)?;
self.current_offset += card_data.len() as u64;
let entry = SpoolEntry { offset, length };
self.entries.push(entry.clone());
Ok(entry)
}
pub fn finalize(mut self) -> Result<()> {
let index_offset = self.current_offset;
let card_count = self.entries.len() as u32;
for entry in &self.entries {
self.output.write_all(&entry.offset.to_le_bytes())?;
self.output.write_all(&entry.length.to_le_bytes())?;
}
self.output.seek(SeekFrom::Start(5))?; self.output.write_all(&card_count.to_le_bytes())?;
self.output.write_all(&index_offset.to_le_bytes())?;
self.output.sync_all()?;
Ok(())
}
}
pub struct SpoolReader {
file: File,
entries: Vec<SpoolEntry>,
}
impl SpoolReader {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut file = File::open(path)?;
let mut magic = [0u8; 4];
file.read_exact(&mut magic)?;
if &magic != MAGIC {
return Err(DataSpoolError::Decompression(
"Invalid spool magic bytes".into(),
));
}
let mut version = [0u8; 1];
file.read_exact(&mut version)?;
if version[0] != VERSION {
return Err(DataSpoolError::InvalidFormat);
}
let mut card_count_bytes = [0u8; 4];
file.read_exact(&mut card_count_bytes)?;
let card_count = u32::from_le_bytes(card_count_bytes);
let mut index_offset_bytes = [0u8; 8];
file.read_exact(&mut index_offset_bytes)?;
let index_offset = u64::from_le_bytes(index_offset_bytes);
file.seek(SeekFrom::Start(index_offset))?;
let mut entries = Vec::with_capacity(card_count as usize);
for _ in 0..card_count {
let mut offset_bytes = [0u8; 8];
file.read_exact(&mut offset_bytes)?;
let offset = u64::from_le_bytes(offset_bytes);
let mut length_bytes = [0u8; 4];
file.read_exact(&mut length_bytes)?;
let length = u32::from_le_bytes(length_bytes);
entries.push(SpoolEntry { offset, length });
}
Ok(Self { file, entries })
}
pub fn open_embedded<P: AsRef<Path>>(path: P, base_offset: u64) -> Result<Self> {
let mut file = File::open(path)?;
file.seek(SeekFrom::Start(base_offset))?;
let mut magic = [0u8; 4];
file.read_exact(&mut magic)?;
if &magic != MAGIC {
return Err(DataSpoolError::Decompression(
"Invalid spool magic bytes".into(),
));
}
let mut version = [0u8; 1];
file.read_exact(&mut version)?;
if version[0] != VERSION {
return Err(DataSpoolError::InvalidFormat);
}
let mut card_count_bytes = [0u8; 4];
file.read_exact(&mut card_count_bytes)?;
let card_count = u32::from_le_bytes(card_count_bytes);
let mut index_offset_bytes = [0u8; 8];
file.read_exact(&mut index_offset_bytes)?;
let index_offset = u64::from_le_bytes(index_offset_bytes);
file.seek(SeekFrom::Start(base_offset + index_offset))?;
let mut entries = Vec::with_capacity(card_count as usize);
for _ in 0..card_count {
let mut offset_bytes = [0u8; 8];
file.read_exact(&mut offset_bytes)?;
let offset = u64::from_le_bytes(offset_bytes);
let mut length_bytes = [0u8; 4];
file.read_exact(&mut length_bytes)?;
let length = u32::from_le_bytes(length_bytes);
entries.push(SpoolEntry {
offset: base_offset + offset,
length,
});
}
Ok(Self { file, entries })
}
pub fn card_count(&self) -> usize {
self.entries.len()
}
pub fn read_card(&mut self, index: usize) -> Result<Vec<u8>> {
if index >= self.entries.len() {
return Err(DataSpoolError::Decompression(format!(
"Card index {} out of bounds (max: {})",
index,
self.entries.len() - 1
)));
}
let entry = &self.entries[index];
self.read_card_at(entry.offset, entry.length as usize)
}
pub fn read_card_at(&mut self, offset: u64, length: usize) -> Result<Vec<u8>> {
self.file.seek(SeekFrom::Start(offset))?;
let mut buffer = vec![0u8; length];
self.file.read_exact(&mut buffer)?;
Ok(buffer)
}
pub fn get_entry(&self, index: usize) -> Option<&SpoolEntry> {
self.entries.get(index)
}
pub fn entries(&self) -> &[SpoolEntry] {
&self.entries
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_spool_roundtrip() {
let temp_path = "test_spool.spool";
let card1 = b"BP01\x01\x08code:apisome data 1";
let card2 = b"BP01\x01\x08code:apisome data 2 longer";
let card3 = b"BP01\x01\x08code:apidata 3";
{
let mut builder = SpoolBuilder::new(temp_path).unwrap();
let entry1 = builder.add_card(card1).unwrap();
let entry2 = builder.add_card(card2).unwrap();
let entry3 = builder.add_card(card3).unwrap();
assert_eq!(entry1.length, card1.len() as u32);
assert_eq!(entry2.length, card2.len() as u32);
assert_eq!(entry3.length, card3.len() as u32);
builder.finalize().unwrap();
}
{
let mut reader = SpoolReader::open(temp_path).unwrap();
assert_eq!(reader.card_count(), 3);
let read1 = reader.read_card(0).unwrap();
let read2 = reader.read_card(1).unwrap();
let read3 = reader.read_card(2).unwrap();
assert_eq!(&read1, card1);
assert_eq!(&read2, card2);
assert_eq!(&read3, card3);
}
fs::remove_file(temp_path).unwrap();
}
#[test]
fn test_spool_direct_access() {
let temp_path = "test_spool_direct.spool";
let card = b"BP01\x01\x08code:apitest data";
{
let mut builder = SpoolBuilder::new(temp_path).unwrap();
let entry = builder.add_card(card).unwrap();
let offset = entry.offset;
let length = entry.length;
builder.finalize().unwrap();
let mut reader = SpoolReader::open(temp_path).unwrap();
let read = reader.read_card_at(offset, length as usize).unwrap();
assert_eq!(&read, card);
}
fs::remove_file(temp_path).unwrap();
}
#[test]
fn test_spool_open_embedded() {
let temp_spool = "test_embedded_source.spool";
let temp_host = "test_embedded_host.bin";
let card1 = b"BP01\x01\x08code:apicard one data";
let card2 = b"BP01\x01\x08code:apicard two longer data here";
{
let mut builder = SpoolBuilder::new(temp_spool).unwrap();
builder.add_card(card1).unwrap();
builder.add_card(card2).unwrap();
builder.finalize().unwrap();
}
let spool_bytes = fs::read(temp_spool).unwrap();
let prefix = vec![0xAA; 64];
let suffix = vec![0xBB; 32];
let mut host = Vec::new();
host.extend_from_slice(&prefix);
host.extend_from_slice(&spool_bytes);
host.extend_from_slice(&suffix);
fs::write(temp_host, &host).unwrap();
{
let mut reader = SpoolReader::open_embedded(temp_host, 64).unwrap();
assert_eq!(reader.card_count(), 2);
let read1 = reader.read_card(0).unwrap();
let read2 = reader.read_card(1).unwrap();
assert_eq!(&read1, card1);
assert_eq!(&read2, card2);
}
fs::remove_file(temp_spool).unwrap();
fs::remove_file(temp_host).unwrap();
}
}