mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-08 14:32:24 -04:00
Merge commit 'fec26926a8
' as 'tokio-tar'
This commit is contained in:
commit
18ba8f49c4
34 changed files with 6427 additions and 0 deletions
610
tokio-tar/src/archive.rs
Normal file
610
tokio-tar/src/archive.rs
Normal file
|
@ -0,0 +1,610 @@
|
|||
use std::{
|
||||
cmp,
|
||||
path::Path,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
io::{self, AsyncRead as Read, AsyncReadExt},
|
||||
sync::Mutex,
|
||||
};
|
||||
use tokio_stream::*;
|
||||
|
||||
use crate::{
|
||||
entry::{EntryFields, EntryIo},
|
||||
error::TarError,
|
||||
other, Entry, GnuExtSparseHeader, GnuSparseHeader, Header,
|
||||
};
|
||||
|
||||
/// A top-level representation of an archive file.
|
||||
///
|
||||
/// This archive can have an entry added to it and it can be iterated over.
|
||||
#[derive(Debug)]
|
||||
pub struct Archive<R: Read + Unpin> {
|
||||
inner: Arc<ArchiveInner<R>>,
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> Clone for Archive<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Archive {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArchiveInner<R> {
|
||||
pos: AtomicU64,
|
||||
unpack_xattrs: bool,
|
||||
preserve_permissions: bool,
|
||||
preserve_mtime: bool,
|
||||
ignore_zeros: bool,
|
||||
obj: Mutex<R>,
|
||||
}
|
||||
|
||||
/// Configure the archive.
|
||||
pub struct ArchiveBuilder<R: Read + Unpin> {
|
||||
obj: R,
|
||||
unpack_xattrs: bool,
|
||||
preserve_permissions: bool,
|
||||
preserve_mtime: bool,
|
||||
ignore_zeros: bool,
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> ArchiveBuilder<R> {
|
||||
/// Create a new builder.
|
||||
pub fn new(obj: R) -> Self {
|
||||
ArchiveBuilder {
|
||||
unpack_xattrs: false,
|
||||
preserve_permissions: false,
|
||||
preserve_mtime: true,
|
||||
ignore_zeros: false,
|
||||
obj,
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicate whether extended file attributes (xattrs on Unix) are preserved
|
||||
/// when unpacking this archive.
|
||||
///
|
||||
/// This flag is disabled by default and is currently only implemented on
|
||||
/// Unix using xattr support. This may eventually be implemented for
|
||||
/// Windows, however, if other archive implementations are found which do
|
||||
/// this as well.
|
||||
pub fn set_unpack_xattrs(mut self, unpack_xattrs: bool) -> Self {
|
||||
self.unpack_xattrs = unpack_xattrs;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indicate whether extended permissions (like suid on Unix) are preserved
|
||||
/// when unpacking this entry.
|
||||
///
|
||||
/// This flag is disabled by default and is currently only implemented on
|
||||
/// Unix.
|
||||
pub fn set_preserve_permissions(mut self, preserve: bool) -> Self {
|
||||
self.preserve_permissions = preserve;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indicate whether access time information is preserved when unpacking
|
||||
/// this entry.
|
||||
///
|
||||
/// This flag is enabled by default.
|
||||
pub fn set_preserve_mtime(mut self, preserve: bool) -> Self {
|
||||
self.preserve_mtime = preserve;
|
||||
self
|
||||
}
|
||||
|
||||
/// Ignore zeroed headers, which would otherwise indicate to the archive that it has no more
|
||||
/// entries.
|
||||
///
|
||||
/// This can be used in case multiple tar archives have been concatenated together.
|
||||
pub fn set_ignore_zeros(mut self, ignore_zeros: bool) -> Self {
|
||||
self.ignore_zeros = ignore_zeros;
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct the archive, ready to accept inputs.
|
||||
pub fn build(self) -> Archive<R> {
|
||||
let Self {
|
||||
unpack_xattrs,
|
||||
preserve_permissions,
|
||||
preserve_mtime,
|
||||
ignore_zeros,
|
||||
obj,
|
||||
} = self;
|
||||
|
||||
Archive {
|
||||
inner: Arc::new(ArchiveInner {
|
||||
unpack_xattrs,
|
||||
preserve_permissions,
|
||||
preserve_mtime,
|
||||
ignore_zeros,
|
||||
obj: Mutex::new(obj),
|
||||
pos: 0.into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin + Sync + Send> Archive<R> {
|
||||
/// Create a new archive with the underlying object as the reader.
|
||||
pub fn new(obj: R) -> Archive<R> {
|
||||
Archive {
|
||||
inner: Arc::new(ArchiveInner {
|
||||
unpack_xattrs: false,
|
||||
preserve_permissions: false,
|
||||
preserve_mtime: true,
|
||||
ignore_zeros: false,
|
||||
obj: Mutex::new(obj),
|
||||
pos: 0.into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwrap this archive, returning the underlying object.
|
||||
pub fn into_inner(self) -> Result<R, Self> {
|
||||
let Self { inner } = self;
|
||||
|
||||
match Arc::try_unwrap(inner) {
|
||||
Ok(inner) => Ok(inner.obj.into_inner()),
|
||||
Err(inner) => Err(Self { inner }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an stream over the entries in this archive.
|
||||
///
|
||||
/// Note that care must be taken to consider each entry within an archive in
|
||||
/// sequence. If entries are processed out of sequence (from what the
|
||||
/// stream returns), then the contents read for each entry may be
|
||||
/// corrupted.
|
||||
pub fn entries(&mut self) -> io::Result<Entries<R>> {
|
||||
if self.inner.pos.load(Ordering::SeqCst) != 0 {
|
||||
return Err(other(
|
||||
"cannot call entries unless archive is at \
|
||||
position 0",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Entries {
|
||||
archive: self.clone(),
|
||||
next: 0,
|
||||
gnu_longlink: None,
|
||||
gnu_longname: None,
|
||||
pax_extensions: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct an stream over the raw entries in this archive.
|
||||
///
|
||||
/// Note that care must be taken to consider each entry within an archive in
|
||||
/// sequence. If entries are processed out of sequence (from what the
|
||||
/// stream returns), then the contents read for each entry may be
|
||||
/// corrupted.
|
||||
pub fn entries_raw(&mut self) -> io::Result<RawEntries<R>> {
|
||||
if self.inner.pos.load(Ordering::SeqCst) != 0 {
|
||||
return Err(other(
|
||||
"cannot call entries_raw unless archive is at \
|
||||
position 0",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(RawEntries {
|
||||
archive: self.clone(),
|
||||
next: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Unpacks the contents tarball into the specified `dst`.
|
||||
///
|
||||
/// This function will iterate over the entire contents of this tarball,
|
||||
/// extracting each file in turn to the location specified by the entry's
|
||||
/// path name.
|
||||
///
|
||||
/// This operation is relatively sensitive in that it will not write files
|
||||
/// outside of the path specified by `dst`. Files in the archive which have
|
||||
/// a '..' in their path are skipped during the unpacking process.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio::fs::File;
|
||||
/// use tokio_tar::Archive;
|
||||
///
|
||||
/// let mut ar = Archive::new(File::open("foo.tar").await?);
|
||||
/// ar.unpack("foo").await?;
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn unpack<P: AsRef<Path>>(&mut self, dst: P) -> io::Result<()> {
|
||||
let mut entries = self.entries()?;
|
||||
let mut pinned = Pin::new(&mut entries);
|
||||
while let Some(entry) = pinned.next().await {
|
||||
let mut file = entry.map_err(|e| TarError::new("failed to iterate over archive", e))?;
|
||||
file.unpack_in(dst.as_ref()).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream of `Entry`s.
|
||||
pub struct Entries<R: Read + Unpin> {
|
||||
archive: Archive<R>,
|
||||
next: u64,
|
||||
gnu_longname: Option<Vec<u8>>,
|
||||
gnu_longlink: Option<Vec<u8>>,
|
||||
pax_extensions: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
macro_rules! ready_opt_err {
|
||||
($val:expr) => {
|
||||
match futures_core::ready!($val) {
|
||||
Some(Ok(val)) => val,
|
||||
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||
None => return Poll::Ready(None),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ready_err {
|
||||
($val:expr) => {
|
||||
match futures_core::ready!($val) {
|
||||
Ok(val) => val,
|
||||
Err(err) => return Poll::Ready(Some(Err(err))),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> Stream for Entries<R> {
|
||||
type Item = io::Result<Entry<Archive<R>>>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
let entry = ready_opt_err!(poll_next_raw(self.archive.clone(), &mut self.next, cx));
|
||||
|
||||
if entry.header().as_gnu().is_some() && entry.header().entry_type().is_gnu_longname() {
|
||||
if self.gnu_longname.is_some() {
|
||||
return Poll::Ready(Some(Err(other(
|
||||
"two long name entries describing \
|
||||
the same member",
|
||||
))));
|
||||
}
|
||||
|
||||
let mut ef = EntryFields::from(entry);
|
||||
let val = ready_err!(Pin::new(&mut ef).poll_read_all(cx));
|
||||
self.gnu_longname = Some(val);
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.header().as_gnu().is_some() && entry.header().entry_type().is_gnu_longlink() {
|
||||
if self.gnu_longlink.is_some() {
|
||||
return Poll::Ready(Some(Err(other(
|
||||
"two long name entries describing \
|
||||
the same member",
|
||||
))));
|
||||
}
|
||||
let mut ef = EntryFields::from(entry);
|
||||
let val = ready_err!(Pin::new(&mut ef).poll_read_all(cx));
|
||||
self.gnu_longlink = Some(val);
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.header().as_ustar().is_some()
|
||||
&& entry.header().entry_type().is_pax_local_extensions()
|
||||
{
|
||||
if self.pax_extensions.is_some() {
|
||||
return Poll::Ready(Some(Err(other(
|
||||
"two pax extensions entries describing \
|
||||
the same member",
|
||||
))));
|
||||
}
|
||||
let mut ef = EntryFields::from(entry);
|
||||
let val = ready_err!(Pin::new(&mut ef).poll_read_all(cx));
|
||||
self.pax_extensions = Some(val);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut fields = EntryFields::from(entry);
|
||||
fields.long_pathname = self.gnu_longname.take();
|
||||
fields.long_linkname = self.gnu_longlink.take();
|
||||
fields.pax_extensions = self.pax_extensions.take();
|
||||
|
||||
ready_err!(poll_parse_sparse_header(
|
||||
self.archive.clone(),
|
||||
&mut self.next,
|
||||
&mut fields,
|
||||
cx
|
||||
));
|
||||
|
||||
return Poll::Ready(Some(Ok(fields.into_entry())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream of raw `Entry`s.
|
||||
pub struct RawEntries<R: Read + Unpin> {
|
||||
archive: Archive<R>,
|
||||
next: u64,
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> Stream for RawEntries<R> {
|
||||
type Item = io::Result<Entry<Archive<R>>>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
poll_next_raw(self.archive.clone(), &mut self.next, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next_raw<R: Read + Unpin>(
|
||||
mut archive: Archive<R>,
|
||||
next: &mut u64,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<io::Result<Entry<Archive<R>>>>> {
|
||||
let mut header = Header::new_old();
|
||||
let mut header_pos = *next;
|
||||
|
||||
loop {
|
||||
// Seek to the start of the next header in the archive
|
||||
let delta = *next - archive.inner.pos.load(Ordering::SeqCst);
|
||||
|
||||
match futures_core::ready!(poll_skip(&mut archive, cx, delta)) {
|
||||
Ok(_) => {}
|
||||
Err(err) => return Poll::Ready(Some(Err(err))),
|
||||
}
|
||||
|
||||
// EOF is an indicator that we are at the end of the archive.
|
||||
match futures_core::ready!(poll_try_read_all(&mut archive, cx, header.as_mut_bytes())) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => return Poll::Ready(None),
|
||||
Err(err) => return Poll::Ready(Some(Err(err))),
|
||||
}
|
||||
|
||||
// If a header is not all zeros, we have another valid header.
|
||||
// Otherwise, check if we are ignoring zeros and continue, or break as if this is the
|
||||
// end of the archive.
|
||||
if !header.as_bytes().iter().all(|i| *i == 0) {
|
||||
*next += 512;
|
||||
break;
|
||||
}
|
||||
|
||||
if !archive.inner.ignore_zeros {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
*next += 512;
|
||||
header_pos = *next;
|
||||
}
|
||||
|
||||
// Make sure the checksum is ok
|
||||
let sum = header.as_bytes()[..148]
|
||||
.iter()
|
||||
.chain(&header.as_bytes()[156..])
|
||||
.fold(0, |a, b| a + (*b as u32))
|
||||
+ 8 * 32;
|
||||
let cksum = header.cksum()?;
|
||||
if sum != cksum {
|
||||
return Poll::Ready(Some(Err(other("archive header checksum mismatch"))));
|
||||
}
|
||||
|
||||
let file_pos = *next;
|
||||
let size = header.entry_size()?;
|
||||
|
||||
let data = EntryIo::Data(archive.clone().take(size));
|
||||
|
||||
let ret = EntryFields {
|
||||
size,
|
||||
header_pos,
|
||||
file_pos,
|
||||
data: vec![data],
|
||||
header,
|
||||
long_pathname: None,
|
||||
long_linkname: None,
|
||||
pax_extensions: None,
|
||||
unpack_xattrs: archive.inner.unpack_xattrs,
|
||||
preserve_permissions: archive.inner.preserve_permissions,
|
||||
preserve_mtime: archive.inner.preserve_mtime,
|
||||
read_state: None,
|
||||
};
|
||||
|
||||
// Store where the next entry is, rounding up by 512 bytes (the size of
|
||||
// a header);
|
||||
let size = (size + 511) & !(512 - 1);
|
||||
*next += size;
|
||||
|
||||
Poll::Ready(Some(Ok(ret.into_entry())))
|
||||
}
|
||||
|
||||
fn poll_parse_sparse_header<R: Read + Unpin>(
|
||||
mut archive: Archive<R>,
|
||||
next: &mut u64,
|
||||
entry: &mut EntryFields<Archive<R>>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
if !entry.header.entry_type().is_gnu_sparse() {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
let gnu = match entry.header.as_gnu() {
|
||||
Some(gnu) => gnu,
|
||||
None => return Poll::Ready(Err(other("sparse entry type listed but not GNU header"))),
|
||||
};
|
||||
|
||||
// Sparse files are represented internally as a list of blocks that are
|
||||
// read. Blocks are either a bunch of 0's or they're data from the
|
||||
// underlying archive.
|
||||
//
|
||||
// Blocks of a sparse file are described by the `GnuSparseHeader`
|
||||
// structure, some of which are contained in `GnuHeader` but some of
|
||||
// which may also be contained after the first header in further
|
||||
// headers.
|
||||
//
|
||||
// We read off all the blocks here and use the `add_block` function to
|
||||
// incrementally add them to the list of I/O block (in `entry.data`).
|
||||
// The `add_block` function also validates that each chunk comes after
|
||||
// the previous, we don't overrun the end of the file, and each block is
|
||||
// aligned to a 512-byte boundary in the archive itself.
|
||||
//
|
||||
// At the end we verify that the sparse file size (`Header::size`) is
|
||||
// the same as the current offset (described by the list of blocks) as
|
||||
// well as the amount of data read equals the size of the entry
|
||||
// (`Header::entry_size`).
|
||||
entry.data.truncate(0);
|
||||
|
||||
let mut cur = 0;
|
||||
let mut remaining = entry.size;
|
||||
{
|
||||
let data = &mut entry.data;
|
||||
let reader = archive.clone();
|
||||
let size = entry.size;
|
||||
let mut add_block = |block: &GnuSparseHeader| -> io::Result<_> {
|
||||
if block.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let off = block.offset()?;
|
||||
let len = block.length()?;
|
||||
|
||||
if (size - remaining) % 512 != 0 {
|
||||
return Err(other(
|
||||
"previous block in sparse file was not \
|
||||
aligned to 512-byte boundary",
|
||||
));
|
||||
} else if off < cur {
|
||||
return Err(other(
|
||||
"out of order or overlapping sparse \
|
||||
blocks",
|
||||
));
|
||||
} else if cur < off {
|
||||
let block = io::repeat(0).take(off - cur);
|
||||
data.push(EntryIo::Pad(block));
|
||||
}
|
||||
cur = off
|
||||
.checked_add(len)
|
||||
.ok_or_else(|| other("more bytes listed in sparse file than u64 can hold"))?;
|
||||
remaining = remaining.checked_sub(len).ok_or_else(|| {
|
||||
other(
|
||||
"sparse file consumed more data than the header \
|
||||
listed",
|
||||
)
|
||||
})?;
|
||||
data.push(EntryIo::Data(reader.clone().take(len)));
|
||||
Ok(())
|
||||
};
|
||||
for block in gnu.sparse.iter() {
|
||||
add_block(block)?
|
||||
}
|
||||
if gnu.is_extended() {
|
||||
let mut ext = GnuExtSparseHeader::new();
|
||||
ext.isextended[0] = 1;
|
||||
while ext.is_extended() {
|
||||
match futures_core::ready!(poll_try_read_all(&mut archive, cx, ext.as_mut_bytes()))
|
||||
{
|
||||
Ok(true) => {}
|
||||
Ok(false) => return Poll::Ready(Err(other("failed to read extension"))),
|
||||
Err(err) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
|
||||
*next += 512;
|
||||
for block in ext.sparse.iter() {
|
||||
add_block(block)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cur != gnu.real_size()? {
|
||||
return Poll::Ready(Err(other(
|
||||
"mismatch in sparse file chunks and \
|
||||
size in header",
|
||||
)));
|
||||
}
|
||||
entry.size = cur;
|
||||
if remaining > 0 {
|
||||
return Poll::Ready(Err(other(
|
||||
"mismatch in sparse file chunks and \
|
||||
entry size in header",
|
||||
)));
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> Read for Archive<R> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
into: &mut io::ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let mut r = if let Ok(v) = self.inner.obj.try_lock() {
|
||||
v
|
||||
} else {
|
||||
return Poll::Pending;
|
||||
};
|
||||
|
||||
let res = futures_core::ready!(Pin::new(&mut *r).poll_read(cx, into));
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.inner
|
||||
.pos
|
||||
.fetch_add(into.filled().len() as u64, Ordering::SeqCst);
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Err(err) => Poll::Ready(Err(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to fill the buffer from the reader.
|
||||
///
|
||||
/// If the reader reaches its end before filling the buffer at all, returns `false`.
|
||||
/// Otherwise returns `true`.
|
||||
fn poll_try_read_all<R: Read + Unpin>(
|
||||
mut source: R,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<bool>> {
|
||||
let mut read = 0;
|
||||
while read < buf.len() {
|
||||
let mut read_buf = io::ReadBuf::new(&mut buf[read..]);
|
||||
match futures_core::ready!(Pin::new(&mut source).poll_read(cx, &mut read_buf)) {
|
||||
Ok(()) if read_buf.filled().is_empty() => {
|
||||
if read == 0 {
|
||||
return Poll::Ready(Ok(false));
|
||||
}
|
||||
|
||||
return Poll::Ready(Err(other("failed to read entire block")));
|
||||
}
|
||||
Ok(()) => read += read_buf.filled().len(),
|
||||
Err(err) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(true))
|
||||
}
|
||||
|
||||
/// Skip n bytes on the given source.
|
||||
fn poll_skip<R: Read + Unpin>(
|
||||
mut source: R,
|
||||
cx: &mut Context<'_>,
|
||||
mut amt: u64,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let mut buf = [0u8; 4096 * 8];
|
||||
while amt > 0 {
|
||||
let n = cmp::min(amt, buf.len() as u64);
|
||||
let mut read_buf = io::ReadBuf::new(&mut buf[..n as usize]);
|
||||
match futures_core::ready!(Pin::new(&mut source).poll_read(cx, &mut read_buf)) {
|
||||
Ok(()) if read_buf.filled().is_empty() => {
|
||||
return Poll::Ready(Err(other("unexpected EOF during skip")));
|
||||
}
|
||||
Ok(()) => {
|
||||
amt -= read_buf.filled().len() as u64;
|
||||
}
|
||||
Err(err) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
633
tokio-tar/src/builder.rs
Normal file
633
tokio-tar/src/builder.rs
Normal file
|
@ -0,0 +1,633 @@
|
|||
use crate::{
|
||||
header::{bytes2path, path2bytes, HeaderMode},
|
||||
other, EntryType, Header,
|
||||
};
|
||||
use std::{borrow::Cow, fs::Metadata, path::Path};
|
||||
use tokio::{
|
||||
fs,
|
||||
io::{self, AsyncRead as Read, AsyncReadExt, AsyncWrite as Write, AsyncWriteExt},
|
||||
};
|
||||
|
||||
/// A structure for building archives
|
||||
///
|
||||
/// This structure has methods for building up an archive from scratch into any
|
||||
/// arbitrary writer.
|
||||
pub struct Builder<W: Write + Unpin + Send + 'static> {
|
||||
mode: HeaderMode,
|
||||
follow: bool,
|
||||
finished: bool,
|
||||
obj: Option<W>,
|
||||
cancellation: Option<tokio::sync::oneshot::Sender<W>>,
|
||||
}
|
||||
|
||||
impl<W: Write + Unpin + Send + 'static> Builder<W> {
|
||||
/// Create a new archive builder with the underlying object as the
|
||||
/// destination of all data written. The builder will use
|
||||
/// `HeaderMode::Complete` by default.
|
||||
pub fn new(obj: W) -> Builder<W> {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<W>();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(mut w) = rx.await {
|
||||
let _ = w.write_all(&[0; 1024]).await;
|
||||
}
|
||||
});
|
||||
Builder {
|
||||
mode: HeaderMode::Complete,
|
||||
follow: true,
|
||||
finished: false,
|
||||
obj: Some(obj),
|
||||
cancellation: Some(tx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the HeaderMode that will be used when reading fs Metadata for
|
||||
/// methods that implicitly read metadata for an input Path. Notably, this
|
||||
/// does _not_ apply to `append(Header)`.
|
||||
pub fn mode(&mut self, mode: HeaderMode) {
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
/// Follow symlinks, archiving the contents of the file they point to rather
|
||||
/// than adding a symlink to the archive. Defaults to true.
|
||||
pub fn follow_symlinks(&mut self, follow: bool) {
|
||||
self.follow = follow;
|
||||
}
|
||||
|
||||
/// Gets shared reference to the underlying object.
|
||||
pub fn get_ref(&self) -> &W {
|
||||
self.obj.as_ref().unwrap()
|
||||
}
|
||||
|
||||
/// Gets mutable reference to the underlying object.
|
||||
///
|
||||
/// Note that care must be taken while writing to the underlying
|
||||
/// object. But, e.g. `get_mut().flush()` is claimed to be safe and
|
||||
/// useful in the situations when one needs to be ensured that
|
||||
/// tar entry was flushed to the disk.
|
||||
pub fn get_mut(&mut self) -> &mut W {
|
||||
self.obj.as_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Unwrap this archive, returning the underlying object.
|
||||
///
|
||||
/// This function will finish writing the archive if the `finish` function
|
||||
/// hasn't yet been called, returning any I/O error which happens during
|
||||
/// that operation.
|
||||
pub async fn into_inner(mut self) -> io::Result<W> {
|
||||
if !self.finished {
|
||||
self.finish().await?;
|
||||
}
|
||||
Ok(self.obj.take().unwrap())
|
||||
}
|
||||
|
||||
/// Adds a new entry to this archive.
|
||||
///
|
||||
/// This function will append the header specified, followed by contents of
|
||||
/// the stream specified by `data`. To produce a valid archive the `size`
|
||||
/// field of `header` must be the same as the length of the stream that's
|
||||
/// being written. Additionally the checksum for the header should have been
|
||||
/// set via the `set_cksum` method.
|
||||
///
|
||||
/// Note that this will not attempt to seek the archive to a valid position,
|
||||
/// so if the archive is in the middle of a read or some other similar
|
||||
/// operation then this may corrupt the archive.
|
||||
///
|
||||
/// Also note that after all entries have been written to an archive the
|
||||
/// `finish` function needs to be called to finish writing the archive.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error for any intermittent I/O error which
|
||||
/// occurs when either reading or writing.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio_tar::{Builder, Header};
|
||||
///
|
||||
/// let mut header = Header::new_gnu();
|
||||
/// header.set_path("foo")?;
|
||||
/// header.set_size(4);
|
||||
/// header.set_cksum();
|
||||
///
|
||||
/// let mut data: &[u8] = &[1, 2, 3, 4];
|
||||
///
|
||||
/// let mut ar = Builder::new(Vec::new());
|
||||
/// ar.append(&header, data).await?;
|
||||
/// let data = ar.into_inner().await?;
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn append<R: Read + Unpin>(
|
||||
&mut self,
|
||||
header: &Header,
|
||||
mut data: R,
|
||||
) -> io::Result<()> {
|
||||
append(self.get_mut(), header, &mut data).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a new entry to this archive with the specified path.
|
||||
///
|
||||
/// This function will set the specified path in the given header, which may
|
||||
/// require appending a GNU long-name extension entry to the archive first.
|
||||
/// The checksum for the header will be automatically updated via the
|
||||
/// `set_cksum` method after setting the path. No other metadata in the
|
||||
/// header will be modified.
|
||||
///
|
||||
/// Then it will append the header, followed by contents of the stream
|
||||
/// specified by `data`. To produce a valid archive the `size` field of
|
||||
/// `header` must be the same as the length of the stream that's being
|
||||
/// written.
|
||||
///
|
||||
/// Note that this will not attempt to seek the archive to a valid position,
|
||||
/// so if the archive is in the middle of a read or some other similar
|
||||
/// operation then this may corrupt the archive.
|
||||
///
|
||||
/// Also note that after all entries have been written to an archive the
|
||||
/// `finish` function needs to be called to finish writing the archive.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error for any intermittent I/O error which
|
||||
/// occurs when either reading or writing.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio_tar::{Builder, Header};
|
||||
///
|
||||
/// let mut header = Header::new_gnu();
|
||||
/// header.set_size(4);
|
||||
/// header.set_cksum();
|
||||
///
|
||||
/// let mut data: &[u8] = &[1, 2, 3, 4];
|
||||
///
|
||||
/// let mut ar = Builder::new(Vec::new());
|
||||
/// ar.append_data(&mut header, "really/long/path/to/foo", data).await?;
|
||||
/// let data = ar.into_inner().await?;
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn append_data<P: AsRef<Path>, R: Read + Unpin>(
|
||||
&mut self,
|
||||
header: &mut Header,
|
||||
path: P,
|
||||
data: R,
|
||||
) -> io::Result<()> {
|
||||
prepare_header_path(self.get_mut(), header, path.as_ref()).await?;
|
||||
header.set_cksum();
|
||||
self.append(&header, data).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a file on the local filesystem to this archive.
|
||||
///
|
||||
/// This function will open the file specified by `path` and insert the file
|
||||
/// into the archive with the appropriate metadata set, returning any I/O
|
||||
/// error which occurs while writing. The path name for the file inside of
|
||||
/// this archive will be the same as `path`, and it is required that the
|
||||
/// path is a relative path.
|
||||
///
|
||||
/// Note that this will not attempt to seek the archive to a valid position,
|
||||
/// so if the archive is in the middle of a read or some other similar
|
||||
/// operation then this may corrupt the archive.
|
||||
///
|
||||
/// Also note that after all files have been written to an archive the
|
||||
/// `finish` function needs to be called to finish writing the archive.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio_tar::Builder;
|
||||
///
|
||||
/// let mut ar = Builder::new(Vec::new());
|
||||
///
|
||||
/// ar.append_path("foo/bar.txt").await?;
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn append_path<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let mode = self.mode;
|
||||
let follow = self.follow;
|
||||
append_path_with_name(self.get_mut(), path.as_ref(), None, mode, follow).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a file on the local filesystem to this archive under another name.
|
||||
///
|
||||
/// This function will open the file specified by `path` and insert the file
|
||||
/// into the archive as `name` with appropriate metadata set, returning any
|
||||
/// I/O error which occurs while writing. The path name for the file inside
|
||||
/// of this archive will be `name` is required to be a relative path.
|
||||
///
|
||||
/// Note that this will not attempt to seek the archive to a valid position,
|
||||
/// so if the archive is in the middle of a read or some other similar
|
||||
/// operation then this may corrupt the archive.
|
||||
///
|
||||
/// Also note that after all files have been written to an archive the
|
||||
/// `finish` function needs to be called to finish writing the archive.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio_tar::Builder;
|
||||
///
|
||||
/// let mut ar = Builder::new(Vec::new());
|
||||
///
|
||||
/// // Insert the local file "foo/bar.txt" in the archive but with the name
|
||||
/// // "bar/foo.txt".
|
||||
/// ar.append_path_with_name("foo/bar.txt", "bar/foo.txt").await?;
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn append_path_with_name<P: AsRef<Path>, N: AsRef<Path>>(
|
||||
&mut self,
|
||||
path: P,
|
||||
name: N,
|
||||
) -> io::Result<()> {
|
||||
let mode = self.mode;
|
||||
let follow = self.follow;
|
||||
append_path_with_name(
|
||||
self.get_mut(),
|
||||
path.as_ref(),
|
||||
Some(name.as_ref()),
|
||||
mode,
|
||||
follow,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a file to this archive with the given path as the name of the file
|
||||
/// in the archive.
|
||||
///
|
||||
/// This will use the metadata of `file` to populate a `Header`, and it will
|
||||
/// then append the file to the archive with the name `path`.
|
||||
///
|
||||
/// Note that this will not attempt to seek the archive to a valid position,
|
||||
/// so if the archive is in the middle of a read or some other similar
|
||||
/// operation then this may corrupt the archive.
|
||||
///
|
||||
/// Also note that after all files have been written to an archive the
|
||||
/// `finish` function needs to be called to finish writing the archive.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio::fs::File;
|
||||
/// use tokio_tar::Builder;
|
||||
///
|
||||
/// let mut ar = Builder::new(Vec::new());
|
||||
///
|
||||
/// // Open the file at one location, but insert it into the archive with a
|
||||
/// // different name.
|
||||
/// let mut f = File::open("foo/bar/baz.txt").await?;
|
||||
/// ar.append_file("bar/baz.txt", &mut f).await?;
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn append_file<P: AsRef<Path>>(
|
||||
&mut self,
|
||||
path: P,
|
||||
file: &mut fs::File,
|
||||
) -> io::Result<()> {
|
||||
let mode = self.mode;
|
||||
append_file(self.get_mut(), path.as_ref(), file, mode).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a directory to this archive with the given path as the name of the
|
||||
/// directory in the archive.
|
||||
///
|
||||
/// This will use `stat` to populate a `Header`, and it will then append the
|
||||
/// directory to the archive with the name `path`.
|
||||
///
|
||||
/// Note that this will not attempt to seek the archive to a valid position,
|
||||
/// so if the archive is in the middle of a read or some other similar
|
||||
/// operation then this may corrupt the archive.
|
||||
///
|
||||
/// Also note that after all files have been written to an archive the
|
||||
/// `finish` function needs to be called to finish writing the archive.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio::fs;
|
||||
/// use tokio_tar::Builder;
|
||||
///
|
||||
/// let mut ar = Builder::new(Vec::new());
|
||||
///
|
||||
/// // Use the directory at one location, but insert it into the archive
|
||||
/// // with a different name.
|
||||
/// ar.append_dir("bardir", ".").await?;
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn append_dir<P, Q>(&mut self, path: P, src_path: Q) -> io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let mode = self.mode;
|
||||
append_dir(self.get_mut(), path.as_ref(), src_path.as_ref(), mode).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a directory and all of its contents (recursively) to this archive
|
||||
/// with the given path as the name of the directory in the archive.
|
||||
///
|
||||
/// Note that this will not attempt to seek the archive to a valid position,
|
||||
/// so if the archive is in the middle of a read or some other similar
|
||||
/// operation then this may corrupt the archive.
|
||||
///
|
||||
/// Also note that after all files have been written to an archive the
|
||||
/// `finish` function needs to be called to finish writing the archive.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio::fs;
|
||||
/// use tokio_tar::Builder;
|
||||
///
|
||||
/// let mut ar = Builder::new(Vec::new());
|
||||
///
|
||||
/// // Use the directory at one location, but insert it into the archive
|
||||
/// // with a different name.
|
||||
/// ar.append_dir_all("bardir", ".").await?;
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn append_dir_all<P, Q>(&mut self, path: P, src_path: Q) -> io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
Q: AsRef<Path>,
|
||||
{
|
||||
let mode = self.mode;
|
||||
let follow = self.follow;
|
||||
append_dir_all(
|
||||
self.get_mut(),
|
||||
path.as_ref(),
|
||||
src_path.as_ref(),
|
||||
mode,
|
||||
follow,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finish writing this archive, emitting the termination sections.
|
||||
///
|
||||
/// This function should only be called when the archive has been written
|
||||
/// entirely and if an I/O error happens the underlying object still needs
|
||||
/// to be acquired.
|
||||
///
|
||||
/// In most situations the `into_inner` method should be preferred.
|
||||
pub async fn finish(&mut self) -> io::Result<()> {
|
||||
if self.finished {
|
||||
return Ok(());
|
||||
}
|
||||
self.finished = true;
|
||||
self.get_mut().write_all(&[0; 1024]).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn append<Dst: Write + Unpin + ?Sized, Data: Read + Unpin + ?Sized>(
|
||||
mut dst: &mut Dst,
|
||||
header: &Header,
|
||||
mut data: &mut Data,
|
||||
) -> io::Result<()> {
|
||||
dst.write_all(header.as_bytes()).await?;
|
||||
let len = io::copy(&mut data, &mut dst).await?;
|
||||
|
||||
// Pad with zeros if necessary.
|
||||
let buf = [0; 512];
|
||||
let remaining = 512 - (len % 512);
|
||||
if remaining < 512 {
|
||||
dst.write_all(&buf[..remaining as usize]).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn append_path_with_name<Dst: Write + Unpin + ?Sized>(
|
||||
dst: &mut Dst,
|
||||
path: &Path,
|
||||
name: Option<&Path>,
|
||||
mode: HeaderMode,
|
||||
follow: bool,
|
||||
) -> io::Result<()> {
|
||||
let stat = if follow {
|
||||
fs::metadata(path).await.map_err(|err| {
|
||||
io::Error::new(
|
||||
err.kind(),
|
||||
format!("{} when getting metadata for {}", err, path.display()),
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
fs::symlink_metadata(path).await.map_err(|err| {
|
||||
io::Error::new(
|
||||
err.kind(),
|
||||
format!("{} when getting metadata for {}", err, path.display()),
|
||||
)
|
||||
})?
|
||||
};
|
||||
let ar_name = name.unwrap_or(path);
|
||||
if stat.is_file() {
|
||||
append_fs(
|
||||
dst,
|
||||
ar_name,
|
||||
&stat,
|
||||
&mut fs::File::open(path).await?,
|
||||
mode,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
} else if stat.is_dir() {
|
||||
append_fs(dst, ar_name, &stat, &mut io::empty(), mode, None).await?;
|
||||
Ok(())
|
||||
} else if stat.file_type().is_symlink() {
|
||||
let link_name = fs::read_link(path).await?;
|
||||
append_fs(
|
||||
dst,
|
||||
ar_name,
|
||||
&stat,
|
||||
&mut io::empty(),
|
||||
mode,
|
||||
Some(&link_name),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(other(&format!("{} has unknown file type", path.display())))
|
||||
}
|
||||
}
|
||||
|
||||
async fn append_file<Dst: Write + Unpin + ?Sized>(
|
||||
dst: &mut Dst,
|
||||
path: &Path,
|
||||
file: &mut fs::File,
|
||||
mode: HeaderMode,
|
||||
) -> io::Result<()> {
|
||||
let stat = file.metadata().await?;
|
||||
append_fs(dst, path, &stat, file, mode, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn append_dir<Dst: Write + Unpin + ?Sized>(
|
||||
dst: &mut Dst,
|
||||
path: &Path,
|
||||
src_path: &Path,
|
||||
mode: HeaderMode,
|
||||
) -> io::Result<()> {
|
||||
let stat = fs::metadata(src_path).await?;
|
||||
append_fs(dst, path, &stat, &mut io::empty(), mode, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_header(size: u64, entry_type: EntryType) -> Header {
|
||||
let mut header = Header::new_gnu();
|
||||
let name = b"././@LongLink";
|
||||
header.as_gnu_mut().unwrap().name[..name.len()].clone_from_slice(&name[..]);
|
||||
header.set_mode(0o644);
|
||||
header.set_uid(0);
|
||||
header.set_gid(0);
|
||||
header.set_mtime(0);
|
||||
// + 1 to be compliant with GNU tar
|
||||
header.set_size(size + 1);
|
||||
header.set_entry_type(entry_type);
|
||||
header.set_cksum();
|
||||
header
|
||||
}
|
||||
|
||||
async fn prepare_header_path<Dst: Write + Unpin + ?Sized>(
|
||||
dst: &mut Dst,
|
||||
header: &mut Header,
|
||||
path: &Path,
|
||||
) -> io::Result<()> {
|
||||
// Try to encode the path directly in the header, but if it ends up not
|
||||
// working (probably because it's too long) then try to use the GNU-specific
|
||||
// long name extension by emitting an entry which indicates that it's the
|
||||
// filename.
|
||||
if let Err(e) = header.set_path(path) {
|
||||
let data = path2bytes(&path)?;
|
||||
let max = header.as_old().name.len();
|
||||
// Since e isn't specific enough to let us know the path is indeed too
|
||||
// long, verify it first before using the extension.
|
||||
if data.len() < max {
|
||||
return Err(e);
|
||||
}
|
||||
let header2 = prepare_header(data.len() as u64, EntryType::GNULongName);
|
||||
// null-terminated string
|
||||
let mut data2 = data.chain(io::repeat(0).take(1));
|
||||
append(dst, &header2, &mut data2).await?;
|
||||
// Truncate the path to store in the header we're about to emit to
|
||||
// ensure we've got something at least mentioned.
|
||||
let path = bytes2path(Cow::Borrowed(&data[..max]))?;
|
||||
header.set_path(&path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prepare_header_link<Dst: Write + Unpin + ?Sized>(
|
||||
dst: &mut Dst,
|
||||
header: &mut Header,
|
||||
link_name: &Path,
|
||||
) -> io::Result<()> {
|
||||
// Same as previous function but for linkname
|
||||
if let Err(e) = header.set_link_name(&link_name) {
|
||||
let data = path2bytes(&link_name)?;
|
||||
if data.len() < header.as_old().linkname.len() {
|
||||
return Err(e);
|
||||
}
|
||||
let header2 = prepare_header(data.len() as u64, EntryType::GNULongLink);
|
||||
let mut data2 = data.chain(io::repeat(0).take(1));
|
||||
append(dst, &header2, &mut data2).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn append_fs<Dst: Write + Unpin + ?Sized, R: Read + Unpin + ?Sized>(
|
||||
dst: &mut Dst,
|
||||
path: &Path,
|
||||
meta: &Metadata,
|
||||
read: &mut R,
|
||||
mode: HeaderMode,
|
||||
link_name: Option<&Path>,
|
||||
) -> io::Result<()> {
|
||||
let mut header = Header::new_gnu();
|
||||
|
||||
prepare_header_path(dst, &mut header, path).await?;
|
||||
header.set_metadata_in_mode(meta, mode);
|
||||
if let Some(link_name) = link_name {
|
||||
prepare_header_link(dst, &mut header, link_name).await?;
|
||||
}
|
||||
header.set_cksum();
|
||||
append(dst, &header, read).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn append_dir_all<Dst: Write + Unpin + ?Sized>(
|
||||
dst: &mut Dst,
|
||||
path: &Path,
|
||||
src_path: &Path,
|
||||
mode: HeaderMode,
|
||||
follow: bool,
|
||||
) -> io::Result<()> {
|
||||
let mut stack = vec![(src_path.to_path_buf(), true, false)];
|
||||
while let Some((src, is_dir, is_symlink)) = stack.pop() {
|
||||
let dest = path.join(src.strip_prefix(&src_path).unwrap());
|
||||
|
||||
// In case of a symlink pointing to a directory, is_dir is false, but src.is_dir() will return true
|
||||
if is_dir || (is_symlink && follow && src.is_dir()) {
|
||||
let mut entries = fs::read_dir(&src).await?;
|
||||
while let Some(entry) = entries.next_entry().await.transpose() {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type().await?;
|
||||
stack.push((entry.path(), file_type.is_dir(), file_type.is_symlink()));
|
||||
}
|
||||
if dest != Path::new("") {
|
||||
append_dir(dst, &dest, &src, mode).await?;
|
||||
}
|
||||
} else if !follow && is_symlink {
|
||||
let stat = fs::symlink_metadata(&src).await?;
|
||||
let link_name = fs::read_link(&src).await?;
|
||||
append_fs(dst, &dest, &stat, &mut io::empty(), mode, Some(&link_name)).await?;
|
||||
} else {
|
||||
append_file(dst, &dest, &mut fs::File::open(src).await?, mode).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<W: Write + Unpin + Send + 'static> Drop for Builder<W> {
|
||||
fn drop(&mut self) {
|
||||
// TODO: proper async cancellation
|
||||
if !self.finished {
|
||||
let _ = self
|
||||
.cancellation
|
||||
.take()
|
||||
.unwrap()
|
||||
.send(self.obj.take().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
955
tokio-tar/src/entry.rs
Normal file
955
tokio-tar/src/entry.rs
Normal file
|
@ -0,0 +1,955 @@
|
|||
use crate::{
|
||||
error::TarError, header::bytes2path, other, pax::pax_extensions, Archive, Header, PaxExtensions,
|
||||
};
|
||||
use filetime::{self, FileTime};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp, fmt,
|
||||
io::{Error, ErrorKind, SeekFrom},
|
||||
marker,
|
||||
path::{Component, Path, PathBuf},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
fs,
|
||||
fs::OpenOptions,
|
||||
io::{self, AsyncRead as Read, AsyncReadExt, AsyncSeekExt},
|
||||
};
|
||||
|
||||
/// A read-only view into an entry of an archive.
|
||||
///
|
||||
/// This structure is a window into a portion of a borrowed archive which can
|
||||
/// be inspected. It acts as a file handle by implementing the Reader trait. An
|
||||
/// entry cannot be rewritten once inserted into an archive.
|
||||
pub struct Entry<R: Read + Unpin> {
|
||||
fields: EntryFields<R>,
|
||||
_ignored: marker::PhantomData<Archive<R>>,
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> fmt::Debug for Entry<R> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Entry")
|
||||
.field("fields", &self.fields)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// private implementation detail of `Entry`, but concrete (no type parameters)
|
||||
// and also all-public to be constructed from other modules.
|
||||
pub struct EntryFields<R: Read + Unpin> {
|
||||
pub long_pathname: Option<Vec<u8>>,
|
||||
pub long_linkname: Option<Vec<u8>>,
|
||||
pub pax_extensions: Option<Vec<u8>>,
|
||||
pub header: Header,
|
||||
pub size: u64,
|
||||
pub header_pos: u64,
|
||||
pub file_pos: u64,
|
||||
pub data: Vec<EntryIo<R>>,
|
||||
pub unpack_xattrs: bool,
|
||||
pub preserve_permissions: bool,
|
||||
pub preserve_mtime: bool,
|
||||
pub(crate) read_state: Option<EntryIo<R>>,
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> fmt::Debug for EntryFields<R> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("EntryFields")
|
||||
.field("long_pathname", &self.long_pathname)
|
||||
.field("long_linkname", &self.long_linkname)
|
||||
.field("pax_extensions", &self.pax_extensions)
|
||||
.field("header", &self.header)
|
||||
.field("size", &self.size)
|
||||
.field("header_pos", &self.header_pos)
|
||||
.field("file_pos", &self.file_pos)
|
||||
.field("data", &self.data)
|
||||
.field("unpack_xattrs", &self.unpack_xattrs)
|
||||
.field("preserve_permissions", &self.preserve_permissions)
|
||||
.field("preserve_mtime", &self.preserve_mtime)
|
||||
.field("read_state", &self.read_state)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum EntryIo<R: Read + Unpin> {
|
||||
Pad(io::Take<io::Repeat>),
|
||||
Data(io::Take<R>),
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> fmt::Debug for EntryIo<R> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
EntryIo::Pad(_) => write!(f, "EntryIo::Pad"),
|
||||
EntryIo::Data(_) => write!(f, "EntryIo::Data"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When unpacking items the unpacked thing is returned to allow custom
|
||||
/// additional handling by users. Today the File is returned, in future
|
||||
/// the enum may be extended with kinds for links, directories etc.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Unpacked {
|
||||
/// A file was unpacked.
|
||||
File(fs::File),
|
||||
/// A directory, hardlink, symlink, or other node was unpacked.
|
||||
Other,
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> Entry<R> {
|
||||
/// Returns the path name for this entry.
|
||||
///
|
||||
/// This method may fail if the pathname is not valid Unicode and this is
|
||||
/// called on a Windows platform.
|
||||
///
|
||||
/// Note that this function will convert any `\` characters to directory
|
||||
/// separators, and it will not always return the same value as
|
||||
/// `self.header().path()` as some archive formats have support for longer
|
||||
/// path names described in separate entries.
|
||||
///
|
||||
/// It is recommended to use this method instead of inspecting the `header`
|
||||
/// directly to ensure that various archive formats are handled correctly.
|
||||
pub fn path(&self) -> io::Result<Cow<Path>> {
|
||||
self.fields.path()
|
||||
}
|
||||
|
||||
/// Returns the raw bytes listed for this entry.
|
||||
///
|
||||
/// Note that this function will convert any `\` characters to directory
|
||||
/// separators, and it will not always return the same value as
|
||||
/// `self.header().path_bytes()` as some archive formats have support for
|
||||
/// longer path names described in separate entries.
|
||||
pub fn path_bytes(&self) -> Cow<[u8]> {
|
||||
self.fields.path_bytes()
|
||||
}
|
||||
|
||||
/// Returns the link name for this entry, if any is found.
|
||||
///
|
||||
/// This method may fail if the pathname is not valid Unicode and this is
|
||||
/// called on a Windows platform. `Ok(None)` being returned, however,
|
||||
/// indicates that the link name was not present.
|
||||
///
|
||||
/// Note that this function will convert any `\` characters to directory
|
||||
/// separators, and it will not always return the same value as
|
||||
/// `self.header().link_name()` as some archive formats have support for
|
||||
/// longer path names described in separate entries.
|
||||
///
|
||||
/// It is recommended to use this method instead of inspecting the `header`
|
||||
/// directly to ensure that various archive formats are handled correctly.
|
||||
pub fn link_name(&self) -> io::Result<Option<Cow<Path>>> {
|
||||
self.fields.link_name()
|
||||
}
|
||||
|
||||
/// Returns the link name for this entry, in bytes, if listed.
|
||||
///
|
||||
/// Note that this will not always return the same value as
|
||||
/// `self.header().link_name_bytes()` as some archive formats have support for
|
||||
/// longer path names described in separate entries.
|
||||
pub fn link_name_bytes(&self) -> Option<Cow<[u8]>> {
|
||||
self.fields.link_name_bytes()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the pax extensions contained in this entry.
|
||||
///
|
||||
/// Pax extensions are a form of archive where extra metadata is stored in
|
||||
/// key/value pairs in entries before the entry they're intended to
|
||||
/// describe. For example this can be used to describe long file name or
|
||||
/// other metadata like atime/ctime/mtime in more precision.
|
||||
///
|
||||
/// The returned iterator will yield key/value pairs for each extension.
|
||||
///
|
||||
/// `None` will be returned if this entry does not indicate that it itself
|
||||
/// contains extensions, or if there were no previous extensions describing
|
||||
/// it.
|
||||
///
|
||||
/// Note that global pax extensions are intended to be applied to all
|
||||
/// archive entries.
|
||||
///
|
||||
/// Also note that this function will read the entire entry if the entry
|
||||
/// itself is a list of extensions.
|
||||
pub async fn pax_extensions(&mut self) -> io::Result<Option<PaxExtensions<'_>>> {
|
||||
self.fields.pax_extensions().await
|
||||
}
|
||||
|
||||
/// Returns access to the header of this entry in the archive.
|
||||
///
|
||||
/// This provides access to the metadata for this entry in the archive.
|
||||
pub fn header(&self) -> &Header {
|
||||
&self.fields.header
|
||||
}
|
||||
|
||||
/// Returns the starting position, in bytes, of the header of this entry in
|
||||
/// the archive.
|
||||
///
|
||||
/// The header is always a contiguous section of 512 bytes, so if the
|
||||
/// underlying reader implements `Seek`, then the slice from `header_pos` to
|
||||
/// `header_pos + 512` contains the raw header bytes.
|
||||
pub fn raw_header_position(&self) -> u64 {
|
||||
self.fields.header_pos
|
||||
}
|
||||
|
||||
/// Returns the starting position, in bytes, of the file of this entry in
|
||||
/// the archive.
|
||||
///
|
||||
/// If the file of this entry is continuous (e.g. not a sparse file), and
|
||||
/// if the underlying reader implements `Seek`, then the slice from
|
||||
/// `file_pos` to `file_pos + entry_size` contains the raw file bytes.
|
||||
pub fn raw_file_position(&self) -> u64 {
|
||||
self.fields.file_pos
|
||||
}
|
||||
|
||||
/// Writes this file to the specified location.
|
||||
///
|
||||
/// This function will write the entire contents of this file into the
|
||||
/// location specified by `dst`. Metadata will also be propagated to the
|
||||
/// path `dst`.
|
||||
///
|
||||
/// This function will create a file at the path `dst`, and it is required
|
||||
/// that the intermediate directories are created. Any existing file at the
|
||||
/// location `dst` will be overwritten.
|
||||
///
|
||||
/// > **Note**: This function does not have as many sanity checks as
|
||||
/// > `Archive::unpack` or `Entry::unpack_in`. As a result if you're
|
||||
/// > thinking of unpacking untrusted tarballs you may want to review the
|
||||
/// > implementations of the previous two functions and perhaps implement
|
||||
/// > similar logic yourself.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio::fs::File;
|
||||
/// use tokio_tar::Archive;
|
||||
/// use tokio_stream::*;
|
||||
///
|
||||
/// let mut ar = Archive::new(File::open("foo.tar").await?);
|
||||
/// let mut entries = ar.entries()?;
|
||||
/// let mut i = 0;
|
||||
/// while let Some(file) = entries.next().await {
|
||||
/// let mut file = file?;
|
||||
/// file.unpack(format!("file-{}", i)).await?;
|
||||
/// i += 1;
|
||||
/// }
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn unpack<P: AsRef<Path>>(&mut self, dst: P) -> io::Result<Unpacked> {
|
||||
self.fields.unpack(None, dst.as_ref()).await
|
||||
}
|
||||
|
||||
/// Extracts this file under the specified path, avoiding security issues.
|
||||
///
|
||||
/// This function will write the entire contents of this file into the
|
||||
/// location obtained by appending the path of this file in the archive to
|
||||
/// `dst`, creating any intermediate directories if needed. Metadata will
|
||||
/// also be propagated to the path `dst`. Any existing file at the location
|
||||
/// `dst` will be overwritten.
|
||||
///
|
||||
/// This function carefully avoids writing outside of `dst`. If the file has
|
||||
/// a '..' in its path, this function will skip it and return false.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
/// #
|
||||
/// use tokio::{fs::File, stream::*};
|
||||
/// use tokio_tar::Archive;
|
||||
/// use tokio_stream::*;
|
||||
///
|
||||
/// let mut ar = Archive::new(File::open("foo.tar").await?);
|
||||
/// let mut entries = ar.entries()?;
|
||||
/// let mut i = 0;
|
||||
/// while let Some(file) = entries.next().await {
|
||||
/// let mut file = file.unwrap();
|
||||
/// file.unpack_in("target").await?;
|
||||
/// i += 1;
|
||||
/// }
|
||||
/// #
|
||||
/// # Ok(()) }) }
|
||||
/// ```
|
||||
pub async fn unpack_in<P: AsRef<Path>>(&mut self, dst: P) -> io::Result<bool> {
|
||||
self.fields.unpack_in(dst.as_ref()).await
|
||||
}
|
||||
|
||||
/// Indicate whether extended file attributes (xattrs on Unix) are preserved
|
||||
/// when unpacking this entry.
|
||||
///
|
||||
/// This flag is disabled by default and is currently only implemented on
|
||||
/// Unix using xattr support. This may eventually be implemented for
|
||||
/// Windows, however, if other archive implementations are found which do
|
||||
/// this as well.
|
||||
pub fn set_unpack_xattrs(&mut self, unpack_xattrs: bool) {
|
||||
self.fields.unpack_xattrs = unpack_xattrs;
|
||||
}
|
||||
|
||||
/// Indicate whether extended permissions (like suid on Unix) are preserved
|
||||
/// when unpacking this entry.
|
||||
///
|
||||
/// This flag is disabled by default and is currently only implemented on
|
||||
/// Unix.
|
||||
pub fn set_preserve_permissions(&mut self, preserve: bool) {
|
||||
self.fields.preserve_permissions = preserve;
|
||||
}
|
||||
|
||||
/// Indicate whether access time information is preserved when unpacking
|
||||
/// this entry.
|
||||
///
|
||||
/// This flag is enabled by default.
|
||||
pub fn set_preserve_mtime(&mut self, preserve: bool) {
|
||||
self.fields.preserve_mtime = preserve;
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> Read for Entry<R> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
into: &mut io::ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.as_mut().fields).poll_read(cx, into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> EntryFields<R> {
|
||||
pub fn from(entry: Entry<R>) -> Self {
|
||||
entry.fields
|
||||
}
|
||||
|
||||
pub fn into_entry(self) -> Entry<R> {
|
||||
Entry {
|
||||
fields: self,
|
||||
_ignored: marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn poll_read_all(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<io::Result<Vec<u8>>> {
|
||||
// Preallocate some data but don't let ourselves get too crazy now.
|
||||
let cap = cmp::min(self.size, 128 * 1024);
|
||||
let mut buf = Vec::with_capacity(cap as usize);
|
||||
|
||||
// Copied from futures::ReadToEnd
|
||||
match futures_core::ready!(poll_read_all_internal(self, cx, &mut buf)) {
|
||||
Ok(_) => Poll::Ready(Ok(buf)),
|
||||
Err(err) => Poll::Ready(Err(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_all(&mut self) -> io::Result<Vec<u8>> {
|
||||
// Preallocate some data but don't let ourselves get too crazy now.
|
||||
let cap = cmp::min(self.size, 128 * 1024);
|
||||
let mut v = Vec::with_capacity(cap as usize);
|
||||
self.read_to_end(&mut v).await.map(|_| v)
|
||||
}
|
||||
|
||||
fn path(&self) -> io::Result<Cow<'_, Path>> {
|
||||
bytes2path(self.path_bytes())
|
||||
}
|
||||
|
||||
fn path_bytes(&self) -> Cow<[u8]> {
|
||||
match self.long_pathname {
|
||||
Some(ref bytes) => {
|
||||
if let Some(&0) = bytes.last() {
|
||||
Cow::Borrowed(&bytes[..bytes.len() - 1])
|
||||
} else {
|
||||
Cow::Borrowed(bytes)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(ref pax) = self.pax_extensions {
|
||||
let pax = pax_extensions(pax)
|
||||
.filter_map(|f| f.ok())
|
||||
.find(|f| f.key_bytes() == b"path")
|
||||
.map(|f| f.value_bytes());
|
||||
if let Some(field) = pax {
|
||||
return Cow::Borrowed(field);
|
||||
}
|
||||
}
|
||||
self.header.path_bytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the path in a "lossy" way, used for error reporting ONLY.
|
||||
fn path_lossy(&self) -> String {
|
||||
String::from_utf8_lossy(&self.path_bytes()).to_string()
|
||||
}
|
||||
|
||||
fn link_name(&self) -> io::Result<Option<Cow<Path>>> {
|
||||
match self.link_name_bytes() {
|
||||
Some(bytes) => bytes2path(bytes).map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn link_name_bytes(&self) -> Option<Cow<[u8]>> {
|
||||
match self.long_linkname {
|
||||
Some(ref bytes) => {
|
||||
if let Some(&0) = bytes.last() {
|
||||
Some(Cow::Borrowed(&bytes[..bytes.len() - 1]))
|
||||
} else {
|
||||
Some(Cow::Borrowed(bytes))
|
||||
}
|
||||
}
|
||||
None => self.header.link_name_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn pax_extensions(&mut self) -> io::Result<Option<PaxExtensions<'_>>> {
|
||||
if self.pax_extensions.is_none() {
|
||||
if !self.header.entry_type().is_pax_global_extensions()
|
||||
&& !self.header.entry_type().is_pax_local_extensions()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
self.pax_extensions = Some(self.read_all().await?);
|
||||
}
|
||||
Ok(Some(pax_extensions(self.pax_extensions.as_ref().unwrap())))
|
||||
}
|
||||
|
||||
async fn unpack_in(&mut self, dst: &Path) -> io::Result<bool> {
|
||||
// Notes regarding bsdtar 2.8.3 / libarchive 2.8.3:
|
||||
// * Leading '/'s are trimmed. For example, `///test` is treated as
|
||||
// `test`.
|
||||
// * If the filename contains '..', then the file is skipped when
|
||||
// extracting the tarball.
|
||||
// * '//' within a filename is effectively skipped. An error is
|
||||
// logged, but otherwise the effect is as if any two or more
|
||||
// adjacent '/'s within the filename were consolidated into one
|
||||
// '/'.
|
||||
//
|
||||
// Most of this is handled by the `path` module of the standard
|
||||
// library, but we specially handle a few cases here as well.
|
||||
|
||||
let mut file_dst = dst.to_path_buf();
|
||||
{
|
||||
let path = self.path().map_err(|e| {
|
||||
TarError::new(
|
||||
&format!("invalid path in entry header: {}", self.path_lossy()),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
for part in path.components() {
|
||||
match part {
|
||||
// Leading '/' characters, root paths, and '.'
|
||||
// components are just ignored and treated as "empty
|
||||
// components"
|
||||
Component::Prefix(..) | Component::RootDir | Component::CurDir => continue,
|
||||
|
||||
// If any part of the filename is '..', then skip over
|
||||
// unpacking the file to prevent directory traversal
|
||||
// security issues. See, e.g.: CVE-2001-1267,
|
||||
// CVE-2002-0399, CVE-2005-1918, CVE-2007-4131
|
||||
Component::ParentDir => return Ok(false),
|
||||
|
||||
Component::Normal(part) => file_dst.push(part),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip cases where only slashes or '.' parts were seen, because
|
||||
// this is effectively an empty filename.
|
||||
if *dst == *file_dst {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Skip entries without a parent (i.e. outside of FS root)
|
||||
let parent = match file_dst.parent() {
|
||||
Some(p) => p,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
if parent.symlink_metadata().is_err() {
|
||||
println!("create_dir_all {:?}", parent);
|
||||
fs::create_dir_all(&parent).await.map_err(|e| {
|
||||
TarError::new(&format!("failed to create `{}`", parent.display()), e)
|
||||
})?;
|
||||
}
|
||||
|
||||
let canon_target = self.validate_inside_dst(&dst, parent).await?;
|
||||
|
||||
self.unpack(Some(&canon_target), &file_dst)
|
||||
.await
|
||||
.map_err(|e| TarError::new(&format!("failed to unpack `{}`", file_dst.display()), e))?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Unpack as destination directory `dst`.
|
||||
async fn unpack_dir(&mut self, dst: &Path) -> io::Result<()> {
|
||||
// If the directory already exists just let it slide
|
||||
match fs::create_dir(dst).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) => {
|
||||
if err.kind() == ErrorKind::AlreadyExists {
|
||||
let prev = fs::metadata(dst).await;
|
||||
if prev.map(|m| m.is_dir()).unwrap_or(false) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(Error::new(
|
||||
err.kind(),
|
||||
format!("{} when creating dir {}", err, dst.display()),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns access to the header of this entry in the archive.
|
||||
async fn unpack(&mut self, target_base: Option<&Path>, dst: &Path) -> io::Result<Unpacked> {
|
||||
let kind = self.header.entry_type();
|
||||
|
||||
if kind.is_dir() {
|
||||
self.unpack_dir(dst).await?;
|
||||
if let Ok(mode) = self.header.mode() {
|
||||
set_perms(dst, None, mode, self.preserve_permissions).await?;
|
||||
}
|
||||
return Ok(Unpacked::Other);
|
||||
} else if kind.is_hard_link() || kind.is_symlink() {
|
||||
let src = match self.link_name()? {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
return Err(other(&format!(
|
||||
"hard link listed for {} but no link name found",
|
||||
String::from_utf8_lossy(self.header.as_bytes())
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if src.iter().count() == 0 {
|
||||
return Err(other(&format!(
|
||||
"symlink destination for {} is empty",
|
||||
String::from_utf8_lossy(self.header.as_bytes())
|
||||
)));
|
||||
}
|
||||
|
||||
if kind.is_hard_link() {
|
||||
let link_src = match target_base {
|
||||
// If we're unpacking within a directory then ensure that
|
||||
// the destination of this hard link is both present and
|
||||
// inside our own directory. This is needed because we want
|
||||
// to make sure to not overwrite anything outside the root.
|
||||
//
|
||||
// Note that this logic is only needed for hard links
|
||||
// currently. With symlinks the `validate_inside_dst` which
|
||||
// happens before this method as part of `unpack_in` will
|
||||
// use canonicalization to ensure this guarantee. For hard
|
||||
// links though they're canonicalized to their existing path
|
||||
// so we need to validate at this time.
|
||||
Some(ref p) => {
|
||||
let link_src = p.join(src);
|
||||
self.validate_inside_dst(p, &link_src).await?;
|
||||
link_src
|
||||
}
|
||||
None => src.into_owned(),
|
||||
};
|
||||
fs::hard_link(&link_src, dst).await.map_err(|err| {
|
||||
Error::new(
|
||||
err.kind(),
|
||||
format!(
|
||||
"{} when hard linking {} to {}",
|
||||
err,
|
||||
link_src.display(),
|
||||
dst.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
symlink(&src, dst).await.map_err(|err| {
|
||||
Error::new(
|
||||
err.kind(),
|
||||
format!(
|
||||
"{} when symlinking {} to {}",
|
||||
err,
|
||||
src.display(),
|
||||
dst.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
};
|
||||
return Ok(Unpacked::Other);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[allow(unused_variables)]
|
||||
async fn symlink(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn symlink(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
tokio::fs::os::windows::symlink_file(src, dst).await
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
async fn symlink(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
tokio::fs::symlink(src, dst).await
|
||||
}
|
||||
} else if kind.is_pax_global_extensions()
|
||||
|| kind.is_pax_local_extensions()
|
||||
|| kind.is_gnu_longname()
|
||||
|| kind.is_gnu_longlink()
|
||||
{
|
||||
return Ok(Unpacked::Other);
|
||||
};
|
||||
|
||||
// Old BSD-tar compatibility.
|
||||
// Names that have a trailing slash should be treated as a directory.
|
||||
// Only applies to old headers.
|
||||
if self.header.as_ustar().is_none() && self.path_bytes().ends_with(b"/") {
|
||||
self.unpack_dir(dst).await?;
|
||||
if let Ok(mode) = self.header.mode() {
|
||||
set_perms(dst, None, mode, self.preserve_permissions).await?;
|
||||
}
|
||||
return Ok(Unpacked::Other);
|
||||
}
|
||||
|
||||
// Note the lack of `else` clause above. According to the FreeBSD
|
||||
// documentation:
|
||||
//
|
||||
// > A POSIX-compliant implementation must treat any unrecognized
|
||||
// > typeflag value as a regular file.
|
||||
//
|
||||
// As a result if we don't recognize the kind we just write out the file
|
||||
// as we would normally.
|
||||
|
||||
// Ensure we write a new file rather than overwriting in-place which
|
||||
// is attackable; if an existing file is found unlink it.
|
||||
async fn open(dst: &Path) -> io::Result<fs::File> {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(dst)
|
||||
.await
|
||||
}
|
||||
|
||||
let mut f = async {
|
||||
let mut f = match open(dst).await {
|
||||
Ok(f) => Ok(f),
|
||||
Err(err) => {
|
||||
if err.kind() != ErrorKind::AlreadyExists {
|
||||
Err(err)
|
||||
} else {
|
||||
match fs::remove_file(dst).await {
|
||||
Ok(()) => open(dst).await,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::NotFound => open(dst).await,
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}?;
|
||||
for io in self.data.drain(..) {
|
||||
match io {
|
||||
EntryIo::Data(mut d) => {
|
||||
let expected = d.limit();
|
||||
if io::copy(&mut d, &mut f).await? != expected {
|
||||
return Err(other("failed to write entire file"));
|
||||
}
|
||||
}
|
||||
EntryIo::Pad(d) => {
|
||||
// TODO: checked cast to i64
|
||||
let to = SeekFrom::Current(d.limit() as i64);
|
||||
let size = f.seek(to).await?;
|
||||
f.set_len(size).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok::<fs::File, io::Error>(f)
|
||||
}
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let header = self.header.path_bytes();
|
||||
TarError::new(
|
||||
&format!(
|
||||
"failed to unpack `{}` into `{}`",
|
||||
String::from_utf8_lossy(&header),
|
||||
dst.display()
|
||||
),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
|
||||
if self.preserve_mtime {
|
||||
if let Ok(mtime) = self.header.mtime() {
|
||||
let mtime = FileTime::from_unix_time(mtime as i64, 0);
|
||||
filetime::set_file_times(&dst, mtime, mtime).map_err(|e| {
|
||||
TarError::new(&format!("failed to set mtime for `{}`", dst.display()), e)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
if let Ok(mode) = self.header.mode() {
|
||||
set_perms(dst, Some(&mut f), mode, self.preserve_permissions).await?;
|
||||
}
|
||||
if self.unpack_xattrs {
|
||||
set_xattrs(self, dst).await?;
|
||||
}
|
||||
return Ok(Unpacked::File(f));
|
||||
|
||||
async fn set_perms(
|
||||
dst: &Path,
|
||||
f: Option<&mut fs::File>,
|
||||
mode: u32,
|
||||
preserve: bool,
|
||||
) -> Result<(), TarError> {
|
||||
_set_perms(dst, f, mode, preserve).await.map_err(|e| {
|
||||
TarError::new(
|
||||
&format!(
|
||||
"failed to set permissions to {:o} \
|
||||
for `{}`",
|
||||
mode,
|
||||
dst.display()
|
||||
),
|
||||
e,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
async fn _set_perms(
|
||||
dst: &Path,
|
||||
f: Option<&mut fs::File>,
|
||||
mode: u32,
|
||||
preserve: bool,
|
||||
) -> io::Result<()> {
|
||||
use std::os::unix::prelude::*;
|
||||
|
||||
let mode = if preserve { mode } else { mode & 0o777 };
|
||||
let perm = std::fs::Permissions::from_mode(mode as _);
|
||||
match f {
|
||||
Some(f) => f.set_permissions(perm).await,
|
||||
None => fs::set_permissions(dst, perm).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn _set_perms(
|
||||
dst: &Path,
|
||||
f: Option<&mut fs::File>,
|
||||
mode: u32,
|
||||
_preserve: bool,
|
||||
) -> io::Result<()> {
|
||||
if mode & 0o200 == 0o200 {
|
||||
return Ok(());
|
||||
}
|
||||
match f {
|
||||
Some(f) => {
|
||||
let mut perm = f.metadata().await?.permissions();
|
||||
perm.set_readonly(true);
|
||||
f.set_permissions(perm).await
|
||||
}
|
||||
None => {
|
||||
let mut perm = fs::metadata(dst).await?.permissions();
|
||||
perm.set_readonly(true);
|
||||
fs::set_permissions(dst, perm).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[allow(unused_variables)]
|
||||
async fn _set_perms(
|
||||
dst: &Path,
|
||||
f: Option<&mut fs::File>,
|
||||
mode: u32,
|
||||
_preserve: bool,
|
||||
) -> io::Result<()> {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "xattr"))]
|
||||
async fn set_xattrs<R: Read + Unpin>(
|
||||
me: &mut EntryFields<R>,
|
||||
dst: &Path,
|
||||
) -> io::Result<()> {
|
||||
use std::{ffi::OsStr, os::unix::prelude::*};
|
||||
|
||||
let exts = match me.pax_extensions().await {
|
||||
Ok(Some(e)) => e,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let exts = exts
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| {
|
||||
let key = e.key_bytes();
|
||||
let prefix = b"SCHILY.xattr.";
|
||||
if key.starts_with(prefix) {
|
||||
Some((&key[prefix.len()..], e))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|(key, e)| (OsStr::from_bytes(key), e.value_bytes()));
|
||||
|
||||
for (key, value) in exts {
|
||||
xattr::set(dst, key, value).map_err(|e| {
|
||||
TarError::new(
|
||||
&format!(
|
||||
"failed to set extended \
|
||||
attributes to {}. \
|
||||
Xattrs: key={:?}, value={:?}.",
|
||||
dst.display(),
|
||||
key,
|
||||
String::from_utf8_lossy(value)
|
||||
),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Windows does not completely support posix xattrs
|
||||
// https://en.wikipedia.org/wiki/Extended_file_attributes#Windows_NT
|
||||
#[cfg(any(
|
||||
windows,
|
||||
target_os = "redox",
|
||||
not(feature = "xattr"),
|
||||
target_arch = "wasm32"
|
||||
))]
|
||||
async fn set_xattrs<R: Read + Unpin>(_: &mut EntryFields<R>, _: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn validate_inside_dst(&self, dst: &Path, file_dst: &Path) -> io::Result<PathBuf> {
|
||||
// Abort if target (canonical) parent is outside of `dst`
|
||||
let canon_parent = file_dst.canonicalize().map_err(|err| {
|
||||
Error::new(
|
||||
err.kind(),
|
||||
format!("{} while canonicalizing {}", err, file_dst.display()),
|
||||
)
|
||||
})?;
|
||||
let canon_target = dst.canonicalize().map_err(|err| {
|
||||
Error::new(
|
||||
err.kind(),
|
||||
format!("{} while canonicalizing {}", err, dst.display()),
|
||||
)
|
||||
})?;
|
||||
if !canon_parent.starts_with(&canon_target) {
|
||||
let err = TarError::new(
|
||||
&format!(
|
||||
"trying to unpack outside of destination path: {}",
|
||||
canon_target.display()
|
||||
),
|
||||
// TODO: use ErrorKind::InvalidInput here? (minor breaking change)
|
||||
Error::new(ErrorKind::Other, "Invalid argument"),
|
||||
);
|
||||
return Err(err.into());
|
||||
}
|
||||
Ok(canon_target)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> Read for EntryFields<R> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
into: &mut io::ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let mut this = self.get_mut();
|
||||
loop {
|
||||
if this.read_state.is_none() {
|
||||
if this.data.is_empty() {
|
||||
this.read_state = None;
|
||||
} else {
|
||||
let data = &mut this.data;
|
||||
this.read_state = Some(data.remove(0));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref mut io) = &mut this.read_state {
|
||||
let ret = Pin::new(io).poll_read(cx, into);
|
||||
match ret {
|
||||
Poll::Ready(Ok(())) if into.filled().is_empty() => {
|
||||
this.read_state = None;
|
||||
if this.data.is_empty() {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Ok(())) => {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Poll::Ready(Err(err)) => {
|
||||
return Poll::Ready(Err(err));
|
||||
}
|
||||
Poll::Pending => {
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unable to pull another value from `data`, so we are done.
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Unpin> Read for EntryIo<R> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
into: &mut io::ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
EntryIo::Pad(ref mut io) => Pin::new(io).poll_read(cx, into),
|
||||
EntryIo::Data(ref mut io) => Pin::new(io).poll_read(cx, into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Guard<'a> {
|
||||
buf: &'a mut Vec<u8>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Drop for Guard<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.buf.set_len(self.len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_read_all_internal<R: Read + ?Sized>(
|
||||
mut rd: Pin<&mut R>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> Poll<io::Result<usize>> {
|
||||
let mut g = Guard {
|
||||
len: buf.len(),
|
||||
buf,
|
||||
};
|
||||
let ret;
|
||||
loop {
|
||||
if g.len == g.buf.len() {
|
||||
unsafe {
|
||||
g.buf.reserve(32);
|
||||
let capacity = g.buf.capacity();
|
||||
g.buf.set_len(capacity);
|
||||
|
||||
let buf = &mut g.buf[g.len..];
|
||||
std::ptr::write_bytes(buf.as_mut_ptr(), 0, buf.len());
|
||||
}
|
||||
}
|
||||
|
||||
let mut read_buf = io::ReadBuf::new(&mut g.buf[g.len..]);
|
||||
match futures_core::ready!(rd.as_mut().poll_read(cx, &mut read_buf)) {
|
||||
Ok(()) if read_buf.filled().is_empty() => {
|
||||
ret = Poll::Ready(Ok(g.len));
|
||||
break;
|
||||
}
|
||||
Ok(()) => g.len += read_buf.filled().len(),
|
||||
Err(e) => {
|
||||
ret = Poll::Ready(Err(e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
189
tokio-tar/src/entry_type.rs
Normal file
189
tokio-tar/src/entry_type.rs
Normal file
|
@ -0,0 +1,189 @@
|
|||
// See https://en.wikipedia.org/wiki/Tar_%28computing%29#UStar_format
|
||||
/// Indicate for the type of file described by a header.
|
||||
///
|
||||
/// Each `Header` has an `entry_type` method returning an instance of this type
|
||||
/// which can be used to inspect what the header is describing.
|
||||
|
||||
/// A non-exhaustive enum representing the possible entry types
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum EntryType {
|
||||
/// Regular file
|
||||
Regular,
|
||||
/// Hard link
|
||||
Link,
|
||||
/// Symbolic link
|
||||
Symlink,
|
||||
/// Character device
|
||||
Char,
|
||||
/// Block device
|
||||
Block,
|
||||
/// Directory
|
||||
Directory,
|
||||
/// Named pipe (fifo)
|
||||
Fifo,
|
||||
/// Implementation-defined 'high-performance' type, treated as regular file
|
||||
Continuous,
|
||||
/// GNU extension - long file name
|
||||
GNULongName,
|
||||
/// GNU extension - long link name (link target)
|
||||
GNULongLink,
|
||||
/// GNU extension - sparse file
|
||||
GNUSparse,
|
||||
/// Global extended header
|
||||
XGlobalHeader,
|
||||
/// Extended Header
|
||||
XHeader,
|
||||
/// Unknown header,
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
impl EntryType {
|
||||
/// Creates a new entry type from a raw byte.
|
||||
///
|
||||
/// Note that the other named constructors of entry type may be more
|
||||
/// appropriate to create a file type from.
|
||||
pub fn new(byte: u8) -> EntryType {
|
||||
match byte {
|
||||
b'\x00' | b'0' => EntryType::Regular,
|
||||
b'1' => EntryType::Link,
|
||||
b'2' => EntryType::Symlink,
|
||||
b'3' => EntryType::Char,
|
||||
b'4' => EntryType::Block,
|
||||
b'5' => EntryType::Directory,
|
||||
b'6' => EntryType::Fifo,
|
||||
b'7' => EntryType::Continuous,
|
||||
b'x' => EntryType::XHeader,
|
||||
b'g' => EntryType::XGlobalHeader,
|
||||
b'L' => EntryType::GNULongName,
|
||||
b'K' => EntryType::GNULongLink,
|
||||
b'S' => EntryType::GNUSparse,
|
||||
other => EntryType::Other(other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the raw underlying byte that this entry type represents.
|
||||
pub fn as_byte(self) -> u8 {
|
||||
match self {
|
||||
EntryType::Regular => b'0',
|
||||
EntryType::Link => b'1',
|
||||
EntryType::Symlink => b'2',
|
||||
EntryType::Char => b'3',
|
||||
EntryType::Block => b'4',
|
||||
EntryType::Directory => b'5',
|
||||
EntryType::Fifo => b'6',
|
||||
EntryType::Continuous => b'7',
|
||||
EntryType::XHeader => b'x',
|
||||
EntryType::XGlobalHeader => b'g',
|
||||
EntryType::GNULongName => b'L',
|
||||
EntryType::GNULongLink => b'K',
|
||||
EntryType::GNUSparse => b'S',
|
||||
EntryType::Other(other) => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new entry type representing a regular file.
|
||||
pub fn file() -> EntryType {
|
||||
EntryType::Regular
|
||||
}
|
||||
|
||||
/// Creates a new entry type representing a hard link.
|
||||
pub fn hard_link() -> EntryType {
|
||||
EntryType::Link
|
||||
}
|
||||
|
||||
/// Creates a new entry type representing a symlink.
|
||||
pub fn symlink() -> EntryType {
|
||||
EntryType::Symlink
|
||||
}
|
||||
|
||||
/// Creates a new entry type representing a character special device.
|
||||
pub fn character_special() -> EntryType {
|
||||
EntryType::Char
|
||||
}
|
||||
|
||||
/// Creates a new entry type representing a block special device.
|
||||
pub fn block_special() -> EntryType {
|
||||
EntryType::Block
|
||||
}
|
||||
|
||||
/// Creates a new entry type representing a directory.
|
||||
pub fn dir() -> EntryType {
|
||||
EntryType::Directory
|
||||
}
|
||||
|
||||
/// Creates a new entry type representing a FIFO.
|
||||
pub fn fifo() -> EntryType {
|
||||
EntryType::Fifo
|
||||
}
|
||||
|
||||
/// Creates a new entry type representing a contiguous file.
|
||||
pub fn contiguous() -> EntryType {
|
||||
EntryType::Continuous
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a regular file.
|
||||
pub fn is_file(self) -> bool {
|
||||
self == EntryType::Regular
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a hard link.
|
||||
pub fn is_hard_link(self) -> bool {
|
||||
self == EntryType::Link
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a symlink.
|
||||
pub fn is_symlink(self) -> bool {
|
||||
self == EntryType::Symlink
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a character special device.
|
||||
pub fn is_character_special(self) -> bool {
|
||||
self == EntryType::Char
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a block special device.
|
||||
pub fn is_block_special(self) -> bool {
|
||||
self == EntryType::Block
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a directory.
|
||||
pub fn is_dir(self) -> bool {
|
||||
self == EntryType::Directory
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a FIFO.
|
||||
pub fn is_fifo(self) -> bool {
|
||||
self == EntryType::Fifo
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a contiguous file.
|
||||
pub fn is_contiguous(self) -> bool {
|
||||
self == EntryType::Continuous
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a GNU long name header.
|
||||
pub fn is_gnu_longname(self) -> bool {
|
||||
self == EntryType::GNULongName
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a GNU sparse header.
|
||||
pub fn is_gnu_sparse(self) -> bool {
|
||||
self == EntryType::GNUSparse
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a GNU long link header.
|
||||
pub fn is_gnu_longlink(self) -> bool {
|
||||
self == EntryType::GNULongLink
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a GNU long name header.
|
||||
pub fn is_pax_global_extensions(self) -> bool {
|
||||
self == EntryType::XGlobalHeader
|
||||
}
|
||||
|
||||
/// Returns whether this type represents a GNU long link header.
|
||||
pub fn is_pax_local_extensions(self) -> bool {
|
||||
self == EntryType::XHeader
|
||||
}
|
||||
}
|
40
tokio-tar/src/error.rs
Normal file
40
tokio-tar/src/error.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use std::{error, fmt};
|
||||
|
||||
use tokio::io::{self, Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TarError {
|
||||
desc: String,
|
||||
io: io::Error,
|
||||
}
|
||||
|
||||
impl TarError {
|
||||
pub fn new(desc: &str, err: Error) -> TarError {
|
||||
TarError {
|
||||
desc: desc.to_string(),
|
||||
io: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for TarError {
|
||||
fn description(&self) -> &str {
|
||||
&self.desc
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
Some(&self.io)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TarError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.desc.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TarError> for Error {
|
||||
fn from(t: TarError) -> Error {
|
||||
Error::new(t.io.kind(), t)
|
||||
}
|
||||
}
|
1620
tokio-tar/src/header.rs
Normal file
1620
tokio-tar/src/header.rs
Normal file
File diff suppressed because it is too large
Load diff
45
tokio-tar/src/lib.rs
Normal file
45
tokio-tar/src/lib.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
//! A library for reading and writing TAR archives in an async fashion.
|
||||
//!
|
||||
//! This library provides utilities necessary to manage [TAR archives][1]
|
||||
//! abstracted over a reader or writer. Great strides are taken to ensure that
|
||||
//! an archive is never required to be fully resident in memory, and all objects
|
||||
//! provide largely a streaming interface to read bytes from.
|
||||
//!
|
||||
//! [1]: http://en.wikipedia.org/wiki/Tar_%28computing%29
|
||||
|
||||
// More docs about the detailed tar format can also be found here:
|
||||
// http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current
|
||||
|
||||
// NB: some of the coding patterns and idioms here may seem a little strange.
|
||||
// This is currently attempting to expose a super generic interface while
|
||||
// also not forcing clients to codegen the entire crate each time they use
|
||||
// it. To that end lots of work is done to ensure that concrete
|
||||
// implementations are all found in this crate and the generic functions are
|
||||
// all just super thin wrappers (e.g. easy to codegen).
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::io::{Error, ErrorKind};
|
||||
|
||||
pub use crate::{
|
||||
archive::{Archive, ArchiveBuilder, Entries},
|
||||
builder::Builder,
|
||||
entry::{Entry, Unpacked},
|
||||
entry_type::EntryType,
|
||||
header::{
|
||||
GnuExtSparseHeader, GnuHeader, GnuSparseHeader, Header, HeaderMode, OldHeader, UstarHeader,
|
||||
},
|
||||
pax::{PaxExtension, PaxExtensions},
|
||||
};
|
||||
|
||||
mod archive;
|
||||
mod builder;
|
||||
mod entry;
|
||||
mod entry_type;
|
||||
mod error;
|
||||
mod header;
|
||||
mod pax;
|
||||
|
||||
fn other(msg: &str) -> Error {
|
||||
Error::new(ErrorKind::Other, msg)
|
||||
}
|
88
tokio-tar/src/pax.rs
Normal file
88
tokio-tar/src/pax.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use std::{slice, str};
|
||||
|
||||
use tokio::io;
|
||||
|
||||
use crate::other;
|
||||
|
||||
/// An iterator over the pax extensions in an archive entry.
|
||||
///
|
||||
/// This iterator yields structures which can themselves be parsed into
|
||||
/// key/value pairs.
|
||||
pub struct PaxExtensions<'entry> {
|
||||
data: slice::Split<'entry, u8, fn(&u8) -> bool>,
|
||||
}
|
||||
|
||||
/// A key/value pair corresponding to a pax extension.
|
||||
pub struct PaxExtension<'entry> {
|
||||
key: &'entry [u8],
|
||||
value: &'entry [u8],
|
||||
}
|
||||
|
||||
pub fn pax_extensions(a: &[u8]) -> PaxExtensions {
|
||||
PaxExtensions {
|
||||
data: a.split(|a| *a == b'\n'),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'entry> Iterator for PaxExtensions<'entry> {
|
||||
type Item = io::Result<PaxExtension<'entry>>;
|
||||
|
||||
fn next(&mut self) -> Option<io::Result<PaxExtension<'entry>>> {
|
||||
let line = match self.data.next() {
|
||||
Some(line) if line.is_empty() => return None,
|
||||
Some(line) => line,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
Some(
|
||||
line.iter()
|
||||
.position(|b| *b == b' ')
|
||||
.and_then(|i| {
|
||||
str::from_utf8(&line[..i])
|
||||
.ok()
|
||||
.and_then(|len| len.parse::<usize>().ok().map(|j| (i + 1, j)))
|
||||
})
|
||||
.and_then(|(kvstart, reported_len)| {
|
||||
if line.len() + 1 == reported_len {
|
||||
line[kvstart..]
|
||||
.iter()
|
||||
.position(|b| *b == b'=')
|
||||
.map(|equals| (kvstart, equals))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|(kvstart, equals)| PaxExtension {
|
||||
key: &line[kvstart..kvstart + equals],
|
||||
value: &line[kvstart + equals + 1..],
|
||||
})
|
||||
.ok_or_else(|| other("malformed pax extension")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'entry> PaxExtension<'entry> {
|
||||
/// Returns the key for this key/value pair parsed as a string.
|
||||
///
|
||||
/// May fail if the key isn't actually utf-8.
|
||||
pub fn key(&self) -> Result<&'entry str, str::Utf8Error> {
|
||||
str::from_utf8(self.key)
|
||||
}
|
||||
|
||||
/// Returns the underlying raw bytes for the key of this key/value pair.
|
||||
pub fn key_bytes(&self) -> &'entry [u8] {
|
||||
self.key
|
||||
}
|
||||
|
||||
/// Returns the value for this key/value pair parsed as a string.
|
||||
///
|
||||
/// May fail if the value isn't actually utf-8.
|
||||
pub fn value(&self) -> Result<&'entry str, str::Utf8Error> {
|
||||
str::from_utf8(self.value)
|
||||
}
|
||||
|
||||
/// Returns the underlying raw bytes for this value of this key/value pair.
|
||||
pub fn value_bytes(&self) -> &'entry [u8] {
|
||||
self.value
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue