use crate::oem::{Item, MAX_IMAGE_BYTES, MAX_ITEMS, Src, clean_name, le32, le64, read_at};
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
pub(crate) const KDZ_MAGIC: [u8; 8] = [0x24, 0x05, 0x00, 0x00, 0x38, 0x31, 0x25, 0x80];
pub(crate) const KDZ_V1_MAGIC: [u8; 8] = [0x28, 0x05, 0x00, 0x00, 0x34, 0x31, 0x25, 0x80];
pub(crate) const DZ_MAGIC: [u8; 4] = [0x32, 0x96, 0x18, 0x74];
const CHUNK_MAGIC: [u8; 4] = [0x30, 0x12, 0x95, 0x78];
const KDZ_RECORD: usize = 272;
const KDZ_MAX_RECORDS: usize = 64;
const DZ_HDR: usize = 512;
const SECTOR: u64 = 512;
pub(crate) struct Chunk {
data_at: u64,
data_size: u64,
target_size: u64,
out_offset: u64,
}
pub(crate) fn parse_kdz(f: &mut File, file_len: u64) -> Result<Vec<Item>> {
let mut magic = [0u8; 8];
read_at(f, 0, &mut magic)?;
if magic == KDZ_V1_MAGIC {
bail!("lg-kdz: the v1 KDZ header variant is recognized but not supported");
}
if magic != KDZ_MAGIC {
bail!("lg-kdz: bad magic");
}
let mut dz = None;
for i in 0..KDZ_MAX_RECORDS {
let at = 8 + (i * KDZ_RECORD) as u64;
if at + KDZ_RECORD as u64 > file_len {
bail!("lg-kdz: file table runs past the end of the file");
}
let mut rec = [0u8; KDZ_RECORD];
read_at(f, at, &mut rec)?;
let cut = rec[..256].iter().position(|&b| b == 0).unwrap_or(256);
if cut == 0 {
break;
}
let name = String::from_utf8_lossy(&rec[..cut]).to_ascii_lowercase();
let (size, offset) = (le64(&rec, 256), le64(&rec, 264));
if offset.checked_add(size).is_none_or(|end| end > file_len) {
bail!("lg-kdz: entry {name:?} lies outside the file");
}
if name.ends_with(".dz") {
if dz.is_some() {
bail!("lg-kdz: more than one .dz entry");
}
dz = Some((offset, size));
}
}
let Some((offset, size)) = dz else {
bail!("lg-kdz: no .dz entry in the file table");
};
parse_dz(f, offset, size)
}
pub(crate) fn parse_dz(f: &mut File, base: u64, region_len: u64) -> Result<Vec<Item>> {
if region_len < DZ_HDR as u64 {
bail!("lg-dz: too short for a DZ header");
}
let mut h = [0u8; DZ_HDR];
read_at(f, base, &mut h)?;
if h[..4] != DZ_MAGIC {
bail!("lg-dz: bad magic");
}
let count = le32(&h, 0xC0) as usize;
if count == 0 || count > MAX_ITEMS * 64 {
bail!("lg-dz: implausible chunk count {count}");
}
let end = base + region_len;
let mut pos = base + DZ_HDR as u64;
let mut parts: Vec<(String, Vec<Chunk>)> = Vec::new();
for _ in 0..count {
if pos + DZ_HDR as u64 > end {
bail!("lg-dz: chunk table runs past the end of the data");
}
read_at(f, pos, &mut h)?;
if h[..4] != CHUNK_MAGIC {
bail!("lg-dz: bad chunk magic at offset {pos}");
}
let name = clean_name(&h[8..40])?;
let (target, data) = (le32(&h, 0x28) as u64, le32(&h, 0x2C) as u64);
let (start, part_start) = (le32(&h, 0x40) as u64, le32(&h, 0x48) as u64);
let data_at = pos + DZ_HDR as u64;
if data_at + data > end {
bail!("lg-dz: chunk data for {name} runs past the end of the file");
}
let Some(rel) = start.checked_sub(part_start) else {
bail!("lg-dz: chunk of {name} starts before its partition");
};
let out_offset = rel * SECTOR;
if out_offset + target > MAX_IMAGE_BYTES {
bail!("lg-dz: {name} would exceed the {MAX_IMAGE_BYTES} byte image limit");
}
let chunk = Chunk {
data_at,
data_size: data,
target_size: target,
out_offset,
};
match parts.iter_mut().find(|(n, _)| *n == name) {
Some((_, v)) => v.push(chunk),
None => parts.push((name, vec![chunk])),
}
if parts.len() > MAX_ITEMS {
bail!("lg-dz: more than {MAX_ITEMS} partitions");
}
pos = data_at + data;
}
Ok(parts
.into_iter()
.map(|(name, chunks)| Item {
name: format!("{name}.img"),
size: chunks
.iter()
.map(|c| c.out_offset + c.target_size)
.max()
.unwrap_or(0),
src: Src::Chunks(chunks),
})
.collect())
}
pub(crate) fn write_chunks(file: &File, chunks: &[Chunk], out: &mut File) -> Result<()> {
for c in chunks {
let mut src = file.try_clone()?;
src.seek(SeekFrom::Start(c.data_at))?;
let mut z = flate2::read::ZlibDecoder::new(src.take(c.data_size)).take(c.target_size + 1);
out.seek(SeekFrom::Start(c.out_offset))?;
let n = std::io::copy(&mut z, out).context("inflating a DZ chunk")?;
if n != c.target_size {
bail!(
"a DZ chunk inflated to {n} bytes, its header says {}",
c.target_size
);
}
}
Ok(())
}
#[cfg(test)]
pub(crate) fn dz_bytes_for_test(parts: &[(&str, Vec<u8>)]) -> Vec<u8> {
use flate2::{Compression, write::ZlibEncoder};
use std::io::Write;
let mut chunks: Vec<(Vec<u8>, Vec<u8>)> = Vec::new(); let mut idx = 0u32;
for (pi, (name, data)) in parts.iter().enumerate() {
let part_start = 1000 * (pi as u32 + 1);
let split = if data.len() >= 4096 {
data.len() / 2
} else {
data.len()
};
let pieces: Vec<&[u8]> = if split == data.len() {
vec![data]
} else {
vec![&data[..split], &data[split..]]
};
let mut done = 0usize;
for piece in pieces {
let mut z = ZlibEncoder::new(Vec::new(), Compression::default());
z.write_all(piece).unwrap();
let comp = z.finish().unwrap();
let mut h = vec![0u8; DZ_HDR];
h[..4].copy_from_slice(&CHUNK_MAGIC);
h[4..8].copy_from_slice(&idx.to_le_bytes());
h[8..8 + name.len()].copy_from_slice(name.as_bytes());
h[0x28..0x2C].copy_from_slice(&(piece.len() as u32).to_le_bytes());
h[0x2C..0x30].copy_from_slice(&(comp.len() as u32).to_le_bytes());
let start = part_start + (done as u64 / SECTOR) as u32;
h[0x40..0x44].copy_from_slice(&start.to_le_bytes());
h[0x44..0x48].copy_from_slice(&(piece.len().div_ceil(512) as u32).to_le_bytes());
h[0x48..0x4C].copy_from_slice(&part_start.to_le_bytes());
chunks.push((h, comp));
done += piece.len();
idx += 1;
}
}
let mut out = vec![0u8; DZ_HDR];
out[..4].copy_from_slice(&DZ_MAGIC);
out[4..8].copy_from_slice(&2u32.to_le_bytes());
out[16..24].copy_from_slice(b"LG-TEST\0");
out[0xC0..0xC4].copy_from_slice(&(chunks.len() as u32).to_le_bytes());
for (h, d) in chunks {
out.extend_from_slice(&h);
out.extend_from_slice(&d);
}
out
}
#[cfg(test)]
pub(crate) fn build_dz_for_test(path: &std::path::Path, parts: &[(&str, Vec<u8>)]) {
std::fs::write(path, dz_bytes_for_test(parts)).unwrap();
}
#[cfg(test)]
pub(crate) fn build_kdz_for_test(path: &std::path::Path, parts: &[(&str, Vec<u8>)]) {
let dz = dz_bytes_for_test(parts);
let other = b"not a partition".to_vec();
let table_end = 8 + 3 * KDZ_RECORD as u64;
let other_at = table_end.next_multiple_of(0x1000);
let dz_at = (other_at + other.len() as u64).next_multiple_of(0x1000);
let mut out = vec![0u8; table_end as usize];
out[..8].copy_from_slice(&KDZ_MAGIC);
for (i, (name, size, at)) in [
("LGUP_Common.dll", other.len() as u64, other_at),
("fw_test.dz", dz.len() as u64, dz_at),
]
.iter()
.enumerate()
{
let r = 8 + i * KDZ_RECORD;
out[r..r + name.len()].copy_from_slice(name.as_bytes());
out[r + 256..r + 264].copy_from_slice(&size.to_le_bytes());
out[r + 264..r + 272].copy_from_slice(&at.to_le_bytes());
}
out.resize(other_at as usize, 0);
out.extend_from_slice(&other);
out.resize(dz_at as usize, 0);
out.extend_from_slice(&dz);
std::fs::write(path, out).unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::ExtractOptions;
use crate::oem::tests::expected;
use crate::oem::{Kind, detect, extract, list};
use crate::testutil::Scratch;
fn raw_parts() -> Vec<(&'static str, Vec<u8>)> {
vec![("boot", expected("boot")), ("system", expected("system"))]
}
#[test]
fn a_dz_unwraps_chunks_into_partitions() {
let d = Scratch::new("lg-dz");
let dz = d.join("fw.dz");
build_dz_for_test(&dz, &raw_parts());
assert_eq!(detect(&dz), Some(Kind::LgDz));
assert_eq!(crate::detect::identify_path(&dz).unwrap().id, "lg-dz");
let opts = ExtractOptions::default();
assert_eq!(
list(Kind::LgDz, &dz, &opts).unwrap(),
[
("boot.img".to_string(), 100),
("system.img".to_string(), 4096)
]
);
let out = d.join("out");
extract(Kind::LgDz, &dz, &out, &opts).unwrap();
assert_eq!(
std::fs::read(out.join("boot.img")).unwrap(),
expected("boot")
);
assert_eq!(
std::fs::read(out.join("system.img")).unwrap(),
expected("system"),
"two chunks of one partition are placed back to back"
);
}
#[test]
fn a_kdz_is_unwrapped_through_its_dz_entry() {
let d = Scratch::new("lg-kdz");
let kdz = d.join("fw.kdz");
build_kdz_for_test(&kdz, &raw_parts());
assert_eq!(detect(&kdz), Some(Kind::LgKdz));
assert_eq!(crate::detect::identify_path(&kdz).unwrap().id, "lg-kdz");
let out = d.join("out");
let r = crate::extract::extract_all_noted(&kdz, &out, &ExtractOptions::default()).unwrap();
assert_eq!(r.done.len(), 2);
assert_eq!(
std::fs::read(out.join("boot.img")).unwrap(),
expected("boot")
);
assert_eq!(
std::fs::read(out.join("system.img")).unwrap(),
expected("system")
);
assert_eq!(
crate::extract::partition_names(&kdz).unwrap(),
["boot", "system"]
);
}
#[test]
fn corrupt_input_is_refused_not_guessed() {
let d = Scratch::new("lg-bad");
let good = dz_bytes_for_test(&raw_parts());
let opts = ExtractOptions::default();
let try_dz = |n: &str, b: &[u8]| {
let p = d.join(format!("{n}.dz"));
std::fs::write(&p, b).unwrap();
extract(Kind::LgDz, &p, &d.join(format!("o-{n}")), &opts)
};
assert!(try_dz("trunc", &good[..700]).is_err());
let mut b = good.clone();
b[0xC0..0xC4].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(try_dz("count", &b).is_err());
let mut b = good.clone();
b[512..516].copy_from_slice(b"xxxx"); assert!(try_dz("chunkmagic", &b).is_err());
let mut b = good.clone();
b[512 + 0x28..512 + 0x2C].copy_from_slice(&7u32.to_le_bytes()); assert!(try_dz("target", &b).is_err());
let mut b = good.clone();
let data_at = 1024;
b[data_at + 2] ^= 0xFF; assert!(try_dz("zlib", &b).is_err());
let mut b = good.clone();
b[512 + 8..512 + 12].copy_from_slice(b"../x"); assert!(try_dz("name", &b).is_err());
assert!(!d.join("o-zlib").join("boot.img.part").exists());
let p = d.join("v1.kdz");
let mut v1 = KDZ_V1_MAGIC.to_vec();
v1.resize(4096, 0);
std::fs::write(&p, v1).unwrap();
let e = extract(Kind::LgKdz, &p, &d.join("o-v1"), &opts).unwrap_err();
assert!(e.to_string().contains("not supported"), "{e}");
}
}