use std::fs;
use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
use hex::FromHex;
use crate::Cli;
use crate::parse::Im4p;
pub fn ensure_outdir(outdir: &Path, force: bool) -> Result<()> {
if outdir.exists() {
if !outdir.is_dir() { bail!("outdir exists and is not a directory"); }
if !force && !is_empty_dir(outdir)? {
bail!("outdir exists and is not empty; pass --force to proceed");
}
} else {
fs::create_dir_all(outdir).with_context(|| format!("mkdir -p {:?}", outdir))?;
}
Ok(())
}
fn is_empty_dir(p: &Path) -> Result<bool> {
if !p.is_dir() { return Ok(false); }
for _ in fs::read_dir(p)? {
return Ok(false);
}
Ok(true)
}
pub fn resolve_iv_key(cli: &Cli, im4p: &Im4p) -> Result<(Vec<u8>, Vec<u8>)> {
let iv_cli = if let Some(ref hx) = cli.iv_hex {
Some(decode_hex_exact(hx, 16)?)
} else { None };
let key_cli = if let Some(ref hx) = cli.key_hex {
let k = decode_hex(hx)?;
match k.len() { 16|24|32 => Some(k), _ => bail!("key hex must be 16/24/32 bytes"), }
} else { None };
if let (Some(iv), Some(key)) = (iv_cli, key_cli) {
return Ok((iv, key));
}
if let Some(ref entries) = im4p.kbag_summary {
let pick = entries.iter().find(|e| e.kclass == 1).or(entries.get(0))
.ok_or_else(|| anyhow!("KBAG present but empty"))?;
if pick.iv.len() != 16 { bail!("KBAG IV not 16 bytes"); }
if !matches!(pick.key.len(), 16|24|32) { bail!("KBAG key is not 16/24/32 bytes"); }
return Ok((pick.iv.clone(), pick.key.clone()));
}
bail!("no IV/Key provided and no KBAG available");
}
pub fn decode_hex_exact(h: &str, n: usize) -> Result<Vec<u8>> {
let v: Vec<u8> = Vec::from_hex(h).map_err(|e| anyhow!("hex: {e}"))?;
if v.len() != n { bail!("expected {n} bytes, got {}", v.len()); }
Ok(v)
}
pub fn decode_hex(h: &str) -> Result<Vec<u8>> {
Ok(Vec::from_hex(h).map_err(|e| anyhow!("hex: {e}"))?)
}
pub fn validate_decryption(data: &[u8]) -> (bool, Option<String>) {
if data.len() < 4 {
return (false, Some("too short".into()));
}
let magic = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
match magic {
0xfeedfacf | 0xcffaedfe | 0xfeedface | 0xcefaedfe => {
return (true, Some("Mach-O".into()));
}
0x62767832 => return (true, Some("LZFSE (bvx2)".into())), 0x636f6d70 => return (true, Some("complzss".into())), 0x59535331 => return (true, Some("YSS1 (lzss)".into())), 0x496d6733 => return (true, Some("IMG3".into())), 0x494d4734 => return (true, Some("IMG4".into())), _ => {}
}
let header_len = data.len().min(64);
let header_zeros = data[..header_len].iter().filter(|&&b| b == 0).count();
let header_ones = data[..header_len].iter().filter(|&&b| b == 0xFF).count();
if header_zeros == header_len {
return (false, Some("all-zero header".into()));
}
if header_ones == header_len {
return (false, Some("all-0xFF header".into()));
}
if data.len() >= 16 {
let unique_bytes: std::collections::HashSet<u8> =
data[..16].iter().copied().collect();
if unique_bytes.len() >= 3 {
return (true, Some("structured binary".into()));
}
}
(true, Some("unknown format".into()))
}
pub fn try_decompress(_input: &[u8]) -> Result<Option<(String, Vec<u8>)>> {
#[cfg(feature = "lzfse")]
{
if crate::decompress_lzfse::looks_like_lzfse(_input) {
let out = crate::decompress_lzfse::decompress_lzfse(_input)?;
return Ok(Some(("im4p.decompressed.lzfse".into(), out)));
}
}
#[cfg(feature = "lzss")]
{
if crate::decompress_lzss::looks_like_lzss(_input) {
let out = crate::decompress_lzss::decompress_lzss(_input)?;
return Ok(Some(("im4p.decompressed.lzss".into(), out)));
}
}
Ok(None)
}