use crate::extract::{ExtractOptions, create_part};
use crate::sparse;
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
pub(crate) const MAX_ITEMS: usize = 1024;
pub(crate) const MAX_IMAGE_BYTES: u64 = 1 << 34;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Kind {
HuaweiApp,
LgKdz,
LgDz,
SonySin,
CoolpadCpb,
NokiaNb0,
}
impl Kind {
pub fn id(self) -> &'static str {
match self {
Kind::HuaweiApp => "huawei-update-app",
Kind::LgKdz => "lg-kdz",
Kind::LgDz => "lg-dz",
Kind::SonySin => "sony-sin",
Kind::CoolpadCpb => "coolpad-cpb",
Kind::NokiaNb0 => "nokia-nb0",
}
}
pub fn description(self) -> &'static str {
match self {
Kind::HuaweiApp => "Huawei UPDATE.APP firmware container",
Kind::LgKdz => "LG KDZ firmware container",
Kind::LgDz => "LG DZ firmware container (zlib partition chunks)",
Kind::SonySin => "Sony .sin firmware image",
Kind::CoolpadCpb => {
"Coolpad .cpb firmware container (recognized but unsupported, often encrypted)"
}
Kind::NokiaNb0 => "Nokia NB0 firmware container",
}
}
pub fn from_id(id: &str) -> Option<Kind> {
[
Kind::HuaweiApp,
Kind::LgKdz,
Kind::LgDz,
Kind::SonySin,
Kind::CoolpadCpb,
Kind::NokiaNb0,
]
.into_iter()
.find(|k| k.id() == id)
}
}
pub fn kind_by_extension(path: &Path) -> Option<Kind> {
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
match ext.as_str() {
"sin" => Some(Kind::SonySin),
"cpb" => Some(Kind::CoolpadCpb),
"nb0" => Some(Kind::NokiaNb0),
_ => None,
}
}
pub fn detect(path: &Path) -> Option<Kind> {
if !std::fs::metadata(path).ok()?.is_file() {
return None;
}
let mut head = Vec::with_capacity(16);
File::open(path)
.ok()?
.take(16)
.read_to_end(&mut head)
.ok()?;
match crate::detect::sniff(&head) {
Some(known) => Kind::from_id(known.id),
None => kind_by_extension(path),
}
}
pub(crate) struct Item {
pub name: String,
pub size: u64,
pub src: Src,
}
pub(crate) enum Src {
Range { offset: u64, len: u64 },
Chunks(Vec<crate::lgkdz::Chunk>),
}
pub(crate) fn clean_name(raw: &[u8]) -> Result<String> {
let cut = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
let s = std::str::from_utf8(&raw[..cut]).unwrap_or("");
let ok = !s.is_empty()
&& s.len() <= 64
&& !s.starts_with('.')
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'));
if !ok {
bail!("unsafe or empty partition name {:?}", &raw[..cut.min(32)]);
}
Ok(s.to_ascii_lowercase())
}
pub(crate) fn le32(b: &[u8], at: usize) -> u32 {
u32::from_le_bytes(b[at..at + 4].try_into().unwrap())
}
pub(crate) fn le64(b: &[u8], at: usize) -> u64 {
u64::from_le_bytes(b[at..at + 8].try_into().unwrap())
}
pub(crate) fn read_at(f: &mut File, at: u64, buf: &mut [u8]) -> Result<()> {
f.seek(SeekFrom::Start(at))?;
f.read_exact(buf)
.with_context(|| format!("container ends early (reading {} bytes at {at})", buf.len()))
}
pub(crate) fn range_item(
f: &mut File,
name: String,
offset: u64,
len: u64,
file_len: u64,
) -> Result<Item> {
if offset.checked_add(len).is_none_or(|end| end > file_len) {
bail!("{name}: range {offset}+{len} lies outside the {file_len} byte container");
}
let mut size = len;
if len >= 4 {
let mut head = [0u8; 4];
read_at(f, offset, &mut head)?;
if head == [0x3A, 0xFF, 0x26, 0xED] {
f.seek(SeekFrom::Start(offset))?;
size = sparse::read_header(&mut f.by_ref().take(len))
.with_context(|| format!("{name}: bad sparse payload"))?
.image_bytes();
}
}
if size > MAX_IMAGE_BYTES {
bail!("{name}: image of {size} bytes exceeds the {MAX_IMAGE_BYTES} byte limit");
}
Ok(Item {
name,
size,
src: Src::Range { offset, len },
})
}
fn parse(kind: Kind, path: &Path) -> Result<Vec<Item>> {
let mut f = File::open(path).with_context(|| format!("opening {}", path.display()))?;
let len = f.metadata()?.len();
let items = match kind {
Kind::HuaweiApp => crate::huawei::parse(&mut f, len)?,
Kind::LgKdz => crate::lgkdz::parse_kdz(&mut f, len)?,
Kind::LgDz => crate::lgkdz::parse_dz(&mut f, 0, len)?,
Kind::SonySin => crate::sonysin::parse(&mut f, len, path)?,
Kind::CoolpadCpb => {
return Err(anyhow::anyhow!(
"coolpad-cpb: Coolpad .cpb container (detected by extension, it has no magic): not supported. The layout is not publicly documented and bodies are commonly encrypted with vendor-specific keys, so nothing is guessed; decrypt/unpack it with the vendor tool first, then run android-doctor on the partition images"
));
}
Kind::NokiaNb0 => crate::nokianb0::parse(&mut f, len)?,
};
let mut seen = std::collections::HashSet::new();
for it in &items {
if !seen.insert(it.name.as_str()) {
bail!("{}: duplicate image name {}", kind.id(), it.name);
}
}
Ok(items)
}
fn selected(kind: Kind, path: &Path, opts: &ExtractOptions) -> Result<Vec<Item>> {
let mut items = parse(kind, path)?;
if let Some(only) = &opts.only {
let stem = |n: &str| n.strip_suffix(".img").unwrap_or(n).to_string();
let avail: Vec<String> = items.iter().map(|i| stem(&i.name)).collect();
if let Some(bad) = only.iter().find(|o| !avail.contains(o)) {
bail!("unknown name {bad:?}; available: {}", avail.join(", "));
}
items.retain(|i| only.contains(&stem(&i.name)));
}
Ok(items)
}
pub fn list(kind: Kind, input: &Path, opts: &ExtractOptions) -> Result<Vec<(String, u64)>> {
let mut out: Vec<_> = selected(kind, input, opts)?
.into_iter()
.map(|i| (i.name, i.size))
.collect();
out.sort_unstable();
Ok(out)
}
pub fn partition_names(kind: Kind, input: &Path) -> Result<Vec<String>> {
let mut names: Vec<String> = parse(kind, input)?
.into_iter()
.map(|i| i.name.trim_end_matches(".img").to_string())
.collect();
names.sort_unstable();
Ok(names)
}
pub fn extract(
kind: Kind,
input: &Path,
out_dir: &Path,
opts: &ExtractOptions,
) -> Result<Vec<PathBuf>> {
let items = selected(kind, input, opts)?;
std::fs::create_dir_all(out_dir)?;
if kind == Kind::SonySin
&& !opts.quiet
&& let Some(Item {
src: Src::Range { offset, .. },
..
}) = items.first()
{
eprintln!(
"note: sony-sin: skipped {offset} bytes of header (hash table and signature blocks); they are not verified, so the image is not proven authentic"
);
}
if !opts.force {
for it in &items {
if out_dir.join(&it.name).symlink_metadata().is_ok() {
bail!(
"{} already exists in {}; pass --force to overwrite",
it.name,
out_dir.display()
);
}
}
}
let file = File::open(input).with_context(|| format!("opening {}", input.display()))?;
let mut paths = Vec::new();
for it in &items {
let final_path = out_dir.join(&it.name);
let tmp = out_dir.join(format!("{}.part", it.name));
let written = (|| -> Result<()> {
let mut out = create_part(&tmp)?;
match &it.src {
Src::Range { offset, len } => write_range(&file, *offset, *len, &mut out),
Src::Chunks(chunks) => crate::lgkdz::write_chunks(&file, chunks, &mut out),
}
})();
if let Err(e) = written {
let _ = std::fs::remove_file(&tmp);
return Err(e.context(format!("extracting {}", it.name)));
}
std::fs::rename(&tmp, &final_path)?;
paths.push(final_path);
}
paths.sort_unstable();
Ok(paths)
}
fn write_range(file: &File, offset: u64, len: u64, out: &mut File) -> Result<()> {
let mut r = file.try_clone()?;
r.seek(SeekFrom::Start(offset))?;
let mut head = [0u8; 4];
let is_sparse = len >= 4 && r.read_exact(&mut head).is_ok() && head == [0x3A, 0xFF, 0x26, 0xED];
r.seek(SeekFrom::Start(offset))?;
let mut r = r.take(len);
if is_sparse {
sparse::unsparse(vec![r], out)?;
} else {
let n = std::io::copy(&mut r, out)?;
if n != len {
bail!("container ends early: wanted {len} bytes, got {n}");
}
}
Ok(())
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::testutil::Scratch;
pub(crate) fn fake_parts() -> Vec<(&'static str, Vec<u8>)> {
let mut sparse = Vec::new();
sparse.extend_from_slice(&0xED26_FF3Au32.to_le_bytes());
sparse.extend_from_slice(&[1, 0, 0, 0]); sparse.extend_from_slice(&28u16.to_le_bytes());
sparse.extend_from_slice(&12u16.to_le_bytes());
sparse.extend_from_slice(&4096u32.to_le_bytes()); sparse.extend_from_slice(&1u32.to_le_bytes()); sparse.extend_from_slice(&1u32.to_le_bytes()); sparse.extend_from_slice(&0u32.to_le_bytes()); sparse.extend_from_slice(&0xCAC1u16.to_le_bytes()); sparse.extend_from_slice(&[0, 0]);
sparse.extend_from_slice(&1u32.to_le_bytes());
sparse.extend_from_slice(&(12u32 + 4096).to_le_bytes());
sparse.extend_from_slice(&[0x5A; 4096]);
vec![("boot", vec![0xB0; 100]), ("system", sparse)]
}
pub(crate) fn expected(name: &str) -> Vec<u8> {
match name {
"boot" => vec![0xB0; 100],
_ => vec![0x5A; 4096],
}
}
#[test]
fn names_are_sanitised() {
assert_eq!(clean_name(b"SYSTEM\0\0\0").unwrap(), "system");
assert!(clean_name(b"").is_err());
assert!(clean_name(b"../x").is_err());
assert!(clean_name(b"a/b").is_err());
assert!(clean_name(b".hid").is_err());
}
#[test]
fn cpb_is_named_but_refused_with_a_clear_reason() {
let d = Scratch::new("oem-cpb");
let p = d.join("a.cpb");
std::fs::write(&p, b"whatever").unwrap();
let kind = detect(&p).unwrap();
assert_eq!(kind.id(), "coolpad-cpb");
let e = extract(kind, &p, &d.join("out"), &ExtractOptions::default()).unwrap_err();
let m = e.to_string();
assert!(
m.contains("Coolpad .cpb") && m.contains("not supported"),
"{e}"
);
assert!(m.contains("vendor-specific keys"), "{e}");
assert!(list(kind, &p, &ExtractOptions::default()).is_err());
}
#[test]
fn identify_names_each_container() {
let d = Scratch::new("oem-identify");
let parts = fake_parts();
let raw = [("boot", expected("boot"))];
crate::huawei::build_for_test(&d.join("a.app"), &parts);
crate::lgkdz::build_kdz_for_test(&d.join("b.kdz"), &raw);
crate::lgkdz::build_dz_for_test(&d.join("c.dz"), &raw);
crate::sonysin::build_for_test(&d.join("d.sin"), &parts[0].1);
std::fs::write(d.join("e.cpb"), b"opaque").unwrap();
crate::nokianb0::build_for_test(&d.join("f.NB0"), &parts);
for (file, id) in [
("a.app", "huawei-update-app"),
("b.kdz", "lg-kdz"),
("c.dz", "lg-dz"),
("d.sin", "sony-sin"),
("e.cpb", "coolpad-cpb"),
("f.NB0", "nokia-nb0"),
] {
let got = crate::detect::identify_path(&d.join(file)).unwrap();
assert_eq!(got.id, id, "{file}");
assert_eq!(Kind::from_id(id).unwrap().description(), got.description);
}
std::fs::write(d.join("g.bin"), b"opaque").unwrap();
let unknown = crate::detect::identify_path(&d.join("g.bin")).unwrap();
assert_eq!(unknown.id, "unknown");
let mut empty_zip = b"PK\x05\x06".to_vec();
empty_zip.extend_from_slice(&[0u8; 18]);
std::fs::write(d.join("h.sin"), empty_zip).unwrap();
assert_eq!(
crate::detect::identify_path(&d.join("h.sin")).unwrap().id,
"zip"
);
assert_eq!(detect(&d.join("h.sin")), None);
}
#[test]
fn synthetic_fixtures_can_be_written_for_a_manual_run() {
let scratch = Scratch::new("oem-fixtures");
let dir = std::env::var_os("ANDROID_DOCTOR_WRITE_FIXTURES")
.map(PathBuf::from)
.unwrap_or_else(|| scratch.to_path_buf());
std::fs::create_dir_all(&dir).unwrap();
let parts = fake_parts();
crate::huawei::build_for_test(&dir.join("UPDATE.APP"), &parts);
let raw = [("boot", expected("boot")), ("system", expected("system"))];
crate::lgkdz::build_kdz_for_test(&dir.join("fw.kdz"), &raw);
crate::lgkdz::build_dz_for_test(&dir.join("fw.dz"), &raw);
crate::sonysin::build_for_test(&dir.join("system_X-FLASH-ALL-1234.sin"), &parts[1].1);
std::fs::write(dir.join("fw.cpb"), b"CPB?").unwrap();
crate::nokianb0::build_for_test(&dir.join("fw.nb0"), &parts);
}
}