1use 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
20fn 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
59fn 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
79pub 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 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 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 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 => {} 1 => {
262 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 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
340pub 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 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
489fn 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}