use crate::extract::{ExtractOptions, create_part};
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
pub(crate) const ENTRY_SIZE: usize = 2580;
pub(crate) const TABLE_START: usize = 60;
const TABLE_END: usize = 69721;
const NAME_LEN: usize = 0x40;
const LEN_AT: usize = 0x400;
const START_AT: usize = 0x40C;
const MAX_FILE_BYTES: u64 = 1 << 30;
const MAX_NAME_CHARS: usize = 256;
pub struct Entry {
pub name: String,
pub offset: u64,
pub length: u64,
}
pub fn is_pac(input: &Path) -> bool {
let Some(ext) = input.extension().and_then(|e| e.to_str()) else {
return false;
};
if !ext.eq_ignore_ascii_case("pac") {
return false;
}
let Ok(mut f) = File::open(input) else {
return false;
};
let mut name_bytes = [0u8; NAME_LEN];
if f.seek(SeekFrom::Start(TABLE_START as u64)).is_err() {
return false;
}
if f.read_exact(&mut name_bytes).is_err() {
return false;
}
name_bytes.iter().any(|&b| b != 0)
}
fn decode_name(field: &[u8]) -> Result<String> {
let units = field.len() / 2;
let u16s: Vec<u16> = (0..units)
.map(|i| u16::from_le_bytes([field[i * 2], field[i * 2 + 1]]))
.collect();
let cut = u16s.iter().position(|&u| u == 0).unwrap_or(u16s.len());
String::from_utf16(&u16s[..cut]).with_context(|| "invalid UTF-16 in PAC entry name")
}
fn is_safe_name(name: &str) -> bool {
!name.is_empty()
&& !name.contains('/')
&& !name.contains('\\')
&& !name.contains("..")
&& name.chars().all(|c| !c.is_control() && c != ':')
}
fn read_table(input: &Path) -> Result<Vec<Entry>> {
let mut f = File::open(input).with_context(|| format!("opening {}", input.display()))?;
let file_size = f.metadata()?.len();
let mut entries = Vec::new();
let mut need = vec![0u8; ENTRY_SIZE];
let mut pos = TABLE_START;
while pos + ENTRY_SIZE <= TABLE_END {
if pos as u64 + ENTRY_SIZE as u64 > file_size {
break;
}
f.seek(SeekFrom::Start(pos as u64))?;
f.read_exact(&mut need)?;
let length = u32::from_le_bytes(need[LEN_AT..LEN_AT + 4].try_into().unwrap()) as u64;
let start_pos = u32::from_le_bytes(need[START_AT..START_AT + 4].try_into().unwrap()) as u64;
if start_pos == 0 {
pos += ENTRY_SIZE;
continue;
}
if start_pos
.checked_add(length)
.is_some_and(|end| end > file_size)
{
pos += ENTRY_SIZE;
continue;
}
if length > MAX_FILE_BYTES {
bail!(
"PAC entry claims {} bytes for {}, exceeds the {} byte limit",
length,
input.display(),
MAX_FILE_BYTES
);
}
let name = decode_name(&need[..NAME_LEN])?;
if !is_safe_name(&name) {
bail!("unsafe PAC file name {name:?}");
}
if name.len() > MAX_NAME_CHARS {
bail!("PAC file name is too long ({name})");
}
entries.push(Entry {
name: name.clone(),
offset: start_pos,
length,
});
pos += ENTRY_SIZE;
}
Ok(entries)
}
pub fn list(input: &Path, opts: &ExtractOptions) -> Result<Vec<(String, u64)>> {
let entries = read_table(input)?;
let only = opts.only.as_ref();
let mut out: Vec<(String, u64)> = entries
.into_iter()
.filter(|e| only.is_none_or(|o| o.iter().any(|name| name == &e.name)))
.map(|e| (e.name, e.length))
.collect();
out.sort_unstable_by(|a, b| a.0.cmp(&b.0));
Ok(out)
}
pub fn extract(input: &Path, out_dir: &Path, opts: &ExtractOptions) -> Result<Vec<PathBuf>> {
let entries = read_table(input)?;
let only = opts.only.as_ref();
std::fs::create_dir_all(out_dir)?;
if !opts.force {
for e in &entries {
if only.is_some_and(|o| !o.iter().any(|n| n == &e.name)) {
continue;
}
if out_dir.join(&e.name).symlink_metadata().is_ok() {
bail!(
"{} already exists in {}; pass --force to overwrite",
e.name,
out_dir.display()
);
}
}
}
let mut paths = Vec::new();
let mut file = File::open(input).with_context(|| format!("opening {}", input.display()))?;
for entry in &entries {
if only.is_some_and(|o| !o.iter().any(|n| n == &entry.name)) {
continue;
}
let final_path = out_dir.join(&entry.name);
let tmp_path = out_dir.join(format!("{}.part", entry.name));
let copy = (|| -> Result<()> {
let mut out = create_part(&tmp_path)?;
file.seek(SeekFrom::Start(entry.offset))?;
let remaining = entry.length as usize;
let mut buf = vec![0u8; 64 * 1024];
let mut left = remaining;
while left > 0 {
let n = left.min(buf.len());
file.read_exact(&mut buf[..n])?;
out.write_all(&buf[..n])?;
left -= n;
}
Ok(())
})();
if let Err(e) = copy {
let _ = std::fs::remove_file(&tmp_path);
return Err(e.context(format!("extracting {}", entry.name)));
}
std::fs::rename(&tmp_path, &final_path)?;
paths.push(final_path);
}
paths.sort_unstable();
Ok(paths)
}
#[cfg(test)]
pub(crate) fn make_entry(name: &str, data_offset: u64, data_len: u64) -> Vec<u8> {
let mut entry = vec![0u8; ENTRY_SIZE];
for (i, c) in name.chars().enumerate() {
let u = c as u16;
let idx = i * 2;
if idx + 1 >= NAME_LEN {
break;
}
entry[idx] = (u & 0xff) as u8;
entry[idx + 1] = (u >> 8) as u8;
}
entry[LEN_AT..LEN_AT + 4].copy_from_slice(&(data_len as u32).to_le_bytes());
entry[START_AT..START_AT + 4].copy_from_slice(&(data_offset as u32).to_le_bytes());
entry
}
#[cfg(test)]
pub(crate) fn build_pac_for_test(path: &Path) -> PathBuf {
build_pac(path, &[("system".to_string(), vec![0xAA; 16])]);
path.to_path_buf()
}
#[cfg(test)]
fn build_pac(path: &Path, files: &[(String, Vec<u8>)]) {
let n = files.len();
let table_end = TABLE_START + n * ENTRY_SIZE;
let data_start = table_end as u64;
let mut f = std::fs::File::create(path).unwrap();
f.write_all(&[0u8; TABLE_START]).unwrap();
let mut data_offset = data_start;
for (name, data) in files {
let entry = make_entry(name, data_offset, data.len() as u64);
f.write_all(&entry).unwrap();
data_offset += data.len() as u64;
}
for (_, data) in files {
f.write_all(data).unwrap();
}
f.flush().unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_name_strips_nul_padding() {
let mut field = vec![0u8; NAME_LEN];
field[0] = b'h';
field[2] = b'i';
let result = decode_name(&field).unwrap();
assert_eq!(result, "hi");
}
#[test]
fn build_pac_for_test_creates_valid_pac() {
use crate::testutil::Scratch;
let dir = Scratch::new(&format!("pac-test-{}", std::process::id()));
let path = dir.join("test.pac");
let returned = build_pac_for_test(&path);
assert_eq!(returned, path);
assert!(path.exists());
assert!(is_pac(&path));
}
}