pcf_debug/plugin/mod.rs
1//! The partition-decoder plugin system.
2//!
3//! A *decoder* turns a partition's raw bytes into a renderer-agnostic tree of
4//! named fields ([`FieldNode`]). The CLI and HTML renderers both consume that
5//! tree, so a decoder is written once and displayed everywhere.
6//!
7//! Decoders are registered statically (compiled into the binary). Adding a new
8//! format means writing a module that implements [`PartitionDecoder`] and adding
9//! one line to [`DecoderRegistry::with_builtins`]. The trait is deliberately
10//! object-safe and the data types carry no borrowed state, so a future dynamic
11//! (shared-library) backend could be added behind a feature without reworking
12//! any decoder.
13
14mod pfs;
15mod raw;
16
17pub use pfs::{PfsNodeDecoder, PfsSessionDecoder};
18pub use raw::RawDecoder;
19
20/// A decoded field's value, kept independent of any output format.
21#[derive(Debug, Clone, PartialEq)]
22pub enum FieldValue {
23 /// A grouping node with no value of its own.
24 None,
25 U64(u64),
26 Bytes(Vec<u8>),
27 Text(String),
28 Uid([u8; 16]),
29 /// A numeric code with a human name, e.g. `kind = 1 (file)`.
30 Enum {
31 raw: u64,
32 name: String,
33 },
34 /// A bitset with the names of the bits that are set.
35 Flags {
36 raw: u64,
37 set: Vec<String>,
38 },
39}
40
41/// One node in a decoded field tree.
42#[derive(Debug, Clone, PartialEq)]
43pub struct FieldNode {
44 pub name: String,
45 pub value: FieldValue,
46 /// Byte range *within the partition data* this field occupies, if any.
47 pub range: Option<(u64, u64)>,
48 /// An optional remark, e.g. `"magic OK"` or `"reserved must be 0"`.
49 pub note: Option<String>,
50 pub children: Vec<FieldNode>,
51}
52
53impl FieldNode {
54 /// A grouping node (no value, no range).
55 pub fn group(name: impl Into<String>) -> Self {
56 Self {
57 name: name.into(),
58 value: FieldValue::None,
59 range: None,
60 note: None,
61 children: Vec::new(),
62 }
63 }
64
65 /// A leaf node carrying a value and the byte range it covers.
66 pub fn leaf(name: impl Into<String>, value: FieldValue, range: (u64, u64)) -> Self {
67 Self {
68 name: name.into(),
69 value,
70 range: Some(range),
71 note: None,
72 children: Vec::new(),
73 }
74 }
75
76 /// Attach a note (builder style).
77 pub fn with_note(mut self, note: impl Into<String>) -> Self {
78 self.note = Some(note.into());
79 self
80 }
81
82 /// Append a child (builder style).
83 pub fn child(mut self, c: FieldNode) -> Self {
84 self.children.push(c);
85 self
86 }
87
88 /// Append a child in place.
89 pub fn push(&mut self, c: FieldNode) {
90 self.children.push(c);
91 }
92}
93
94/// Metadata handed to a decoder alongside the partition's bytes.
95#[derive(Debug, Clone, Copy)]
96pub struct PartitionMeta<'a> {
97 pub partition_type: u32,
98 pub uid: &'a [u8; 16],
99 pub label: &'a str,
100}
101
102/// The result of decoding one partition.
103#[derive(Debug, Clone)]
104pub struct Decoded {
105 /// Human name of the format that was decoded, e.g. `"PFS_NODE"`.
106 pub format_name: String,
107 pub fields: Vec<FieldNode>,
108 /// Non-fatal spec violations and remarks surfaced to the user.
109 pub warnings: Vec<String>,
110}
111
112/// A plugin that turns partition bytes into a field tree.
113pub trait PartitionDecoder {
114 /// Stable identifier, used for `--decoder` selection and HTML anchors.
115 fn name(&self) -> &'static str;
116
117 /// Cheap test: does this decoder claim the partition? May inspect the type
118 /// and/or sniff a magic prefix.
119 fn matches(&self, meta: &PartitionMeta, data: &[u8]) -> bool;
120
121 /// Full decode. Must never panic: on malformed input it returns whatever
122 /// fields it could read plus `warnings`.
123 fn decode(&self, meta: &PartitionMeta, data: &[u8]) -> Decoded;
124}
125
126/// An ordered set of decoders. The first decoder whose `matches` returns true
127/// wins; [`RawDecoder`] is always last and matches everything.
128pub struct DecoderRegistry {
129 decoders: Vec<Box<dyn PartitionDecoder>>,
130}
131
132impl DecoderRegistry {
133 /// The registry with all built-in decoders: PFS node, PFS session, then the
134 /// raw fallback.
135 pub fn with_builtins() -> Self {
136 Self {
137 decoders: vec![
138 Box::new(PfsNodeDecoder),
139 Box::new(PfsSessionDecoder),
140 Box::new(RawDecoder),
141 ],
142 }
143 }
144
145 /// Insert a decoder ahead of the raw fallback.
146 pub fn register(&mut self, d: Box<dyn PartitionDecoder>) {
147 let insert_at = self.decoders.len().saturating_sub(1);
148 self.decoders.insert(insert_at, d);
149 }
150
151 /// All decoder names, in priority order.
152 pub fn names(&self) -> Vec<&'static str> {
153 self.decoders.iter().map(|d| d.name()).collect()
154 }
155
156 /// Decode `data`, picking the first matching decoder.
157 pub fn decode(&self, meta: &PartitionMeta, data: &[u8]) -> Decoded {
158 for d in &self.decoders {
159 if d.matches(meta, data) {
160 return d.decode(meta, data);
161 }
162 }
163 // RawDecoder matches everything, so this is unreachable in practice.
164 RawDecoder.decode(meta, data)
165 }
166
167 /// Decode with a specific decoder by name, if present.
168 pub fn decode_with(&self, name: &str, meta: &PartitionMeta, data: &[u8]) -> Option<Decoded> {
169 self.decoders
170 .iter()
171 .find(|d| d.name() == name)
172 .map(|d| d.decode(meta, data))
173 }
174}
175
176impl Default for DecoderRegistry {
177 fn default() -> Self {
178 Self::with_builtins()
179 }
180}
181
182/// Read a little-endian `u16` at `off`, or `None` if out of bounds.
183pub(crate) fn le_u16(data: &[u8], off: usize) -> Option<u16> {
184 Some(u16::from_le_bytes(data.get(off..off + 2)?.try_into().ok()?))
185}
186
187/// Read a little-endian `u32` at `off`, or `None` if out of bounds.
188pub(crate) fn le_u32(data: &[u8], off: usize) -> Option<u32> {
189 Some(u32::from_le_bytes(data.get(off..off + 4)?.try_into().ok()?))
190}
191
192/// Read a little-endian `u64` at `off`, or `None` if out of bounds.
193pub(crate) fn le_u64(data: &[u8], off: usize) -> Option<u64> {
194 Some(u64::from_le_bytes(data.get(off..off + 8)?.try_into().ok()?))
195}
196
197/// Read a 16-byte UID at `off`.
198pub(crate) fn uid_at(data: &[u8], off: usize) -> Option<[u8; 16]> {
199 data.get(off..off + 16)?.try_into().ok()
200}