mtree2/
lib.rs

1//! A library for iterating through entries of an mtree.
2//!
3//! *mtree* is a data format used for describing a sequence of files. Their
4//! location is record, along with optional extra values like checksums, size,
5//! permissions etc.
6//!
7//! For details on the spec see [mtree(5)](https://www.freebsd.org/cgi/man.cgi?mtree(5)).
8//!
9//! # Examples
10//!
11//! ```
12//! use mtree2::MTree;
13//! use std::time::{SystemTime, UNIX_EPOCH};
14//!
15//! // We're going to load data from a string so this example with pass doctest,
16//! // but there's no reason you can't use a file, or any other data source.
17//! let raw_data = "
18//! /set type=file uid=0 gid=0 mode=644
19//! ./.BUILDINFO time=1523250074.300237174 size=8602 md5digest=13c0a46c2fb9f18a1a237d4904b6916e \
20//!     sha256digest=db1941d00645bfaab04dd3898ee8b8484874f4880bf03f717adf43a9f30d9b8c
21//! ./.PKGINFO time=1523250074.276237110 size=682 md5digest=fdb9ac9040f2e78f3561f27e5b31c815 \
22//!     sha256digest=5d41b48b74d490b7912bdcef6cf7344322c52024c0a06975b64c3ca0b4c452d1
23//! /set mode=755
24//! ./usr time=1523250049.905171912 type=dir
25//! ./usr/bin time=1523250065.373213293 type=dir
26//! ";
27//! let entries = MTree::from_reader(raw_data.as_bytes());
28//! for entry in entries {
29//!     // Normally you'd want to handle any errors
30//!     let entry = entry.unwrap();
31//!     // We can print out a human-readable copy of the entry
32//!     println!("{}", entry);
33//!     // Let's check that if there is a modification time, it's in the past
34//!     if let Some(time) = entry.time() {
35//!         assert!(time < SystemTime::now());
36//!     }
37//!     // We might also want to take a checksum of the file, and compare it to the digests
38//!     // supplied by mtree, but this example doesn't have access to a filesystem.
39//! }
40//! ```
41//! # Crate features
42//!
43//! By default, pathnames are parsed as ASCII, with characters outside of the 95
44//! printable ASCII characters encoded as backshlash followed by three octal digits according to [mtree(5)](https://www.freebsd.org/cgi/man.cgi?mtree(5)).
45//!
46//!
47//! Parsing of [strsvis VIS_CSTYLE](https://man.netbsd.org/strsvis.3)
48//! coded characters is added in addition as specified in [mtree(8)](https://man.netbsd.org/mtree.8) for the netbsd6 flavor.
49
50pub use parser::FileMode;
51pub use parser::FileType;
52pub use parser::Format;
53use parser::Keyword;
54use parser::LineParseError;
55use parser::MTreeLine;
56pub use parser::ParserError;
57use parser::SpecialKind;
58use std::env;
59use std::ffi::OsStr;
60use std::fmt;
61use std::io::BufRead;
62use std::io::BufReader;
63use std::io::Read;
64use std::io::Split;
65use std::io::{self};
66use std::os::unix::ffi::OsStrExt;
67use std::path::Path;
68use std::path::PathBuf;
69use std::time::SystemTime;
70use std::time::UNIX_EPOCH;
71use util::decode_escapes_path;
72
73mod parser;
74mod util;
75
76#[cfg(not(unix))]
77compiler_error!("This library currently only supports unix, due to windows using utf-16 for paths");
78
79/// An mtree parser (start here).
80///
81/// This is the main struct for the lib. Semantically, an mtree file is a
82/// sequence of filesystem records. These are provided as an iterator. Use the
83/// `from_reader` function to construct an instance.
84pub struct MTree<R>
85where
86    R: Read,
87{
88    /// The iterator over lines (lines are guaranteed to end in \n since we only
89    /// support unix).
90    inner: Split<BufReader<R>>,
91    /// The current working directory for dir calculations.
92    cwd: PathBuf,
93    /// These are set with the '/set' and '/unset' special functions.
94    default_params: Params,
95}
96
97impl<R> MTree<R>
98where
99    R: Read,
100{
101    /// The constructor function for an `MTree` instance.
102    ///
103    /// This uses the current working directory as the base for relative paths.
104    /// Relative specifications are allowed to exceed the starting level but are
105    /// truncated to the root.
106    pub fn from_reader(reader: R) -> Self {
107        Self {
108            inner: BufReader::new(reader).split(b'\n'),
109            cwd: env::current_dir().unwrap_or_default(),
110            default_params: Params::default(),
111        }
112    }
113
114    /// The constructor function for an `MTree` instance.
115    ///
116    /// This uses the provided path  as the base for relative paths.
117    /// Relative specifications are allowed to exceed the starting level but are
118    /// truncated to the root.
119    pub fn from_reader_with_cwd(reader: R, cwd: PathBuf) -> Self {
120        Self {
121            inner: BufReader::new(reader).split(b'\n'),
122            cwd,
123            default_params: Params::default(),
124        }
125    }
126
127    /// The constructor function for an `MTree` instance.
128    ///
129    /// This uses an empty `PathBuf` as the base for relative paths.
130    /// Relative specifications are allowed to exceed the starting level but are
131    /// truncated to the root.
132    pub fn from_reader_with_empty_cwd(reader: R) -> Self {
133        Self {
134            inner: BufReader::new(reader).split(b'\n'),
135            cwd: PathBuf::new(),
136            default_params: Params::default(),
137        }
138    }
139
140    /// This is a helper function to make error handling easier.
141    fn next_entry(
142        &mut self,
143        line: Result<Vec<u8>, LineParseError>,
144    ) -> Result<Option<Entry>, LineParseError> {
145        let line = line?;
146        let line = MTreeLine::from_bytes(&line)?;
147        Ok(match line {
148            MTreeLine::Blank | MTreeLine::Comment => None,
149            MTreeLine::Special(SpecialKind::Set, keywords) => {
150                self.default_params.set_list(keywords.into_iter());
151                None
152            }
153            // this won't work because keywords need to be parsed without arguments.
154            MTreeLine::Special(SpecialKind::Unset, _keywords) => unimplemented!(),
155            MTreeLine::Relative(path, keywords) => {
156                let mut params = self.default_params.clone();
157                params.set_list(keywords.into_iter());
158                let filepath = self.cwd.join(&path);
159                if params.file_type == Some(FileType::Directory) {
160                    self.cwd.push(&path);
161                }
162
163                Some(Entry {
164                    path: filepath,
165                    params,
166                })
167            }
168            MTreeLine::DotDot => {
169                // Only pop if we're not at root or first level
170                if self.cwd.parent().and_then(|p| p.parent()).is_some() {
171                    self.cwd.pop();
172                }
173                None
174            }
175            MTreeLine::Full(path, keywords) => {
176                let mut params = self.default_params.clone();
177                params.set_list(keywords.into_iter());
178                Some(Entry { path, params })
179            }
180        })
181    }
182}
183
184impl<R> Iterator for MTree<R>
185where
186    R: Read,
187{
188    type Item = Result<Entry, Error>;
189
190    fn next(&mut self) -> Option<Result<Entry, Error>> {
191        let mut accumulated_line: Option<Vec<u8>> = None;
192
193        while let Some(line_result) = self.inner.next() {
194            // Handle IO errors from reading the line
195            let line = match line_result {
196                Ok(line) => line,
197                Err(e) => return Some(Err(Error::Io(e))),
198            };
199
200            // If we have an accumulated line, append to it, otherwise use current line
201            let current_input = if let Some(mut acc) = accumulated_line.take() {
202                acc.extend_from_slice(&line);
203                Ok(acc)
204            } else {
205                Ok(line)
206            };
207
208            // Try to parse the current input into an entry
209            match self.next_entry(current_input) {
210                Ok(Some(entry)) => return Some(Ok(entry)),
211                Ok(None) => continue, // Skip blank lines, comments etc
212                Err(LineParseError::WrappedLine(partial)) => {
213                    accumulated_line = Some(partial);
214                    continue;
215                }
216                Err(LineParseError::Io(e)) => return Some(Err(Error::Io(e))),
217                Err(LineParseError::Parser(e)) => return Some(Err(Error::Parser(e))),
218            }
219        }
220
221        // At EOF - handle any remaining accumulated line
222        if let Some(final_line) = accumulated_line {
223            // Try to parse the final accumulated line
224            match self.next_entry(Ok(final_line)) {
225                Ok(Some(entry)) => Some(Ok(entry)),
226                Ok(None) => None,
227                Err(LineParseError::WrappedLine(_)) => {
228                    // This shouldn't happen since we checked for trailing backslash
229                    Some(Err(Error::Parser(ParserError(
230                        "unexpected wrapped line at end of file".to_owned(),
231                    ))))
232                }
233                Err(LineParseError::Io(e)) => Some(Err(Error::Io(e))),
234                Err(LineParseError::Parser(e)) => Some(Err(Error::Parser(e))),
235            }
236        } else {
237            None
238        }
239    }
240}
241
242/// An entry in the mtree file.
243///
244/// Entries have a path to the entity in question, and a list of optional
245/// params.
246#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
247pub struct Entry {
248    /// The path of this entry
249    path: PathBuf,
250    /// All parameters applicable to this entry
251    params: Params,
252}
253
254impl fmt::Display for Entry {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        writeln!(f, r#"mtree entry for "{}""#, self.path.display())?;
257        write!(f, "{}", self.params)
258    }
259}
260
261impl Entry {
262    /// The path of this entry
263    pub fn path(&self) -> &Path {
264        self.path.as_ref()
265    }
266
267    /// `cksum` The checksum of the file using the default algorithm specified
268    /// by the cksum(1) utility.
269    pub fn checksum(&self) -> Option<u64> {
270        self.params.checksum
271    }
272
273    /// `device` The device number for *block* or *char* file types.
274    pub fn device(&self) -> Option<&Device> {
275        self.params.device.as_deref()
276    }
277
278    /// `contents` The full pathname of a file that holds the contents of this
279    /// file.
280    pub fn contents(&self) -> Option<&Path> {
281        self.params.contents.as_ref().map(AsRef::as_ref)
282    }
283
284    /// `flags` The file flags as a symbolic name.
285    pub fn flags(&self) -> Option<&[u8]> {
286        self.params.flags.as_ref().map(AsRef::as_ref)
287    }
288
289    /// `gid` The file group as a numeric value.
290    pub fn gid(&self) -> Option<u32> {
291        self.params.gid
292    }
293
294    /// `gname` The file group as a symbolic name.
295    ///
296    /// The name can be up to 32 chars and must match regex
297    /// `[a-z_][a-z0-9_-]*[$]?`.
298    pub fn gname(&self) -> Option<&[u8]> {
299        self.params.gname.as_ref().map(AsRef::as_ref)
300    }
301
302    /// `ignore` Ignore any file hierarchy below this line.
303    pub fn ignore(&self) -> bool {
304        self.params.ignore
305    }
306
307    /// `inode` The inode number.
308    pub fn inode(&self) -> Option<u64> {
309        self.params.inode
310    }
311
312    /// `link` The target of the symbolic link when type=link.
313    pub fn link(&self) -> Option<&Path> {
314        self.params.link.as_ref().map(AsRef::as_ref)
315    }
316
317    /// `md5|md5digest` The MD5 message digest of the file.
318    pub fn md5(&self) -> Option<u128> {
319        self.params.md5
320    }
321
322    /// `mode` The current file's permissions as a numeric (octal) or symbolic
323    /// value.
324    pub fn mode(&self) -> Option<FileMode> {
325        self.params.mode
326    }
327
328    /// `nlink` The number of hard links the file is expected to have.
329    pub fn nlink(&self) -> Option<u64> {
330        self.params.nlink
331    }
332
333    /// `nochange` Make sure this file or directory exists but otherwise ignore
334    /// all attributes.
335    pub fn no_change(&self) -> bool {
336        self.params.no_change
337    }
338
339    /// `optional` The file is optional; do not complain about the file if it is
340    /// not in the file hierarchy.
341    pub fn optional(&self) -> bool {
342        self.params.optional
343    }
344
345    /// `resdevice` The "resident" device number of the file, e.g. the ID of the
346    /// device that contains the file. Its format is the same as the one for
347    /// `device`.
348    pub fn resident_device(&self) -> Option<&Device> {
349        self.params.resident_device.as_deref()
350    }
351
352    /// `rmd160|rmd160digest|ripemd160digest` The RIPEMD160 message digest of
353    /// the file.
354    pub fn rmd160(&self) -> Option<&[u8; 20]> {
355        self.params.rmd160.as_ref().map(AsRef::as_ref)
356    }
357
358    /// `sha1|sha1digest` The FIPS 160-1 ("SHA-1") message digest of the file.
359    pub fn sha1(&self) -> Option<&[u8; 20]> {
360        self.params.sha1.as_ref().map(AsRef::as_ref)
361    }
362
363    /// `sha256|sha256digest` The FIPS 180-2 ("SHA-256") message digest of the
364    /// file.
365    pub fn sha256(&self) -> Option<&[u8; 32]> {
366        self.params.sha256.as_ref()
367    }
368
369    /// `sha384|sha384digest` The FIPS 180-2 ("SHA-384") message digest of the
370    /// file.
371    pub fn sha384(&self) -> Option<&[u8; 48]> {
372        self.params.sha384.as_ref().map(AsRef::as_ref)
373    }
374
375    /// `sha512|sha512digest` The FIPS 180-2 ("SHA-512") message digest of the
376    /// file.
377    pub fn sha512(&self) -> Option<&[u8; 64]> {
378        self.params.sha512.as_ref().map(AsRef::as_ref)
379    }
380
381    /// `size` The size, in bytes, of the file.
382    pub fn size(&self) -> Option<u64> {
383        self.params.size
384    }
385
386    /// `time` The last modification time of the file.
387    pub fn time(&self) -> Option<SystemTime> {
388        self.params.time
389    }
390
391    /// `type` The type of the file.
392    pub fn file_type(&self) -> Option<FileType> {
393        self.params.file_type
394    }
395
396    /// The file owner as a numeric value.
397    pub fn uid(&self) -> Option<u32> {
398        self.params.uid
399    }
400
401    /// The file owner as a symbolic name.
402    ///
403    /// The name can be up to 32 chars and must match regex
404    /// `[a-z_][a-z0-9_-]*[$]?`.
405    pub fn uname(&self) -> Option<&[u8]> {
406        self.params.uname.as_ref().map(AsRef::as_ref)
407    }
408}
409
410/// All possible parameters to an entry.
411///
412/// All parameters are optional. `ignore`, `nochange` and `optional` all have no
413/// value, and so `true` represets their presence.
414#[derive(Default, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
415struct Params {
416    /// `cksum` The checksum of the file using the default algorithm specified
417    /// by the cksum(1) utility.
418    pub checksum: Option<u64>,
419    /// `device` The device number for *block* or *char* file types.
420    pub device: Option<Box<Device>>,
421    /// `contents` The full pathname of a file that holds the contents of this
422    /// file.
423    pub contents: Option<PathBuf>,
424    /// `flags` The file flags as a symbolic name.
425    pub flags: Option<Box<[u8]>>,
426    /// `gid` The file group as a numeric value.
427    pub gid: Option<u32>,
428    /// `gname` The file group as a symbolic name.
429    ///
430    /// The name can be up to 32 chars and must match regex
431    /// `[a-z_][a-z0-9_-]*[$]?`.
432    pub gname: Option<Box<[u8]>>,
433    /// `ignore` Ignore any file hierarchy below this line.
434    pub ignore: bool,
435    /// `inode` The inode number.
436    pub inode: Option<u64>,
437    /// `link` The target of the symbolic link when type=link.
438    pub link: Option<PathBuf>,
439    /// `md5|md5digest` The MD5 message digest of the file.
440    pub md5: Option<u128>,
441    /// `mode` The current file's permissions as a numeric (octal) or symbolic
442    /// value.
443    pub mode: Option<FileMode>,
444    /// `nlink` The number of hard links the file is expected to have.
445    pub nlink: Option<u64>,
446    /// `nochange` Make sure this file or directory exists but otherwise ignore
447    /// all attributes.
448    pub no_change: bool,
449    /// `optional` The file is optional; do not complain about the file if it is
450    /// not in the file hierarchy.
451    pub optional: bool,
452    /// `resdevice` The "resident" device number of the file, e.g. the ID of the
453    /// device that contains the file. Its format is the same as the one for
454    /// `device`.
455    pub resident_device: Option<Box<Device>>,
456    /// `rmd160|rmd160digest|ripemd160digest` The RIPEMD160 message digest of
457    /// the file.
458    pub rmd160: Option<Box<[u8; 20]>>,
459    /// `sha1|sha1digest` The FIPS 160-1 ("SHA-1") message digest of the file.
460    pub sha1: Option<Box<[u8; 20]>>,
461    /// `sha256|sha256digest` The FIPS 180-2 ("SHA-256") message digest of the
462    /// file.
463    pub sha256: Option<[u8; 32]>,
464    /// `sha384|sha384digest` The FIPS 180-2 ("SHA-384") message digest of the
465    /// file.
466    pub sha384: Option<Box<[u8; 48]>>,
467    /// `sha512|sha512digest` The FIPS 180-2 ("SHA-512") message digest of the
468    /// file.
469    pub sha512: Option<Box<[u8; 64]>>,
470    /// `size` The size, in bytes, of the file.
471    pub size: Option<u64>,
472    /// `time` The last modification time of the file.
473    pub time: Option<SystemTime>,
474    /// `type` The type of the file.
475    pub file_type: Option<FileType>,
476    /// The file owner as a numeric value.
477    pub uid: Option<u32>,
478    /// The file owner as a symbolic name.
479    ///
480    /// The name can be up to 32 chars and must match regex
481    /// `[a-z_][a-z0-9_-]*[$]?`.
482    pub uname: Option<Box<[u8]>>,
483}
484
485impl Params {
486    /// Helper method to set a number of parsed keywords.
487    fn set_list<'a>(&mut self, keywords: impl Iterator<Item = Keyword<'a>>) {
488        for keyword in keywords {
489            self.set(keyword);
490        }
491    }
492
493    /// Set a parameter from a parsed keyword.
494    fn set(&mut self, keyword: Keyword<'_>) {
495        match keyword {
496            Keyword::Checksum(cksum) => self.checksum = Some(cksum),
497            Keyword::DeviceRef(device) => self.device = Some(Box::new(device.to_device())),
498            Keyword::Contents(contents) => {
499                self.contents = Some(Path::new(OsStr::from_bytes(contents)).to_owned());
500            }
501            Keyword::Flags(flags) => self.flags = Some(flags.into()),
502            Keyword::Gid(gid) => self.gid = Some(gid),
503            Keyword::Gname(gname) => self.gname = Some(gname.into()),
504            Keyword::Ignore => self.ignore = true,
505            Keyword::Inode(inode) => self.inode = Some(inode),
506            Keyword::Link(link) => {
507                self.link = decode_escapes_path(&mut link.to_vec());
508            }
509            Keyword::Md5(md5) => self.md5 = Some(md5),
510            Keyword::Mode(mode) => self.mode = Some(mode),
511            Keyword::NLink(nlink) => self.nlink = Some(nlink),
512            Keyword::NoChange => self.no_change = false,
513            Keyword::Optional => self.optional = false,
514            Keyword::ResidentDeviceRef(device) => {
515                self.resident_device = Some(Box::new(device.to_device()));
516            }
517            Keyword::Rmd160(rmd160) => self.rmd160 = Some(Box::new(rmd160)),
518            Keyword::Sha1(sha1) => self.sha1 = Some(Box::new(sha1)),
519            Keyword::Sha256(sha256) => self.sha256 = Some(sha256),
520            Keyword::Sha384(sha384) => self.sha384 = Some(Box::new(sha384)),
521            Keyword::Sha512(sha512) => self.sha512 = Some(Box::new(sha512)),
522            Keyword::Size(size) => self.size = Some(size),
523            Keyword::Time(time) => self.time = Some(UNIX_EPOCH + time),
524            Keyword::Type(ty) => self.file_type = Some(ty),
525            Keyword::Uid(uid) => self.uid = Some(uid),
526            Keyword::Uname(uname) => self.uname = Some(uname.into()),
527        }
528    }
529
530    /*
531    /// Empty this params list (better mem usage than creating a new one).
532    fn clear(&mut self) {
533        self.checksum = None;
534        self.device = None;
535        self.contents = None;
536        self.flags = None;
537        self.gid = None;
538        self.gname = None;
539        self.ignore = false;
540        self.inode = None;
541        self.link = None;
542        self.md5 = None;
543        self.mode = None;
544        self.nlink = None;
545        self.no_change = false;
546        self.optional = false;
547        self.resident_device = None;
548        self.rmd160 = None;
549        self.sha1 = None;
550        self.sha256 = None;
551        self.sha384 = None;
552        self.sha512 = None;
553        self.size = None;
554        self.time = None;
555        self.file_type = None;
556        self.uid = None;
557        self.uname = None;
558    }
559    */
560}
561
562impl fmt::Display for Params {
563    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564        if let Some(v) = self.checksum {
565            writeln!(f, "checksum: {v}")?;
566        }
567        if let Some(ref v) = self.device {
568            writeln!(f, "device: {v:?}")?;
569        }
570        if let Some(ref v) = self.contents {
571            writeln!(f, "contents: {}", v.display())?;
572        }
573        if let Some(ref v) = self.flags {
574            writeln!(f, "flags: {v:?}")?;
575        }
576        if let Some(v) = self.gid
577            && v != 0
578        {
579            writeln!(f, "gid: {v}")?;
580        }
581        if let Some(ref v) = self.gname {
582            writeln!(f, "gname: {}", String::from_utf8_lossy(v))?;
583        }
584        if self.ignore {
585            writeln!(f, "ignore")?;
586        }
587        if let Some(v) = self.inode {
588            writeln!(f, "inode: {v}")?;
589        }
590        if let Some(ref v) = self.link {
591            writeln!(f, "link: {}", v.display())?;
592        }
593        if let Some(ref v) = self.md5 {
594            writeln!(f, "md5: {v:x}")?;
595        }
596        if let Some(ref v) = self.mode {
597            writeln!(f, "mode: {v}")?;
598        }
599        if let Some(v) = self.nlink {
600            writeln!(f, "nlink: {v}")?;
601        }
602        if self.no_change {
603            writeln!(f, "no change")?;
604        }
605        if self.optional {
606            writeln!(f, "optional")?;
607        }
608        if let Some(ref v) = self.resident_device {
609            writeln!(f, "resident device: {v:?}")?;
610        }
611        if let Some(ref v) = self.rmd160 {
612            write!(f, "rmd160: ")?;
613            for ch in v.iter() {
614                write!(f, "{ch:x}")?;
615            }
616            writeln!(f)?;
617        }
618        if let Some(ref v) = self.sha1 {
619            write!(f, "sha1: ")?;
620            for ch in v.iter() {
621                write!(f, "{ch:x}")?;
622            }
623            writeln!(f)?;
624        }
625        if let Some(ref v) = self.sha256 {
626            write!(f, "sha256: ")?;
627            for ch in v {
628                write!(f, "{ch:x}")?;
629            }
630            writeln!(f)?;
631        }
632        if let Some(ref v) = self.sha384 {
633            write!(f, "sha384: ")?;
634            for ch in v.iter() {
635                write!(f, "{ch:x}")?;
636            }
637            writeln!(f)?;
638        }
639        if let Some(ref v) = self.sha512 {
640            write!(f, "sha512: ")?;
641            for ch in v.iter() {
642                write!(f, "{ch:x}")?;
643            }
644            writeln!(f)?;
645        }
646        if let Some(v) = self.size {
647            writeln!(f, "size: {v}")?;
648        }
649        if let Some(v) = self.time {
650            writeln!(f, "modification time: {v:?}")?;
651        }
652        if let Some(v) = self.file_type {
653            writeln!(f, "file type: {v}")?;
654        }
655        if let Some(v) = self.uid
656            && v != 0
657        {
658            writeln!(f, "uid: {v}")?;
659        }
660        if let Some(ref v) = self.uname {
661            writeln!(f, "uname: {}", String::from_utf8_lossy(v))?;
662        }
663        Ok(())
664    }
665}
666
667/// A unix device.
668///
669/// The parsing for this could probably do with some work.
670#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
671pub struct Device {
672    /// The device format.
673    pub format: Format,
674    /// The device major identifier.
675    pub major: Vec<u8>,
676    /// The device minor identifier.
677    pub minor: Vec<u8>,
678    /// The device subunit identifier, if applicable.
679    pub subunit: Option<Vec<u8>>,
680}
681
682/// The error type for this crate.
683///
684/// There are 2 possible ways that this lib can fail - there can be a problem
685/// parsing a record, or there can be a fault in the underlying reader.
686#[derive(Debug)]
687pub enum Error {
688    /// There was an i/o error reading data from the reader.
689    Io(io::Error),
690    /// There was a problem parsing the records.
691    Parser(ParserError),
692}
693
694impl fmt::Display for Error {
695    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
696        f.write_str(match self {
697            Self::Io(..) => "an i/o error occured while reading the mtree",
698            Self::Parser(..) => "an error occured while parsing the mtree",
699        })
700    }
701}
702
703impl std::error::Error for Error {
704    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
705        match self {
706            Self::Io(err) => Some(err),
707            Self::Parser(err) => Some(err),
708        }
709    }
710}
711
712impl From<io::Error> for Error {
713    fn from(from: io::Error) -> Self {
714        Self::Io(from)
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721    use std::io::Cursor;
722    #[test]
723    fn test_wrapped_line_at_eof() {
724        let data = r"# .
725mtree_test \
726size=581  \";
727        let mtree = MTree::from_reader_with_empty_cwd(Cursor::new(data.as_bytes()));
728        let entry = mtree.into_iter().next().unwrap().unwrap();
729        let should = Entry {
730            path: PathBuf::from("mtree_test"),
731            params: Params {
732                checksum: None,
733                device: None,
734                contents: None,
735                flags: None,
736                gid: None,
737                gname: None,
738                ignore: false,
739                inode: None,
740                link: None,
741                md5: None,
742                mode: None,
743                nlink: None,
744                no_change: false,
745                optional: false,
746                resident_device: None,
747                rmd160: None,
748                sha1: None,
749                sha256: None,
750                sha384: None,
751                sha512: None,
752                size: Some(581),
753                time: None,
754                file_type: None,
755                uid: None,
756                uname: None,
757            },
758        };
759        assert_eq!(entry, should);
760    }
761}