use crate::{
Duration,
archive::{InternalArchiveDataWriter, InternalDataWriter, write_file_entry},
chunk::RawChunk,
cipher::CipherWriter,
compress::CompressionWriter,
entry::{
DataKind, Entry, EntryHeader, EntryName, EntryReference, ExtendedAttribute, LinkTargetType,
Metadata, NormalEntry, Permission, SolidEntry, SolidHeader, WriteCipher, WriteOption,
WriteOptions, get_writer, get_writer_context, private::SealedEntryExt,
},
io::{FlattenWriter, TryIntoInner},
};
#[cfg(feature = "unstable-async")]
use futures_io::AsyncWrite;
use std::{
io::{self, prelude::*},
num::NonZeroU32,
};
#[cfg(feature = "unstable-async")]
use std::{
pin::Pin,
task::{Context, Poll},
};
pub struct EntryBuilder {
header: EntryHeader,
phsf: Option<String>,
iv: Option<Vec<u8>>,
data: Option<CompressionWriter<CipherWriter<FlattenWriter>>>,
created: Option<Duration>,
last_modified: Option<Duration>,
accessed: Option<Duration>,
permission: Option<Permission>,
link_target_type: Option<LinkTargetType>,
store_file_size: bool,
file_size: u128,
xattrs: Vec<ExtendedAttribute>,
extra_chunks: Vec<RawChunk>,
}
impl EntryBuilder {
const fn new(header: EntryHeader) -> Self {
Self {
header,
phsf: None,
iv: None,
data: None,
created: None,
last_modified: None,
accessed: None,
permission: None,
link_target_type: None,
store_file_size: true,
file_size: 0,
xattrs: Vec::new(),
extra_chunks: Vec::new(),
}
}
#[inline]
pub const fn new_dir(name: EntryName) -> Self {
Self::new(EntryHeader::for_dir(name))
}
#[inline]
pub fn new_file(name: EntryName, option: impl WriteOption) -> io::Result<Self> {
let header = EntryHeader::for_file(
option.compression(),
option.encryption(),
option.cipher_mode(),
name,
);
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 {
data: Some(writer),
iv,
phsf,
..Self::new(header)
})
}
fn new_link(header: EntryHeader, source: EntryReference) -> io::Result<Self> {
let option = WriteOptions::store();
let context = get_writer_context(option)?;
let mut writer = get_writer(FlattenWriter::new(), &context)?;
writer.write_all(source.as_bytes())?;
let (iv, phsf) = match context.cipher {
None => (None, None),
Some(WriteCipher { context: c, .. }) => (Some(c.iv), Some(c.phsf)),
};
Ok(Self {
data: Some(writer),
iv,
phsf,
..Self::new(header)
})
}
#[inline]
pub fn new_symlink(name: EntryName, source: EntryReference) -> io::Result<Self> {
Self::new_link(EntryHeader::for_symlink(name), source)
}
#[inline]
pub fn new_hard_link(name: EntryName, source: EntryReference) -> io::Result<Self> {
Self::new_link(EntryHeader::for_hard_link(name), source)
}
#[inline]
pub fn created(&mut self, since_unix_epoch: impl Into<Option<Duration>>) -> &mut Self {
self.created = since_unix_epoch.into();
self
}
#[inline]
pub fn modified(&mut self, since_unix_epoch: impl Into<Option<Duration>>) -> &mut Self {
self.last_modified = since_unix_epoch.into();
self
}
#[inline]
pub fn accessed(&mut self, since_unix_epoch: impl Into<Option<Duration>>) -> &mut Self {
self.accessed = since_unix_epoch.into();
self
}
#[inline]
pub fn permission(&mut self, permission: impl Into<Option<Permission>>) -> &mut Self {
self.permission = permission.into();
self
}
#[inline]
pub fn link_target_type(
&mut self,
link_target_type: impl Into<Option<LinkTargetType>>,
) -> &mut Self {
self.link_target_type = link_target_type.into();
self
}
#[inline]
pub fn file_size(&mut self, store: bool) -> &mut Self {
self.store_file_size = store;
self
}
#[inline]
pub fn add_xattr(&mut self, xattr: ExtendedAttribute) -> &mut Self {
self.xattrs.push(xattr);
self
}
#[inline]
pub fn add_extra_chunk<T: Into<RawChunk>>(&mut self, chunk: T) -> &mut Self {
self.extra_chunks.push(chunk.into());
self
}
#[inline]
pub fn max_chunk_size(&mut self, size: NonZeroU32) -> &mut Self {
if let Some(data) = &mut self.data {
data.get_mut()
.get_mut()
.set_max_chunk_size(size.get() as usize);
}
self
}
#[inline]
#[must_use = "building an entry without using it is wasteful"]
pub fn build(self) -> io::Result<NormalEntry> {
let mut data = if let Some(data) = self.data {
data.try_into_inner()?.try_into_inner()?.inner
} else {
Vec::new()
};
if let Some(iv) = self.iv {
data.insert(0, iv);
}
let metadata = Metadata {
raw_file_size: match (self.store_file_size, self.header.data_kind) {
(true, DataKind::File) => Some(self.file_size),
_ => None,
},
compressed_size: data.iter().map(|d| d.len()).sum(),
created: self.created,
modified: self.last_modified,
accessed: self.accessed,
permission: self.permission,
link_target_type: self.link_target_type,
};
Ok(NormalEntry {
header: self.header,
phsf: self.phsf,
extra: self.extra_chunks,
data,
metadata,
xattrs: self.xattrs,
})
}
}
impl Write for EntryBuilder {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if let Some(w) = &mut self.data {
return w.write(buf).inspect(|len| self.file_size += *len as u128);
}
Ok(buf.len())
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
if let Some(w) = &mut self.data {
return w.flush();
}
Ok(())
}
}
#[cfg(feature = "unstable-async")]
impl AsyncWrite for EntryBuilder {
#[inline]
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Poll::Ready(self.get_mut().write(buf))
}
#[inline]
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(self.get_mut().flush())
}
#[inline]
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
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<impl Entry + Sized> {
self.build_as_entry()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entry::RawEntry;
use crate::entry::private::SealedEntryExt;
use crate::{ChunkType, ReadOptions};
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use wasm_bindgen_test::wasm_bindgen_test as test;
#[test]
fn entry_extra_chunk() {
let mut builder = EntryBuilder::new_dir("dir".into());
builder.add_extra_chunk(RawChunk::from_data(
ChunkType::private(*b"abCd").unwrap(),
[],
));
let entry = builder.build().unwrap();
assert_eq!(
&entry.extra[0],
&RawChunk::from_data(ChunkType::private(*b"abCd").unwrap(), []),
);
}
#[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(None).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(None).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 fltp_symlink_roundtrip() {
let mut builder =
EntryBuilder::new_symlink("link_name".into(), "target_dir".into()).unwrap();
builder.link_target_type(LinkTargetType::Directory);
let entry = builder.build().unwrap();
let chunks = entry.into_chunks();
let raw = RawEntry(chunks);
let restored = NormalEntry::try_from(raw).unwrap();
assert_eq!(
restored.metadata().link_target_type(),
Some(LinkTargetType::Directory)
);
}
#[test]
fn fltp_hardlink_roundtrip() {
let mut builder =
EntryBuilder::new_hard_link("dir_hardlink".into(), "target_dir".into()).unwrap();
builder.link_target_type(LinkTargetType::Directory);
let entry = builder.build().unwrap();
let chunks = entry.into_chunks();
let raw = RawEntry(chunks);
let restored = NormalEntry::try_from(raw).unwrap();
assert_eq!(
restored.metadata().link_target_type(),
Some(LinkTargetType::Directory)
);
}
#[test]
fn fltp_absent_returns_none() {
let builder = EntryBuilder::new_symlink("link_name".into(), "target".into()).unwrap();
let entry = builder.build().unwrap();
let chunks = entry.into_chunks();
let raw = RawEntry(chunks);
let restored = NormalEntry::try_from(raw).unwrap();
assert_eq!(restored.metadata().link_target_type(), None);
}
#[test]
fn fltp_on_regular_file_is_preserved() {
let mut builder =
EntryBuilder::new_file("regular.txt".into(), WriteOptions::store()).unwrap();
builder.link_target_type(LinkTargetType::File);
let entry = builder.build().unwrap();
let chunks = entry.into_chunks();
let raw = RawEntry(chunks);
let restored = NormalEntry::try_from(raw).unwrap();
assert_eq!(
restored.metadata().link_target_type(),
Some(LinkTargetType::File)
);
}
}