#![warn(missing_docs)]
use nom::{
branch::alt,
bytes::complete::{tag, take, take_until},
character::complete::{digit1, oct_digit0, space0},
combinator::{iterator, map, map_parser, map_res},
error::ErrorKind,
sequence::{pair, terminated},
*,
};
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq)]
pub struct TarEntry<'a> {
pub header: TarHeader<'a>,
pub contents: &'a [u8],
}
#[derive(Debug, PartialEq, Eq)]
pub struct TarEntryStreaming<'a> {
pub header: TarHeader<'a>,
pub header_len: u64,
pub content_len: u64,
pub padding_len: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub struct TarHeader<'a> {
pub name: &'a str,
pub mode: u64,
pub uid: u64,
pub gid: u64,
pub size: u64,
pub mtime: u64,
pub typeflag: TypeFlag,
pub linkname: &'a str,
pub ustar: ExtraHeader<'a>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TypeFlag {
NormalFile,
HardLink,
SymbolicLink,
CharacterSpecial,
BlockSpecial,
Directory,
Fifo,
ContiguousFile,
PaxGlobal,
Pax,
GnuDirectory,
GnuLongLink,
GnuLongName,
GnuSparse,
GnuVolumeHeader,
VendorSpecific(u8),
}
#[derive(Debug, PartialEq, Eq)]
pub enum ExtraHeader<'a> {
UStar(UStarHeader<'a>),
Padding,
}
#[derive(Debug, PartialEq, Eq)]
pub struct UStarHeader<'a> {
pub uname: &'a str,
pub gname: &'a str,
pub devmajor: u64,
pub devminor: u64,
pub extra: UStarExtraHeader<'a>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum UStarExtraHeader<'a> {
Posix(PosixExtraHeader<'a>),
Gnu(GnuExtraHeader),
}
#[derive(Debug, PartialEq, Eq)]
pub struct PosixExtraHeader<'a> {
pub prefix: &'a str,
}
#[derive(Debug, PartialEq, Eq)]
pub struct GnuExtraHeader {
pub atime: u64,
pub ctime: u64,
pub offset: u64,
pub sparses: Vec<Sparse>,
pub realsize: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Sparse {
pub offset: u64,
pub numbytes: u64,
}
fn parse_bool(i: &[u8]) -> IResult<&[u8], bool> {
map(take(1usize), |i: &[u8]| i[0] != 0)(i)
}
fn parse_str(size: usize) -> impl FnMut(&[u8]) -> IResult<&[u8], &str> {
move |input| {
let s = map_res(alt((take_until("\0"), take(size))), std::str::from_utf8);
map_parser(take(size), s)(input)
}
}
fn parse_octal(n: usize) -> impl FnMut(&[u8]) -> IResult<&[u8], u64> {
move |i| {
let (rest, input) = take(n)(i)?;
let (i, value) = terminated(oct_digit0, space0)(input)?;
if i.input_len() == 0 || i[0] == 0 {
let value = value
.iter()
.fold(0, |acc, v| acc * 8 + u64::from(*v - b'0'));
Ok((rest, value))
} else {
Err(nom::Err::Error(error_position!(i, ErrorKind::OctDigit)))
}
}
}
fn parse_type_flag(i: &[u8]) -> IResult<&[u8], TypeFlag> {
let (c, rest) = match i.split_first() {
Some((c, rest)) => (c, rest),
None => return Err(nom::Err::Incomplete(Needed::new(1))),
};
let flag = match c {
b'0' | b'\0' => TypeFlag::NormalFile,
b'1' => TypeFlag::HardLink,
b'2' => TypeFlag::SymbolicLink,
b'3' => TypeFlag::CharacterSpecial,
b'4' => TypeFlag::BlockSpecial,
b'5' => TypeFlag::Directory,
b'6' => TypeFlag::Fifo,
b'7' => TypeFlag::ContiguousFile,
b'g' => TypeFlag::PaxGlobal,
b'x' | b'X' => TypeFlag::Pax,
b'D' => TypeFlag::GnuDirectory,
b'K' => TypeFlag::GnuLongLink,
b'L' => TypeFlag::GnuLongName,
b'S' => TypeFlag::GnuSparse,
b'V' => TypeFlag::GnuVolumeHeader,
b'A'..=b'Z' => TypeFlag::VendorSpecific(*c),
_ => return Err(nom::Err::Error(error_position!(i, ErrorKind::Fail))),
};
Ok((rest, flag))
}
fn parse_sparse(i: &[u8]) -> IResult<&[u8], Sparse> {
let (i, (offset, numbytes)) = pair(parse_octal(12), parse_octal(12))(i)?;
Ok((i, Sparse { offset, numbytes }))
}
fn parse_sparses(i: &[u8], count: usize) -> IResult<&[u8], Vec<Sparse>> {
let mut it = iterator(i, parse_sparse);
let res = it
.take(count)
.filter(|s| !(s.offset == 0 && s.numbytes == 0))
.collect();
let (i, ()) = it.finish()?;
Ok((i, res))
}
fn add_to_vec(sparses: &mut Vec<Sparse>, extra: Vec<Sparse>) -> &mut Vec<Sparse> {
sparses.extend(extra);
sparses
}
fn parse_extra_sparses<'a, 'b>(
i: &'a [u8],
isextended: bool,
sparses: &'b mut Vec<Sparse>,
) -> IResult<&'a [u8], &'b mut Vec<Sparse>> {
if isextended {
let (i, sps) = parse_sparses(i, 21)?;
let (i, extended) = parse_bool(i)?;
let (i, _) = take(7usize)(i)?;
parse_extra_sparses(i, extended, add_to_vec(sparses, sps))
} else {
Ok((i, sparses))
}
}
fn parse_extra_posix(i: &[u8]) -> IResult<&[u8], UStarExtraHeader<'_>> {
let (i, prefix) = terminated(parse_str(155), take(12usize))(i)?;
let header = UStarExtraHeader::Posix(PosixExtraHeader { prefix });
Ok((i, header))
}
fn parse_extra_gnu(i: &[u8]) -> IResult<&[u8], UStarExtraHeader<'_>> {
let mut sparses = Vec::new();
let (i, atime) = parse_octal(12)(i)?;
let (i, ctime) = parse_octal(12)(i)?;
let (i, offset) = parse_octal(12)(i)?;
let (i, _) = take(4usize)(i)?; let (i, _) = take(1usize)(i)?;
let (i, sps) = parse_sparses(i, 4)?;
let (i, isextended) = parse_bool(i)?;
let (i, realsize) = parse_octal(12)(i)?;
let (i, _) = take(17usize)(i)?;
let (i, _) = parse_extra_sparses(i, isextended, add_to_vec(&mut sparses, sps))?;
let header = GnuExtraHeader {
atime,
ctime,
offset,
sparses,
realsize,
};
let header = UStarExtraHeader::Gnu(header);
Ok((i, header))
}
fn parse_ustar(
magic: &'static str,
version: &'static str,
mut extra: impl FnMut(&[u8]) -> IResult<&[u8], UStarExtraHeader>,
) -> impl FnMut(&[u8]) -> IResult<&[u8], ExtraHeader> {
move |input| {
let (i, _) = tag(magic)(input)?;
let (i, _) = tag(version)(i)?;
let (i, uname) = parse_str(32)(i)?;
let (i, gname) = parse_str(32)(i)?;
let (i, devmajor) = parse_octal(8)(i)?;
let (i, devminor) = parse_octal(8)(i)?;
let (i, extra) = extra(i)?;
let header = ExtraHeader::UStar(UStarHeader {
uname,
gname,
devmajor,
devminor,
extra,
});
Ok((i, header))
}
}
fn parse_old(i: &[u8]) -> IResult<&[u8], ExtraHeader<'_>> {
map(take(255usize), |_| ExtraHeader::Padding)(i) }
fn parse_header(i: &[u8]) -> IResult<&[u8], TarHeader<'_>> {
debug_assert!(i.len() >= 512);
let header_chksum = i[..148].iter().map(|b| *b as u64).sum::<u64>()
+ i[156..512].iter().map(|b| *b as u64).sum::<u64>()
+ 8 * (b' ' as u64);
let (i, name) = parse_str(100)(i)?;
let (i, mode) = parse_octal(8)(i)?;
let (i, uid) = parse_octal(8)(i)?;
let (i, gid) = parse_octal(8)(i)?;
let (i, size) = parse_octal(12)(i)?;
let (i, mtime) = parse_octal(12)(i)?;
let (i, chksum) = parse_octal(8)(i)?;
if header_chksum != chksum {
return Err(Err::Error(error_position!(i, ErrorKind::Fail)));
}
let (i, typeflag) = parse_type_flag(i)?;
let (i, linkname) = parse_str(100)(i)?;
let (i, ustar) = alt((
parse_ustar("ustar ", " \0", parse_extra_gnu),
parse_ustar("ustar\0", "00", parse_extra_posix),
parse_old,
))(i)?;
let header = TarHeader {
name,
mode,
uid,
gid,
size,
mtime,
typeflag,
linkname,
ustar,
};
Ok((i, header))
}
pub fn parse_entry_streaming(i: &[u8]) -> IResult<&[u8], Option<TarEntryStreaming<'_>>> {
let len = i.len();
{
let (i, block) = take(512usize)(i)?;
if block == [0u8; 512] {
return Ok((i, None));
}
}
let (i, header) = parse_header(i)?;
let header_len = (len - i.len()) as u64;
let content_len = header.size;
let padding_len = match content_len % 512 {
0 => 0,
t => 512 - t,
};
Ok((
i,
Some(TarEntryStreaming {
header,
header_len,
content_len,
padding_len,
}),
))
}
fn parse_entry(i: &[u8]) -> IResult<&[u8], Option<TarEntry<'_>>> {
let (i, entry) = parse_entry_streaming(i)?;
if let Some(entry) = entry {
let (i, contents) = terminated(
take(entry.content_len as usize),
take(entry.padding_len as usize),
)(i)?;
Ok((
i,
Some(TarEntry {
header: entry.header,
contents,
}),
))
} else {
Ok((i, None))
}
}
pub fn parse_tar(i: &[u8]) -> IResult<&[u8], Vec<TarEntry<'_>>> {
let mut it = iterator(i, parse_entry);
let entries = it.flatten().collect();
let (i, ()) = it.finish()?;
Ok((i, entries))
}
pub fn parse_long_name(i: &[u8]) -> IResult<&[u8], &str> {
parse_str(i.len())(i)
}
fn parse_pax_item(i: &[u8]) -> IResult<&[u8], (&str, &str)> {
let (i, len) = map_res(terminated(digit1, tag(" ")), std::str::from_utf8)(i)?;
let (i, key) = map_res(terminated(take_until("="), tag("=")), std::str::from_utf8)(i)?;
let (i, value) = map_res(terminated(take_until("\n"), tag("\n")), std::str::from_utf8)(i)?;
if let Ok(len_usize) = len.parse::<usize>() {
debug_assert_eq!(len_usize, len.len() + key.len() + value.len() + 3);
}
Ok((i, (key, value)))
}
pub fn parse_pax(i: &[u8]) -> IResult<&[u8], HashMap<&str, &str>> {
let mut it = iterator(i, parse_pax_item);
let map = it.collect();
let (i, ()) = it.finish()?;
Ok((i, map))
}
#[cfg(test)]
mod parser_test {
use crate::*;
use nom::error::ErrorKind;
const EMPTY: &[u8] = b"";
#[test]
fn parse_octal_ok_test() {
assert_eq!(parse_octal(3)(b"756"), Ok((EMPTY, 494)));
assert_eq!(parse_octal(8)(b"756\0 234"), Ok((EMPTY, 494)));
assert_eq!(parse_octal(8)(b"756 \0"), Ok((EMPTY, 494)));
assert_eq!(parse_octal(0)(b""), Ok((EMPTY, 0)));
}
#[test]
fn parse_octal_error_test() {
let t1: &[u8] = b"1238";
let _e: &[u8] = b"8";
let t2: &[u8] = b"a";
let t3: &[u8] = b"A";
assert_eq!(
parse_octal(4)(t1),
Err(nom::Err::Error(error_position!(_e, ErrorKind::OctDigit)))
);
assert_eq!(
parse_octal(1)(t2),
Err(nom::Err::Error(error_position!(t2, ErrorKind::OctDigit)))
);
assert_eq!(
parse_octal(1)(t3),
Err(nom::Err::Error(error_position!(t3, ErrorKind::OctDigit)))
);
}
#[test]
fn parse_str_test() {
let s: &[u8] = b"foobar\0\0\0\0baz";
let baz: &[u8] = b"baz";
assert_eq!(parse_str(10)(s), Ok((baz, "foobar")));
}
#[test]
fn parse_sparses_test() {
let sparses = std::iter::repeat(0u8).take(12 * 2 * 4).collect::<Vec<_>>();
assert_eq!(parse_sparses(&sparses, 4), Ok((EMPTY, vec![])));
}
#[test]
fn parse_pax_test() {
let item: &[u8] = b"25 ctime=1084839148.1212\nfoo";
let foo: &[u8] = b"foo";
assert_eq!(
parse_pax_item(item),
Ok((foo, ("ctime", "1084839148.1212")))
);
}
}
#[cfg(test)]
mod tar_test {
use crate::*;
use std::io::{Read, Seek};
use tempfile::tempfile;
const LIB_RS_FILE: &str = "src/lib.rs";
#[test]
fn basic() {
let file = tempfile().unwrap();
let mut archive = tar::Builder::new(file);
archive
.append_path_with_name(LIB_RS_FILE, "lib.rs")
.unwrap();
let mut file = archive.into_inner().unwrap();
file.rewind().unwrap();
let mut buffer = vec![];
file.read_to_end(&mut buffer).unwrap();
let (_, entries) = parse_tar(&buffer).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].header.typeflag, TypeFlag::NormalFile);
assert_eq!(entries[0].header.name, "lib.rs");
assert_eq!(entries[0].contents, std::fs::read(LIB_RS_FILE).unwrap());
}
#[test]
fn gnu_long() {
let name = "a".repeat(1024);
let file = tempfile().unwrap();
let mut archive = tar::Builder::new(file);
archive.append_path_with_name(LIB_RS_FILE, &name).unwrap();
let mut file = archive.into_inner().unwrap();
file.rewind().unwrap();
let mut buffer = vec![];
file.read_to_end(&mut buffer).unwrap();
let (_, entries) = parse_tar(&buffer).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].header.typeflag, TypeFlag::GnuLongName);
assert_eq!(parse_long_name(entries[0].contents).unwrap().1, &name);
assert_eq!(entries[1].contents, std::fs::read(LIB_RS_FILE).unwrap());
}
#[test]
fn posix_long() {
let name_prefix = "a".repeat(80);
let name_postfix = "b".repeat(80);
let name = format!("{name_prefix}/{name_postfix}");
let file = tempfile().unwrap();
let mut archive = tar::Builder::new(file);
{
let mut header = tar::Header::new_ustar();
let file = std::fs::File::open(LIB_RS_FILE).unwrap();
let size = file.metadata().unwrap().len();
header.set_size(size);
archive.append_data(&mut header, name, file).unwrap();
}
let mut file = archive.into_inner().unwrap();
file.rewind().unwrap();
let mut buffer = vec![];
file.read_to_end(&mut buffer).unwrap();
let (_, entries) = parse_tar(&buffer).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].header.typeflag, TypeFlag::NormalFile);
assert_eq!(entries[0].header.name, name_postfix);
if let ExtraHeader::UStar(extra) = &entries[0].header.ustar {
if let UStarExtraHeader::Posix(extra) = &extra.extra {
assert_eq!(extra.prefix, name_prefix);
} else {
unreachable!()
}
} else {
unreachable!()
}
assert_eq!(entries[0].contents, std::fs::read(LIB_RS_FILE).unwrap());
}
}