use { crate::{filetype::Flag, nix, Algorithm, Checksum, Error, File, FileType, Special}, md5::{Digest, Md5}, sha1::Sha1, sha2::Sha256, std::{ collections::HashMap, env, fs, io::{BufReader, BufWriter, Read, Write}, os::unix::fs::{symlink, MetadataExt}, path::{Path, PathBuf}, sync::Mutex, }, }; #[derive(Debug, PartialEq)] enum Kind { Normal, Dir, Char, Block, Pipe, } impl From for Kind { fn from(value: u32) -> Self { if value & 0o40000 != 0 { Self::Dir } else if value & 0o20_000 != 0 { Self::Char } else if value & 0o60_000 != 0 { Self::Block } else if value & 0o10_000 != 0 { Self::Pipe } else if value & 0o100_000 != 0 { Self::Normal } else { unreachable!(); } } } /// A representation of a file and it's associated metadata. #[derive(Debug)] pub struct Node { /// The filesystem path to this file pub name: String, /// The user id of this file's owner pub uid: u32, /// The group id of this file's owner pub gid: u32, /// The most recent modification time of this file pub mtime: u64, /// The Unix permissions bits of this file pub mode: u16, /// The type of file this node represents pub filetype: FileType, } impl Node { #[allow(clippy::similar_names)] /// Reads a `Node` from an archive file or stream of Nodes. /// > Note: this function reads an already created node. To create a new node /// > from a file, use the `from_path` method. /// # Errors /// Returns `crate::Error` if io fails or certain other circumstances pub fn read(reader: &mut T) -> Result { let mut len = [0; 2]; reader.read_exact(&mut len)?; let len = u16::from_le_bytes(len); if len == 0 { return Ok(Self { name: String::new(), mode: 0, uid: 0, gid: 0, mtime: 0, filetype: FileType::Eof, }); } let mut name = Vec::with_capacity(len.into()); let mut handle = reader.take(len.into()); handle.read_to_end(&mut name)?; let mut buf = [0; 18]; reader.read_exact(&mut buf)?; let uid: [u8; 4] = buf[0..4].try_into()?; let gid: [u8; 4] = buf[4..8].try_into()?; let mtime: [u8; 8] = buf[8..16].try_into()?; let raw_mode: [u8; 2] = buf[16..18].try_into()?; let raw_mode = u16::from_le_bytes(raw_mode); let (flag, mode) = Flag::extract_from_raw(raw_mode)?; let filetype = FileType::read(reader, flag)?; Ok(Self { name: String::from_utf8(name)?, uid: u32::from_le_bytes(uid), gid: u32::from_le_bytes(gid), mtime: u64::from_le_bytes(mtime), mode, filetype, }) } /// Write a `Node` struct into it's on-disk archive representation. /// > Note: this function saves the data to the archive format's on-disk /// > representation. To extract the contents of a `Node` and write out the /// > file it represents, use the `extract` method instead. /// # Errors /// Returns `crate::Error` if io fails or certain other circumstances pub fn write(&self, writer: &mut T) -> Result<(), Error> { let len: u16 = self.name.len().try_into()?; writer.write_all(&len.to_le_bytes())?; writer.write_all(self.name.as_bytes())?; writer.write_all(&self.uid.to_le_bytes())?; writer.write_all(&self.gid.to_le_bytes())?; writer.write_all(&self.mtime.to_le_bytes())?; let mode = Flag::from(&self.filetype).append_mode(self.mode); writer.write_all(&mode.to_le_bytes())?; self.filetype.write(writer)?; Ok(()) } #[allow(clippy::similar_names)] /// Creates a new node from a file which exists on the filesystem /// ### Parameters /// - path - the path to this file /// - checksum - a zeroed out `Checksum` variant to be used if the inline /// checksumming feature is to be used /// - links - this should be passed to each invocation of `from_path` used /// during the creation of a single archive, to identify hard links and to /// avoid writing their data out more than once. /// # Errors /// Returns `crate::Error` if io fails or certain other circumstances pub fn from_path( path: &str, algorithm: Algorithm, links: &Mutex>, ) -> Result { let name = String::from(path); let meta = fs::symlink_metadata(path)?; let mode = meta.mode(); let uid = meta.uid(); let gid = meta.gid(); let mtime = meta.mtime().try_into()?; let ft = meta.file_type(); let filetype = 'blk: { if ft.is_dir() { FileType::Directory } else if ft.is_symlink() { let target = fs::read_link(path)?; let target = target .to_str() .ok_or(Error::Other("bad path".to_string()))? .to_string(); FileType::SoftLink(target) } else { if meta.nlink() > 1 { if let Ok(mut list) = links.lock() { let inode = meta.ino(); if let Some(target) = list.get(&inode).cloned() { break 'blk FileType::HardLink(target); } list.insert(inode, name.clone()); } } let kind = Kind::from(mode); if kind == Kind::Char { let device = Special::from_rdev(meta.rdev()); break 'blk FileType::Character(device); } else if kind == Kind::Block { let device = Special::from_rdev(meta.rdev()); break 'blk FileType::Block(device); } else if kind == Kind::Pipe { break 'blk FileType::Fifo; } else if kind == Kind::Normal { let mut data = vec![]; let fd = fs::File::open(path)?; let mut reader = BufReader::new(fd); let len = reader.read_to_end(&mut data)?.try_into()?; let checksum = match algorithm { Algorithm::Md5 => { let mut hasher = Md5::new(); hasher.update(&data); let cs = hasher.finalize().into(); Checksum::Md5(cs) } Algorithm::Sha1 => { let mut hasher = Sha1::new(); hasher.update(&data); let cs = hasher.finalize().into(); Checksum::Sha1(cs) } Algorithm::Sha256 => { let mut hasher = Sha256::new(); hasher.update(&data); let cs = hasher.finalize().into(); Checksum::Sha256(cs) } Algorithm::Skip => Checksum::Skip, }; break 'blk FileType::Normal(File { len, checksum, data, }); } return Err(Error::UnknownFileType); } }; let mode = Flag::from(&filetype).append_mode(u16::try_from(mode & 0o7777)?); Ok(Self { name, uid, gid, mtime, mode, filetype, }) } /// Recreates the original file on disk which this node represents /// # Errors /// Returns `crate::Error` if io fails or certain other circumstances pub fn extract(&self, prefix: Option<&str>) -> Result<(), Error> { let euid = nix::geteuid(); let path = 'blk: { if let Some(prefix) = prefix { if self.name.starts_with('/') { break 'blk format!("{prefix}{}", self.name); } break 'blk format!("{prefix}/{}", self.name); } self.name.clone() }; let n_path = if path.starts_with('/') { PathBuf::from(&path) } else if let Ok(mut p) = env::current_dir() { p.push(&path); p } else { PathBuf::from(&format!("./{path}")) }; if let Some(p) = n_path.parent() { if !p.exists() { self.mkdir(p)?; } } match self.filetype { FileType::Eof => {} FileType::Fifo => { nix::mkfifo(&path, self.mode.into())?; if euid == 0 { nix::chown(&path, self.uid, self.gid)?; } } FileType::Block(ref b) => { if euid == 0 { nix::mknod(&path, self.mode.into(), b.major, b.minor)?; } } FileType::Character(ref c) => { if euid == 0 { nix::mknod(&path, self.mode.into(), c.major, c.minor)?; } } FileType::Normal(ref n) => { { let fd = fs::OpenOptions::new() .create(true) .truncate(true) .write(true) .open(&n_path)?; let mut writer = BufWriter::new(fd); writer.write_all(&n.data)?; } if euid == 0 { nix::chown(&path, self.uid, self.gid)?; } nix::chmod(&path, self.mode.into())?; } FileType::HardLink(ref t) => { let target = if let Some(prefix) = prefix { format!("{prefix}/{t}") } else { t.to_string() }; fs::hard_link(target, &path)?; } FileType::SoftLink(ref t) => { symlink(&self.name, t)?; } FileType::Directory => { self.mkdir(&PathBuf::from(&path))?; } } Ok(()) } fn mkdir(&self, dir: &Path) -> Result<(), Error> { if !dir.exists() { fs::create_dir_all(dir)?; } if nix::geteuid() == 0 { nix::chown(dir.to_str().ok_or(Error::NulError)?, self.uid, self.gid)?; } nix::chmod( dir.to_str().ok_or(Error::BadPath)?, (self.mode & 0o7777 | 0o100).into(), )?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use core::panic; use std::{fmt::Write, fs::remove_file}; static LI: &[u8] = include_bytes!("../test/li.txt"); #[test] fn get_kind_dir() { let meta = fs::symlink_metadata("test").unwrap(); let mode = meta.mode(); let kind = Kind::from(mode); assert_eq!(kind, Kind::Dir); } #[test] fn get_kind_fifo() { let _ = remove_file("test/fifo"); nix::mkfifo("test/fifo", 0o644).unwrap(); let meta = fs::symlink_metadata("test/fifo").unwrap(); let mode = meta.mode(); let kind = Kind::from(mode); assert_eq!(kind, Kind::Pipe); } #[test] #[cfg(target_os = "linux")] fn get_kind_char() { let meta = fs::symlink_metadata("/dev/null").unwrap(); let mode = meta.mode(); let kind = Kind::from(mode); assert_eq!(kind, Kind::Char); } #[test] fn from_file_path() { let links = Mutex::new(HashMap::new()); let node = Node::from_path("test/li.txt", Algorithm::Sha256, &links).unwrap(); let FileType::Normal(f) = node.filetype else { panic!() }; assert_eq!(f.len, 1005); let Checksum::Sha256(sum) = f.checksum else { panic!() }; let mut s = String::new(); for c in &sum { write!(s, "{c:02x}").unwrap(); } assert_eq!( s, "5f1b6e6e31682fb6683db2e78db11e624527c897618f1a5b0a0b5256f557c22d" ); } #[test] fn from_symlink_path() { let _res = remove_file("test/lilnk.txt"); symlink("li.txt", "test/lilnk.txt").unwrap(); let links = Mutex::new(HashMap::new()); let node = Node::from_path("test/lilnk.txt", Algorithm::Skip, &links).unwrap(); let FileType::SoftLink(tgt) = node.filetype else { eprintln!("Incorrect filetype: {:?}", node.filetype); panic!(); }; assert_eq!(tgt, "li.txt"); } #[test] fn from_hardlink_path() { let _res = remove_file("test/lorem.txt"); fs::hard_link("test/li.txt", "test/lorem.txt").unwrap(); let links = Mutex::new(HashMap::new()); let _node0 = Node::from_path("test/li.txt", Algorithm::Sha256, &links).unwrap(); let node1 = Node::from_path("test/lorem.txt", Algorithm::Sha256, &links).unwrap(); match node1.filetype { FileType::HardLink(s) => assert_eq!(s, "test/li.txt"), _ => panic!(), } } #[test] fn from_fifo_path() { let _res = remove_file("test/fifo"); nix::mkfifo("test/fifo", 0o644).unwrap(); let links = Mutex::new(HashMap::new()); let node = Node::from_path("test/fifo", Algorithm::Skip, &links).unwrap(); let FileType::Fifo = node.filetype else { eprintln!("Incorrect filetype: {:?}", node.filetype); panic!(); }; } #[test] fn load_store_file() { { let fd = fs::File::create("test/li.node").unwrap(); let mut writer = BufWriter::new(fd); let links = Mutex::new(HashMap::new()); let node = Node::from_path("test/li.txt", Algorithm::Sha1, &links).unwrap(); let FileType::Normal(_) = node.filetype else { eprintln!("Created wrong filetype: {:?}", node.filetype); panic!(); }; node.write(&mut writer).unwrap(); } let meta = fs::metadata("test/li.txt").unwrap(); let fd = fs::File::open("test/li.node").unwrap(); let mut reader = BufReader::new(fd); let node = Node::read(&mut reader).unwrap(); assert_eq!(meta.mode() & 0o777, node.mode as u32); assert_eq!(meta.uid(), node.uid); assert_eq!(meta.gid(), node.gid); assert_eq!(meta.mtime(), node.mtime as i64); let FileType::Normal(f) = node.filetype else { eprintln!("Read incorrect filetype: {:?}", node.filetype); panic!() }; let Checksum::Sha1(sum) = f.checksum else { panic!() }; let mut s = String::new(); for c in &sum { write!(s, "{c:02x}").unwrap(); } assert_eq!(s, "9bf3e5b5efd22f932e100b86c83482787e82a682"); assert_eq!(LI, f.data); } #[test] fn load_store_symlink() { { let _res = remove_file("test/lilnk.txt"); symlink("li.txt", "test/lilnk.txt").unwrap(); let _res = remove_file("test/lilnk.node"); let fd = fs::File::create("test/lilnk.node").unwrap(); let mut writer = BufWriter::new(fd); let links = Mutex::new(HashMap::new()); let node = Node::from_path("test/lilnk.txt", Algorithm::Sha1, &links).unwrap(); let FileType::SoftLink(ref tgt) = node.filetype else { eprintln!("Created wrong filetype: {:?}", node.filetype); panic!(); }; assert_eq!(tgt, "li.txt"); node.write(&mut writer).unwrap(); } let fd = fs::File::open("test/lilnk.node").unwrap(); let mut reader = BufReader::new(fd); let node = Node::read(&mut reader).unwrap(); let FileType::SoftLink(ref tgt) = node.filetype else { eprintln!("Read incorrect filetype: {:?}", node.filetype); panic!(); }; assert_eq!(tgt, "li.txt"); } #[test] fn load_store_hardlink() { { let _res = remove_file("test/lihlnk.txt"); fs::hard_link("test/li.txt", "test/lihlnk.txt").unwrap(); let _res = remove_file("test/lihlnk.node"); let fd = fs::File::create("test/lihlnk.node").unwrap(); let mut writer = BufWriter::new(fd); let links = Mutex::new(HashMap::new()); let _node = Node::from_path("test/li.txt", Algorithm::Sha1, &links).unwrap(); let node = Node::from_path("test/lihlnk.txt", Algorithm::Sha1, &links).unwrap(); let FileType::HardLink(ref tgt) = node.filetype else { eprintln!("Created wrong filetype: {:?}", node.filetype); panic!(); }; assert_eq!(tgt, "test/li.txt"); node.write(&mut writer).unwrap(); } let fd = fs::File::open("test/lihlnk.node").unwrap(); let mut reader = BufReader::new(fd); let node = Node::read(&mut reader).unwrap(); let FileType::HardLink(ref tgt) = node.filetype else { eprintln!("Read incorrect filetype: {:?}", node.filetype); panic!(); }; assert_eq!(tgt, "test/li.txt"); } #[test] fn load_store_fifo() { { let _res = remove_file("test/fifo"); nix::mkfifo("test/fifo", 0o644).unwrap(); let links = Mutex::new(HashMap::new()); let node = Node::from_path("test/fifo", Algorithm::Skip, &links).unwrap(); let FileType::Fifo = node.filetype else { eprintln!("Incorrect filetype: {:?}", node.filetype); panic!(); }; let _res = remove_file("test/fifo.node"); let fd = fs::File::create("test/fifo.node").unwrap(); let mut writer = BufWriter::new(fd); node.write(&mut writer).unwrap(); } let fd = fs::File::open("test/fifo.node").unwrap(); let mut reader = BufReader::new(fd); let node = Node::read(&mut reader).unwrap(); let FileType::Fifo = node.filetype else { eprintln!("Read incorrect filetype: {:?}", node.filetype); panic!(); }; } #[test] fn extract_file() { let links = Mutex::new(HashMap::new()); let node = Node::from_path("test/li.txt", Algorithm::Sha256, &links).unwrap(); node.extract(Some("test/output")).unwrap(); let f = fs::read_to_string("test/output/test/li.txt").unwrap(); assert_eq!(f.as_bytes(), LI); } }