use super::prepend_data_prefix;
use crate::{
archive::{InternalArchiveDataWriter, InternalDataWriter, write_stream_entry},
chunk::{ChunkType, RawChunk},
cipher::CipherWriter,
compress::CompressionWriter,
entry::{
DataKind, Entry, EntryName, EntryWriteAttributes, 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>,
prefix: Option<Vec<u8>>,
max_chunk_size: NonZeroU32,
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, ChunkType::SHED, &header.to_bytes())?;
let writer = get_writer(FlattenWriter::new(), &context)?;
let (prefix, phsf) = match context.cipher {
None => (None, None),
Some(WriteCipher { context: c, .. }) => (Some(c.prefix_bytes()), Some(c.phsf)),
};
Ok(Self {
header,
prefix,
max_chunk_size: NonZeroU32::MAX,
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,
attributes: impl Into<EntryWriteAttributes>,
f: F,
) -> io::Result<()>
where
F: FnOnce(&mut SolidEntryDataWriter) -> io::Result<()>,
{
let option = WriteOptions::store();
write_stream_entry(
&mut self.data,
name,
DataKind::FILE,
attributes.into(),
option,
self.max_file_chunk_size,
|w| {
let mut writer = SolidEntryDataWriter(w);
f(&mut writer)?;
Ok(writer.0)
},
)
}
#[inline]
pub fn write_opaque<F>(
&mut self,
name: EntryName,
kind: DataKind,
attributes: impl Into<EntryWriteAttributes>,
f: F,
) -> io::Result<()>
where
F: FnOnce(&mut SolidEntryDataWriter) -> io::Result<()>,
{
write_stream_entry(
&mut self.data,
name,
kind,
attributes.into(),
WriteOptions::store(),
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.max_chunk_size = size;
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(prefix) = self.prefix {
prepend_data_prefix(&mut data, prefix, self.max_chunk_size);
}
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, CipherMode, Compression, Encryption, HashAlgorithm, Metadata, 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_gcm_header_respects_max_chunk_size() {
let options = WriteOptions::builder()
.encryption(Encryption::AES)
.cipher_mode(CipherMode::GCM)
.hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
.password(Some("password"))
.build();
let mut builder = SolidEntryBuilder::new(options).unwrap();
builder.max_chunk_size(NonZeroU32::new(8).unwrap());
builder
.write_file("entry".into(), Metadata::new(), |w| w.write_all(b"x"))
.unwrap();
let solid_entry = builder.build_as_entry().unwrap();
assert!(
solid_entry.data.iter().all(|chunk| chunk.len() <= 8),
"every SDAT body must respect max_chunk_size"
);
let mut entries = solid_entry
.entries(ReadOptions::with_password(Some("password")))
.unwrap();
let entry = entries.next().unwrap().unwrap();
let mut reader = entry.reader(ReadOptions::builder().build()).unwrap();
let mut plain = Vec::new();
reader.read_to_end(&mut plain).unwrap();
assert_eq!(plain, b"x");
}
#[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_entry_builder_write_opaque_uses_store() {
let kind = DataKind::new_private(202).unwrap();
let mut builder = SolidEntryBuilder::new(
WriteOptions::builder()
.compression(Compression::ZSTANDARD)
.build(),
)
.unwrap();
builder
.write_opaque("entry".into(), kind, Metadata::new(), |writer| {
writer.write_all(b"opaque")
})
.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.header().data_kind(), kind);
assert_eq!(entry.header().compression(), Compression::NO);
let mut data = Vec::new();
entry
.reader(ReadOptions::builder().build())
.unwrap()
.read_to_end(&mut data)
.unwrap();
assert_eq!(data, b"opaque");
}
#[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]);
}
}