1use core::fmt;
12use std::fmt::Write as _;
13use std::path::{Path, PathBuf};
14
15use crate::encryption::EncryptionScheme;
16use crate::error::{SmallHex, VfsError, VfsResult};
17use crate::fs::{FileId, FsKind, StreamId};
18use crate::pathspec::{Guid, Layer, NodeAddr, PathSpec, SnapshotRef};
19use crate::registry::ContainerFormat;
20use crate::volume::VolumeScheme;
21
22const SCHEME: &str = "fvfs:";
23const LAYER_SEP: char = '|';
24
25fn err(detail: &str, ctx: &str) -> VfsError {
26 VfsError::Decode {
27 layer: "pathspec-uri",
28 offset: 0,
29 detail: detail.to_string(),
30 bytes: SmallHex::new(ctx.as_bytes()),
31 }
32}
33
34fn pct_encode(bytes: &[u8]) -> String {
37 let mut s = String::with_capacity(bytes.len());
38 for &b in bytes {
39 if b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-') {
40 s.push(b as char);
41 } else {
42 let _ = write!(s, "%{b:02X}");
43 }
44 }
45 s
46}
47
48fn pct_decode(s: &str) -> VfsResult<Vec<u8>> {
49 let b = s.as_bytes();
50 let mut out = Vec::with_capacity(b.len());
51 let mut i = 0;
52 while let Some(&c) = b.get(i) {
53 if c == b'%' {
54 let hex = b
55 .get(i + 1..i + 3)
56 .ok_or_else(|| err("truncated percent escape", s))?;
57 let hex = core::str::from_utf8(hex).map_err(|_| err("non-ascii percent escape", s))?;
58 let v = u8::from_str_radix(hex, 16).map_err(|_| err("bad percent hex", s))?;
59 out.push(v);
60 i += 3;
61 } else {
62 out.push(c);
63 i += 1;
64 }
65 }
66 Ok(out)
67}
68
69#[cfg(unix)]
70fn path_to_bytes(p: &Path) -> Vec<u8> {
71 use std::os::unix::ffi::OsStrExt;
72 p.as_os_str().as_bytes().to_vec()
73}
74#[cfg(not(unix))]
75fn path_to_bytes(p: &Path) -> Vec<u8> {
76 p.to_string_lossy().into_owned().into_bytes()
77}
78#[cfg(unix)]
79fn bytes_to_path(b: &[u8]) -> PathBuf {
80 use std::os::unix::ffi::OsStrExt;
81 PathBuf::from(std::ffi::OsStr::from_bytes(b))
82}
83#[cfg(not(unix))]
84fn bytes_to_path(b: &[u8]) -> PathBuf {
85 PathBuf::from(String::from_utf8_lossy(b).into_owned())
86}
87
88fn container_token(f: ContainerFormat) -> &'static str {
91 match f {
92 ContainerFormat::Ewf => "ewf",
93 ContainerFormat::Vmdk => "vmdk",
94 ContainerFormat::Vhdx => "vhdx",
95 ContainerFormat::Vhd => "vhd",
96 ContainerFormat::Qcow2 => "qcow2",
97 ContainerFormat::Dmg => "dmg",
98 ContainerFormat::Aff4 => "aff4",
99 ContainerFormat::Ad1 => "ad1",
100 ContainerFormat::Dar => "dar",
101 ContainerFormat::Raw => "raw",
102 ContainerFormat::Auto => "auto",
103 }
104}
105fn parse_container(t: &str, ctx: &str) -> VfsResult<ContainerFormat> {
106 Ok(match t {
107 "ewf" => ContainerFormat::Ewf,
108 "vmdk" => ContainerFormat::Vmdk,
109 "vhdx" => ContainerFormat::Vhdx,
110 "vhd" => ContainerFormat::Vhd,
111 "qcow2" => ContainerFormat::Qcow2,
112 "dmg" => ContainerFormat::Dmg,
113 "aff4" => ContainerFormat::Aff4,
114 "ad1" => ContainerFormat::Ad1,
115 "dar" => ContainerFormat::Dar,
116 "raw" => ContainerFormat::Raw,
117 "auto" => ContainerFormat::Auto,
118 _ => return Err(err("unknown container format", ctx)),
119 })
120}
121fn volume_token(s: VolumeScheme) -> &'static str {
122 match s {
123 VolumeScheme::Mbr => "mbr",
124 VolumeScheme::Gpt => "gpt",
125 VolumeScheme::Apm => "apm",
126 VolumeScheme::Vss => "vss",
127 VolumeScheme::ApfsContainer => "apfscontainer",
128 VolumeScheme::Lvm => "lvm",
129 }
130}
131fn parse_volume_scheme(t: &str, ctx: &str) -> VfsResult<VolumeScheme> {
132 Ok(match t {
133 "mbr" => VolumeScheme::Mbr,
134 "gpt" => VolumeScheme::Gpt,
135 "apm" => VolumeScheme::Apm,
136 "vss" => VolumeScheme::Vss,
137 "apfscontainer" => VolumeScheme::ApfsContainer,
138 "lvm" => VolumeScheme::Lvm,
139 _ => return Err(err("unknown volume scheme", ctx)),
140 })
141}
142fn encryption_token(s: EncryptionScheme) -> &'static str {
143 match s {
144 EncryptionScheme::Bitlocker => "bitlocker",
145 EncryptionScheme::Luks1 => "luks1",
146 EncryptionScheme::Luks2 => "luks2",
147 EncryptionScheme::FileVault => "filevault",
148 EncryptionScheme::ApfsEncrypted => "apfsencrypted",
149 EncryptionScheme::VeraCrypt => "veracrypt",
150 }
151}
152fn parse_encryption(t: &str, ctx: &str) -> VfsResult<EncryptionScheme> {
153 Ok(match t {
154 "bitlocker" => EncryptionScheme::Bitlocker,
155 "luks1" => EncryptionScheme::Luks1,
156 "luks2" => EncryptionScheme::Luks2,
157 "filevault" => EncryptionScheme::FileVault,
158 "apfsencrypted" => EncryptionScheme::ApfsEncrypted,
159 "veracrypt" => EncryptionScheme::VeraCrypt,
160 _ => return Err(err("unknown encryption scheme", ctx)),
161 })
162}
163fn parse_fs_kind(t: &str, ctx: &str) -> VfsResult<FsKind> {
164 FsKind::known()
165 .iter()
166 .copied()
167 .find(|k| k.as_str() == t)
168 .ok_or_else(|| err("unknown filesystem kind", ctx))
169}
170
171fn u64_field(t: &str, ctx: &str) -> VfsResult<u64> {
172 t.parse::<u64>().map_err(|_| err("expected integer", ctx))
173}
174fn u32_field(t: &str, ctx: &str) -> VfsResult<u32> {
175 t.parse::<u32>().map_err(|_| err("expected u32", ctx))
176}
177fn usize_field(t: &str, ctx: &str) -> VfsResult<usize> {
178 t.parse::<usize>().map_err(|_| err("expected index", ctx))
179}
180
181fn guid_hex(g: Guid) -> String {
182 let mut s = String::with_capacity(32);
183 for b in g.0 {
184 let _ = write!(s, "{b:02x}");
185 }
186 s
187}
188fn parse_guid(t: &str, ctx: &str) -> VfsResult<Guid> {
189 if t.len() != 32 {
190 return Err(err("guid must be 32 hex chars", ctx));
191 }
192 let mut out = [0u8; 16];
193 for (i, slot) in out.iter_mut().enumerate() {
194 let pair = t
195 .get(i * 2..i * 2 + 2)
196 .ok_or_else(|| err("bad guid", ctx))?;
197 *slot = u8::from_str_radix(pair, 16).map_err(|_| err("bad guid hex", ctx))?;
198 }
199 Ok(Guid(out))
200}
201
202fn file_id_token(id: FileId) -> String {
204 match id {
205 FileId::NtfsRef { entry, seq } => format!("ntfsref.{entry}.{seq}"),
206 FileId::ExtInode { ino, gen } => format!("extinode.{ino}.{gen}"),
207 FileId::ApfsOid { oid, xid } => format!("apfsoid.{oid}.{xid}"),
208 FileId::FatDirEntry { cluster, index } => format!("fatdirentry.{cluster}.{index}"),
209 FileId::IsoExtent { block } => format!("isoextent.{block}"),
210 FileId::Opaque(n) => format!("opaque.{n}"),
211 }
212}
213fn parse_file_id(t: &str, ctx: &str) -> VfsResult<FileId> {
214 let mut it = t.split('.');
215 let head = it.next().ok_or_else(|| err("empty file id", ctx))?;
216 let mut num = |name: &str| -> VfsResult<u64> {
217 it.next()
218 .ok_or_else(|| err(name, ctx))
219 .and_then(|s| u64_field(s, ctx))
220 };
221 Ok(match head {
222 "ntfsref" => FileId::NtfsRef {
223 entry: num("entry")?,
224 seq: num("seq")? as u16,
225 },
226 "extinode" => FileId::ExtInode {
227 ino: num("ino")?,
228 gen: num("gen")? as u32,
229 },
230 "apfsoid" => FileId::ApfsOid {
231 oid: num("oid")?,
232 xid: num("xid")?,
233 },
234 "fatdirentry" => FileId::FatDirEntry {
235 cluster: num("cluster")? as u32,
236 index: num("index")? as u16,
237 },
238 "isoextent" => FileId::IsoExtent {
239 block: num("block")? as u32,
240 },
241 "opaque" => FileId::Opaque(num("n")?),
242 _ => return Err(err("unknown file id kind", ctx)),
243 })
244}
245
246fn stream_token(s: StreamId) -> String {
247 match s {
248 StreamId::Default => "default".to_string(),
249 StreamId::Named(n) => format!("named.{n}"),
250 StreamId::ResourceFork => "resourcefork".to_string(),
251 StreamId::Xattr(n) => format!("xattr.{n}"),
252 StreamId::Slack => "slack".to_string(),
253 }
254}
255fn parse_stream(t: &str, ctx: &str) -> VfsResult<StreamId> {
256 let mut it = t.split('.');
257 let head = it.next().unwrap_or("");
258 Ok(match head {
259 "default" => StreamId::Default,
260 "resourcefork" => StreamId::ResourceFork,
261 "slack" => StreamId::Slack,
262 "named" => StreamId::Named(u32_field(it.next().unwrap_or(""), ctx)? as u16),
263 "xattr" => StreamId::Xattr(u32_field(it.next().unwrap_or(""), ctx)? as u16),
264 _ => return Err(err("unknown stream id", ctx)),
265 })
266}
267
268fn node_addr_encode(a: &NodeAddr) -> String {
269 let comps = |cs: &[Vec<u8>]| -> String {
270 let mut s = String::new();
271 for c in cs {
272 s.push('/');
273 s.push_str(&pct_encode(c));
274 }
275 s
276 };
277 match a {
278 NodeAddr::Path(cs) => format!("p{}", comps(cs)),
279 NodeAddr::File(id) => format!("f/{}", file_id_token(*id)),
280 NodeAddr::Both { path, id } => format!("b/{}{}", file_id_token(*id), comps(path)),
281 }
282}
283fn parse_node_addr(t: &str, ctx: &str) -> VfsResult<NodeAddr> {
284 let decode_comps = |rest: &str| -> VfsResult<Vec<Vec<u8>>> {
285 if rest.is_empty() {
286 return Ok(Vec::new());
287 }
288 let body = rest.strip_prefix('/').unwrap_or(rest);
290 body.split('/').map(pct_decode).collect()
291 };
292 let (tag, rest) = t.split_at(t.chars().next().map_or(0, char::len_utf8));
293 match tag {
294 "p" => Ok(NodeAddr::Path(decode_comps(rest)?)),
295 "f" => {
296 let id = rest.strip_prefix('/').unwrap_or(rest);
297 Ok(NodeAddr::File(parse_file_id(id, ctx)?))
298 }
299 "b" => {
300 let rest = rest.strip_prefix('/').unwrap_or(rest);
301 let mut parts = rest.splitn(2, '/');
302 let id = parse_file_id(parts.next().unwrap_or(""), ctx)?;
303 let path = match parts.next() {
304 Some(p) => p
305 .split('/')
306 .map(pct_decode)
307 .collect::<VfsResult<Vec<_>>>()?,
308 None => Vec::new(),
309 };
310 Ok(NodeAddr::Both { path, id })
311 }
312 _ => Err(err("unknown node address", ctx)),
313 }
314}
315
316fn layer_encode(l: &Layer) -> String {
317 match l {
318 Layer::Os { path } => format!("os:{}", pct_encode(&path_to_bytes(path))),
319 Layer::Range { start, len } => format!("range:{start},{len}"),
320 Layer::Container { format } => format!("container:{}", container_token(*format)),
321 Layer::Volume {
322 scheme,
323 index,
324 guid,
325 } => {
326 let mut s = format!("volume:{},{index}", volume_token(*scheme));
327 if let Some(g) = guid {
328 let _ = write!(s, ",{}", guid_hex(*g));
329 }
330 s
331 }
332 Layer::Encryption { scheme } => format!("encryption:{}", encryption_token(*scheme)),
333 Layer::Snapshot { store } => match store {
334 SnapshotRef::VssStore(i) => format!("snapshot:vss,{i}"),
335 SnapshotRef::ApfsXid(x) => format!("snapshot:apfs,{x}"),
336 },
337 Layer::Fs { kind, at } => format!("fs:{},{}", kind.as_str(), node_addr_encode(at)),
338 Layer::Stream { id } => format!("stream:{}", stream_token(*id)),
339 Layer::Archive { member } => match member {
341 None => "archive:".to_string(),
342 Some(i) => format!("archive:{i}"),
343 },
344 }
345}
346
347fn layer_parse(s: &str) -> VfsResult<Layer> {
348 let (tag, body) = s
349 .split_once(':')
350 .ok_or_else(|| err("layer missing tag", s))?;
351 match tag {
352 "os" => Ok(Layer::Os {
353 path: bytes_to_path(&pct_decode(body)?),
354 }),
355 "range" => {
356 let (a, b) = body
357 .split_once(',')
358 .ok_or_else(|| err("range needs start,len", s))?;
359 Ok(Layer::Range {
360 start: u64_field(a, s)?,
361 len: u64_field(b, s)?,
362 })
363 }
364 "container" => Ok(Layer::Container {
365 format: parse_container(body, s)?,
366 }),
367 "volume" => {
368 let mut it = body.split(',');
369 let scheme = parse_volume_scheme(it.next().unwrap_or(""), s)?;
370 let index = usize_field(it.next().ok_or_else(|| err("volume needs index", s))?, s)?;
371 let guid = match it.next() {
372 Some(g) => Some(parse_guid(g, s)?),
373 None => None,
374 };
375 Ok(Layer::Volume {
376 scheme,
377 index,
378 guid,
379 })
380 }
381 "encryption" => Ok(Layer::Encryption {
382 scheme: parse_encryption(body, s)?,
383 }),
384 "snapshot" => {
385 let (kind, num) = body
386 .split_once(',')
387 .ok_or_else(|| err("snapshot needs kind,id", s))?;
388 let store = match kind {
389 "vss" => SnapshotRef::VssStore(usize_field(num, s)?),
390 "apfs" => SnapshotRef::ApfsXid(u64_field(num, s)?),
391 _ => return Err(err("unknown snapshot kind", s)),
392 };
393 Ok(Layer::Snapshot { store })
394 }
395 "fs" => {
396 let (kind, at) = body
397 .split_once(',')
398 .ok_or_else(|| err("fs needs kind,addr", s))?;
399 Ok(Layer::Fs {
400 kind: parse_fs_kind(kind, s)?,
401 at: parse_node_addr(at, s)?,
402 })
403 }
404 "stream" => Ok(Layer::Stream {
405 id: parse_stream(body, s)?,
406 }),
407 "archive" => Ok(Layer::Archive {
408 member: if body.is_empty() {
409 None
410 } else {
411 Some(usize_field(body, s)?)
412 },
413 }),
414 _ => Err(err("unknown layer tag", s)),
415 }
416}
417
418impl PathSpec {
419 #[must_use]
422 pub fn to_uri(&self) -> String {
423 let mut s = String::from(SCHEME);
424 let layers = self.layers();
425 for (i, l) in layers.iter().enumerate() {
426 if i > 0 {
427 s.push(LAYER_SEP);
428 }
429 s.push_str(&layer_encode(l));
430 }
431 s
432 }
433
434 pub fn from_uri(s: &str) -> VfsResult<PathSpec> {
437 let rest = s
438 .strip_prefix(SCHEME)
439 .ok_or_else(|| err("missing fvfs: scheme", s))?;
440 let mut layers = rest.split(LAYER_SEP);
441 let first = layers.next().ok_or_else(|| err("empty spec", s))?;
442 let mut spec = PathSpec::root(layer_parse(first)?);
443 for l in layers {
444 spec = spec.push(layer_parse(l)?);
445 }
446 Ok(spec)
447 }
448}
449
450impl fmt::Display for PathSpec {
453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454 let comps = |cs: &[Vec<u8>]| {
455 cs.iter()
456 .map(|c| String::from_utf8_lossy(c).into_owned())
457 .collect::<Vec<_>>()
458 .join("/")
459 };
460 let mut first = true;
461 for l in self.layers() {
462 if !first {
463 write!(f, " | ")?;
464 }
465 first = false;
466 match l {
467 Layer::Os { path } => write!(f, "os:{}", path.display())?,
468 Layer::Range { start, len } => write!(f, "range[{start}+{len}]")?,
469 Layer::Container { format } => write!(f, "{}", container_token(*format))?,
470 Layer::Volume { scheme, index, .. } => {
471 write!(f, "{}#{index}", volume_token(*scheme))?;
472 }
473 Layer::Encryption { scheme } => write!(f, "{}", encryption_token(*scheme))?,
474 Layer::Snapshot { store } => match store {
475 SnapshotRef::VssStore(i) => write!(f, "vss#{i}")?,
476 SnapshotRef::ApfsXid(x) => write!(f, "apfs@{x}")?,
477 },
478 Layer::Fs { kind, at } => match at {
479 NodeAddr::Path(cs) | NodeAddr::Both { path: cs, .. } => {
480 write!(f, "{}:/{}", kind.as_str(), comps(cs))?;
481 }
482 NodeAddr::File(id) => write!(f, "{}#{}", kind.as_str(), file_id_token(*id))?,
483 },
484 Layer::Stream { id } => write!(f, ":{}", stream_token(*id))?,
485 Layer::Archive { member } => match member {
486 None => write!(f, "archive")?,
487 Some(i) => write!(f, "archive#{i}")?,
488 },
489 }
490 }
491 Ok(())
492 }
493}
494
495#[cfg(feature = "serde")]
496impl serde::Serialize for PathSpec {
497 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
498 s.serialize_str(&self.to_uri())
499 }
500}
501
502#[cfg(feature = "serde")]
503impl<'de> serde::Deserialize<'de> for PathSpec {
504 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
505 let uri = String::deserialize(d)?;
506 PathSpec::from_uri(&uri).map_err(serde::de::Error::custom)
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use crate::encryption::EncryptionScheme;
513 use crate::fs::{FileId, FsKind, StreamId};
514 use crate::pathspec::{Guid, Layer, NodeAddr, PathSpec, SnapshotRef};
515 use crate::registry::ContainerFormat;
516 use crate::volume::VolumeScheme;
517
518 fn roundtrip(spec: &PathSpec) {
519 let uri = spec.to_uri();
520 let back = PathSpec::from_uri(&uri).expect("parse own output");
521 assert_eq!(&back, spec, "round-trip changed the spec; uri={uri}");
522 }
523
524 #[test]
525 fn rich_chain_round_trips() {
526 let spec = PathSpec::os("/evidence/DC01.E01")
527 .push(Layer::Container {
528 format: ContainerFormat::Ewf,
529 })
530 .push(Layer::Volume {
531 scheme: VolumeScheme::Gpt,
532 index: 1,
533 guid: Some(Guid([
534 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76,
535 0x54, 0x32, 0x10,
536 ])),
537 })
538 .push(Layer::Fs {
539 kind: FsKind::NTFS,
540 at: NodeAddr::Path(vec![
541 b"Windows".to_vec(),
542 b"System32".to_vec(),
543 b"config".to_vec(),
544 b"SYSTEM".to_vec(),
545 ]),
546 })
547 .push(Layer::Stream {
548 id: StreamId::Named(3),
549 });
550 roundtrip(&spec);
551 }
552
553 #[test]
554 fn hostile_path_bytes_round_trip_losslessly() {
555 let nasty = b"a|b%c/d e\xff\x00z".to_vec();
558 let spec = PathSpec::os("/img.raw").push(Layer::Fs {
559 kind: FsKind::EXT,
560 at: NodeAddr::Path(vec![nasty.clone(), b"plain".to_vec()]),
561 });
562 let uri = spec.to_uri();
563 assert!(!uri.contains("a|b"), "delimiter leaked: {uri}");
565 roundtrip(&spec);
566 }
567
568 #[test]
569 fn every_layer_kind_round_trips() {
570 roundtrip(&PathSpec::os("/x").push(Layer::Range {
571 start: 512,
572 len: 1_048_576,
573 }));
574 roundtrip(&PathSpec::os("/x").push(Layer::Encryption {
575 scheme: EncryptionScheme::Bitlocker,
576 }));
577 roundtrip(&PathSpec::os("/x").push(Layer::Snapshot {
578 store: SnapshotRef::VssStore(4),
579 }));
580 roundtrip(&PathSpec::os("/x").push(Layer::Snapshot {
581 store: SnapshotRef::ApfsXid(9_000_000),
582 }));
583 for id in [
585 FileId::NtfsRef { entry: 5, seq: 2 },
586 FileId::ExtInode { ino: 12, gen: 7 },
587 FileId::ApfsOid { oid: 99, xid: 3 },
588 FileId::FatDirEntry {
589 cluster: 8,
590 index: 1,
591 },
592 FileId::IsoExtent { block: 40 },
593 FileId::Opaque(123),
594 ] {
595 roundtrip(&PathSpec::os("/x").push(Layer::Fs {
596 kind: FsKind::APFS,
597 at: NodeAddr::File(id),
598 }));
599 }
600 roundtrip(&PathSpec::os("/x").push(Layer::Fs {
602 kind: FsKind::NTFS,
603 at: NodeAddr::Both {
604 path: vec![b"Users".to_vec(), b"beth".to_vec()],
605 id: FileId::NtfsRef { entry: 91, seq: 1 },
606 },
607 }));
608 for s in [
610 StreamId::Default,
611 StreamId::Named(7),
612 StreamId::ResourceFork,
613 StreamId::Xattr(2),
614 StreamId::Slack,
615 ] {
616 roundtrip(&PathSpec::os("/x").push(Layer::Stream { id: s }));
617 }
618 }
619
620 #[test]
621 fn archive_layer_round_trips() {
622 roundtrip(&PathSpec::os("/case.tgz").push(Layer::Archive { member: None }));
625 roundtrip(&PathSpec::os("/case.zip").push(Layer::Archive { member: Some(3) }));
626 roundtrip(&PathSpec::os("/case.zip").push(Layer::Archive { member: Some(0) }));
627 }
628
629 #[test]
630 fn from_uri_rejects_malformed_archive_member() {
631 assert!(PathSpec::from_uri("fvfs:archive:notanumber").is_err());
633 }
634
635 #[test]
636 fn every_encryption_scheme_token_round_trips() {
637 for scheme in [
640 EncryptionScheme::Bitlocker,
641 EncryptionScheme::Luks1,
642 EncryptionScheme::Luks2,
643 EncryptionScheme::FileVault,
644 EncryptionScheme::ApfsEncrypted,
645 EncryptionScheme::VeraCrypt,
646 ] {
647 roundtrip(&PathSpec::os("/x").push(Layer::Encryption { scheme }));
648 }
649 }
650
651 #[test]
652 fn from_uri_rejects_unknown_encryption_scheme() {
653 assert!(PathSpec::from_uri("fvfs:encryption:rot13").is_err());
656 }
657
658 #[test]
659 fn empty_path_round_trips() {
660 roundtrip(&PathSpec::os("/x").push(Layer::Fs {
661 kind: FsKind::ISO9660,
662 at: NodeAddr::Path(vec![]),
663 }));
664 }
665
666 #[test]
667 fn every_fs_kind_token_round_trips() {
668 for &kind in FsKind::known() {
672 roundtrip(&PathSpec::os("/x").push(Layer::Fs {
673 kind,
674 at: NodeAddr::Path(vec![b"a".to_vec()]),
675 }));
676 }
677 }
678
679 #[test]
680 fn from_uri_rejects_unknown_fs_kind() {
681 assert!(PathSpec::from_uri("fvfs:fs:zzz,path").is_err());
684 }
685
686 #[test]
687 fn from_uri_rejects_garbage() {
688 assert!(PathSpec::from_uri("not-a-spec").is_err());
689 assert!(PathSpec::from_uri("fvfs:").is_err());
690 assert!(PathSpec::from_uri("fvfs:bogustag:x").is_err());
691 assert!(PathSpec::from_uri("fvfs:range:notanumber").is_err());
692 }
693
694 #[cfg(feature = "serde")]
695 #[test]
696 fn serde_round_trips_as_the_canonical_uri() {
697 let spec = PathSpec::os("/evidence/DC01.E01")
698 .push(Layer::Container {
699 format: ContainerFormat::Ewf,
700 })
701 .push(Layer::Fs {
702 kind: FsKind::NTFS,
703 at: NodeAddr::Path(vec![b"Windows".to_vec(), b"a/b".to_vec()]),
704 });
705 let json = serde_json::to_string(&spec).expect("serialize");
706 assert_eq!(json, format!("\"{}\"", spec.to_uri()));
708 let back: PathSpec = serde_json::from_str(&json).expect("deserialize");
709 assert_eq!(back, spec);
710 assert!(serde_json::from_str::<PathSpec>("\"not-a-spec\"").is_err());
712 }
713
714 #[test]
715 fn human_display_is_readable_and_not_the_uri() {
716 let spec = PathSpec::os("/evidence/DC01.E01")
717 .push(Layer::Container {
718 format: ContainerFormat::Ewf,
719 })
720 .push(Layer::Fs {
721 kind: FsKind::NTFS,
722 at: NodeAddr::Path(vec![b"Windows".to_vec()]),
723 });
724 let human = format!("{spec}");
725 assert!(human.contains("DC01.E01"), "human form: {human}");
726 assert!(human.contains("Windows"), "human form: {human}");
727 assert_ne!(human, spec.to_uri());
728 }
729}