mod gzip;
mod sevenz;
mod tar;
mod zip;
pub(crate) use gzip::extract_gzip_with_bytes;
#[cfg(test)]
pub(crate) use gzip::{decompress_gzip, extract_gzip, extract_gzip_metadata, extract_gzip_text_content};
pub(crate) use sevenz::{extract_7z_file_bytes, extract_7z_metadata, extract_7z_text_content};
pub(crate) use tar::{extract_tar_file_bytes, extract_tar_metadata, extract_tar_text_content};
pub(crate) use zip::{extract_zip_file_bytes, extract_zip_metadata, extract_zip_text_content};
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone)]
pub struct ArchiveMetadata {
pub format: String,
pub file_list: Vec<ArchiveEntry>,
pub file_count: usize,
pub total_size: u64,
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
pub path: String,
pub size: u64,
pub is_dir: bool,
}
impl ArchiveEntry {
pub fn confined_path(&self) -> Option<String> {
if self.path.contains('\0') {
return None;
}
let normalized = self.path.replace('\\', "/");
if has_drive_or_unc_prefix(&normalized) {
return None;
}
let mut stack: Vec<&str> = Vec::new();
for segment in normalized.split('/') {
match segment {
"" | "." => {}
".." => {
stack.pop()?;
}
other => stack.push(other),
}
}
if stack.is_empty() {
return None;
}
Some(stack.join("/"))
}
}
fn has_drive_or_unc_prefix(path: &str) -> bool {
let bytes = path.as_bytes();
path.starts_with("//") || (bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':')
}
pub(crate) const TEXT_EXTENSIONS: &[&str] = &[
".txt",
".md",
".markdown",
".json",
".jsonl",
".ndjson",
".xml",
".html",
".htm",
".csv",
".tsv",
".log",
".yaml",
".yml",
".toml",
".ini",
".cfg",
".conf",
".properties",
".env",
".rst",
".adoc",
".tex",
".sql",
".rs",
".py",
".js",
".mjs",
".cjs",
".ts",
".tsx",
".jsx",
".go",
".java",
".kt",
".rb",
".php",
".c",
".h",
".cpp",
".cc",
".hpp",
".cs",
".swift",
".sh",
".bash",
".zsh",
".ps1",
".css",
".scss",
".less",
".svg",
".gitignore",
];
pub(crate) fn decode_archive_text(bytes: &[u8], member: &str) -> String {
match std::str::from_utf8(bytes) {
Ok(s) => crate::utils::strip_bom(s).to_string(),
Err(_) => {
let outcome = crate::utils::decode_with_provenance(bytes, None);
tracing::warn!(
member = %member,
replaced_characters = outcome.replaced_characters,
"archive member is not valid UTF-8; decoding with charset detection"
);
crate::utils::strip_bom(&outcome.text).to_string()
}
}
}
#[cfg(test)]
mod tests;