#265 Jul 17, 2026

265. Path::strip_prefix — Compare Paths by Component, Not by Character

Checking path_str.starts_with("/srv/uploads") says yes to /srv/uploads-old too. Paths need component-wise comparison — and Path has it built in.

The string trap

This morning’s bite 264 showed join silently discarding your base. Here’s the sibling mistake: checking containment with string prefixes.

1
2
// looks right, isn't
assert!("/srv/uploads-old/x.png".starts_with("/srv/uploads"));

/srv/uploads-old is a completely different directory, but as a string it starts with /srv/uploads. Any allowlist built on string prefixes has this hole.

The component-aware way

Path::starts_with compares whole components, so a partial directory name never matches:

1
2
3
4
5
6
use std::path::Path;

let base = Path::new("/srv/uploads");

assert!(Path::new("/srv/uploads/cat.png").starts_with(base));
assert!(!Path::new("/srv/uploads-old/cat.png").starts_with(base));

And when you also want the remainder — for display, for re-joining elsewhere — strip_prefix returns it as a relative path instead of a boolean:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::path::Path;

let base = Path::new("/srv/uploads");
let full = Path::new("/srv/uploads/img/cat.png");

let rel = full.strip_prefix(base).unwrap();
assert_eq!(rel, Path::new("img/cat.png"));

// non-matching base → Err, not a mangled path
assert!(Path::new("/etc/passwd").strip_prefix(base).is_err());

Still lexical

Like everything in std::path, this is pure component comparison — nothing touches the filesystem. That means .. is just another component:

1
2
3
4
use std::path::Path;

// true! ".." is not resolved
assert!(Path::new("/srv/uploads/../etc").starts_with("/srv/uploads"));

So starts_with alone is not a traversal guard. Validate components before joining (see bite 264), or canonicalize both sides first — then starts_with on the resolved paths is the real containment check.

← Previous 264. Path::join — One Absolute Path and Your Base Directory Is Gone Next → 266. Path::extension — Splitting on '.' Breaks on .gitignore