Documentation
¶
Overview ¶
Package pathsafe is the single funnel for scaffold/codegen file writes. Containment + conflict detection + atomic write live here so the rest of the tree cannot bypass them by accident.
Design ¶
All scaffold/codegen filesystem writes funnel through WritePlannedFiles. The caller builds a []PlannedFile slice (render phase), wraps it via NewPlanSet (which validates dup-AbsPath; PATHSAFE-PLANSET-TYPED-HARD-01), then calls WritePlannedFiles(realRoot, ps, dryRun) once (execute phase). This gives:
- root containment via ResolveRoot + ContainPath before any write
- all-or-nothing conflict detection (full plan checked before first write)
- atomic write with best-effort rollback on failure (no half-written state)
AI-Hard contract ¶
WritePlannedFiles is the only function in scaffold paths allowed to call os.MkdirAll / os.WriteFile. Direct calls in scaffold packages are statically forbidden by archtest SCAFFOLD-WRITE-FUNNEL-01.
Extension contract ¶
When adding a new scaffold sub-package that writes files, add it to the archtest SCAFFOLD-WRITE-FUNNEL-01 scope list in tools/archtest/scaffold_write_funnel_test.go.
Index ¶
- func ContainPath(realRoot, targetRel string) (string, error)
- func ResolveRoot(root string) (string, error)
- func WriteFile(realRoot, absPath string, content []byte, mode os.FileMode) error
- func WriteFileForce(realRoot, absPath string, content []byte, mode os.FileMode) error
- func WritePlannedFiles(realRoot string, ps PlanSet, dryRun bool) error
- type PlanSet
- type PlannedFile
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ContainPath ¶
ContainPath returns the cleaned absolute path of targetRel under realRoot after asserting no existing parent symlink resolves outside realRoot. Pre: realRoot is the output of ResolveRoot.
Returns an error if:
- targetRel is absolute
- targetRel contains ".." segments that escape realRoot
- any existing parent directory in the resolved path lies outside realRoot
Note: this is a caller-side early-reject check used during plan CONSTRUCTION (e.g., scaffold flag parsing). It accepts symlinks that resolve within realRoot. The authoritative TOCTOU defense at WRITE time is the fd-anchored openat chain in WritePlannedFiles, which rejects ALL parent symlinks regardless of destination. Callers MUST NOT treat a successful ContainPath as a guarantee that subsequent WritePlannedFiles will succeed for the same path.
func ResolveRoot ¶
ResolveRoot returns root resolved through filepath.EvalSymlinks so that subsequent ContainPath comparisons are stable even when root itself is a symlink. Returns an error if root does not exist or cannot be evaluated.
func WriteFile ¶
WriteFile is the single-file shorthand for WritePlannedFiles. Creates parent directories if missing, runs containment + leaf-symlink rejection + O_NOFOLLOW write. Used by codegen.Write so derived-artifact writes share the same safety contract as scaffold writes.
Pre: realRoot is the output of ResolveRoot.
func WriteFileForce ¶
WriteFileForce writes content to absPath, overwriting any existing file. This is the codegen variant: generated files may already exist on disk and need to be replaced. The caller must have already verified the existing content is a generated file (governance.IsGoCellGenerated) before calling.
Internally routed through secureMkdirAllAndWrite (forceOverwrite=true) so the parent walk and leaf write share the fd-anchored TOCTOU defense with the plan funnel. On unix, parent symlinks fail closed via syscall O_NOFOLLOW|O_DIRECTORY; the existing leaf inode is removed via unlinkat relative to the parent fd (symlinks unlinked rather than followed).
realRoot must be non-empty and the output of ResolveRoot; an empty realRoot is rejected with ErrValidationFailed. The dropped/created dir list is discarded: WriteFileForce is the single-shot variant and is not transactional across multiple files.
func WritePlannedFiles ¶
WritePlannedFiles is the SINGLE filesystem write entry for scaffold/codegen.
- Each entry's AbsPath is lexically verified against realRoot via planContainmentPass (path-prefix only); authoritative symlink rejection happens at writePass via the fd-anchored openat(O_NOFOLLOW) chain (unix) or O_EXCL leaf write (windows advisory).
- Each AbsPath must not already exist (conflict detection over the FULL plan — no partial-write semantics). DerivedOverwrite entries are exempt here but still pass the forceOverwritePreflightPass inode-kind gate so dry-run rejects exactly what live would (F2 parity).
- dryRun returns nil after steps 1-2 succeed (validation only, no write).
- Otherwise: mkdir all required directories, write all files, then on the FIRST failure remove every file and directory created during this call (best-effort rollback). Returns the original error wrapped with the rollback outcome.
Pre: realRoot is the output of ResolveRoot. ps may be empty (no-op).
AI-Hard contract: this is the only function in the project allowed to call os.MkdirAll / os.WriteFile in scaffold paths. All other call sites are statically rejected by archtest SCAFFOLD-WRITE-FUNNEL-01. The second parameter is PlanSet (not []PlannedFile) so the dup-reject invariant is established at the type boundary (PATHSAFE-PLANSET-TYPED-HARD-01).
Types ¶
type PlanSet ¶
type PlanSet struct {
// contains filtered or unexported fields
}
PlanSet is the typed container of PlannedFile entries accepted by WritePlannedFiles. The internal slice is package-private, so the only way to obtain a PlanSet is through NewPlanSet (PATHSAFE-PLANSET-TYPED-HARD-01). This lifts the "no duplicate AbsPath in plan" invariant from a runtime Medium guard (the previous duplicatePass) to a type-system Hard guarantee: the WritePlannedFiles signature alone proves that no caller-supplied plan containing duplicates ever enters the funnel.
func NewPlanSet ¶
func NewPlanSet(items []PlannedFile) (PlanSet, error)
NewPlanSet constructs a PlanSet, validating the plan content. Returns ErrConflict if any two entries share the same AbsPath. The input slice is defensively copied so post-construction caller mutation cannot break the invariant.
type PlannedFile ¶
type PlannedFile struct {
AbsPath string
Content []byte
DirMode os.FileMode // optional; defaults 0o755 (per helm/helm chartutil; F12)
FileMode os.FileMode // optional; defaults 0o644 (per helm/helm chartutil; F12)
// contains filtered or unexported fields
}
PlannedFile pairs an absolute output path with its rendered content. Mode encodes file vs Go-source (Go-source files MAY round-trip through codegen.FormatGoSource at the caller before constructing PlannedFile — pathsafe is content-neutral by design).
The forceOverwrite field is package-private (PATHSAFE-FORCEOVERWRITE-TYPED-CTOR-01): callers outside pkg/pathsafe cannot set it via struct-literal; the only public path that produces a force-overwrite entry is DerivedOverwrite. This makes "regenerate semantics opted into by accident" unrepresentable at the type level.
func DerivedOverwrite ¶
func DerivedOverwrite(absPath string, content []byte) PlannedFile
DerivedOverwrite is the SOLE public constructor that returns a PlannedFile with force-overwrite semantics: when fed to WritePlannedFiles, the entry bypasses conflictPass "file already exists" rejection and instead replaces the existing inode (Remove → O_EXCL|O_NOFOLLOW recreate). O_NOFOLLOW is preserved: a leaf symlink at the target path is still removed (not followed) before the fresh file is written.
pathsafe remains content-neutral; callers MUST enforce any content-source gate at the call site (e.g. cellgen's planDerivedArtifact runs governance.IsGoCellGenerated before invoking DerivedOverwrite; archtest SCAFFOLD-DERIVED-FORCEOVERWRITE-01 statically guards the cellgen call site).
func (PlannedFile) IsForceOverwrite ¶
func (f PlannedFile) IsForceOverwrite() bool
IsForceOverwrite reports whether this PlannedFile was constructed via DerivedOverwrite (force-overwrite semantics). The setter (DerivedOverwrite) is the only public path that produces such an entry; this getter exists for test introspection so plan-builder regression tests can assert that derived artifacts are routed through the typed constructor. Production code MUST NOT branch on this getter — the flag is package-private precisely so flow control belongs to pathsafe itself.
For production introspection of derived-vs-skeleton routing, use DerivedOverwrite at construction time and route through WritePlannedFiles — the PlanSet boundary is the single decision point.