pub use parser::FileMode;
pub use parser::FileType;
pub use parser::Format;
use parser::Keyword;
use parser::LineParseError;
use parser::MTreeLine;
pub use parser::ParserError;
use parser::SpecialKind;
use std::env;
use std::ffi::OsStr;
use std::fmt;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::io::Split;
use std::io::{self};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use util::decode_escapes_path;
mod parser;
mod util;
#[cfg(not(unix))]
compiler_error!("This library currently only supports unix, due to windows using utf-16 for paths");
pub struct MTree<R>
where
R: Read,
{
inner: Split<BufReader<R>>,
cwd: PathBuf,
default_params: Params,
}
impl<R> MTree<R>
where
R: Read,
{
pub fn from_reader(reader: R) -> Self {
Self {
inner: BufReader::new(reader).split(b'\n'),
cwd: env::current_dir().unwrap_or_default(),
default_params: Params::default(),
}
}
pub fn from_reader_with_cwd(reader: R, cwd: PathBuf) -> Self {
Self {
inner: BufReader::new(reader).split(b'\n'),
cwd,
default_params: Params::default(),
}
}
pub fn from_reader_with_empty_cwd(reader: R) -> Self {
Self {
inner: BufReader::new(reader).split(b'\n'),
cwd: PathBuf::new(),
default_params: Params::default(),
}
}
fn next_entry(
&mut self,
line: Result<Vec<u8>, LineParseError>,
) -> Result<Option<Entry>, LineParseError> {
let line = line?;
let line = MTreeLine::from_bytes(&line)?;
Ok(match line {
MTreeLine::Blank | MTreeLine::Comment => None,
MTreeLine::Special(SpecialKind::Set, keywords) => {
self.default_params.set_list(keywords.into_iter());
None
}
MTreeLine::Special(SpecialKind::Unset, _keywords) => unimplemented!(),
MTreeLine::Relative(path, keywords) => {
let mut params = self.default_params.clone();
params.set_list(keywords.into_iter());
let filepath = self.cwd.join(&path);
if params.file_type == Some(FileType::Directory) {
self.cwd.push(&path);
}
Some(Entry {
path: filepath,
params,
})
}
MTreeLine::DotDot => {
if self.cwd.parent().and_then(|p| p.parent()).is_some() {
self.cwd.pop();
}
None
}
MTreeLine::Full(path, keywords) => {
let mut params = self.default_params.clone();
params.set_list(keywords.into_iter());
Some(Entry { path, params })
}
})
}
}
impl<R> Iterator for MTree<R>
where
R: Read,
{
type Item = Result<Entry, Error>;
fn next(&mut self) -> Option<Result<Entry, Error>> {
let mut accumulated_line: Option<Vec<u8>> = None;
while let Some(line_result) = self.inner.next() {
let line = match line_result {
Ok(line) => line,
Err(e) => return Some(Err(Error::Io(e))),
};
let current_input = if let Some(mut acc) = accumulated_line.take() {
acc.extend_from_slice(&line);
Ok(acc)
} else {
Ok(line)
};
match self.next_entry(current_input) {
Ok(Some(entry)) => return Some(Ok(entry)),
Ok(None) => continue, Err(LineParseError::WrappedLine(partial)) => {
accumulated_line = Some(partial);
continue;
}
Err(LineParseError::Io(e)) => return Some(Err(Error::Io(e))),
Err(LineParseError::Parser(e)) => return Some(Err(Error::Parser(e))),
}
}
if let Some(final_line) = accumulated_line {
match self.next_entry(Ok(final_line)) {
Ok(Some(entry)) => Some(Ok(entry)),
Ok(None) => None,
Err(LineParseError::WrappedLine(_)) => {
Some(Err(Error::Parser(ParserError(
"unexpected wrapped line at end of file".to_owned(),
))))
}
Err(LineParseError::Io(e)) => Some(Err(Error::Io(e))),
Err(LineParseError::Parser(e)) => Some(Err(Error::Parser(e))),
}
} else {
None
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Entry {
path: PathBuf,
params: Params,
}
impl fmt::Display for Entry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, r#"mtree entry for "{}""#, self.path.display())?;
write!(f, "{}", self.params)
}
}
impl Entry {
pub fn path(&self) -> &Path {
self.path.as_ref()
}
pub fn checksum(&self) -> Option<u64> {
self.params.checksum
}
pub fn device(&self) -> Option<&Device> {
self.params.device.as_deref()
}
pub fn contents(&self) -> Option<&Path> {
self.params.contents.as_ref().map(AsRef::as_ref)
}
pub fn flags(&self) -> Option<&[u8]> {
self.params.flags.as_ref().map(AsRef::as_ref)
}
pub fn gid(&self) -> Option<u32> {
self.params.gid
}
pub fn gname(&self) -> Option<&[u8]> {
self.params.gname.as_ref().map(AsRef::as_ref)
}
pub fn ignore(&self) -> bool {
self.params.ignore
}
pub fn inode(&self) -> Option<u64> {
self.params.inode
}
pub fn link(&self) -> Option<&Path> {
self.params.link.as_ref().map(AsRef::as_ref)
}
pub fn md5(&self) -> Option<u128> {
self.params.md5
}
pub fn mode(&self) -> Option<FileMode> {
self.params.mode
}
pub fn nlink(&self) -> Option<u64> {
self.params.nlink
}
pub fn no_change(&self) -> bool {
self.params.no_change
}
pub fn optional(&self) -> bool {
self.params.optional
}
pub fn resident_device(&self) -> Option<&Device> {
self.params.resident_device.as_deref()
}
pub fn rmd160(&self) -> Option<&[u8; 20]> {
self.params.rmd160.as_ref().map(AsRef::as_ref)
}
pub fn sha1(&self) -> Option<&[u8; 20]> {
self.params.sha1.as_ref().map(AsRef::as_ref)
}
pub fn sha256(&self) -> Option<&[u8; 32]> {
self.params.sha256.as_ref()
}
pub fn sha384(&self) -> Option<&[u8; 48]> {
self.params.sha384.as_ref().map(AsRef::as_ref)
}
pub fn sha512(&self) -> Option<&[u8; 64]> {
self.params.sha512.as_ref().map(AsRef::as_ref)
}
pub fn size(&self) -> Option<u64> {
self.params.size
}
pub fn time(&self) -> Option<SystemTime> {
self.params.time
}
pub fn file_type(&self) -> Option<FileType> {
self.params.file_type
}
pub fn uid(&self) -> Option<u32> {
self.params.uid
}
pub fn uname(&self) -> Option<&[u8]> {
self.params.uname.as_ref().map(AsRef::as_ref)
}
}
#[derive(Default, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
struct Params {
pub checksum: Option<u64>,
pub device: Option<Box<Device>>,
pub contents: Option<PathBuf>,
pub flags: Option<Box<[u8]>>,
pub gid: Option<u32>,
pub gname: Option<Box<[u8]>>,
pub ignore: bool,
pub inode: Option<u64>,
pub link: Option<PathBuf>,
pub md5: Option<u128>,
pub mode: Option<FileMode>,
pub nlink: Option<u64>,
pub no_change: bool,
pub optional: bool,
pub resident_device: Option<Box<Device>>,
pub rmd160: Option<Box<[u8; 20]>>,
pub sha1: Option<Box<[u8; 20]>>,
pub sha256: Option<[u8; 32]>,
pub sha384: Option<Box<[u8; 48]>>,
pub sha512: Option<Box<[u8; 64]>>,
pub size: Option<u64>,
pub time: Option<SystemTime>,
pub file_type: Option<FileType>,
pub uid: Option<u32>,
pub uname: Option<Box<[u8]>>,
}
impl Params {
fn set_list<'a>(&mut self, keywords: impl Iterator<Item = Keyword<'a>>) {
for keyword in keywords {
self.set(keyword);
}
}
fn set(&mut self, keyword: Keyword<'_>) {
match keyword {
Keyword::Checksum(cksum) => self.checksum = Some(cksum),
Keyword::DeviceRef(device) => self.device = Some(Box::new(device.to_device())),
Keyword::Contents(contents) => {
self.contents = Some(Path::new(OsStr::from_bytes(contents)).to_owned());
}
Keyword::Flags(flags) => self.flags = Some(flags.into()),
Keyword::Gid(gid) => self.gid = Some(gid),
Keyword::Gname(gname) => self.gname = Some(gname.into()),
Keyword::Ignore => self.ignore = true,
Keyword::Inode(inode) => self.inode = Some(inode),
Keyword::Link(link) => {
self.link = decode_escapes_path(&mut link.to_vec());
}
Keyword::Md5(md5) => self.md5 = Some(md5),
Keyword::Mode(mode) => self.mode = Some(mode),
Keyword::NLink(nlink) => self.nlink = Some(nlink),
Keyword::NoChange => self.no_change = true,
Keyword::Optional => self.optional = true,
Keyword::ResidentDeviceRef(device) => {
self.resident_device = Some(Box::new(device.to_device()));
}
Keyword::Rmd160(rmd160) => self.rmd160 = Some(Box::new(rmd160)),
Keyword::Sha1(sha1) => self.sha1 = Some(Box::new(sha1)),
Keyword::Sha256(sha256) => self.sha256 = Some(sha256),
Keyword::Sha384(sha384) => self.sha384 = Some(Box::new(sha384)),
Keyword::Sha512(sha512) => self.sha512 = Some(Box::new(sha512)),
Keyword::Size(size) => self.size = Some(size),
Keyword::Time(time) => self.time = Some(UNIX_EPOCH + time),
Keyword::Type(ty) => self.file_type = Some(ty),
Keyword::Uid(uid) => self.uid = Some(uid),
Keyword::Uname(uname) => self.uname = Some(uname.into()),
}
}
}
impl fmt::Display for Params {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(v) = self.checksum {
writeln!(f, "checksum: {v}")?;
}
if let Some(ref v) = self.device {
writeln!(f, "device: {v:?}")?;
}
if let Some(ref v) = self.contents {
writeln!(f, "contents: {}", v.display())?;
}
if let Some(ref v) = self.flags {
writeln!(f, "flags: {v:?}")?;
}
if let Some(v) = self.gid
&& v != 0
{
writeln!(f, "gid: {v}")?;
}
if let Some(ref v) = self.gname {
writeln!(f, "gname: {}", String::from_utf8_lossy(v))?;
}
if self.ignore {
writeln!(f, "ignore")?;
}
if let Some(v) = self.inode {
writeln!(f, "inode: {v}")?;
}
if let Some(ref v) = self.link {
writeln!(f, "link: {}", v.display())?;
}
if let Some(ref v) = self.md5 {
writeln!(f, "md5: {v:x}")?;
}
if let Some(ref v) = self.mode {
writeln!(f, "mode: {v}")?;
}
if let Some(v) = self.nlink {
writeln!(f, "nlink: {v}")?;
}
if self.no_change {
writeln!(f, "no change")?;
}
if self.optional {
writeln!(f, "optional")?;
}
if let Some(ref v) = self.resident_device {
writeln!(f, "resident device: {v:?}")?;
}
if let Some(ref v) = self.rmd160 {
write!(f, "rmd160: ")?;
for ch in v.iter() {
write!(f, "{ch:x}")?;
}
writeln!(f)?;
}
if let Some(ref v) = self.sha1 {
write!(f, "sha1: ")?;
for ch in v.iter() {
write!(f, "{ch:x}")?;
}
writeln!(f)?;
}
if let Some(ref v) = self.sha256 {
write!(f, "sha256: ")?;
for ch in v {
write!(f, "{ch:x}")?;
}
writeln!(f)?;
}
if let Some(ref v) = self.sha384 {
write!(f, "sha384: ")?;
for ch in v.iter() {
write!(f, "{ch:x}")?;
}
writeln!(f)?;
}
if let Some(ref v) = self.sha512 {
write!(f, "sha512: ")?;
for ch in v.iter() {
write!(f, "{ch:x}")?;
}
writeln!(f)?;
}
if let Some(v) = self.size {
writeln!(f, "size: {v}")?;
}
if let Some(v) = self.time {
writeln!(f, "modification time: {v:?}")?;
}
if let Some(v) = self.file_type {
writeln!(f, "file type: {v}")?;
}
if let Some(v) = self.uid
&& v != 0
{
writeln!(f, "uid: {v}")?;
}
if let Some(ref v) = self.uname {
writeln!(f, "uname: {}", String::from_utf8_lossy(v))?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct Device {
pub format: Format,
pub major: Vec<u8>,
pub minor: Vec<u8>,
pub subunit: Option<Vec<u8>>,
}
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Parser(ParserError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Io(..) => "an i/o error occured while reading the mtree",
Self::Parser(..) => "an error occured while parsing the mtree",
})
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::Parser(err) => Some(err),
}
}
}
impl From<io::Error> for Error {
fn from(from: io::Error) -> Self {
Self::Io(from)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_wrapped_line_at_eof() {
let data = r"# .
mtree_test \
size=581 \";
let mtree = MTree::from_reader_with_empty_cwd(Cursor::new(data.as_bytes()));
let entry = mtree.into_iter().next().unwrap().unwrap();
let should = Entry {
path: PathBuf::from("mtree_test"),
params: Params {
checksum: None,
device: None,
contents: None,
flags: None,
gid: None,
gname: None,
ignore: false,
inode: None,
link: None,
md5: None,
mode: None,
nlink: None,
no_change: false,
optional: false,
resident_device: None,
rmd160: None,
sha1: None,
sha256: None,
sha384: None,
sha512: None,
size: Some(581),
time: None,
file_type: None,
uid: None,
uname: None,
},
};
assert_eq!(entry, should);
}
#[test]
fn test_nochange_optional_flags() {
let data = "foo nochange optional\n";
let mut mtree = MTree::from_reader_with_empty_cwd(Cursor::new(data.as_bytes()));
let entry = mtree.next().unwrap().unwrap();
assert!(entry.no_change());
assert!(entry.optional());
}
}