use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use crate::contract::links::{find_entry_through_link, find_unresolvable_link, EntryKind, PayloadEntry};
use crate::error::{fail, Error, Result};
const IGNORED_NAMES: &[&str] = &["__pycache__", ".DS_Store"];
pub fn sha256_file(path: &Path) -> Result<String> {
use std::io::Read as _;
let mut file = std::fs::File::open(path)
.map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
let mut hasher = Sha256::new();
let mut buffer = vec![0u8; 128 * 1024];
loop {
let read = file
.read(&mut buffer)
.map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
let digest = hasher.finalize();
let mut hex = String::with_capacity(digest.len() * 2);
for byte in digest {
use std::fmt::Write as _;
let _ = write!(hex, "{byte:02x}");
}
Ok(hex)
}
pub fn collect_entries(root: &Path) -> Result<Vec<PayloadEntry>> {
let mut entries = Vec::new();
collect_into(root, root, &mut entries)?;
entries.sort_by(|left, right| left.path.cmp(&right.path));
Ok(entries)
}
fn collect_into(root: &Path, current: &Path, entries: &mut Vec<PayloadEntry>) -> Result<()> {
let mut names: Vec<PathBuf> = std::fs::read_dir(current)
.map_err(|error| Error::new(format!("cannot read {}: {error}", current.display())))?
.filter_map(std::result::Result::ok)
.map(|entry| entry.path())
.collect();
names.sort();
for path in names {
let name = path
.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or_default()
.to_string();
#[allow(clippy::case_sensitive_file_extension_comparisons)]
if IGNORED_NAMES.contains(&name.as_str()) || name.ends_with(".pyc") {
continue;
}
let relative = relative_forward_slash(root, &path)?;
let metadata = std::fs::symlink_metadata(&path)
.map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
if metadata.is_symlink() {
let target = std::fs::read_link(&path)
.map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
entries.push(PayloadEntry::link(
relative,
target.to_string_lossy().replace('\\', "/"),
));
} else if metadata.is_dir() {
collect_into(root, &path, entries)?;
} else if metadata.is_file() {
entries.push(PayloadEntry::file(relative));
} else {
fail!("box special entries are not allowed: {relative}");
}
}
Ok(())
}
fn relative_forward_slash(root: &Path, path: &Path) -> Result<String> {
let Ok(relative) = path.strip_prefix(root) else {
fail!("Unsafe relative path: {}", path.display());
};
Ok(relative
.components()
.map(|component| component.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/"))
}
pub fn collect_files(root: &Path) -> Result<BTreeSet<String>> {
Ok(collect_entries(root)?
.into_iter()
.filter(|entry| entry.kind != EntryKind::Directory)
.map(|entry| entry.path)
.collect())
}
pub fn payload_size(root: &Path) -> Result<u64> {
let mut total = 0u64;
for entry in collect_entries(root)? {
if entry.kind == EntryKind::Directory {
continue;
}
let path = crate::path::join_relative(root, &entry.path);
let metadata = std::fs::symlink_metadata(&path)
.map_err(|error| Error::new(format!("cannot read {}: {error}", path.display())))?;
total = total.saturating_add(metadata.len());
}
Ok(total)
}
pub fn validate_extracted_tree(root: &Path, allow_links: bool) -> Result<()> {
let entries = collect_entries(root)?;
if !allow_links {
if let Some(link) = entries.iter().find(|entry| entry.kind == EntryKind::Link) {
fail!("Archive links and special entries are not allowed: {}", link.path);
}
return Ok(());
}
if let Some(path) = find_unresolvable_link(&entries) {
fail!("Extracted link does not resolve to a file inside the payload: {path}");
}
if let Some(path) = find_entry_through_link(&entries) {
fail!("Extracted entry would be written through a link: {path}");
}
Ok(())
}