use std::io::{Read, Seek, SeekFrom};
use crate::date_time::DosDateTime;
use crate::error::{ArchiveError, Result};
#[derive(Debug, Clone)]
pub struct TarFileHeader {
pub name: String,
pub size: u64,
pub mtime: u64,
pub mode: u32,
pub entry_type: TarEntryType,
pub link_name: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TarEntryType {
Regular,
HardLink,
Symlink,
Char,
Block,
Directory,
Fifo,
Continuous,
Other(u8),
}
impl From<tar::EntryType> for TarEntryType {
fn from(t: tar::EntryType) -> Self {
match t {
tar::EntryType::Regular => TarEntryType::Regular,
tar::EntryType::Link => TarEntryType::HardLink,
tar::EntryType::Symlink => TarEntryType::Symlink,
tar::EntryType::Char => TarEntryType::Char,
tar::EntryType::Block => TarEntryType::Block,
tar::EntryType::Directory => TarEntryType::Directory,
tar::EntryType::Fifo => TarEntryType::Fifo,
tar::EntryType::Continuous => TarEntryType::Continuous,
_ => TarEntryType::Other(t.as_byte()),
}
}
}
impl TarFileHeader {
pub fn modified_time(&self) -> Option<DosDateTime> {
if self.mtime == 0 {
return None;
}
use chrono::{Datelike, TimeZone, Timelike, Utc};
if let Some(dt) = Utc.timestamp_opt(self.mtime as i64, 0).single() {
let year = dt.year() as u16;
let month = dt.month() as u16;
let day = dt.day() as u16;
let hour = dt.hour() as u16;
let minute = dt.minute() as u16;
let second = dt.second() as u16;
if year >= 1980 {
let dos_date = ((year - 1980) << 9) | (month << 5) | day;
let dos_time = (hour << 11) | (minute << 5) | (second / 2);
let combined = ((dos_date as u32) << 16) | (dos_time as u32);
return Some(DosDateTime::new(combined));
}
}
None
}
}
struct TarEntry {
header: TarFileHeader,
data_offset: u64,
}
pub struct TarArchive<T: Read + Seek> {
reader: T,
entries: Vec<TarEntry>,
current_index: usize,
}
impl<T: Read + Seek> TarArchive<T> {
pub fn new(mut reader: T) -> Result<Self> {
let mut entries = Vec::new();
reader.seek(SeekFrom::Start(0))?;
{
let mut archive = tar::Archive::new(&mut reader);
for entry_result in archive
.entries()
.map_err(|e| ArchiveError::io_error(format!("Failed to read TAR entries: {}", e)))?
{
let entry = entry_result.map_err(|e| ArchiveError::io_error(format!("Failed to read TAR entry: {}", e)))?;
let header = entry.header();
let name = entry
.path()
.map_err(|e| ArchiveError::io_error(format!("Failed to read entry path: {}", e)))?
.to_string_lossy()
.to_string();
let size = header.size().unwrap_or(0);
let mtime = header.mtime().unwrap_or(0);
let mode = header.mode().unwrap_or(0);
let entry_type = header.entry_type().into();
let link_name = header.link_name().ok().flatten().map(|p| p.to_string_lossy().to_string());
let raw_header_position = entry.raw_header_position();
let data_offset = raw_header_position + 512;
entries.push(TarEntry {
header: TarFileHeader {
name,
size,
mtime,
mode,
entry_type,
link_name,
},
data_offset,
});
}
}
reader.seek(SeekFrom::Start(0))?;
Ok(Self {
reader,
entries,
current_index: 0,
})
}
pub fn get_next_entry(&mut self) -> Result<Option<TarFileHeader>> {
if self.current_index >= self.entries.len() {
return Ok(None);
}
let entry = &self.entries[self.current_index];
Ok(Some(entry.header.clone()))
}
pub fn skip(&mut self, _header: &TarFileHeader) -> Result<()> {
if self.current_index < self.entries.len() {
self.current_index += 1;
}
Ok(())
}
pub fn read(&mut self, header: &TarFileHeader) -> Result<Vec<u8>> {
let entry = self
.entries
.iter()
.find(|e| e.header.name == header.name)
.ok_or_else(|| ArchiveError::io_error(format!("Entry not found: {}", header.name)))?;
self.reader.seek(SeekFrom::Start(entry.data_offset))?;
let mut data = vec![0u8; entry.header.size as usize];
self.reader.read_exact(&mut data)?;
self.current_index += 1;
Ok(data)
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
}