use crate::header::PosixHeader;
use crate::{TypeFlag, BLOCKSIZE};
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
use arrayvec::ArrayString;
use core::fmt::{Debug, Formatter};
use core::str::{FromStr, Utf8Error};
pub struct ArchiveEntry<'a> {
filename: ArrayString<100>,
data: &'a [u8],
size: usize,
}
#[allow(unused)]
impl<'a> ArchiveEntry<'a> {
pub const fn new(filename: ArrayString<100>, data: &'a [u8]) -> Self {
ArchiveEntry {
filename,
data,
size: data.len(),
}
}
pub const fn filename(&self) -> ArrayString<100> {
self.filename
}
pub const fn data(&self) -> &'a [u8] {
self.data
}
pub fn data_as_str(&self) -> Result<&'a str, Utf8Error> {
core::str::from_utf8(self.data)
}
pub const fn size(&self) -> usize {
self.size
}
}
impl<'a> Debug for ArchiveEntry<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ArchiveEntry")
.field("filename", &self.filename().as_str())
.field("size", &self.size())
.field("data", &"<bytes>")
.finish()
}
}
#[cfg(feature = "alloc")]
#[derive(Debug)]
pub struct TarArchive {
data: Box<[u8]>,
}
#[cfg(feature = "alloc")]
impl TarArchive {
pub fn new(data: Box<[u8]>) -> Self {
assert_eq!(
data.len() % BLOCKSIZE,
0,
"data must be a multiple of BLOCKSIZE={}, len is {}",
BLOCKSIZE,
data.len(),
);
Self { data }
}
pub fn entries(&self) -> ArchiveIterator {
ArchiveIterator::new(self.data.as_ref())
}
}
#[cfg(feature = "alloc")]
impl From<Box<[u8]>> for TarArchive {
fn from(data: Box<[u8]>) -> Self {
Self::new(data)
}
}
#[cfg(feature = "alloc")]
impl From<TarArchive> for Box<[u8]> {
fn from(ar: TarArchive) -> Self {
ar.data
}
}
#[derive(Debug)]
pub struct TarArchiveRef<'a> {
data: &'a [u8],
}
#[allow(unused)]
impl<'a> TarArchiveRef<'a> {
pub fn new(data: &'a [u8]) -> Self {
assert_eq!(
data.len() % BLOCKSIZE,
0,
"data must be a multiple of BLOCKSIZE={}",
BLOCKSIZE
);
Self { data }
}
pub const fn entries(&self) -> ArchiveIterator {
ArchiveIterator::new(self.data)
}
}
#[derive(Debug)]
pub struct ArchiveIterator<'a> {
archive_data: &'a [u8],
block_index: usize,
}
impl<'a> ArchiveIterator<'a> {
pub const fn new(archive: &'a [u8]) -> Self {
Self {
archive_data: archive,
block_index: 0,
}
}
fn next_hdr(&self, block_index: usize) -> &'a PosixHeader {
let hdr_ptr = &self.archive_data[block_index * BLOCKSIZE];
unsafe { (hdr_ptr as *const u8).cast::<PosixHeader>().as_ref() }.unwrap()
}
}
impl<'a> Iterator for ArchiveIterator<'a> {
type Item = ArchiveEntry<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.block_index * BLOCKSIZE >= self.archive_data.len() {
log::warn!("Reached end of Tar archive data without finding zero/end blocks!");
return None;
}
let hdr = self.next_hdr(self.block_index);
if hdr.is_zero_block() {
let next_hdr = self.next_hdr(self.block_index + 1);
if next_hdr.is_zero_block() {
log::debug!("End of Tar archive with two zero blocks!");
} else {
log::warn!("Zero block found at end of Tar archive, but only one instead of two!");
}
return None;
}
if hdr.typeflag != TypeFlag::AREGTYPE && hdr.typeflag != TypeFlag::REGTYPE {
log::warn!(
"Found entry of type={:?}, but only files are supported",
hdr.typeflag
);
return None;
}
if hdr.name.is_empty() {
log::warn!("Found empty file name",);
}
let data_block_count = hdr.payload_block_count();
let i_begin = (self.block_index + 1) * BLOCKSIZE;
let i_end = i_begin + data_block_count * BLOCKSIZE;
let file_block_bytes = &self.archive_data[i_begin..i_end];
let file_bytes = &file_block_bytes[0..hdr.size.val()];
self.block_index += data_block_count + 1;
Some(ArchiveEntry::new(
ArrayString::from_str(hdr.name.as_string().as_str()).unwrap(),
file_bytes,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::vec::Vec;
#[test]
fn test_archive_list() {
let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default.tar"));
let entries = archive.entries().collect::<Vec<_>>();
println!("{:#?}", entries);
}
#[test]
fn test_archive_entries() {
#[cfg(feature = "alloc")]
{
let data = include_bytes!("../tests/gnu_tar_default.tar")
.to_vec()
.into_boxed_slice();
let archive = TarArchive::new(data.clone());
let entries = archive.entries().collect::<Vec<_>>();
assert_archive_content(&entries);
let archive = TarArchive::from(data.clone());
let entries = archive.entries().collect::<Vec<_>>();
assert_archive_content(&entries);
assert_eq!(data, archive.into());
}
let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default.tar"));
let entries = archive.entries().collect::<Vec<_>>();
assert_archive_content(&entries);
let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_gnu.tar"));
let entries = archive.entries().collect::<Vec<_>>();
assert_archive_content(&entries);
let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_oldgnu.tar"));
let entries = archive.entries().collect::<Vec<_>>();
assert_archive_content(&entries);
let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar.tar"));
let entries = archive.entries().collect::<Vec<_>>();
assert_archive_content(&entries);
let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_v7.tar"));
let entries = archive.entries().collect::<Vec<_>>();
assert_archive_content(&entries);
}
fn assert_archive_content(entries: &[ArchiveEntry]) {
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].filename().as_str(), "bye_world_513b.txt");
assert_eq!(entries[0].size(), 513);
assert_eq!(entries[0].data().len(), 513);
assert_eq!(
entries[0].data_as_str().expect("Invalid UTF-8"),
include_str!("../tests/bye_world_513b.txt")
);
assert_eq!(entries[1].filename().as_str(), "hello_world_513b.txt");
assert_eq!(entries[1].size(), 513);
assert_eq!(entries[1].data().len(), 513);
assert_eq!(
entries[1].data_as_str().expect("Invalid UTF-8"),
include_str!("../tests/hello_world_513b.txt")
);
assert_eq!(entries[2].filename().as_str(), "hello_world.txt");
assert_eq!(entries[2].size(), 12);
assert_eq!(entries[2].data().len(), 12);
assert_eq!(
entries[2].data_as_str().expect("Invalid UTF-8"),
"Hello World\n",
"file content must match"
);
}
}