use std::fs::File;
use std::io::Read;
use std::path::Path;
use crate::create::PAYLOAD_PREFIX;
use crate::error::PackError;
use crate::manifest::{MANIFEST_NAME, Manifest, PACK_FORMAT_VERSION};
const MAX_MANIFEST_BYTES: u64 = 64 * 1024 * 1024;
pub fn inspect(archive: &Path) -> Result<Manifest, PackError> {
let file = File::open(archive)?;
let decoder = zstd::stream::Decoder::new(file)?;
let mut tar = tar::Archive::new(decoder);
if let Some(first) = tar.entries()?.next() {
let mut entry = first?;
let path = entry.path()?.to_path_buf();
if path.as_os_str() == MANIFEST_NAME {
let mut text = String::new();
let read = entry
.by_ref()
.take(MAX_MANIFEST_BYTES + 1)
.read_to_string(&mut text)? as u64;
if read > MAX_MANIFEST_BYTES {
return Err(PackError::ManifestTooLarge {
limit: MAX_MANIFEST_BYTES,
});
}
return Ok(Manifest::from_toml(&text)?);
}
}
Err(PackError::MissingManifest)
}
pub fn verify(archive: &Path) -> Result<Manifest, PackError> {
let manifest = inspect(archive)?;
if manifest.format_version > PACK_FORMAT_VERSION {
return Err(PackError::UnsupportedFormat {
found: manifest.format_version,
supported: PACK_FORMAT_VERSION,
});
}
Ok(manifest)
}
pub fn list_payload_paths(archive: &Path) -> Result<Vec<String>, PackError> {
Ok(scan_payload(archive)?.paths)
}
pub(crate) struct Payload {
pub(crate) paths: Vec<String>,
pub(crate) hard_links: Vec<(String, String)>,
}
pub(crate) fn scan_payload(archive: &Path) -> Result<Payload, PackError> {
use crate::restore::EntryPlan;
let file = File::open(archive)?;
let decoder = zstd::stream::Decoder::new(file)?;
let mut tar = tar::Archive::new(decoder);
let prefix = format!("{PAYLOAD_PREFIX}/");
let mut payload = Payload {
paths: Vec::new(),
hard_links: Vec::new(),
};
for entry in tar.entries()? {
let entry = entry?;
let path = entry.path()?;
let s = path.to_string_lossy();
let Some(rest) = s.strip_prefix(&prefix) else {
continue;
};
if rest.is_empty() {
continue;
}
let rel = rest.trim_end_matches('/').to_string();
match crate::restore::entry_plan(entry.header().entry_type()) {
EntryPlan::Extract => payload.paths.push(rel),
EntryPlan::HardLink => {
let target = entry
.link_name()?
.map(|t| t.display().to_string())
.unwrap_or_default();
payload.hard_links.push((rel, target));
}
EntryPlan::Refuse(kind) => {
return Err(PackError::UnusableArchiveEntry {
path: rel,
kind: kind.to_string(),
});
}
}
}
Ok(payload)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::create::{CreateOptions, create};
use std::fs;
use tempfile::TempDir;
#[test]
fn test_inspect_rejects_non_archive() {
let dir = TempDir::new().expect("tempdir");
let bogus = dir.path().join("not.pack");
fs::write(&bogus, b"definitely not zstd").expect("write");
assert!(inspect(&bogus).is_err());
}
#[test]
fn test_inspect_requires_manifest_first() {
let dir = TempDir::new().expect("tempdir");
let out = dir.path().join("hand.pack");
let file = fs::File::create(&out).expect("create");
let encoder = zstd::stream::Encoder::new(file, 1).expect("encoder");
let mut builder = tar::Builder::new(encoder);
let body = b"x";
let mut header = tar::Header::new_gnu();
header.set_size(body.len() as u64);
header.set_mode(0o644);
header.set_cksum();
builder
.append_data(&mut header, "payload/a.txt", &body[..])
.expect("append");
builder
.into_inner()
.expect("into_inner")
.finish()
.expect("finish");
assert!(matches!(inspect(&out), Err(PackError::MissingManifest)));
}
#[test]
fn test_verify_accepts_current_format() {
let dir = TempDir::new().expect("tempdir");
let root = dir.path().join("proj");
fs::create_dir_all(root.join("src")).expect("mkdir");
fs::write(root.join("src/main.rs"), "fn main() {}").expect("write");
let out = dir.path().join("proj.pack");
create(&CreateOptions::new(&root, &out, "0.13.3")).expect("create");
let manifest = verify(&out).expect("verify");
assert_eq!(manifest.format_version, PACK_FORMAT_VERSION);
}
}