Skip to content

Newtype proof tokens

A pipeline config is full of file paths — sources to read, outputs to write. A hostile or sloppy config could name ../../etc/passwd, or a path with a \0 byte, or a symlink that escapes its sandbox. Somewhere there must be a validator. The real question is harder: how do you guarantee that every path reaching the file loader went through that validator, with no chance some code path forgot to call it? Clinker’s answer is a Rust pattern that turns “was this screened?” from a convention into a fact the compiler checks.

You’ll be able to: read the ValidatedPath newtype, explain how a private field plus a single constructor makes a security property unforgeable, and recognise the “parse, don’t validate” pattern when you meet it elsewhere.

A newtype is a struct that wraps a single existing type to give it a new identity:

clinker-plan ·security.rs ·ValidatedPath type @47d2e12
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ValidatedPath(PathBuf);

A ValidatedPath is a PathBuf, but the type system treats it as distinct — you cannot pass a raw PathBuf where a ValidatedPath is wanted. The crucial detail is invisible at first glance: the inner field has no pub. It is private to the module. So nothing outside security.rs can write ValidatedPath(some_path) to conjure one. The only doorway in is the module’s constructor.

There is exactly one public function that returns a ValidatedPath, and it does the screening before handing one back:

clinker-plan ·security.rs ·validate_path fn @47d2e12
pub fn validate_path(
raw: &Path,
base_dir: &Path,
allow_absolute: bool,
) -> Result<ValidatedPath, Diagnostic> {
// rejects: null bytes; URL-encoded "%2e%2e" traversal; ".." components;
// absolute / drive-anchored paths (unless allowed); then canonicalizes and
// rejects anything that escapes base_dir via a symlink.
// ...
Ok(ValidatedPath(resolved)) // the ONE place a ValidatedPath is born
}

Read what that buys you. Because the field is private, the only way to hold a ValidatedPath is to have called validate_path and gotten Ok. So the type itself is a proof token: possessing one is compile-time evidence that the path was canonicalized, scoped to its base directory, and screened for traversal and null-byte attacks. The module doc names the pattern outright:

//! The only way to obtain a `ValidatedPath` is by calling `validate_path`.
//! The newtype's inner field is private, so downstream code that consumes a
//! `ValidatedPath` has compile-time proof that the path has been canonicalized,
//! scoped to its base directory, and screened for directory-traversal and
//! null-byte attacks.
//! This is the "token of proof" pattern.

The payoff is at the file loader. SourceDb::load — the function that actually opens a file — does not take a PathBuf. It takes a ValidatedPath, by value:

clinker-plan ·span.rs ·SourceDb type @47d2e12
crates/clinker-plan/src/span.rs
pub fn load(&mut self, path: ValidatedPath) -> std::io::Result<FileId> {
// by the time we're here, the path is PROVEN screened — its type says so
}

Now trace the consequence. To call load, you need a ValidatedPath. To get a ValidatedPath, you must call validate_path. There is no other path through the type system. A programmer who forgets to validate doesn’t get a subtle runtime hole — they get a compile error, because they’re holding a PathBuf where a ValidatedPath is required. The class of bug “we loaded a file without screening its path” has been designed out of existence.

This is the essence of “parse, don’t validate”: instead of checking a value and then passing the same loose type onward (hoping every later reader re-checks or trusts it), you parse it once into a more specific type that can only exist when the check passed. The proof rides along in the type.

Forge one — and watch the compiler refuse

Section titled “Forge one — and watch the compiler refuse”

The module-privacy mechanism is small enough to hold in your hand. Run this, then uncomment the forged line:

rust // editable

The first path loads, the second is rejected by the validator — and the commented forgery will not compile if you uncomment it, because the field is private to the security module. That refusal is the whole guarantee. In real clinker the same refusal is locked in by a compile_fail doctest that tries the forgery on purpose and asserts the compiler rejects it.

Strip away the security specifics and the reusable idea is this: a newtype is only a proof token if its inner value is private. A pub struct ValidatedPath(pub PathBuf) would prove nothing — anyone could build one from any path. Privacy is what makes the constructor the sole gate, and the sole gate is what makes the type mean something. You’ll see the same shape in the next two lessons: Spanned<T> and CompiledPlan are both “you can only get one by going through the right door” types.

// quick check

Why can't a programmer accidentally call SourceDb::load with an unscreened path?

A private field turned a security check into a type. Next we’ll see the parser side of the same philosophy: how clinker funnels all YAML through one chokepoint and keeps the source location of every value, so a typo in a config points at the exact line that’s wrong.