Merge commit 'fec26926a8' as 'tokio-tar'

This commit is contained in:
Thomas Eizinger 2021-02-25 11:20:47 +11:00
commit 18ba8f49c4
34 changed files with 6427 additions and 0 deletions

610
tokio-tar/src/archive.rs Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

45
tokio-tar/src/lib.rs Normal file
View 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
View 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
}
}