use std::collections::HashMap;
use std::fs::File;
use std::ops::Range;
use std::path::{Path, PathBuf};
use crate::checksum::crc32;
use crate::error::{Error, Result};
use crate::segment::identifier::SegmentIdentifier;
use crate::tar_archive::binary_references::{BinaryReferences, parse_binary_references};
use crate::tar_archive::entry_header::TarEntryHeader;
use crate::tar_archive::graph::{SegmentGraph, parse_segment_graph};
use crate::tar_archive::index::{
SegmentIndex, SegmentIndexEntry, index_entry_disk_size, parse_segment_index,
};
enum ArchiveContent {
Indexed(SegmentIndex),
Recovered {
entries: Vec<(SegmentIdentifier, Range<usize>)>,
lookup: HashMap<SegmentIdentifier, usize>,
},
}
pub struct TarArchiveReader {
path: PathBuf,
file_name: String,
bytes: memmap2::Mmap,
content: ArchiveContent,
}
impl TarArchiveReader {
pub fn open(path: &Path) -> Result<Self> {
let file_name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
let file = File::open(path)?;
let length = file.metadata()?.len();
if length == 0 {
return Err(Error::InvalidFormat {
details: format!("archive {file_name} is empty"),
});
}
let bytes = unsafe { memmap2::Mmap::map(&file)? };
let content = if let Ok(index) = parse_segment_index(&bytes) {
ArchiveContent::Indexed(index)
} else {
let (entries, lookup) = recover_segment_entries(&bytes, &file_name);
ArchiveContent::Recovered { entries, lookup }
};
Ok(Self {
path: path.to_owned(),
file_name,
bytes,
content,
})
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn file_name(&self) -> &str {
&self.file_name
}
#[must_use]
pub fn file_size(&self) -> u64 {
self.bytes.len() as u64
}
#[must_use]
pub fn index(&self) -> Option<&SegmentIndex> {
match &self.content {
ArchiveContent::Indexed(index) => Some(index),
ArchiveContent::Recovered { .. } => None,
}
}
#[must_use]
pub fn is_recovered(&self) -> bool {
matches!(self.content, ArchiveContent::Recovered { .. })
}
#[must_use]
pub fn segment_count(&self) -> usize {
match &self.content {
ArchiveContent::Indexed(index) => index.entries().len(),
ArchiveContent::Recovered { entries, .. } => entries.len(),
}
}
#[must_use]
pub fn contains_segment(&self, segment_identifier: SegmentIdentifier) -> bool {
match &self.content {
ArchiveContent::Indexed(index) => index.find_entry(segment_identifier).is_some(),
ArchiveContent::Recovered { lookup, .. } => lookup.contains_key(&segment_identifier),
}
}
#[must_use]
pub fn segment_data(&self, segment_identifier: SegmentIdentifier) -> Option<&[u8]> {
match &self.content {
ArchiveContent::Indexed(index) => {
let entry = index.find_entry(segment_identifier)?;
let start = entry.position as usize;
let end = start.checked_add(entry.size as usize)?;
self.bytes.get(start..end)
}
ArchiveContent::Recovered { entries, lookup } => {
let position = *lookup.get(&segment_identifier)?;
self.bytes.get(entries[position].1.clone())
}
}
}
pub fn segment_identifiers(&self) -> impl Iterator<Item = SegmentIdentifier> + '_ {
let indexed: Option<&SegmentIndex> = self.index();
let recovered = match &self.content {
ArchiveContent::Indexed(_) => &[][..],
ArchiveContent::Recovered { entries, .. } => entries.as_slice(),
};
indexed
.into_iter()
.flat_map(|index| index.entries().iter().map(|entry| entry.segment_identifier))
.chain(recovered.iter().map(|(identifier, _)| *identifier))
}
#[must_use]
pub fn index_entry(&self, segment_identifier: SegmentIdentifier) -> Option<&SegmentIndexEntry> {
self.index()?.find_entry(segment_identifier)
}
#[must_use]
pub fn segment_graph(&self) -> Option<SegmentGraph> {
parse_segment_graph(&self.bytes, self.index()?)
}
#[must_use]
pub fn binary_references(&self) -> Option<BinaryReferences> {
let index = self.index()?;
let graph = self.segment_graph()?;
let graph_disk_size = 512 + graph.disk_structure_size().div_ceil(512) * 512;
let anchor = self
.bytes
.len()
.checked_sub(1024 + index_entry_disk_size(index) + graph_disk_size)?;
parse_binary_references(&self.bytes, anchor)
}
}
fn parse_segment_entry_name(name: &str) -> Option<(SegmentIdentifier, Option<u32>)> {
if name.len() < 36 || !name.is_char_boundary(36) {
return None;
}
let (uuid_text, rest) = name.split_at(36);
let identifier: SegmentIdentifier = uuid_text.parse().ok()?;
if rest.is_empty() {
return Some((identifier, None));
}
let suffix = rest.strip_prefix('.')?;
if suffix.len() >= 8 && suffix.is_char_boundary(8) {
let (checksum_text, remainder) = suffix.split_at(8);
let is_checksum = checksum_text
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
&& (remainder.is_empty() || remainder.starts_with('.'));
if is_checksum {
let checksum = u32::from_str_radix(checksum_text, 16).ok()?;
return Some((identifier, Some(checksum)));
}
}
Some((identifier, None))
}
type RecoveredSegments = (
Vec<(SegmentIdentifier, Range<usize>)>,
HashMap<SegmentIdentifier, usize>,
);
fn recover_segment_entries(bytes: &[u8], archive_file_name: &str) -> RecoveredSegments {
let mut entries: Vec<(SegmentIdentifier, Range<usize>)> = Vec::new();
let mut lookup: HashMap<SegmentIdentifier, usize> = HashMap::new();
let index_entry_name = format!("{archive_file_name}.idx");
let length = bytes.len();
let mut position = 0usize;
while position + 512 <= length {
let header_bytes = &bytes[position..position + 512];
let position_after_header = position + 512;
let Some(header) = TarEntryHeader::parse(header_bytes) else {
if position_after_header + 1024 == length {
break;
}
position = position_after_header;
continue;
};
let size = header.size;
if position_after_header as i64 + size > length as i64 {
break;
}
let padded_size = ((size + 511) / 512) * 512;
let skip_past_data = |from: usize| -> usize {
let target = from as i64 + padded_size;
if target > from as i64 {
target as usize
} else {
from
}
};
if let Some((identifier, checksum)) = parse_segment_entry_name(&header.name) {
if size >= 0 && (checksum.is_some() || !lookup.contains_key(&identifier)) {
let size = size as usize;
let data = position_after_header..position_after_header + size;
let next_position = skip_past_data(position_after_header);
if let Some(expected) = checksum
&& crc32(&bytes[data.clone()]) != expected
{
position = next_position;
continue;
}
if let Some(&existing) = lookup.get(&identifier) {
entries[existing].1 = data;
} else {
lookup.insert(identifier, entries.len());
entries.push((identifier, data));
}
position = next_position;
} else {
position = position_after_header;
}
} else if header.name != index_entry_name {
position = skip_past_data(position_after_header);
} else {
position = position_after_header;
}
}
(entries, lookup)
}
#[cfg(test)]
mod tests {
use super::{TarArchiveReader, parse_segment_entry_name, recover_segment_entries};
use crate::checksum::crc32;
use crate::segment::identifier::SegmentIdentifier;
fn tar_entry(name: &str, data: &[u8]) -> Vec<u8> {
let mut block = vec![0u8; 512];
block[..name.len()].copy_from_slice(name.as_bytes());
let size_field = format!("{:011o}\0", data.len());
block[124..136].copy_from_slice(size_field.as_bytes());
block.extend_from_slice(data);
block.extend(std::iter::repeat_n(
0u8,
data.len().div_ceil(512) * 512 - data.len(),
));
block
}
fn segment_entry_name(identifier: SegmentIdentifier, data: &[u8]) -> String {
format!("{identifier}.{:08x}", crc32(data))
}
#[test]
fn parses_segment_entry_names() {
let identifier = SegmentIdentifier::new(0xF813_78FB_92B1_4B52, 0xA5C8_E0A6_7152_ED2C);
let bare = identifier.to_string();
assert_eq!(parse_segment_entry_name(&bare), Some((identifier, None)));
let with_checksum = format!("{bare}.0012abcd");
assert_eq!(
parse_segment_entry_name(&with_checksum),
Some((identifier, Some(0x0012_ABCD)))
);
let with_suffix = format!("{bare}.0012abcd.future");
assert_eq!(
parse_segment_entry_name(&with_suffix),
Some((identifier, Some(0x0012_ABCD)))
);
let nine_hexadecimal_digits = format!("{bare}.0012abcde");
assert_eq!(
parse_segment_entry_name(&nine_hexadecimal_digits),
Some((identifier, None))
);
let uppercase_checksum = format!("{bare}.0012ABCD");
assert_eq!(
parse_segment_entry_name(&uppercase_checksum),
Some((identifier, None))
);
assert_eq!(parse_segment_entry_name("data00000a.tar.idx"), None);
assert_eq!(parse_segment_entry_name(""), None);
}
#[test]
fn recovery_scan_collects_valid_segments() {
let first = SegmentIdentifier::new(1, 0xA000_0000_0000_0001);
let second = SegmentIdentifier::new(2, 0xA000_0000_0000_0002);
let first_data = vec![0x11u8; 100];
let second_data = vec![0x22u8; 700];
let mut archive = Vec::new();
archive.extend(tar_entry(
&segment_entry_name(first, &first_data),
&first_data,
));
archive.extend(tar_entry(
&segment_entry_name(second, &second_data),
&second_data,
));
archive.extend_from_slice(&[0u8; 1024]);
let (entries, lookup) = recover_segment_entries(&archive, "data00000a.tar");
assert_eq!(entries.len(), 2);
assert!(lookup.contains_key(&first));
assert_eq!(archive[entries[lookup[&second]].1.clone()], second_data[..]);
}
#[test]
fn recovery_scan_drops_segments_with_wrong_checksum() {
let identifier = SegmentIdentifier::new(1, 0xA000_0000_0000_0001);
let data = vec![0x33u8; 64];
let wrong_name = format!("{identifier}.deadbeef");
let mut archive = tar_entry(&wrong_name, &data);
archive.extend_from_slice(&[0u8; 1024]);
let (entries, _) = recover_segment_entries(&archive, "data00000a.tar");
assert!(entries.is_empty());
}
#[test]
fn recovery_scan_replaces_duplicates_with_checksum() {
let identifier = SegmentIdentifier::new(1, 0xA000_0000_0000_0001);
let old_data = vec![0x44u8; 32];
let new_data = vec![0x55u8; 32];
let mut archive = Vec::new();
archive.extend(tar_entry(
&segment_entry_name(identifier, &old_data),
&old_data,
));
archive.extend(tar_entry(
&segment_entry_name(identifier, &new_data),
&new_data,
));
archive.extend_from_slice(&[0u8; 1024]);
let (entries, lookup) = recover_segment_entries(&archive, "data00000a.tar");
assert_eq!(entries.len(), 1);
assert_eq!(
archive[entries[lookup[&identifier]].1.clone()],
new_data[..]
);
}
#[test]
fn recovery_scan_skips_metadata_and_stops_at_truncation() {
let identifier = SegmentIdentifier::new(1, 0xA000_0000_0000_0001);
let data = vec![0x66u8; 16];
let mut archive = Vec::new();
archive.extend(tar_entry("data00000a.tar.brf", &[0u8; 32]));
archive.extend(tar_entry(&segment_entry_name(identifier, &data), &data));
let mut truncated = vec![0u8; 512];
truncated[..4].copy_from_slice(b"tail");
truncated[124..136].copy_from_slice(b"00000010000\0");
archive.extend(truncated);
let (entries, _) = recover_segment_entries(&archive, "data00000a.tar");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, identifier);
}
#[test]
fn recovery_scan_continues_past_wrapped_size_fields() {
let identifier = SegmentIdentifier::new(2, 0xA000_0000_0000_0002);
let data = vec![0x42u8; 64];
let mut wrapped = vec![0u8; 512];
wrapped[..7].copy_from_slice(b"strange");
wrapped[124..136].copy_from_slice(b"77777777777\0");
let mut archive = wrapped;
archive.extend(tar_entry(&segment_entry_name(identifier, &data), &data));
archive.extend_from_slice(&[0u8; 1024]);
let (entries, _) = recover_segment_entries(&archive, "data00000a.tar");
assert_eq!(
entries.len(),
1,
"the segment after the wrapped entry is recovered"
);
assert_eq!(entries[0].0, identifier);
}
#[test]
fn recovery_scan_survives_non_utf8_entry_names() {
let mut hostile = vec![0u8; 512];
hostile[..35].copy_from_slice(&[b'a'; 35]);
hostile[35] = 0xFF;
hostile[124..136].copy_from_slice(b"00000000000\0");
let mut archive = hostile;
archive.extend_from_slice(&[0u8; 1024]);
let (entries, _) = recover_segment_entries(&archive, "data00000a.tar");
assert!(entries.is_empty());
}
#[test]
fn opens_archive_files_through_recovery_when_index_is_missing() {
let identifier = SegmentIdentifier::new(7, 0xA000_0000_0000_0007);
let data = vec![0x77u8; 256];
let mut archive = tar_entry(&segment_entry_name(identifier, &data), &data);
archive.extend_from_slice(&[0u8; 1024]);
let directory = std::env::temp_dir().join(format!(
"froe-archive-test-{}-{}",
std::process::id(),
line!()
));
std::fs::create_dir_all(&directory).expect("create test directory");
let path = directory.join("data00000a.tar");
std::fs::write(&path, &archive).expect("write test archive");
let reader = TarArchiveReader::open(&path).expect("open archive");
assert!(reader.is_recovered());
assert_eq!(reader.segment_count(), 1);
assert!(reader.contains_segment(identifier));
assert_eq!(
reader.segment_data(identifier).expect("segment data"),
&data[..]
);
std::fs::remove_dir_all(&directory).expect("remove test directory");
}
}