use { crate::{nix, Checksum, Error, File, FileType, Special}, md5::{Digest, Md5}, sha1::Sha1, sha2::Sha256, std::{ collections::HashMap, fs, io::{BufReader, BufWriter, Read, Write}, os::unix::fs::{symlink, DirBuilderExt, 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 & 0o60000 != 0 { Self::Block } else if value & 0o20000 != 0 { Self::Char } else if value & 0o10000 != 0 { Self::Pipe } else if value & 0o40000 != 0 { Self::Dir } else if value & 0o100000 != 0 { Self::Normal } else { panic!(); } } } /// 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 Unix permissions bits of this file pub mode: u32, /// 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 type of file this node represents pub filetype: FileType, } impl Node { /// 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. pub fn read(reader: &mut T) -> Result { let mut len = [0; 8]; reader.read_exact(&mut len)?; let len = u64::from_le_bytes(len); if len == 0 { return Ok(Self { name: "".to_string(), mode: 0, uid: 0, gid: 0, mtime: 0, filetype: FileType::Eof, }); } let mut name = Vec::with_capacity(len.try_into()?); let mut handle = reader.take(len); handle.read_exact(&mut name)?; let mut buf = [0; 20]; reader.read_exact(&mut buf)?; let mode: [u8; 4] = buf[..4].try_into()?; let uid: [u8; 4] = buf[4..8].try_into()?; let gid: [u8; 4] = buf[8..12].try_into()?; let mtime: [u8; 8] = buf[12..].try_into()?; let filetype = FileType::read(reader)?; Ok(Self { name: String::from_utf8(name)?, mode: u32::from_le_bytes(mode), uid: u32::from_le_bytes(uid), gid: u32::from_le_bytes(gid), mtime: u64::from_le_bytes(mtime), 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. pub fn write(&self, writer: &mut T) -> Result<(), Error> { let len = self.name.len() as u64; writer.write_all(&len.to_le_bytes())?; writer.write_all(self.name.as_bytes())?; [self.mode, self.uid, self.gid] .iter() .try_for_each(|f| writer.write_all(&f.to_le_bytes()))?; writer.write_all(&self.mtime.to_le_bytes())?; self.filetype.write(writer)?; Ok(()) } /// 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. pub fn from_path( path: &str, checksum: Checksum, links: &Mutex>, ) -> Result { let name = String::from(path); let fd = fs::File::open(path)?; let meta = fd.metadata()?; let mode = meta.mode(); let uid = meta.uid(); let gid = meta.gid(); let mtime = meta.mtime().try_into()?; let mut reader = BufReader::new(fd); 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); } else { 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 len = meta.len(); let mut data = Vec::with_capacity(len.try_into()?); len = reader.read_to_end(&mut data)?.try_into()?; let checksum = match checksum { Checksum::Md5(mut cs) => { let mut hasher = Md5::new(); hasher.update(&data); cs = hasher.finalize().into(); Checksum::Md5(cs) } Checksum::Sha1(mut cs) => { let mut hasher = Sha1::new(); hasher.update(&data); cs = hasher.finalize().into(); Checksum::Sha1(cs) } Checksum::Sha256(mut cs) => { let mut hasher = Sha256::new(); hasher.update(&data); cs = hasher.finalize().into(); Checksum::Sha256(cs) } Checksum::Skip => checksum, }; break 'blk FileType::Normal(File { len, checksum, data, }); } else { return Err(Error::UnknownFileType); } } }; Ok(Self { name, mode, uid, gid, mtime, filetype, }) } 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); } } self.name.clone() }; match self.filetype { FileType::Eof => {} FileType::Fifo => { nix::mkfifo(&path, self.mode)?; if euid == 0 { nix::chown(&path, self.uid, self.gid)?; } } FileType::Block(ref b) => { if euid == 0 { nix::mknod(&path, self.mode, b.major, b.minor)?; } } FileType::Character(ref c) => { if euid == 0 { nix::mknod(&path, self.mode, c.major, c.minor)?; } } FileType::Normal(ref n) => { let n_path = PathBuf::from(&path); if let Some(p) = n_path.parent() { if !p.exists() { self.mkdir(&p)?; } } { 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)?; } 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))?; } } todo!() } fn mkdir(&self, dir: &Path) -> Result<(), Error> { if let Some(p) = dir.parent() { if !p.exists() { self.mkdir(&p)?; } } if !dir.exists() { fs::DirBuilder::new().mode(self.mode).create(dir)?; } if nix::geteuid() == 0 { nix::chown(dir.to_str().ok_or(Error::NulError)?, self.uid, self.gid)?; } Ok(()) } }