use std::io::SeekFrom;
use std::path::Path;
use tokio::fs::File;
use tokio::io::{
AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt, BufWriter,
};
use crate::format::{SIGNATURE, SIGNATURE_HEADER_SIZE};
use crate::write::codecs::Compressed;
use crate::write::header_encode::HeaderModel;
use crate::write::{EntryMeta, PendingEntry, StreamInfo, WriteOptions, WriteResult};
use crate::{ArchivePath, Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AsyncWriterState {
AcceptingEntries,
Building,
Finished,
Failed,
}
pub struct AsyncWriter<W> {
sink: W,
start_pos: u64,
options: WriteOptions,
state: AsyncWriterState,
entries: Vec<PendingEntry>,
stream_info: StreamInfo,
compressed_bytes: u64,
last_path: Option<String>,
}
impl AsyncWriter<BufWriter<File>> {
pub async fn create_path(path: impl AsRef<Path>) -> Result<Self> {
let file = File::create(path.as_ref()).await.map_err(Error::Io)?;
let writer = BufWriter::new(file);
Self::create(writer).await
}
}
impl<W: AsyncWrite + AsyncSeek + Unpin + Send> AsyncWriter<W> {
pub async fn create(mut sink: W) -> Result<Self> {
let start_pos = sink.stream_position().await.map_err(Error::Io)?;
sink.seek(SeekFrom::Start(start_pos + SIGNATURE_HEADER_SIZE))
.await
.map_err(Error::Io)?;
Ok(Self {
sink,
start_pos,
options: WriteOptions::default(),
state: AsyncWriterState::AcceptingEntries,
entries: Vec::new(),
stream_info: StreamInfo::default(),
compressed_bytes: 0,
last_path: None,
})
}
pub fn options(mut self, options: WriteOptions) -> Self {
self.options = options;
self
}
pub async fn add_path(
&mut self,
disk_path: impl AsRef<Path>,
archive_path: ArchivePath,
) -> Result<()> {
self.checks_before_reading(&archive_path)?;
let disk_path = disk_path.as_ref();
let meta = EntryMeta::from_path_async(disk_path).await?;
if meta.is_directory {
self.add_directory(archive_path, meta).await
} else {
let mut file = File::open(disk_path).await.map_err(Error::Io)?;
let mut data = Vec::new();
file.read_to_end(&mut data).await.map_err(Error::Io)?;
self.add_bytes_internal(archive_path, &data, meta).await
}
}
pub async fn add_directory(
&mut self,
archive_path: ArchivePath,
meta: EntryMeta,
) -> Result<()> {
self.ensure_accepting_entries()?;
self.check_order(&archive_path)?;
let recorded = archive_path.clone();
let entry = PendingEntry {
path: archive_path,
meta: EntryMeta {
is_directory: true,
..meta
},
uncompressed_size: 0,
};
self.entries.push(entry);
self.record_order(&recorded);
Ok(())
}
pub async fn add_stream<R: AsyncRead + Unpin>(
&mut self,
archive_path: ArchivePath,
mut source: R,
meta: EntryMeta,
) -> Result<()> {
self.checks_before_reading(&archive_path)?;
let mut data = Vec::new();
source.read_to_end(&mut data).await.map_err(Error::Io)?;
self.add_bytes_internal(archive_path, &data, meta).await
}
pub async fn add_bytes(&mut self, archive_path: ArchivePath, data: &[u8]) -> Result<()> {
let meta = EntryMeta::file(data.len() as u64);
self.add_bytes_internal(archive_path, data, meta).await
}
fn check_order(&self, archive_path: &ArchivePath) -> Result<()> {
if !self.options.deterministic {
return Ok(());
}
let path = archive_path.as_str();
if let Some(previous) = &self.last_path {
if path < previous.as_str() {
return Err(Error::InvalidArchivePath(format!(
"deterministic mode requires entries in sorted order, \
but '{path}' was added after '{previous}'"
)));
}
}
Ok(())
}
fn record_order(&mut self, archive_path: &ArchivePath) {
match &mut self.last_path {
Some(last) => {
last.clear();
last.push_str(archive_path.as_str());
}
None => self.last_path = Some(archive_path.as_str().to_string()),
}
}
async fn add_bytes_internal(
&mut self,
archive_path: ArchivePath,
data: &[u8],
meta: EntryMeta,
) -> Result<()> {
self.ensure_accepting_entries()?;
self.check_order(&archive_path)?;
let recorded = archive_path.clone();
let crc = crc32fast::hash(data);
let uncompressed_size = data.len() as u64;
if data.is_empty() {
self.entries.push(PendingEntry {
path: archive_path,
meta,
uncompressed_size: 0,
});
self.record_order(&recorded);
return Ok(());
}
let options = self.options.clone();
let data_owned = data.to_vec();
let compressed =
tokio::task::spawn_blocking(move || compress_data_sync(&data_owned, &options))
.await
.map_err(|e| Error::Io(std::io::Error::other(e)))??;
let compressed_size = compressed.data.len() as u64;
self.write_entry_bytes(&compressed.data).await?;
self.compressed_bytes += compressed_size;
self.stream_info.pack_sizes.push(compressed_size);
self.stream_info.unpack_sizes.push(uncompressed_size);
self.stream_info.coder_methods.push(self.options.method);
self.stream_info
.coder_properties
.push(compressed.properties);
self.stream_info.crcs.push(None);
self.stream_info.substream_sizes.push(uncompressed_size);
self.stream_info.substream_crcs.push(crc);
self.stream_info.num_unpack_streams_per_folder.push(1);
self.stream_info.filter_info.push(None);
self.stream_info.bcj2_folder_info.push(None);
#[cfg(feature = "aes")]
self.stream_info.encryption_info.push(None);
self.entries.push(PendingEntry {
path: archive_path,
meta,
uncompressed_size,
});
self.record_order(&recorded);
Ok(())
}
pub async fn finish(self) -> Result<WriteResult> {
let (result, _sink) = self.finish_into_inner().await?;
Ok(result)
}
pub async fn finish_into_inner(mut self) -> Result<(WriteResult, W)> {
self.ensure_accepting_entries()?;
self.state = AsyncWriterState::Building;
let header_pos = self.sink.stream_position().await.map_err(Error::Io)?;
let stream_info = std::mem::take(&mut self.stream_info);
let entries = std::mem::take(&mut self.entries);
let options = self.options.clone();
let (header, stream_info, entries) = tokio::task::spawn_blocking(move || {
let header = HeaderModel {
stream_info: &stream_info,
entries: &entries,
options: &options,
}
.encode_header();
(header, stream_info, entries)
})
.await
.map_err(|e| Error::Io(std::io::Error::other(e)))?;
self.stream_info = stream_info;
self.entries = entries;
let header_data = header?;
self.write_entry_bytes(&header_data).await?;
let archive_len = self.sink.stream_position().await.map_err(Error::Io)?;
self.write_signature_header_async(header_pos, &header_data)
.await?;
self.sink.flush().await.map_err(Error::Io)?;
self.state = AsyncWriterState::Finished;
let result = WriteResult {
entries_written: self
.entries
.iter()
.filter(|e| !e.meta.is_directory && !e.meta.is_anti)
.count(),
directories_written: self.entries.iter().filter(|e| e.meta.is_directory).count(),
total_size: self.entries.iter().map(|e| e.uncompressed_size).sum(),
compressed_size: self.compressed_bytes,
volume_count: 1,
volume_sizes: vec![archive_len - self.start_pos],
};
Ok((result, self.sink))
}
async fn write_signature_header_async(
&mut self,
header_pos: u64,
header_data: &[u8],
) -> Result<()> {
let next_header_offset = header_pos - self.start_pos - SIGNATURE_HEADER_SIZE;
let next_header_size = header_data.len() as u64;
let next_header_crc = crc32fast::hash(header_data);
let mut start_header = Vec::with_capacity(20);
start_header.extend_from_slice(&next_header_offset.to_le_bytes());
start_header.extend_from_slice(&next_header_size.to_le_bytes());
start_header.extend_from_slice(&next_header_crc.to_le_bytes());
let start_header_crc = crc32fast::hash(&start_header);
let start = self.start_pos;
self.sink
.seek(SeekFrom::Start(start))
.await
.map_err(Error::Io)?;
self.sink.write_all(SIGNATURE).await.map_err(Error::Io)?;
self.sink
.write_all(&[0x00, 0x04])
.await
.map_err(Error::Io)?;
self.sink
.write_all(&start_header_crc.to_le_bytes())
.await
.map_err(Error::Io)?;
self.sink
.write_all(&start_header)
.await
.map_err(Error::Io)?;
Ok(())
}
async fn write_entry_bytes(&mut self, data: &[u8]) -> Result<()> {
let resume_as = self.state;
self.state = AsyncWriterState::Failed;
self.sink.write_all(data).await.map_err(Error::Io)?;
self.state = resume_as;
Ok(())
}
fn checks_before_reading(&self, archive_path: &ArchivePath) -> Result<()> {
self.ensure_accepting_entries()?;
self.check_order(archive_path)
}
fn ensure_accepting_entries(&self) -> Result<()> {
if self.state == AsyncWriterState::Failed {
return Err(Error::InvalidFormat(
"an earlier entry failed partway through writing; \
this archive cannot be completed"
.into(),
));
}
if self.state != AsyncWriterState::AcceptingEntries {
return Err(Error::InvalidFormat(
"Writer is not accepting entries".into(),
));
}
#[cfg(feature = "aes")]
if self.options.is_encrypted() || self.options.encrypt_data || self.options.encrypt_header {
return Err(Error::UnsupportedFeature {
feature: "encryption in the async writer",
});
}
if self.options.filter.is_active() {
return Err(Error::UnsupportedFeature {
feature: "pre-compression filters in the async writer",
});
}
if self.options.solid.is_solid() {
return Err(Error::UnsupportedFeature {
feature: "solid archives in the async writer",
});
}
if self.options.comment.is_some() {
return Err(Error::UnsupportedFeature {
feature: "archive comments in the async writer",
});
}
self.options.validate()?;
Ok(())
}
}
impl EntryMeta {
pub async fn from_path_async(path: impl AsRef<Path>) -> Result<Self> {
let metadata = tokio::fs::metadata(path).await.map_err(Error::Io)?;
Ok(Self::from_metadata(&metadata))
}
}
fn compress_data_sync(data: &[u8], options: &WriteOptions) -> Result<Compressed> {
crate::write::compression::compress_data(options, data, true)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_async_writer_create() {
let buffer = std::io::Cursor::new(Vec::new());
let writer = AsyncWriter::create(buffer).await.unwrap();
assert_eq!(writer.state, AsyncWriterState::AcceptingEntries);
}
#[tokio::test]
async fn test_async_writer_options() {
let buffer = std::io::Cursor::new(Vec::new());
let writer = AsyncWriter::create(buffer)
.await
.unwrap()
.options(WriteOptions::new().level(9).unwrap());
assert_eq!(writer.options.level, 9);
}
#[cfg(feature = "lzma2")]
#[tokio::test]
async fn test_async_writer_add_bytes_and_finish() {
let buffer = std::io::Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
let path = ArchivePath::new("test.txt").unwrap();
writer.add_bytes(path, b"Hello, World!").await.unwrap();
let result = writer.finish().await.unwrap();
assert_eq!(result.entries_written, 1);
assert_eq!(result.total_size, 13);
}
#[tokio::test]
async fn test_async_writer_empty_archive() {
let buffer = std::io::Cursor::new(Vec::new());
let writer = AsyncWriter::create(buffer).await.unwrap();
let result = writer.finish().await.unwrap();
assert_eq!(result.entries_written, 0);
}
#[tokio::test]
async fn test_async_writer_with_directory() {
let buffer = std::io::Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
let dir_path = ArchivePath::new("mydir").unwrap();
writer
.add_directory(dir_path, EntryMeta::directory())
.await
.unwrap();
let result = writer.finish().await.unwrap();
assert_eq!(result.entries_written, 0);
assert_eq!(result.directories_written, 1);
}
}