435 lines
15 KiB
Rust
435 lines
15 KiB
Rust
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 & 0o20000 != 0 {
|
|
Self::Char
|
|
} else if value & 0o60000 != 0 {
|
|
Self::Block
|
|
} else if value & 0o10000 != 0 {
|
|
Self::Pipe
|
|
} else if value & 0o100000 != 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 {
|
|
/// 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<T: Read>(reader: &mut T) -> Result<Self, Error> {
|
|
let mut len = [0; 2];
|
|
reader.read_exact(&mut len)?;
|
|
let len = u16::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.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 mask: u16 = 0b111 << 13;
|
|
let mode = raw_mode & mask;
|
|
let flag: u8 = ((raw_mode & !mask) >> 13).try_into()?;
|
|
let flag: Flag = flag.try_into()?;
|
|
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.
|
|
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(())
|
|
}
|
|
|
|
/// 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,
|
|
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);
|
|
} 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 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,
|
|
});
|
|
} else {
|
|
return Err(Error::UnknownFileType);
|
|
}
|
|
}
|
|
};
|
|
Ok(Self {
|
|
name,
|
|
uid,
|
|
gid,
|
|
mtime,
|
|
mode: (mode & !(0o111 << 13)).try_into()?,
|
|
filetype,
|
|
})
|
|
}
|
|
|
|
/// Recreates the original file on disk which this node represents
|
|
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);
|
|
} else {
|
|
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 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 = std::fs::File::create("test/li.node").unwrap();
|
|
let mut writer = std::io::BufWriter::new(fd);
|
|
let links = Mutex::new(HashMap::new());
|
|
let node = Node::from_path("test/li.txt", Algorithm::Sha1, &links).unwrap();
|
|
node.write(&mut writer).unwrap();
|
|
}
|
|
let fd = std::fs::File::open("test/li.node").unwrap();
|
|
let mut reader = BufReader::new(fd);
|
|
let node = Node::read(&mut reader).unwrap();
|
|
let FileType::Normal(f) = node.filetype else {
|
|
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 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);
|
|
}
|
|
}
|