Skip to main content

pcf_debug/cli/
mod.rs

1//! Command-line parsing for `pcf-debug`.
2//!
3//! Hand-written on top of [`std::env::args`] to keep the tool free of any
4//! argument-parsing dependency (matching the reference crate's minimal-deps
5//! posture). The grammar is:
6//!
7//! ```text
8//! pcf-debug <FILE> [subcommand] [flags]
9//! ```
10
11use std::path::PathBuf;
12
13/// Whether to colour the output.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ColorChoice {
16    Auto,
17    Never,
18}
19
20/// A byte range for `hexdump --range start[:len]`.
21#[derive(Debug, Clone, Copy)]
22pub struct ByteRange {
23    pub start: u64,
24    pub len: Option<u64>,
25}
26
27#[derive(Debug, Clone, Default)]
28pub struct HexOpts {
29    pub region: Option<String>,
30    pub range: Option<ByteRange>,
31    pub max_bytes: usize,
32}
33
34#[derive(Debug, Clone, Default)]
35pub struct DecodeOpts {
36    pub uid: Option<String>,
37    pub label: Option<String>,
38    pub decoder: Option<String>,
39}
40
41#[derive(Debug, Clone)]
42pub enum Command {
43    Inspect,
44    Layout,
45    Table,
46    Chain,
47    Hexdump(HexOpts),
48    Decode(DecodeOpts),
49}
50
51#[derive(Debug, Clone)]
52pub struct Args {
53    pub file: PathBuf,
54    pub command: Command,
55    pub html: Option<PathBuf>,
56    pub color: ColorChoice,
57    pub verify: bool,
58}
59
60const HELP: &str = "\
61pcf-debug — inspect and visualise Partitioned Container Format (PCF) files
62
63USAGE:
64    pcf-debug <FILE> [SUBCOMMAND] [FLAGS]
65
66SUBCOMMANDS:
67    inspect   (default) byte map + layout + partition table + chain + diagnostics
68    layout    physical region map only
69    table     partition table only
70    chain     table-block chain tree only
71    hexdump   hexdump regions or an explicit byte range
72    decode    run partition decoders and print field trees
73
74GLOBAL FLAGS:
75    --html <FILE>     also write a self-contained HTML report
76    --no-color        disable ANSI colour (auto-off when stdout is not a TTY)
77    --verify          compute and check hashes (default for inspect)
78    --no-verify       skip hashing
79    -h, --help        show this help
80
81HEXDUMP FLAGS:
82    --region <NAME>   limit to regions matching a kind/label/uid substring
83    --range <S[:L]>   hexdump bytes [S, S+L) (decimal or 0x-hex); L defaults to EOF
84    --max-bytes <N>   cap bytes shown per region (default 512)
85
86DECODE FLAGS:
87    --uid <HEX>       decode only the partition with this UID (hex prefix)
88    --label <S>       decode only partitions whose label contains S
89    --decoder <NAME>  force a specific decoder (e.g. pfs-node, pfs-session, raw)
90";
91
92/// Parse a number that may be decimal or `0x`-prefixed hex.
93fn parse_u64(s: &str) -> Result<u64, String> {
94    let s = s.trim();
95    let r = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
96        u64::from_str_radix(hex, 16)
97    } else {
98        s.parse::<u64>()
99    };
100    r.map_err(|_| format!("invalid number: {s:?}"))
101}
102
103fn parse_range(s: &str) -> Result<ByteRange, String> {
104    match s.split_once(':') {
105        Some((a, b)) => Ok(ByteRange {
106            start: parse_u64(a)?,
107            len: Some(parse_u64(b)?),
108        }),
109        None => Ok(ByteRange {
110            start: parse_u64(s)?,
111            len: None,
112        }),
113    }
114}
115
116/// Outcome of parsing: either runnable args, or a message to print and exit.
117pub enum Parsed {
118    Run(Args),
119    Help,
120}
121
122/// Parse arguments (excluding `argv[0]`).
123pub fn parse(argv: &[String]) -> Result<Parsed, String> {
124    let mut positionals: Vec<String> = Vec::new();
125    let mut html: Option<PathBuf> = None;
126    let mut color = ColorChoice::Auto;
127    let mut verify = true;
128    let mut hex = HexOpts {
129        max_bytes: 512,
130        ..Default::default()
131    };
132    let mut dec = DecodeOpts::default();
133
134    // Take the value following a flag, advancing the cursor.
135    fn value(argv: &[String], i: &mut usize, flag: &str) -> Result<String, String> {
136        *i += 1;
137        argv.get(*i)
138            .cloned()
139            .ok_or_else(|| format!("flag {flag} needs a value"))
140    }
141
142    let mut i = 0;
143    while i < argv.len() {
144        let a = argv[i].clone();
145        match a.as_str() {
146            "-h" | "--help" => return Ok(Parsed::Help),
147            "--no-color" => color = ColorChoice::Never,
148            "--verify" => verify = true,
149            "--no-verify" => verify = false,
150            "--html" => html = Some(PathBuf::from(value(argv, &mut i, &a)?)),
151            "--region" => hex.region = Some(value(argv, &mut i, &a)?),
152            "--range" => hex.range = Some(parse_range(&value(argv, &mut i, &a)?)?),
153            "--max-bytes" => hex.max_bytes = parse_u64(&value(argv, &mut i, &a)?)? as usize,
154            "--uid" => dec.uid = Some(value(argv, &mut i, &a)?),
155            "--label" => dec.label = Some(value(argv, &mut i, &a)?),
156            "--decoder" => dec.decoder = Some(value(argv, &mut i, &a)?),
157            other if other.starts_with('-') => {
158                return Err(format!("unknown flag: {other}"));
159            }
160            _ => positionals.push(a.clone()),
161        }
162        i += 1;
163    }
164
165    if positionals.is_empty() {
166        return Err("missing FILE argument".into());
167    }
168    let file = PathBuf::from(&positionals[0]);
169    let command = match positionals.get(1).map(|s| s.as_str()) {
170        None | Some("inspect") => Command::Inspect,
171        Some("layout") => Command::Layout,
172        Some("table") => Command::Table,
173        Some("chain") => Command::Chain,
174        Some("hexdump") => Command::Hexdump(hex),
175        Some("decode") => Command::Decode(dec),
176        Some(other) => return Err(format!("unknown subcommand: {other}")),
177    };
178
179    Ok(Parsed::Run(Args {
180        file,
181        command,
182        html,
183        color,
184        verify,
185    }))
186}
187
188/// The full help text.
189pub fn help() -> &'static str {
190    HELP
191}