pub mod builder;
mod component_tree;
pub mod extract;
mod name;
use std::{
io,
marker::PhantomData,
path::{Path, PathBuf},
};
use thiserror::Error;
pub use builder::{ArchiveBuilder, BuildError, Builder, EntryMetadata, TraversalError};
pub use name::{NameValidator, default_name_validator};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MemberMetadata {
pub path: String,
pub position: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SpecialKind {
CharacterDevice,
BlockDevice,
Fifo,
}
#[derive(Debug)]
pub enum Member<P> {
File {
metadata: MemberMetadata,
size: u64,
executable: bool,
payload: P,
},
Directory {
metadata: MemberMetadata,
},
SymbolicLink {
metadata: MemberMetadata,
target: String,
},
HardLink {
metadata: MemberMetadata,
target: String,
size: u64,
payload: P,
},
Special {
metadata: MemberMetadata,
kind: SpecialKind,
},
}
impl<P> Member<P> {
pub fn metadata(&self) -> &MemberMetadata {
match self {
Self::File { metadata, .. }
| Self::Directory { metadata }
| Self::SymbolicLink { metadata, .. }
| Self::HardLink { metadata, .. }
| Self::Special { metadata, .. } => metadata,
}
}
fn lend_payload<'a>(self) -> Member<LentPayload<'a, P>> {
match self {
Self::File {
metadata,
size,
executable,
payload,
} => Member::File {
metadata,
size,
executable,
payload: LentPayload::new(payload),
},
Self::Directory { metadata } => Member::Directory { metadata },
Self::SymbolicLink { metadata, target } => Member::SymbolicLink { metadata, target },
Self::HardLink {
metadata,
target,
size,
payload,
} => Member::HardLink {
metadata,
target,
size,
payload: LentPayload::new(payload),
},
Self::Special { metadata, kind } => Member::Special { metadata, kind },
}
}
}
#[expect(
async_fn_in_trait,
reason = "payload readers may be !Send and run on a local executor"
)]
pub trait MemberPayload: Sized {
type Error;
async fn next_chunk(
&mut self,
buffer: &mut Vec<u8>,
target_len: usize,
) -> Result<bool, Self::Error>;
async fn skip(self) -> Result<(), Self::Error>;
}
#[derive(Debug)]
pub struct LentPayload<'a, P> {
payload: P,
cursor: PhantomData<&'a mut ()>,
}
impl<P> LentPayload<'_, P> {
fn new(payload: P) -> Self {
Self {
payload,
cursor: PhantomData,
}
}
}
impl<P: MemberPayload> MemberPayload for LentPayload<'_, P> {
type Error = P::Error;
async fn next_chunk(
&mut self,
buffer: &mut Vec<u8>,
target_len: usize,
) -> Result<bool, Self::Error> {
self.payload.next_chunk(buffer, target_len).await
}
async fn skip(self) -> Result<(), Self::Error> {
self.payload.skip().await
}
}
pub struct Members<A> {
archive: A,
}
impl<A: Archive> Members<A> {
pub async fn next<'a>(
&'a mut self,
) -> Result<Option<Member<LentPayload<'a, A::Payload<'a>>>>, A::Error> {
Ok(self.archive.next_member().await?.map(Member::lend_payload))
}
}
#[expect(
async_fn_in_trait,
reason = "archive readers may be !Send and run on a local executor"
)]
pub trait Archive: Sized {
type Error;
type Payload<'a>: MemberPayload<Error = Self::Error>
where
Self: 'a;
async fn next_member<'a>(
&'a mut self,
) -> Result<Option<Member<Self::Payload<'a>>>, Self::Error>;
fn members(self) -> Members<Self> {
Members { archive: self }
}
async fn extract_in<P: AsRef<Path>>(
self,
destination: P,
policy: extract::ExtractPolicy,
) -> Result<(), ExtractError<Self::Error>> {
extract::extract(self.members(), destination.as_ref(), policy).await
}
}
#[derive(Clone, Debug, Eq, PartialEq, Error)]
pub enum ExtractPolicyViolation {
#[error("archive {context} rejected by name policy: {value:?}")]
NameRejected {
context: &'static str,
value: String,
},
#[error("symbolic-link members are not allowed")]
SymbolicLink,
#[error("native symbolic-link creation is not supported on this platform")]
NativeSymlinkCreationUnsupported,
#[error("hard-link members are not allowed")]
HardLink,
}
#[derive(Debug, Error)]
pub enum ExtractError<E> {
#[error(transparent)]
Archive(E),
#[error("failed to {operation} {path}: {source}")]
Filesystem {
operation: &'static str,
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to complete blocking extraction operation: {0}")]
BlockingTask(#[from] tokio::task::JoinError),
#[error("at byte {position}: unsafe {context} {value:?}: {reason}")]
UnsafePath {
position: u64,
context: &'static str,
value: String,
reason: &'static str,
},
#[error("archive entry collides with existing path {path}")]
PathCollision {
path: PathBuf,
},
#[error("at byte {position}: cannot extract unsupported member type {kind:?} at {path}")]
UnsupportedMember {
position: u64,
path: PathBuf,
kind: SpecialKind,
},
#[error("at byte {position}: invalid link {path} -> {target:?}: {reason}")]
InvalidLink {
position: u64,
path: PathBuf,
target: String,
reason: &'static str,
},
#[error("at byte {position}: extraction policy rejected input: {violation}")]
PolicyViolation {
position: u64,
violation: ExtractPolicyViolation,
},
}
impl<E> ExtractError<E> {
fn policy_violation(position: u64, violation: ExtractPolicyViolation) -> Self {
Self::PolicyViolation {
position,
violation,
}
}
fn invalid_link(position: u64, path: PathBuf, target: String, reason: &'static str) -> Self {
Self::InvalidLink {
position,
path,
target,
reason,
}
}
fn unsafe_path(
position: u64,
context: &'static str,
value: &str,
reason: &'static str,
) -> Self {
Self::UnsafePath {
position,
context,
value: value.to_owned(),
reason,
}
}
fn filesystem(operation: &'static str, path: PathBuf, source: io::Error) -> Self {
Self::Filesystem {
operation,
path,
source,
}
}
}