#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::io::{Seek, Write};
use crate::sfa::{
toc::{
entry::{SectionName, TocEntry},
writer::TocWriter,
},
trailer::writer::TrailerWriter,
};
#[expect(
clippy::struct_field_names,
reason = "upstream sfa struct: `writer` field matches struct name; \
kept verbatim so the upstream-sync diff stays mechanical"
)]
pub struct Writer<W: Write + Seek> {
writer: W,
last_section_pos: u64,
section_name: SectionName,
toc: Vec<TocEntry>,
}
impl<W: Write + Seek> Writer<W> {
pub fn get_mut(&mut self) -> &mut W {
&mut self.writer
}
#[must_use]
pub fn from_writer(writer: W) -> Self {
Self {
writer,
last_section_pos: 0,
section_name: SectionName::new(),
toc: Vec::new(),
}
}
}
#[cfg(feature = "std")]
impl<W: Write + Seek> std::io::Write for Writer<W> {
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.writer.write(buf)
}
}
#[cfg(not(feature = "std"))]
impl<W: Write + Seek> crate::io::Write for Writer<W> {
fn flush(&mut self) -> crate::io::Result<()> {
self.writer.flush()
}
fn write(&mut self, buf: &[u8]) -> crate::io::Result<usize> {
self.writer.write(buf)
}
}
impl<W: Write + Seek> Writer<W> {
pub fn start(&mut self, name: impl Into<SectionName>) -> crate::io::Result<()> {
self.append_toc_entry()?;
self.section_name = name.into();
Ok(())
}
fn append_toc_entry(&mut self) -> crate::io::Result<()> {
let file_pos = self.writer.stream_position()?;
if file_pos > 0 {
let name = core::mem::take(&mut self.section_name);
self.toc.push(TocEntry {
name,
pos: self.last_section_pos,
len: file_pos - self.last_section_pos,
});
}
self.last_section_pos = file_pos;
Ok(())
}
fn append_trailer(writer: &mut W, toc: &[TocEntry]) -> crate::sfa::Result<()> {
let toc_pos = writer.stream_position()?;
let toc_checksum = TocWriter::write_into(&mut *writer, toc)?;
let after_toc_pos = writer.stream_position()?;
let toc_len = after_toc_pos - toc_pos;
TrailerWriter::write_into(writer, toc_checksum, toc_pos, toc_len)
}
pub fn finish(mut self) -> crate::sfa::Result<()> {
log::trace!("Finishing archive");
self.append_toc_entry()?;
Self::append_trailer(&mut self.writer, &self.toc)?;
self.writer.flush()?;
Ok(())
}
pub fn into_inner(mut self) -> crate::sfa::Result<W> {
log::trace!("Finishing archive");
self.append_toc_entry()?;
Self::append_trailer(&mut self.writer, &self.toc)?;
self.writer.flush()?;
Ok(self.writer)
}
}
#[cfg(test)]
#[expect(
clippy::indexing_slicing,
reason = "vendored upstream tests use `toc[i]` indexing freely; \
keep verbatim so the upstream-sync diff stays mechanical"
)]
mod tests {
use super::*;
use crate::sfa::toc::reader::TocReader;
use crate::sfa::trailer::reader::TrailerReader;
use std::fs::File;
use std::io::Write;
use test_log::test;
#[test]
fn writer_empty() -> crate::sfa::Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("file.sfa");
let mut file = File::create(&path)?;
let writer = Writer::from_writer(&mut file);
writer.finish()?;
file.sync_all()?;
drop(file);
let mut reader = File::open(&path)?;
let trailer = TrailerReader::from_reader(&mut reader)?;
assert_eq!(0, trailer.toc_pos);
let toc = TocReader::from_reader(
&mut reader,
trailer.toc_pos,
trailer.toc_len,
trailer.toc_checksum,
)?;
assert_eq!(0, toc.len());
assert!(toc.is_empty());
assert!(toc.section(b"hello").is_none());
Ok(())
}
#[test]
fn writer_simple() -> crate::sfa::Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("file.sfa");
let data = b"hello world";
let mut file = File::create(&path)?;
let mut writer = Writer::from_writer(&mut file);
writer.write_all(data)?;
writer.finish()?;
file.sync_all()?;
drop(file);
let mut reader = File::open(&path)?;
let trailer = TrailerReader::from_reader(&mut reader)?;
assert_eq!(data.len() as u64, trailer.toc_pos);
let toc = TocReader::from_reader(
&mut reader,
trailer.toc_pos,
trailer.toc_len,
trailer.toc_checksum,
)?;
assert_eq!(1, toc.len());
assert!(toc.section(b"hello").is_none());
assert!(toc.section(b"").is_some());
assert_eq!(0, toc[0].pos);
assert_eq!(data.len() as u64, toc[0].len);
assert_eq!(&[] as &[u8], &*toc[0].name);
Ok(())
}
#[test]
fn writer_multiple_sections() -> crate::sfa::Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("file.sfa");
let data = b"hello world";
let data2 = b"hello world2";
let data3 = b"hello world3";
let mut file = File::create(&path)?;
let mut writer = Writer::from_writer(&mut file);
writer.write_all(data)?;
writer.start("section1")?;
writer.write_all(data2)?;
writer.start("section2")?;
writer.write_all(data3)?;
writer.finish()?;
file.sync_all()?;
drop(file);
let mut reader = File::open(&path)?;
let trailer = TrailerReader::from_reader(&mut reader)?;
assert_eq!(
data.len() as u64 + data2.len() as u64 + data3.len() as u64,
trailer.toc_pos,
);
let toc = TocReader::from_reader(
&mut reader,
trailer.toc_pos,
trailer.toc_len,
trailer.toc_checksum,
)?;
assert_eq!(3, toc.len());
assert!(toc.section(b"hello").is_none());
assert!(toc.section(b"").is_some());
assert!(toc.section(b"section1").is_some());
assert!(toc.section(b"section2").is_some());
assert_eq!(0, toc[0].pos);
assert_eq!(data.len() as u64, toc[0].len());
assert_eq!(&[] as &[u8], &*toc[0].name);
assert_eq!(b"section1", &*toc[1].name);
assert_eq!(b"section2", &*toc[2].name);
assert_eq!(data.len() as u64, toc[0].len);
assert_eq!(data2.len() as u64, toc[1].len);
assert_eq!(data3.len() as u64, toc[2].len);
Ok(())
}
}