use std::io::{Cursor, Read, Seek, SeekFrom};
use anyhow::Result;
use rayon::prelude::*;
use rayon::slice::ParallelSlice;
use crate::config::RuntimeConfig;
use crate::engine::chunking::optimal_chunk_size;
use crate::parsers::ParseResult;
use crate::results::metadata::{ArchiveEntry, ArchiveMetadata};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TarFormat {
Tar,
TarGz,
Tgz,
TarBz2,
TarXz,
}
impl TarFormat {
fn as_str(self) -> &'static str {
match self {
TarFormat::Tar => "tar",
TarFormat::TarGz => "tar.gz",
TarFormat::Tgz => "tgz",
TarFormat::TarBz2 => "tar.bz2",
TarFormat::TarXz => "tar.xz",
}
}
}
fn should_omit_tar_entry_path(path: &str) -> bool {
path.contains("/._")
|| path.starts_with("._")
|| path.contains("PaxHeader")
|| path.ends_with('/')
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
fn detect_archive_format(path: &str) -> TarFormat {
let lo = path.to_lowercase();
match () {
() if lo.ends_with(".tar.xz") => TarFormat::TarXz,
() if lo.ends_with(".tar.bz2") => TarFormat::TarBz2,
() if lo.ends_with(".tgz") => TarFormat::Tgz,
() if lo.ends_with(".tar.gz") => TarFormat::TarGz,
() if lo.ends_with(".tar") => TarFormat::Tar,
() => TarFormat::Tar,
}
}
pub fn extract_archive_metadata(
content: &[u8],
stats: &ParseResult,
config: &RuntimeConfig,
) -> Result<ArchiveMetadata> {
let format = detect_archive_format(&stats.file_path);
let compressed_size = stats.byte_count as u64;
let (file_count, uncompressed_size, entries) = stream_tar_entries(content, format, config);
let note = match format {
TarFormat::Tar => None,
TarFormat::TarGz | TarFormat::Tgz | TarFormat::TarBz2 | TarFormat::TarXz => {
Some("full listing for .tar only.".to_string())
}
};
Ok(ArchiveMetadata {
format: Some(format.as_str().to_string()),
file_count,
entries: if entries.is_empty() {
None
} else {
Some(entries)
},
compressed_size: Some(compressed_size),
uncompressed_size: if uncompressed_size > 0 {
Some(uncompressed_size)
} else {
None
},
truncated: None,
note,
})
}
fn stream_tar_entries(
content: &[u8],
format: TarFormat,
config: &RuntimeConfig,
) -> (Option<usize>, u64, Vec<ArchiveEntry>) {
match format {
TarFormat::Tar => {
let (fc, u, e) = stream_tar_entries_seek(content, config);
(Some(fc), u, e)
}
_ => compressed_tar_metadata_no_decompress(content, format),
}
}
fn compressed_tar_metadata_no_decompress(
content: &[u8],
format: TarFormat,
) -> (Option<usize>, u64, Vec<ArchiveEntry>) {
let uncompressed = match format {
TarFormat::TarGz | TarFormat::Tgz => {
if content.len() >= 8 {
let i = content.len() - 4;
u64::from(u32::from_le_bytes([
content[i],
content[i + 1],
content[i + 2],
content[i + 3],
]))
} else {
0
}
}
TarFormat::TarBz2 | TarFormat::TarXz => 0,
TarFormat::Tar => unreachable!("Tar uses seek path"),
};
(None, uncompressed, Vec::new())
}
fn parse_tar_size(bytes: &[u8]) -> u64 {
if bytes.len() < 136 {
return 0;
}
let s = match std::str::from_utf8(&bytes[124..136]) {
Ok(x) => x.trim_end_matches('\0').trim(),
Err(_) => return 0,
};
u64::from_str_radix(s, 8).unwrap_or(0)
}
fn parse_tar_path(bytes: &[u8]) -> String {
let trim_nul = |b: &[u8]| -> String {
let end = b.iter().position(|&x| x == 0).unwrap_or(b.len());
std::str::from_utf8(&b[..end])
.unwrap_or("")
.trim_end()
.to_string()
};
let name = trim_nul(&bytes[0..100.min(bytes.len())]);
if bytes.len() >= 500 {
let prefix = trim_nul(&bytes[345..500]);
if !prefix.is_empty() {
return format!("{prefix}/{name}");
}
}
name
}
fn stream_tar_entries_seek(
content: &[u8],
config: &RuntimeConfig,
) -> (usize, u64, Vec<ArchiveEntry>) {
let mut r = Cursor::new(content);
let mut info: Vec<(u64, u64, Option<String>)> = Vec::new(); let mut maybe_long_name: Option<String> = None;
loop {
let offset = r.stream_position().unwrap_or(0);
let mut buf = [0u8; 512];
if r.read_exact(&mut buf).is_err() {
break;
}
if buf.iter().all(|&b| b == 0) {
break;
}
let size = parse_tar_size(&buf);
let typeflag = buf.get(156).copied().unwrap_or(0);
let block_len = size.div_ceil(512) * 512;
if typeflag == b'L' {
if size > 0 && size <= 4096 {
let len = usize::try_from(size).expect("long name size <= 4096 fits usize");
let mut name_buf = vec![0u8; len];
if r.read_exact(&mut name_buf).is_ok() {
maybe_long_name = Some(String::from_utf8_lossy(&name_buf).into_owned());
}
}
let skip = block_len.saturating_sub(size);
if skip > 0 {
let _ = r.seek(SeekFrom::Current(skip.cast_signed()));
}
} else if typeflag == b'K' {
let _ = r.seek(SeekFrom::Current(block_len.cast_signed()));
} else {
info.push((offset, size, maybe_long_name.take()));
let _ = r.seek(SeekFrom::Current(block_len.cast_signed()));
}
}
let to_resolve = &info[..];
let mut entries = if to_resolve.is_empty() {
Vec::new()
} else if to_resolve.len() >= config.min_collection_size_for_chunking
&& config.target_chunks_per_file > 1
{
let chunk_size = optimal_chunk_size(
to_resolve.len(),
config.target_chunks_per_file,
config.min_collection_size_for_chunking,
);
to_resolve
.par_chunks(chunk_size)
.map(|chunk| {
let mut c = Cursor::new(content);
chunk
.iter()
.map(|(o, s, ln)| {
let path = ln.clone().unwrap_or_else(|| {
let mut buf = [0u8; 512];
let _ = c.seek(SeekFrom::Start(*o));
let _ = c.read_exact(&mut buf);
parse_tar_path(&buf)
});
ArchiveEntry { path, size: *s }
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
.into_iter()
.flatten()
.collect()
} else {
let mut c = Cursor::new(content);
to_resolve
.iter()
.map(|(o, s, ln)| {
let path = ln.clone().unwrap_or_else(|| {
let mut buf = [0u8; 512];
let _ = c.seek(SeekFrom::Start(*o));
let _ = c.read_exact(&mut buf);
parse_tar_path(&buf)
});
ArchiveEntry { path, size: *s }
})
.collect()
};
entries.retain(|e| !should_omit_tar_entry_path(&e.path));
let file_count = entries.len();
let uncompressed_size: u64 = entries.iter().map(|e| e.size).sum();
(file_count, uncompressed_size, entries)
}
crate::no_template_mining!(
extract_archive_templates,
"Archive: container; no template mining."
);