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 around one value
Section titled “A newtype is a struct around one value”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.
The single doorway: validate_path
Section titled “The single doorway: validate_path”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 consumer demands the token
Section titled “The consumer demands the token”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
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:
> output appears here — press Run
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.
Encapsulation is the load-bearing part
Section titled “Encapsulation is the load-bearing part”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?
The private field means ValidatedPath can only be born inside validate_path. Since load demands that type, code holding a raw PathBuf simply won't compile — the screening is enforced by the type system, not by a runtime check or convention.
Prove it to yourself
Section titled “Prove it to yourself”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.