use std::io::Read;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct ArchiveEntryMeta {
pub path: String,
pub size: Option<u64>,
pub entry_type: EntryType,
pub mtime: Option<SystemTime>,
pub mode: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryType {
File,
Directory,
Symlink,
Hardlink,
Other,
}
impl ArchiveEntryMeta {
pub fn file(path: impl Into<String>, size: Option<u64>) -> Self {
Self {
path: path.into(),
size,
entry_type: EntryType::File,
mtime: None,
mode: None,
}
}
pub fn directory(path: impl Into<String>) -> Self {
Self {
path: path.into(),
size: None,
entry_type: EntryType::Directory,
mtime: None,
mode: None,
}
}
pub fn is_file(&self) -> bool {
matches!(self.entry_type, EntryType::File)
}
pub fn is_dir(&self) -> bool {
matches!(self.entry_type, EntryType::Directory)
}
pub fn is_symlink(&self) -> bool {
matches!(self.entry_type, EntryType::Symlink)
}
pub fn file_name(&self) -> &str {
self.path.rsplit('/').next().unwrap_or(&self.path)
}
pub fn parent(&self) -> Option<&str> {
let trimmed = self.path.trim_end_matches('/');
trimmed.rfind('/').map(|pos| &trimmed[..pos])
}
pub fn extension(&self) -> Option<&str> {
let name = self.file_name();
let dot_pos = name.rfind('.')?;
if dot_pos == 0 || dot_pos == name.len() - 1 {
None
} else {
Some(&name[dot_pos + 1..])
}
}
pub fn normalize_path(&mut self) {
self.path = self.path.replace('\\', "/");
while self.path.starts_with('/') {
self.path = self.path[1..].to_string();
}
while self.path.starts_with("./") {
self.path = self.path[2..].to_string();
}
}
}
impl std::fmt::Display for ArchiveEntryMeta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path)?;
if let Some(size) = self.size {
write!(f, " ({} bytes)", size)?;
}
Ok(())
}
}
impl EntryType {
pub fn as_char(&self) -> char {
match self {
EntryType::File => '-',
EntryType::Directory => 'd',
EntryType::Symlink => 'l',
EntryType::Hardlink => 'h',
EntryType::Other => '?',
}
}
}
pub struct ArchiveEntry<'a> {
pub meta: ArchiveEntryMeta,
reader: Box<dyn Read + 'a>,
}
impl<'a> ArchiveEntry<'a> {
pub fn new(meta: ArchiveEntryMeta, reader: impl Read + 'a) -> Self {
Self {
meta,
reader: Box::new(reader),
}
}
pub fn path(&self) -> &str {
&self.meta.path
}
pub fn size(&self) -> Option<u64> {
self.meta.size
}
pub fn is_file(&self) -> bool {
self.meta.is_file()
}
pub fn into_reader(self) -> Box<dyn Read + 'a> {
self.reader
}
pub fn read_to_vec(&mut self) -> std::io::Result<Vec<u8>> {
let capacity = self.meta.size.unwrap_or(1024) as usize;
let mut buf = Vec::with_capacity(capacity);
self.reader.read_to_end(&mut buf)?;
Ok(buf)
}
pub fn read_to_string(&mut self) -> std::io::Result<String> {
let bytes = self.read_to_vec()?;
String::from_utf8(bytes).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid UTF-8: {}", e),
)
})
}
}
impl Read for ArchiveEntry<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.reader.read(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entry_meta_file() {
let meta = ArchiveEntryMeta::file("path/to/file.txt", Some(1024));
assert!(meta.is_file());
assert!(!meta.is_dir());
assert_eq!(meta.file_name(), "file.txt");
assert_eq!(meta.parent(), Some("path/to"));
assert_eq!(meta.extension(), Some("txt"));
}
#[test]
fn test_entry_meta_directory() {
let meta = ArchiveEntryMeta::directory("path/to/dir/");
assert!(meta.is_dir());
assert!(!meta.is_file());
}
#[test]
fn test_entry_meta_no_extension() {
let meta = ArchiveEntryMeta::file("Makefile", None);
assert_eq!(meta.extension(), None);
let meta = ArchiveEntryMeta::file(".gitignore", None);
assert_eq!(meta.extension(), None);
}
#[test]
fn test_normalize_path() {
let mut meta = ArchiveEntryMeta::file("/path\\to\\file.txt", None);
meta.normalize_path();
assert_eq!(meta.path, "path/to/file.txt");
let mut meta = ArchiveEntryMeta::file("./relative/path", None);
meta.normalize_path();
assert_eq!(meta.path, "relative/path");
}
#[test]
fn test_entry_type_char() {
assert_eq!(EntryType::File.as_char(), '-');
assert_eq!(EntryType::Directory.as_char(), 'd');
assert_eq!(EntryType::Symlink.as_char(), 'l');
}
#[test]
fn test_archive_entry_read() {
let content = b"Hello, World!";
let meta = ArchiveEntryMeta::file("test.txt", Some(content.len() as u64));
let mut entry = ArchiveEntry::new(meta, &content[..]);
let result = entry.read_to_string().expect("should read");
assert_eq!(result, "Hello, World!");
}
}