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}