Skip to main content

pcf_debug/plugin/
pfs.rs

1//! Decoders for PFS-MS records (see `specs/PFS-MS-spec-v1.0.txt`):
2//! `PFS_NODE` (partition type `0xAAAA0001`, magic `"PFSN"`) and `PFS_SESSION`
3//! (partition type `0xAAAA0002`, magic `"PFSS"`).
4//!
5//! Both decoders mirror the spec's byte tables field-for-field and report spec
6//! violations as warnings rather than failing.
7
8use pcf::HashAlgo;
9
10use super::{
11    le_u16, le_u32, le_u64, uid_at, Decoded, FieldNode, FieldValue, PartitionDecoder, PartitionMeta,
12};
13
14const PFS_NODE_TYPE: u32 = 0xAAAA_0001;
15const PFS_SESSION_TYPE: u32 = 0xAAAA_0002;
16const NODE_MAGIC: &[u8; 4] = b"PFSN";
17const SESSION_MAGIC: &[u8; 4] = b"PFSS";
18const PFS_MAX_NAME: u16 = 1024;
19
20/// Render a `<algo_id><64-byte hash>` pair as a labelled field, truncated to the
21/// algorithm's significant digest length.
22fn hash_pair(
23    label: &str,
24    data: &[u8],
25    algo_off: usize,
26    hash_off: usize,
27    warnings: &mut Vec<String>,
28) -> FieldNode {
29    let mut node = FieldNode::group(label);
30    let algo_id = data.get(algo_off).copied().unwrap_or(0);
31    let (algo_name, digest_len) = match HashAlgo::from_id(algo_id) {
32        Ok(a) => (crate::model::algo_name(a), a.digest_len()),
33        Err(_) => {
34            warnings.push(format!("{label}: unknown hash algorithm id {algo_id}"));
35            ("unknown", 0)
36        }
37    };
38    node.push(FieldNode::leaf(
39        "algo_id",
40        FieldValue::Enum {
41            raw: algo_id as u64,
42            name: algo_name.into(),
43        },
44        (algo_off as u64, algo_off as u64 + 1),
45    ));
46    if let Some(bytes) = data.get(hash_off..hash_off + 64) {
47        let sig = &bytes[..digest_len.min(64)];
48        node.push(FieldNode::leaf(
49            "hash",
50            FieldValue::Bytes(sig.to_vec()),
51            (hash_off as u64, hash_off as u64 + 64),
52        ));
53    } else {
54        warnings.push(format!("{label}: hash field runs past end of record"));
55    }
56    node
57}
58
59/// Render a `compression_algo_id` byte as a labelled enum field (Section 9.5).
60fn compression_field(data: &[u8], off: usize) -> FieldNode {
61    let id = data.get(off).copied().unwrap_or(0);
62    let name = match id {
63        0 => "none",
64        1 => "DEFLATE",
65        2 => "zstd",
66        3 => "brotli",
67        _ => "reserved",
68    };
69    FieldNode::leaf(
70        "compression_algo_id",
71        FieldValue::Enum {
72            raw: id as u64,
73            name: name.into(),
74        },
75        (off as u64, off as u64 + 1),
76    )
77}
78
79// ---------------------------------------------------------------------------
80// PFS_NODE
81// ---------------------------------------------------------------------------
82
83pub struct PfsNodeDecoder;
84
85impl PartitionDecoder for PfsNodeDecoder {
86    fn name(&self) -> &'static str {
87        "pfs-node"
88    }
89
90    fn matches(&self, meta: &PartitionMeta, data: &[u8]) -> bool {
91        meta.partition_type == PFS_NODE_TYPE || data.get(0..4) == Some(NODE_MAGIC)
92    }
93
94    fn decode(&self, _meta: &PartitionMeta, data: &[u8]) -> Decoded {
95        let mut warnings = Vec::new();
96        let mut fields = Vec::new();
97
98        if data.len() < 54 {
99            warnings.push(format!(
100                "record is {} bytes; PFS_NODE needs at least a 54-byte prefix",
101                data.len()
102            ));
103        }
104
105        // ---- fixed prefix (54 bytes) ------------------------------------
106        let mut prefix = FieldNode::group("fixed prefix");
107
108        let magic_ok = data.get(0..4) == Some(NODE_MAGIC);
109        if !magic_ok {
110            warnings.push("record_magic is not \"PFSN\"".into());
111        }
112        prefix.push(
113            FieldNode::leaf(
114                "record_magic",
115                FieldValue::Text(ascii_or_hex(data.get(0..4).unwrap_or(&[]))),
116                (0, 4),
117            )
118            .with_note(if magic_ok {
119                "magic OK"
120            } else {
121                "expected \"PFSN\""
122            }),
123        );
124
125        let version = data.get(4).copied().unwrap_or(0);
126        prefix.push(FieldNode::leaf(
127            "record_version",
128            FieldValue::U64(version as u64),
129            (4, 5),
130        ));
131
132        let kind = data.get(5).copied().unwrap_or(0);
133        let kind_name = match kind {
134            1 => "file",
135            2 => "directory",
136            _ => {
137                warnings.push(format!(
138                    "kind {kind} is reserved (valid: 1=file, 2=directory)"
139                ));
140                "reserved"
141            }
142        };
143        prefix.push(FieldNode::leaf(
144            "kind",
145            FieldValue::Enum {
146                raw: kind as u64,
147                name: kind_name.into(),
148            },
149            (5, 6),
150        ));
151
152        let flags = le_u16(data, 6).unwrap_or(0);
153        let tombstone = flags & 0x0001 != 0;
154        let mut set = Vec::new();
155        if tombstone {
156            set.push("TOMBSTONE".to_string());
157        }
158        if flags & !0x0001 != 0 {
159            warnings.push(format!("flags {flags:#06x} sets reserved bits (must be 0)"));
160        }
161        prefix.push(FieldNode::leaf(
162            "flags",
163            FieldValue::Flags {
164                raw: flags as u64,
165                set,
166            },
167            (6, 8),
168        ));
169
170        if let Some(node_id) = uid_at(data, 8) {
171            prefix.push(FieldNode::leaf(
172                "node_id",
173                FieldValue::Uid(node_id),
174                (8, 24),
175            ));
176        }
177        if let Some(parent_id) = uid_at(data, 24) {
178            prefix.push(FieldNode::leaf(
179                "parent_id",
180                FieldValue::Uid(parent_id),
181                (24, 40),
182            ));
183        }
184        let mtime = le_u64(data, 40).unwrap_or(0);
185        prefix.push(FieldNode::leaf(
186            "mtime_unix_ms",
187            FieldValue::U64(mtime),
188            (40, 48),
189        ));
190        let mode = le_u32(data, 48).unwrap_or(0);
191        prefix.push(
192            FieldNode::leaf("mode", FieldValue::U64(mode as u64), (48, 52))
193                .with_note(format!("{mode:#o}")),
194        );
195        let name_len = le_u16(data, 52).unwrap_or(0);
196        if name_len > PFS_MAX_NAME {
197            warnings.push(format!(
198                "name_len {name_len} exceeds PFS_MAX_NAME ({PFS_MAX_NAME})"
199            ));
200        }
201        prefix.push(FieldNode::leaf(
202            "name_len",
203            FieldValue::U64(name_len as u64),
204            (52, 54),
205        ));
206        fields.push(prefix);
207
208        // ---- name --------------------------------------------------------
209        let name_end = 54usize + name_len as usize;
210        let name_bytes = data.get(54..name_end).unwrap_or(&[]);
211        if name_bytes.len() != name_len as usize {
212            warnings.push("name runs past end of record".into());
213        }
214        if name_bytes.contains(&0x00) || name_bytes.contains(&b'/') {
215            warnings.push("name must not contain NUL or '/'".into());
216        }
217        let name = String::from_utf8_lossy(name_bytes).into_owned();
218        fields.push(FieldNode::leaf(
219            "name",
220            FieldValue::Text(name),
221            (54, name_end as u64),
222        ));
223
224        // ---- content section (files only, when not tombstoned) ----------
225        if kind == 1 && !tombstone {
226            fields.push(decode_content(data, name_end, &mut warnings));
227        }
228
229        Decoded {
230            format_name: "PFS_NODE".into(),
231            fields,
232            warnings,
233        }
234    }
235}
236
237fn decode_content(data: &[u8], s: usize, warnings: &mut Vec<String>) -> FieldNode {
238    let mut content = FieldNode::group("content");
239    let content_kind = data.get(s).copied().unwrap_or(0xff);
240    let ck_name = match content_kind {
241        0 => "EMPTY",
242        1 => "DIRECT",
243        2 => "DELTA",
244        3 => "INHERIT",
245        _ => {
246            warnings.push(format!("content_kind {content_kind} is unknown"));
247            "unknown"
248        }
249    };
250    content.push(FieldNode::leaf(
251        "content_kind",
252        FieldValue::Enum {
253            raw: content_kind as u64,
254            name: ck_name.into(),
255        },
256        (s as u64, s as u64 + 1),
257    ));
258
259    match content_kind {
260        0 | 3 => {} // EMPTY / INHERIT: no further bytes.
261        1 => {
262            // DIRECT, 91 bytes total (Section 7.3).
263            content.push(compression_field(data, s + 1));
264            if let Some(uid) = uid_at(data, s + 2) {
265                content.push(FieldNode::leaf(
266                    "content_uid",
267                    FieldValue::Uid(uid),
268                    (s as u64 + 2, s as u64 + 18),
269                ));
270            }
271            let full_size = le_u64(data, s + 18).unwrap_or(0);
272            content.push(FieldNode::leaf(
273                "full_size",
274                FieldValue::U64(full_size),
275                (s as u64 + 18, s as u64 + 26),
276            ));
277            content.push(hash_pair("full_hash", data, s + 26, s + 27, warnings));
278            check_trailing(data, s + 91, warnings);
279        }
280        2 => {
281            // DELTA, 165 bytes total (Section 7.3).
282            let patch_algo = data.get(s + 1).copied().unwrap_or(0);
283            let patch_name = if patch_algo == 1 {
284                "VCDIFF"
285            } else {
286                "reserved"
287            };
288            content.push(FieldNode::leaf(
289                "patch_algo_id",
290                FieldValue::Enum {
291                    raw: patch_algo as u64,
292                    name: patch_name.into(),
293                },
294                (s as u64 + 1, s as u64 + 2),
295            ));
296            content.push(compression_field(data, s + 2));
297            if let Some(uid) = uid_at(data, s + 3) {
298                content.push(FieldNode::leaf(
299                    "patch_uid",
300                    FieldValue::Uid(uid),
301                    (s as u64 + 3, s as u64 + 19),
302                ));
303            }
304            let full_size = le_u64(data, s + 19).unwrap_or(0);
305            content.push(FieldNode::leaf(
306                "full_size",
307                FieldValue::U64(full_size),
308                (s as u64 + 19, s as u64 + 27),
309            ));
310            content.push(hash_pair("full_hash", data, s + 27, s + 28, warnings));
311            let base_size = le_u64(data, s + 92).unwrap_or(0);
312            content.push(FieldNode::leaf(
313                "base_full_size",
314                FieldValue::U64(base_size),
315                (s as u64 + 92, s as u64 + 100),
316            ));
317            content.push(hash_pair(
318                "base_full_hash",
319                data,
320                s + 100,
321                s + 101,
322                warnings,
323            ));
324            check_trailing(data, s + 165, warnings);
325        }
326        _ => {}
327    }
328    content
329}
330
331fn check_trailing(data: &[u8], end: usize, warnings: &mut Vec<String>) {
332    if data.len() > end {
333        warnings.push(format!(
334            "{} trailing byte(s) after record",
335            data.len() - end
336        ));
337    }
338}
339
340// ---------------------------------------------------------------------------
341// PFS_SESSION
342// ---------------------------------------------------------------------------
343
344pub struct PfsSessionDecoder;
345
346impl PartitionDecoder for PfsSessionDecoder {
347    fn name(&self) -> &'static str {
348        "pfs-session"
349    }
350
351    fn matches(&self, meta: &PartitionMeta, data: &[u8]) -> bool {
352        meta.partition_type == PFS_SESSION_TYPE || data.get(0..4) == Some(SESSION_MAGIC)
353    }
354
355    fn decode(&self, _meta: &PartitionMeta, data: &[u8]) -> Decoded {
356        let mut warnings = Vec::new();
357        let mut fields = Vec::new();
358
359        if data.len() < 162 {
360            warnings.push(format!(
361                "record is {} bytes; PFS_SESSION needs at least 162",
362                data.len()
363            ));
364        }
365
366        let magic_ok = data.get(0..4) == Some(SESSION_MAGIC);
367        if !magic_ok {
368            warnings.push("record_magic is not \"PFSS\"".into());
369        }
370        fields.push(
371            FieldNode::leaf(
372                "record_magic",
373                FieldValue::Text(ascii_or_hex(data.get(0..4).unwrap_or(&[]))),
374                (0, 4),
375            )
376            .with_note(if magic_ok {
377                "magic OK"
378            } else {
379                "expected \"PFSS\""
380            }),
381        );
382
383        fields.push(FieldNode::leaf(
384            "profile_version_major",
385            FieldValue::U64(data.get(4).copied().unwrap_or(0) as u64),
386            (4, 5),
387        ));
388        fields.push(FieldNode::leaf(
389            "profile_version_minor",
390            FieldValue::U64(data.get(5).copied().unwrap_or(0) as u64),
391            (5, 6),
392        ));
393
394        let reserved = le_u16(data, 6).unwrap_or(0);
395        if reserved != 0 {
396            warnings.push(format!("reserved field is {reserved:#06x} (must be 0)"));
397        }
398        fields.push(FieldNode::leaf(
399            "reserved",
400            FieldValue::U64(reserved as u64),
401            (6, 8),
402        ));
403
404        let session_seq = le_u64(data, 8).unwrap_or(0);
405        fields.push(FieldNode::leaf(
406            "session_seq",
407            FieldValue::U64(session_seq),
408            (8, 16),
409        ));
410        let timestamp = le_u64(data, 16).unwrap_or(0);
411        fields.push(FieldNode::leaf(
412            "timestamp_unix_ms",
413            FieldValue::U64(timestamp),
414            (16, 24),
415        ));
416
417        let prev_algo = data.get(24).copied().unwrap_or(0);
418        fields.push(hash_pair("prev_session_hash", data, 24, 25, &mut warnings));
419
420        let block_count = le_u32(data, 89).unwrap_or(0);
421        if block_count == 0 {
422            warnings.push("block_count must be >= 1".into());
423        }
424        fields.push(FieldNode::leaf(
425            "block_count",
426            FieldValue::U64(block_count as u64),
427            (89, 93),
428        ));
429
430        let member_algo = data.get(93).copied().unwrap_or(0);
431        fields.push(hash_pair(
432            "member_blocks_digest",
433            data,
434            93,
435            94,
436            &mut warnings,
437        ));
438
439        // Spec consistency rules.
440        if prev_algo == 0 && !all_zero(data.get(25..89).unwrap_or(&[])) {
441            warnings.push("prev_session_hash must be 64 zero bytes when its algo id is 0".into());
442        }
443        if block_count == 1 && (member_algo != 0 || !all_zero(data.get(94..158).unwrap_or(&[]))) {
444            warnings.push("member_blocks_digest must be zero when block_count == 1".into());
445        }
446
447        let change_count = le_u16(data, 158).unwrap_or(0);
448        fields.push(
449            FieldNode::leaf(
450                "change_count",
451                FieldValue::U64(change_count as u64),
452                (158, 160),
453            )
454            .with_note("informational"),
455        );
456
457        let writer_len = le_u16(data, 160).unwrap_or(0);
458        fields.push(FieldNode::leaf(
459            "writer_len",
460            FieldValue::U64(writer_len as u64),
461            (160, 162),
462        ));
463        let writer_end = 162usize + writer_len as usize;
464        let writer_bytes = data.get(162..writer_end).unwrap_or(&[]);
465        if writer_bytes.len() != writer_len as usize {
466            warnings.push("writer runs past end of record".into());
467        }
468        if writer_len > 0 {
469            fields.push(FieldNode::leaf(
470                "writer",
471                FieldValue::Text(String::from_utf8_lossy(writer_bytes).into_owned()),
472                (162, writer_end as u64),
473            ));
474        }
475        check_trailing(data, writer_end, &mut warnings);
476
477        Decoded {
478            format_name: "PFS_SESSION".into(),
479            fields,
480            warnings,
481        }
482    }
483}
484
485fn all_zero(b: &[u8]) -> bool {
486    b.iter().all(|&x| x == 0)
487}
488
489/// Render up to four bytes as ASCII if printable, else as hex.
490fn ascii_or_hex(b: &[u8]) -> String {
491    if !b.is_empty() && b.iter().all(|&c| (0x20..0x7f).contains(&c)) {
492        String::from_utf8_lossy(b).into_owned()
493    } else {
494        b.iter()
495            .map(|c| format!("{c:02x}"))
496            .collect::<Vec<_>>()
497            .join(" ")
498    }
499}