Skip to main content

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}