use anyhow::{Result, anyhow, bail};
use std::env;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::PackedDir;
use crate::packer::{Gzip, Packer, SIG, VERSION, Xz};
pub fn pack_raw<P: AsRef<Path>>(dir: P, pack_name: &str) -> Result<()> {
pack(dir, pack_name, Packer::Raw)
}
pub fn pack_xz<P: AsRef<Path>>(dir: P, pack_name: &str) -> Result<()> {
pack(dir, pack_name, Xz::default())
}
pub fn pack_gz<P: AsRef<Path>>(dir: P, pack_name: &str) -> Result<()> {
pack(dir, pack_name, Gzip::default())
}
pub fn pack<P: AsRef<Path>>(dir: P, packed_name: &str, packer: impl Into<Packer>) -> Result<()> {
let packer = packer.into();
let dir_path = dir.as_ref();
if !PackedDir::is_valid_packed_name(packed_name) {
bail!("Invalid bundle name: {packed_name} (must be valid identifier)");
}
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
.map_err(|_| anyhow!("CARGO_MANIFEST_DIR not set - must be called from build script"))?;
let manifest_path = Path::new(&manifest_dir);
let source_dir = if dir_path.is_absolute() {
if !dir_path.exists() {
bail!("Source directory does not exist: {}", dir_path.display());
}
if !dir_path.is_dir() {
bail!("Source path is not a directory: {}", dir_path.display());
}
dir_path.to_path_buf()
} else {
let resolved = manifest_path.join(dir_path);
if !resolved.exists() {
bail!("Source directory does not exist: {}", resolved.display());
}
if !resolved.is_dir() {
bail!("Source path is not a directory: {}", resolved.display());
}
resolved.canonicalize().unwrap_or(resolved)
};
if let Ok(relative_path) = source_dir.strip_prefix(manifest_path) {
println!("cargo:rerun-if-changed={}", relative_path.display());
} else {
println!("cargo:rerun-if-changed={}", source_dir.display());
}
let mut sorted_entries = Vec::new();
let walk = WalkDir::new(&source_dir)
.follow_links(false)
.sort_by_file_name();
for entry in walk {
let entry = entry?;
if entry.metadata()?.is_file() {
sorted_entries.push(entry);
}
}
sorted_entries.sort_by(|a, b| a.path().cmp(b.path()));
let mut files = Vec::new();
for entry in sorted_entries {
let file_path = entry.path();
let archive_path = FileToPack::normalize_path_for_archive(file_path, &source_dir);
let path_str = archive_path.to_string_lossy().to_string();
let path_len = path_str.len();
let max_path_len = u16::MAX as usize;
if path_len > max_path_len {
bail!("Path too long: {path_str:?} ({path_len} bytes, max {max_path_len})");
}
let file_entry = FileToPack::new(path_str, archive_path, file_path, packer)?;
files.push(file_entry);
}
let out_dir = env::var("OUT_DIR")
.map_err(|_| anyhow!("OUT_DIR not set - must be called from build script"))?;
let output_path = Path::new(&out_dir).join(format!("PACKDIR_{packed_name}"));
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = File::create(&output_path)?;
let header = compute_header(&files)?;
file.write_all(&header)?;
for file_entry in files {
file.write_all(&file_entry.packed_data)?;
}
Ok(())
}
#[derive(Debug)]
struct FileToPack {
path: PathBuf,
path_str: String,
unpacked_size: u64,
packed_size: u64,
packed_data: Vec<u8>,
packer: Packer,
}
impl FileToPack {
pub fn new(
path_str: String,
path: impl Into<PathBuf>,
file_path: &Path,
packer: Packer,
) -> Result<Self> {
let path = path.into();
let mut file = File::open(file_path)?;
let mut unpacked_data = Vec::new();
file.read_to_end(&mut unpacked_data)?;
let unpacked_size = unpacked_data.len() as u64;
let packed_data = packer.pack(&unpacked_data)?;
let packed_size = packed_data.len() as u64;
Ok(Self {
path,
path_str,
unpacked_size,
packed_size,
packed_data,
packer,
})
}
fn normalize_path_for_archive(path: &Path, base_dir: &Path) -> PathBuf {
if let Ok(relative) = path.strip_prefix(base_dir) {
if relative.as_os_str().is_empty() {
PathBuf::from("unknown")
} else {
relative.to_path_buf()
}
} else {
if path.to_string_lossy().ends_with('/') || path.to_string_lossy().ends_with('\\') {
PathBuf::from("unknown")
} else {
path.file_name()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("unknown"))
}
}
}
}
fn compute_header(files: &[FileToPack]) -> Result<Vec<u8>> {
if files.len() > u32::MAX as usize {
bail!("Too many files: {} (max {})", files.len(), u32::MAX);
}
let mut header_size = 8 + 1 + 4;
for file in files {
header_size +=
2 + file.path_str.len() + 8 + 8 + 8 + 1 ;
}
let mut header = Vec::with_capacity(header_size);
header.extend_from_slice(SIG);
header.push(VERSION);
header.extend_from_slice(&(files.len() as u32).to_le_bytes());
let mut data_start = header_size as u64;
for file in files {
let path_str = file.path.to_string_lossy();
let path_bytes = path_str.as_bytes();
header.extend_from_slice(&(path_bytes.len() as u16).to_le_bytes());
header.extend_from_slice(path_bytes);
header.extend_from_slice(&file.unpacked_size.to_le_bytes());
header.extend_from_slice(&file.packed_size.to_le_bytes());
header.extend_from_slice(&data_start.to_le_bytes());
header.push(file.packer.into());
data_start += file.packed_size;
}
Ok(header)
}