1use std::path::PathBuf;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ColorChoice {
16 Auto,
17 Never,
18}
19
20#[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
92fn 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
116pub enum Parsed {
118 Run(Args),
119 Help,
120}
121
122pub 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 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
188pub fn help() -> &'static str {
190 HELP
191}