haggis-rs/src/node.rs

522 lines
19 KiB
Rust

#![allow(clippy::similar_names)]
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<u32> 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 the archive is incorrectly formatted
pub fn read<T: Read>(reader: &mut T) -> Result<Self, Error> {
let name = crate::load_string(reader)?;
if name.is_empty() {
return Ok(Self {
name: String::new(),
mode: 0,
uid: 0,
gid: 0,
mtime: 0,
filetype: FileType::Eof,
});
}
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,
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
pub fn write<T: 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<HashMap<u64, String>>,
) -> Result<Self, Error> {
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>,
uid: Option<u32>,
gid: Option<u32>,
) -> 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}"))
};
let uid = match uid {
Some(u) => u,
None => self.uid,
};
let gid = match gid {
Some(g) => g,
None => self.gid,
};
if let Some(p) = n_path.parent() {
if !p.exists() {
self.mkdir(p, uid, gid)?;
}
}
match self.filetype {
FileType::Eof => {}
FileType::Fifo => {
if n_path.exists() {
fs::remove_file(&n_path)?;
}
nix::mkfifo(n_path.to_str().ok_or(Error::BadPath)?, self.mode.into())?;
if euid == 0 {
nix::chown(n_path.to_str().ok_or(Error::BadPath)?, uid, gid)?;
}
}
FileType::Block(ref sp) | FileType::Character(ref sp) => {
if euid == 0 {
if n_path.exists() {
fs::remove_file(&n_path)?;
}
nix::mknod(
n_path.to_str().ok_or(Error::BadPath)?,
self.mode.into(),
sp.major,
sp.minor,
)?;
nix::chown(n_path.to_str().ok_or(Error::BadPath)?, uid, gid)?;
}
}
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(n_path.to_str().ok_or(Error::BadPath)?, uid, gid)?;
}
nix::chmod(n_path.to_str().ok_or(Error::BadPath)?, self.mode.into())?;
}
FileType::HardLink(ref t) => {
let target = if let Some(prefix) = prefix {
format!("{prefix}/{t}")
} else {
t.to_string()
};
if !PathBuf::from(&target).exists() {
let _f = fs::File::create(&target)?;
}
if PathBuf::from(&n_path).exists() {
fs::remove_file(&n_path)?;
}
fs::hard_link(target, &n_path)?;
}
FileType::SoftLink(ref t) => {
if n_path.exists() {
fs::remove_file(&n_path)?;
}
symlink(t, &n_path)?;
}
FileType::Directory => {
self.mkdir(&n_path, uid, gid)?;
}
}
Ok(())
}
fn mkdir(&self, dir: &Path, uid: u32, gid: u32) -> Result<(), Error> {
if !dir.exists() {
fs::create_dir_all(dir)?;
}
if nix::geteuid() == 0 {
nix::chown(dir.to_str().ok_or(Error::NulError)?, uid, 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]
#[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 file_node() {
{
let fd = fs::File::create("test/file.node").unwrap();
let mut writer = BufWriter::new(fd);
let links = Mutex::new(HashMap::new());
let node = Node::from_path("test/li.txt", Algorithm::Sha256, &links).unwrap();
let FileType::Normal(ref f) = node.filetype else {
eprintln!("Created wrong filetype: {:?}", node.filetype);
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"
);
node.write(&mut writer).unwrap();
}
let meta = fs::metadata("test/li.txt").unwrap();
let fd = fs::File::open("test/file.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(ref f) = node.filetype else {
eprintln!("Read incorrect filetype: {:?}", node.filetype);
panic!()
};
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"
);
assert_eq!(LI, f.data);
node.extract(Some("test/output"), None, None).unwrap();
let f = fs::read_to_string("test/output/test/li.txt").unwrap();
assert_eq!(f.as_bytes(), LI);
}
#[test]
fn symlink_node() {
{
let _res = remove_file("test/lilnk.txt");
symlink("li.txt", "test/lilnk.txt").unwrap();
if PathBuf::from("test/lilnk.node").exists() {
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");
node.extract(Some("test/output"), None, None).unwrap();
let tgt = fs::read_link("test/output/test/lilnk.txt").unwrap();
assert_eq!(tgt, PathBuf::from("li.txt"));
}
#[test]
fn hardlink_node() {
{
if PathBuf::from("test/lorem.txt").exists() {
let _res = remove_file("test/lorem.txt");
}
fs::hard_link("test/li.txt", "test/lorem.txt").unwrap();
if PathBuf::from("test/lorem.node").exists() {
let _res = remove_file("test/lorem.node");
}
if PathBuf::from("test/li.node").exists() {
let _res = remove_file("test/li.node");
}
let links = Mutex::new(HashMap::new());
let node0 = Node::from_path("test/li.txt", Algorithm::Sha1, &links).unwrap();
let node1 = Node::from_path("test/lorem.txt", Algorithm::Sha1, &links).unwrap();
let FileType::HardLink(ref tgt) = node1.filetype else {
eprintln!("Created wrong filetype: {:?}", node1.filetype);
panic!();
};
assert_eq!(tgt, "test/li.txt");
let fd = fs::File::create("test/li.node").unwrap();
let mut writer = BufWriter::new(fd);
node0.write(&mut writer).unwrap();
let fd = fs::File::create("test/lorem.node").unwrap();
let mut writer = BufWriter::new(fd);
node1.write(&mut writer).unwrap();
}
let fd = fs::File::open("test/lorem.node").unwrap();
let mut reader = BufReader::new(fd);
let node1 = Node::read(&mut reader).unwrap();
let FileType::HardLink(ref tgt) = node1.filetype else {
eprintln!("Read incorrect filetype: {:?}", node1.filetype);
panic!();
};
assert_eq!(tgt, "test/li.txt");
let fd = fs::File::open("test/li.node").unwrap();
let mut reader = BufReader::new(fd);
let node0 = Node::read(&mut reader).unwrap();
node1.extract(Some("test/output"), None, None).unwrap();
node0.extract(Some("test/output"), None, None).unwrap();
}
#[test]
fn fifo_node() {
{
if PathBuf::from("test/fifo").exists() {
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!();
};
if PathBuf::from("test/fifo.node").exists() {
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!();
};
node.extract(Some("test/output"), None, None).unwrap();
}
}