diff --git a/Cargo.lock b/Cargo.lock index b12cadc..0b3156d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,6 +571,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "hxdmp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b27f28a7466846baca75f0a5244e546e44178eb7f1c07a3820f413e91c6b0" + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -832,6 +838,17 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "pprom" +version = "0.1.0" +dependencies = [ + "hxdmp", + "log", + "pretty_env_logger", + "structopt", + "thiserror", +] + [[package]] name = "pretty_env_logger" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index e69e916..c76d207 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["clicky-core", "clicky-desktop", "clicky-web", "relativity"] +members = ["clicky-core", "clicky-desktop", "clicky-web", "pprom", "relativity"] [profile.release] panic = "abort" diff --git a/pprom/Cargo.toml b/pprom/Cargo.toml new file mode 100644 index 0000000..5fb59c5 --- /dev/null +++ b/pprom/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pprom" +version = "0.1.0" +authors = ["Jean THOMAS "] +edition = "2018" + +[dependencies] +log = "0.4" +pretty_env_logger = "0.3" +structopt = "0.3" +thiserror = "1.0" +hxdmp = "0.2" diff --git a/pprom/README.md b/pprom/README.md new file mode 100644 index 0000000..bf370f4 --- /dev/null +++ b/pprom/README.md @@ -0,0 +1,48 @@ +# pprom + +PortalPlayer-based iPod ROM parsing. + +## Usage + +``` +$ cargo run -p pprom internal_rom_000000-0FFFFF-in1g.bin info + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s + Running `target/debug/pprom internal_rom_000000-0FFFFF-in1g.bin info` +model: IpodNano1g +length: 1048576 bytes +SCfg: 12 keys +``` + +``` +$ cargo run -p pprom internal_rom_000000-0FFFFF-in1g.bin keys + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s + Running `target/debug/pprom internal_rom_000000-0FFFFF-in1g.bin keys` +SCfg v1.1: 12 keys + +key: SrNm +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: FwId +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: HwId +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: Btry +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: RtcA +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: HwVr +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: Regn +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: Mod# +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: HwO1 +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: Cont +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: MLBN +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +key: DrmV +0000: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ +``` + +Data is blank in the above examples for obvious reasons. diff --git a/pprom/src/lib.rs b/pprom/src/lib.rs new file mode 100644 index 0000000..38464ce --- /dev/null +++ b/pprom/src/lib.rs @@ -0,0 +1,73 @@ +mod model; +mod syscfg; + +pub use crate::model::PMPModel; +pub use crate::syscfg::{Record, SysCfg}; + +#[derive(Debug, Clone)] +pub struct Rom { + syscfg: Option, + contents: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RomError {} + +impl std::fmt::Display for RomError { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self {} + } +} + +impl std::error::Error for RomError {} + +impl Default for Rom { + fn default() -> Rom { + Rom::new() + } +} + +impl Rom { + pub fn new() -> Rom { + Rom { + syscfg: None, + contents: Vec::new(), + } + } + + pub fn from_dump(dump: &[u8]) -> Result { + let syscfg = SysCfg::from_rom(dump); + + Ok(Rom { + syscfg, + contents: dump.to_vec(), + }) + } + + pub fn model(&self) -> PMPModel { + // A dump we couldn't find a table in tells us nothing about the model. + let syscfg = match &self.syscfg { + Some(syscfg) => syscfg, + None => return PMPModel::Unknown, + }; + + // Nor does one whose table has no `HwVr` record. + let hw_vr = match syscfg.get(b"HwVr") { + Some(record) => record, + None => return PMPModel::Unknown, + }; + + // The revision is the *second* word of the record, not the first. + let gestalt = hw_vr.word(1); + + PMPModel::from_gestalt(gestalt) + } + + pub fn syscfg(&self) -> Option<&SysCfg> { + self.syscfg.as_ref() + } + + pub fn contents(&self) -> &[u8] { + &self.contents + } +} diff --git a/pprom/src/main.rs b/pprom/src/main.rs new file mode 100644 index 0000000..15f9a55 --- /dev/null +++ b/pprom/src/main.rs @@ -0,0 +1,105 @@ +#[macro_use] +extern crate log; + +use std::fs; +use std::io; +use std::path::PathBuf; + +use structopt::StructOpt; +use thiserror::Error; + +use pprom::Rom; + +#[derive(Error, Debug)] +enum PpromError { + #[error("couldn't read ROM dump {}: {source}", .path.display())] + Read { path: PathBuf, source: io::Error }, + + #[error("couldn't parse ROM dump: {0}")] + Rom(#[from] pprom::RomError), + + #[error("no SCfg table in this dump")] + NoSysCfg, + + #[error("couldn't format a hexdump: {0}")] + Hexdump(#[from] io::Error), +} + +#[derive(StructOpt)] +#[structopt(name = "pprom")] +#[structopt(about = "Explore PortalPlayer-based iPod InternalROM dumps.")] +struct Args { + /// Path to a dumped InternalROM binary. + #[structopt(parse(from_os_str))] + rom: PathBuf, + + #[structopt(subcommand)] + cmd: Option, +} + +#[derive(StructOpt)] +enum Command { + /// Summarize the ROM. This is the default when no subcommand is given. + Info, + /// Dump every key in the ROM's `SCfg` table. + Keys, +} + +fn main() { + pretty_env_logger::init(); + + if let Err(e) = run() { + eprintln!("error: {}", e); + std::process::exit(1); + } +} + +fn run() -> Result<(), PpromError> { + let args = Args::from_args(); + + let dump = fs::read(&args.rom).map_err(|source| PpromError::Read { + path: args.rom.clone(), + source, + })?; + debug!("read {} bytes from {}", dump.len(), args.rom.display()); + + let rom = Rom::from_dump(&dump)?; + + match args.cmd.unwrap_or(Command::Info) { + Command::Info => cmd_info(&rom), + Command::Keys => cmd_keys(&rom), + } +} + +fn cmd_info(rom: &Rom) -> Result<(), PpromError> { + println!("model: {:?}", rom.model()); + println!("length: {} bytes", rom.contents().len()); + + match rom.syscfg() { + Some(cfg) => println!("SCfg: {} keys", cfg.records.len()), + None => println!("SCfg: not found"), + } + + Ok(()) +} + +fn cmd_keys(rom: &Rom) -> Result<(), PpromError> { + let cfg = rom.syscfg().ok_or(PpromError::NoSysCfg)?; + + println!( + "SCfg v{}.{}: {} keys", + cfg.version.0, + cfg.version.1, + cfg.records.len(), + ); + println!(); + + for r in &cfg.records { + println!("key: {}", r.tag_str()); + let mut buf = Vec::new(); + hxdmp::hexdump(&r.value, &mut buf)?; + println!("{}", String::from_utf8_lossy(&buf)); + } + + Ok(()) +} diff --git a/pprom/src/model.rs b/pprom/src/model.rs new file mode 100644 index 0000000..5344850 --- /dev/null +++ b/pprom/src/model.rs @@ -0,0 +1,56 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PMPModel { + /// iPod (1st Generation) + Ipod1g, + + /// iPod (2nd Generation) + Ipod2g, + + /// iPod (3rd Generation) + Ipod3g, + + /// iPod (4th Generation) + Ipod4g, + + /// iPod (5th Generation) + Ipod5g, + + /// iPod mini (1st Generation) + IpodMini1g, + + /// iPod mini (2nd Generation) + IpodMini2g, + + /// iPod color + IpodColor, + + /// iPod photo + IpodPhoto, + + /// iPod Nano (1st Generation) + IpodNano1g, + + /// Unknown Portable Media Player + Unknown, +} + +impl PMPModel { + pub fn from_gestalt(gestalt: u32) -> PMPModel { + use PMPModel::*; + + // Sourced from: http://www.ipodlinux.org/Generations/ + match gestalt { + 0x00010000 | 0x00010001 | 0x00010002 => Ipod1g, + 0x00020000 | 0x00020001 => Ipod2g, + 0x00030001 => Ipod3g, + 0x00050013 | 0x00050014 => Ipod4g, + 0x000B0005 | 0x000B0010 => Ipod5g, + 0x00040013 => IpodMini1g, + 0x00070002 => IpodMini2g, + 0x000C0005 | 0x000C0006 => IpodNano1g, + 0x00060000 => IpodPhoto, + 0x00060004 => IpodColor, + _ => Unknown, + } + } +} \ No newline at end of file diff --git a/pprom/src/syscfg.rs b/pprom/src/syscfg.rs new file mode 100644 index 0000000..3afc9f0 --- /dev/null +++ b/pprom/src/syscfg.rs @@ -0,0 +1,101 @@ +//! SCfg/SysCfg table, key/value store for serial number, model number, etc. +//! +//! 24-byte header sits at `0x2000`, followed by fixed 20-byte records: +//! +//! ```text +//! header +//! +0 tag "SCfg" +//! +4 u32 total length +//! +8 u32 base address +//! +12 u16,u16 version (?) +//! +16 u32 (zero) +//! +20 u32 record count +//! ``` +//! +//! ```text +//! record +//! +0 tag +//! +4 value (16B) +//! ``` +//! +//! Tags are stored in reverse endianness (SCfg -> gfCS) + +use std::convert::TryInto; + +/// How far into the dump to look for the header. The table lives early, and +/// bounding the scan avoids matching the copies in mirrored banks further in. +const SCAN_LIMIT: usize = 0x1_0000; + +const HEADER_LEN: usize = 24; +const RECORD_LEN: usize = 20; +const VALUE_LEN: usize = RECORD_LEN - 4; +const SCFG_TAG: &[u8; 4] = b"SCfg"; + +fn u32le(bytes: &[u8]) -> u32 { + u32::from_le_bytes(bytes.try_into().unwrap()) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Record { + pub tag: [u8; 4], + pub value: [u8; VALUE_LEN], +} + +impl Record { + pub fn tag_str(&self) -> &str { + std::str::from_utf8(&self.tag).unwrap_or("????") + } + + /// Word `n` of the value, little-endian. Panics if `n > 3`. + pub fn word(&self, n: usize) -> u32 { + u32le(&self.value[n * 4..n * 4 + 4]) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SysCfg { + pub version: (u16, u16), + pub records: Vec, +} + +impl SysCfg { + pub fn from_rom(rom: &[u8]) -> Option { + let needle = [SCFG_TAG[3], SCFG_TAG[2], SCFG_TAG[1], SCFG_TAG[0]]; + let limit = rom.len().min(SCAN_LIMIT); + let offset = rom[..limit].windows(4).position(|w| w == needle)?; + let header = rom.get(offset..offset + HEADER_LEN)?; + + let len = u32le(&header[4..8]) as usize; + //let base = u32le(&header[8..12]); + let version = ( + u16::from_le_bytes(header[12..14].try_into().ok()?), + u16::from_le_bytes(header[14..16].try_into().ok()?), + ); + let count = u32le(&header[20..24]) as usize; + + // Check that length reported in the header matches the computed size + if len != HEADER_LEN + count * RECORD_LEN { + return None; + } + + let mut records = Vec::with_capacity(count); + for i in 0..count { + let at = offset + HEADER_LEN + i * RECORD_LEN; + let raw = rom.get(at..at + RECORD_LEN)?; + records.push(Record { + tag: [raw[3], raw[2], raw[1], raw[0]], + value: raw[4..].try_into().ok()?, + }); + } + + Some(SysCfg { + version, + records, + }) + } + + /// Look up a record by tag (e.g. `b"HwVr"`) + pub fn get(&self, tag: &[u8; 4]) -> Option<&Record> { + self.records.iter().find(|r| &r.tag == tag) + } +}