From 1829ef0dd02e4c3941e84a141cf407e13c21287c Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 26 Jun 2020 09:53:59 +0200 Subject: [PATCH] some initial sequential encoder/decoder testing Tests basic files, hardlinks, symlinks. No special metadata yet. Signed-off-by: Wolfgang Bumiller --- tests/simple/fs.rs | 193 ++++++++++++++++++++++++++++++++++++++++--- tests/simple/main.rs | 15 +++- 2 files changed, 194 insertions(+), 14 deletions(-) diff --git a/tests/simple/fs.rs b/tests/simple/fs.rs index af76339..3b4b61a 100644 --- a/tests/simple/fs.rs +++ b/tests/simple/fs.rs @@ -1,32 +1,68 @@ -use std::ffi::CString; -use std::path::PathBuf; +use std::collections::HashMap; +use std::io::Read; +use std::path::{Path, PathBuf}; -use anyhow::{bail, Error}; +use anyhow::{bail, format_err, Error}; use pxar::decoder::sync as decoder; use pxar::decoder::SeqRead; use pxar::encoder::sync as encoder; use pxar::encoder::{LinkOffset, SeqWrite}; use pxar::format::{self, mode}; +use pxar::EntryKind as PxarEntryKind; use pxar::Metadata; -#[derive(Debug, Eq, PartialEq)] +pub struct HardlinkInfo { + link: LinkOffset, + path: PathBuf, +} + +pub type HardlinkList = HashMap; + +#[derive(Debug, Eq)] pub enum EntryKind { Invalid, File(Vec), Directory(Vec), Symlink(PathBuf), - Hardlink, // TODO + Hardlink(String), Device(format::Device), Socket, Fifo, } -#[derive(Debug, Eq, PartialEq)] +impl PartialEq for EntryKind { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + // When decoding we don't get our hardlink "names" back, so this cannot be part of the + // comparison. Hardlink consistency needs to be checked separately. + (EntryKind::Hardlink(a), EntryKind::Hardlink(b)) => a == b, + + (EntryKind::Invalid, EntryKind::Invalid) => true, + (EntryKind::File(a), EntryKind::File(b)) => a == b, + (EntryKind::Directory(a), EntryKind::Directory(b)) => a == b, + (EntryKind::Symlink(a), EntryKind::Symlink(b)) => a == b, + (EntryKind::Device(a), EntryKind::Device(b)) => a == b, + (EntryKind::Socket, EntryKind::Socket) => true, + (EntryKind::Fifo, EntryKind::Fifo) => true, + _ => false, + } + } +} + +#[derive(Debug, Eq)] pub struct Entry { pub name: String, pub metadata: Metadata, pub entry: EntryKind, + pub link_key: Option, +} + +impl PartialEq for Entry { + fn eq(&self, other: &Self) -> bool { + // When decoding we don't get hardlink names back, so we skip those in our eq check. + self.name == other.name && self.metadata == other.metadata && self.entry == other.entry + } } impl Entry { @@ -35,6 +71,7 @@ impl Entry { name: name.to_string(), metadata: Metadata::default(), entry: EntryKind::Invalid, + link_key: None, } } @@ -48,7 +85,30 @@ impl Entry { self } - pub fn encode_into(&self, encoder: &mut encoder::Encoder) -> Result<(), Error> + /// NOTE: For test cases to be equal via the `==` comparison link keys must be equal to the full + /// canonical path, as decoded hardlinks will use the `entry.path()` from the pxar decoder as + /// value for `EntryKind::Hardlink`. + pub fn link_key(mut self, key: impl Into) -> Self { + self.link_key = Some(key.into()); + self + } + + fn no_hardlink(&self) -> Result<(), Error> { + if let Some(key) = &self.link_key { + bail!( + "hardlink key on entry which may not be hard-linked: {}", + key + ); + } + Ok(()) + } + + pub fn encode_into( + &self, + encoder: &mut encoder::Encoder, + hardlinks: &mut HardlinkList, + path: &Path, + ) -> Result<(), Error> where T: SeqWrite, { @@ -56,33 +116,127 @@ impl Entry { EntryKind::Invalid => bail!("invalid entry encountered"), EntryKind::File(data) => { - let _link: LinkOffset = encoder.add_file( + let link: LinkOffset = encoder.add_file( &self.metadata, &self.name, data.len() as u64, &mut &data[..], )?; + if let Some(key) = &self.link_key { + let info = HardlinkInfo { + link, + path: path.join(&self.name), + }; + if hardlinks.insert(key.clone(), info).is_some() { + bail!("duplicate hardlink key: {}", key); + } + } } EntryKind::Directory(entries) => { + self.no_hardlink()?; let mut dir = encoder.create_directory(&self.name, &self.metadata)?; + let path = path.join(&self.name); for entry in entries { - entry.encode_into(&mut dir)?; + entry.encode_into(&mut dir, hardlinks, &path)?; } dir.finish()?; } EntryKind::Symlink(path) => { + self.no_hardlink()?; let _: () = encoder.add_symlink(&self.metadata, &self.name, path)?; } + EntryKind::Hardlink(key) => { + self.no_hardlink()?; + if let Some(info) = hardlinks.get(key) { + let _: () = encoder.add_hardlink(&self.name, &info.path, info.link)?; + } else { + bail!("missing hardlink target key: {}", key); + } + } + other => bail!("TODO: encode_entry for {:?}", other), } Ok(()) } - pub fn decode_from(decoder: &mut decoder::Decoder) -> Result<(), Error> { - todo!(); + pub fn decode_from(decoder: &mut decoder::Decoder) -> Result { + decoder.enable_goodbye_entries(true); + Self::decode_root(decoder) + } + + fn decode_root(decoder: &mut decoder::Decoder) -> Result { + let item = decoder + .next() + .ok_or_else(|| format_err!("empty archive?"))??; + + match item.kind() { + PxarEntryKind::Directory => (), + other => bail!("invalid entry kind for root node: {:?}", other), + } + + let mut root = Entry::new("/").metadata(item.metadata().clone()); + root.decode_directory(decoder)?; + Ok(root) + } + + fn decode_directory( + &mut self, + decoder: &mut decoder::Decoder, + ) -> Result<(), Error> { + let mut contents = Vec::new(); + + while let Some(item) = decoder.next().transpose()? { + let make_entry = + || -> Result { + Ok(Entry::new(item.file_name().to_str().ok_or_else(|| { + format_err!("non-utf8 name in test: {:?}", item.file_name()) + })?) + .metadata(item.metadata().clone()) + .link_key(item.path().as_os_str().to_str().ok_or_else(|| { + format_err!("non-utf8 path in test: {:?}", item.file_name()) + })?)) + }; + match item.kind() { + PxarEntryKind::GoodbyeTable => break, + PxarEntryKind::File { size, .. } => { + let mut data = Vec::new(); + decoder + .contents() + .ok_or_else(|| { + format_err!("failed to get contents for file entry: {:?}", item.path()) + })? + .read_to_end(&mut data)?; + contents.push(make_entry()?.entry(EntryKind::File(data))); + } + PxarEntryKind::Directory => { + let mut dir = make_entry()?; + dir.decode_directory(decoder)?; + contents.push(dir); + } + PxarEntryKind::Symlink(link) => { + contents.push(make_entry()?.entry(EntryKind::Symlink(link.into()))); + } + PxarEntryKind::Hardlink(link) => { + contents.push( + make_entry()?.entry(EntryKind::Hardlink( + link.as_os_str() + .to_str() + .ok_or_else(|| { + format_err!("non-utf8 hardlink entry: {:?}", link.as_os_str()) + })? + .to_string(), + )), + ); + } + other => todo!("pxar kind {:?}", other), + } + } + + self.entry = EntryKind::Directory(contents); + Ok(()) } } @@ -108,5 +262,22 @@ pub fn test_fs() -> Entry { Entry::new("bin") .metadata(Metadata::builder(mode::IFLNK | 0o777)) .entry(EntryKind::Symlink(PathBuf::from("usr/bin"))), + Entry::new("usr") + .metadata(Metadata::dir_builder(0o755)) + .entry(EntryKind::Directory(vec![ + Entry::new("bin") + .metadata(Metadata::dir_builder(0o755)) + .entry(EntryKind::Directory(vec![ + Entry::new("bzip2") + .metadata(Metadata::file_builder(0o755)) + .entry(EntryKind::File(b"This is an executable".to_vec())) + .link_key("/usr/bin/bzip2"), + Entry::new("cat") + .metadata(Metadata::file_builder(0o755)) + .entry(EntryKind::File(b"This is another executable".to_vec())), + Entry::new("bunzip2") + .entry(EntryKind::Hardlink("/usr/bin/bzip2".to_string())), + ])), + ])), ])) } diff --git a/tests/simple/main.rs b/tests/simple/main.rs index a2e9954..88e5900 100644 --- a/tests/simple/main.rs +++ b/tests/simple/main.rs @@ -1,8 +1,10 @@ +use std::path::Path; + use anyhow::{bail, Error}; use pxar::decoder::sync as decoder; use pxar::encoder::sync as encoder; -use pxar::encoder::{LinkOffset, SeqWrite}; +use pxar::encoder::SeqWrite; mod fs; @@ -10,10 +12,12 @@ fn encode_directory( encoder: &mut encoder::Encoder, entry: &fs::Entry, ) -> Result<(), Error> { + let mut hardlinks = fs::HardlinkList::new(); + match &entry.entry { fs::EntryKind::Directory(entries) => { for entry in entries { - entry.encode_into(encoder)?; + entry.encode_into(encoder, &mut hardlinks, Path::new("/"))?; } Ok(()) } @@ -33,5 +37,10 @@ fn test1() { assert!(!file.is_empty(), "encoder did not write any data"); - let mut decoder = decoder::Decoder::from_std(&mut &file[..]).expect("failed to create decoder"); + let mut input = &file[..]; + let mut decoder = decoder::Decoder::from_std(&mut input).expect("failed to create decoder"); + let decoded_fs = + fs::Entry::decode_from(&mut decoder).expect("failed to decode previously encoded archive"); + + assert_eq!(test_fs, decoded_fs); } -- 2.39.5