use crate::{
archive::{InternalArchiveDataWriter, InternalDataWriter, write_file_entry},
chunk::RawChunk,
cipher::CipherWriter,
compress::CompressionWriter,
entry::{
Entry, EntryName, Metadata, NormalEntry, SolidEntry, SolidHeader, WriteCipher, WriteOption,
WriteOptions, get_writer, get_writer_context, private::SealedEntryExt,
},
io::{FlattenWriter, TryIntoInner},
};
use std::{
io::{self, prelude::*},
num::NonZeroU32,
};
pub struct SolidEntryDataWriter<'a>(
InternalArchiveDataWriter<&'a mut InternalDataWriter<FlattenWriter>>,
);
impl Write for SolidEntryDataWriter<'_> {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}
pub struct SolidEntryBuilder {
header: SolidHeader,
phsf: Option<String>,
iv: Option<Vec<u8>>,
data: CompressionWriter<CipherWriter<FlattenWriter>>,
extra: Vec<RawChunk>,
max_file_chunk_size: Option<NonZeroU32>,
}
impl SolidEntryBuilder {
#[inline]
pub fn new(option: impl WriteOption) -> io::Result<Self> {
let header = SolidHeader::new(
option.compression(),
option.encryption(),
option.cipher_mode(),
);
let context = get_writer_context(option)?;
let writer = get_writer(FlattenWriter::new(), &context)?;
let (iv, phsf) = match context.cipher {
None => (None, None),
Some(WriteCipher { context: c, .. }) => (Some(c.iv), Some(c.phsf)),
};
Ok(Self {
header,
iv,
phsf,
data: writer,
extra: Vec::new(),
max_file_chunk_size: None,
})
}
#[inline]
pub fn add_entry<T>(&mut self, entry: NormalEntry<T>) -> io::Result<usize>
where
NormalEntry<T>: Entry,
{
entry.write_in(&mut self.data)
}
#[inline]
pub fn write_file<F>(&mut self, name: EntryName, metadata: Metadata, mut f: F) -> io::Result<()>
where
F: FnMut(&mut SolidEntryDataWriter) -> io::Result<()>,
{
let option = WriteOptions::store();
write_file_entry(
&mut self.data,
name,
metadata,
option,
self.max_file_chunk_size,
|w| {
let mut writer = SolidEntryDataWriter(w);
f(&mut writer)?;
Ok(writer.0)
},
)
}
#[inline]
pub fn add_extra_chunk<T: Into<RawChunk>>(&mut self, chunk: T) {
self.extra.push(chunk.into());
}
#[inline]
pub fn max_chunk_size(&mut self, size: NonZeroU32) -> &mut Self {
self.data
.get_mut()
.get_mut()
.set_max_chunk_size(size.get() as usize);
self
}
#[inline]
pub fn max_file_chunk_size(&mut self, size: NonZeroU32) -> &mut Self {
self.max_file_chunk_size = Some(size);
self
}
fn build_as_entry(self) -> io::Result<SolidEntry> {
Ok(SolidEntry {
header: self.header,
phsf: self.phsf,
data: {
let mut data = self.data.try_into_inner()?.try_into_inner()?.inner;
if let Some(iv) = self.iv {
data.insert(0, iv);
}
data
},
extra: self.extra,
})
}
#[inline]
#[must_use = "building an entry without using it is wasteful"]
pub fn build(self) -> io::Result<SolidEntry> {
self.build_as_entry()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ChunkType, ReadOptions};
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use wasm_bindgen_test::wasm_bindgen_test as test;
#[test]
fn solid_entry_extra_chunk() {
let mut builder = SolidEntryBuilder::new(WriteOptions::store()).unwrap();
builder.add_extra_chunk(RawChunk::from_data(
ChunkType::private(*b"abCd").unwrap(),
[],
));
let entry = builder.build_as_entry().unwrap();
assert_eq!(
&entry.extra[0],
&RawChunk::from_data(ChunkType::private(*b"abCd").unwrap(), []),
);
}
#[test]
fn solid_entry_builder_write_file_with_max_chunk_size() {
let mut builder = SolidEntryBuilder::new(WriteOptions::store()).unwrap();
builder.max_chunk_size(NonZeroU32::new(8).unwrap());
builder
.write_file("entry".into(), Metadata::new(), |w| {
w.write_all(b"abcdefghijklmnopqrstuvwxyz")
})
.unwrap();
let solid_entry = builder.build_as_entry().unwrap();
let mut entries = solid_entry.entries(ReadOptions::builder().build()).unwrap();
let entry = entries.next().unwrap().unwrap();
let mut reader = entry.reader(ReadOptions::builder().build()).unwrap();
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
assert_eq!(b"abcdefghijklmnopqrstuvwxyz", &buf[..]);
assert!(
solid_entry.data.len() > 1,
"Data should be split into multiple chunks"
);
}
#[test]
fn solid_entry_builder_write_file() {
let mut builder = SolidEntryBuilder::new(WriteOptions::store()).unwrap();
builder
.write_file("entry".into(), Metadata::new(), |w| {
w.write_all("テストデータ".as_bytes())
})
.unwrap();
let solid_entry = builder.build_as_entry().unwrap();
let mut entries = solid_entry.entries(ReadOptions::builder().build()).unwrap();
let entry = entries.next().unwrap().unwrap();
let mut reader = entry.reader(ReadOptions::builder().build()).unwrap();
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
assert_eq!("テストデータ".as_bytes(), &buf[..]);
}
#[test]
fn solid_write_file_with_xattrs_metadata_round_trips() {
use crate::entry::{ExtendedAttribute, XattrName, XattrValue};
let xattr = ExtendedAttribute::new(
XattrName::try_from("user.k").unwrap(),
XattrValue::try_from(b"v".as_slice()).unwrap(),
);
let mut builder = SolidEntryBuilder::new(WriteOptions::store()).unwrap();
builder
.write_file(
"entry".into(),
Metadata::new().with_xattrs(vec![xattr.clone()]),
|w| w.write_all(b"data"),
)
.unwrap();
let solid_entry = builder.build_as_entry().unwrap();
let mut entries = solid_entry.entries(ReadOptions::builder().build()).unwrap();
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.metadata().xattrs(), &[xattr]);
}
}