Documentation
¶
Overview ¶
Package canonical produces the exact bytes a DevProof subject commits to: path normalization, tree records, JSON, tar, and gzip.
Everything here is a compatibility surface. These functions may not consult a clock, the environment, the working directory, randomness, or filesystem enumeration order (DP-012); .golangci.yaml enforces that with a depguard rule rather than trusting the convention.
Index ¶
- Constants
- func BuildConfig(records []FileRecord) (*bundle.Config, error)
- func CanonicalizeJSON(raw []byte) ([]byte, error)
- func MarshalJSON(v any) ([]byte, error)
- func ParseManifest(data []byte) (*artifact.Manifest, error)
- func WriteTreeRecords(w io.Writer, records []FileRecord) error
- type ContentSource
- type Digest
- func DigestOf(b []byte) Digest
- func EncodeConfig(cfg *bundle.Config) (Digest, []byte, error)
- func EncodeManifest(m *artifact.Manifest) (Digest, []byte, error)
- func JSONDigest(v any) (Digest, []byte, error)
- func ParseDigest(s string) (Digest, error)
- func TreeDigest(records []FileRecord) (Digest, error)
- func VerifyConfigTreeDigest(cfg *bundle.Config) (Digest, error)
- type FileRecord
- type GzipWriter
- type Path
- type PathLimits
- type PathSet
- type Subject
- type TarWriter
Constants ¶
const DigestSize = bundle.DigestSize
DigestSize is the length of a SHA-256 digest in bytes.
const GzipCompressionLevel = 9
GzipCompressionLevel is the DEFLATE level format v1 compresses at.
const MaxFileSize = int64(1)<<33 - 1
MaxFileSize is the largest file format v1 can represent: eleven octal digits in the 12-byte USTAR size field, one byte short of 8 GiB.
Files larger than this are rejected rather than described with a PAX size record. Two reasons. The rule for when PAX appears collapses to a single case — a path too long for the name field — so there is one thing to specify and one thing to verify. And a PAX size path could not be meaningfully tested without an 8 GiB fixture, which would leave an untested branch in the code that decides artifact identity.
Raising this is a format-version decision.
const Separator = "/"
Separator is the only path separator a canonical path may contain. Native separators are the source adapter's problem: it splits on them and rejoins with this, so that a literal backslash in a POSIX filename never silently becomes a directory boundary.
Variables ¶
This section is empty.
Functions ¶
func BuildConfig ¶
func BuildConfig(records []FileRecord) (*bundle.Config, error)
BuildConfig produces the config blob describing records.
records must already be sorted and validated; WriteTreeRecords is called to derive the tree digest, so an unsorted or malformed set fails here rather than producing a config that describes a tree nobody can reproduce.
func CanonicalizeJSON ¶
CanonicalizeJSON rewrites already-valid JSON into its RFC 8785 form.
func MarshalJSON ¶
MarshalJSON encodes v as RFC 8785 canonical JSON.
Canonical JSON is what lets a lock, a config, or a policy be identified by digest: two encoders that agree on the value must agree on the bytes, so key order, whitespace, and escaping cannot carry information.
Numbers are restricted to integers. RFC 8785 specifies ECMAScript double formatting for the general case, which is a large and subtle surface, and no DevProof document contains a non-integer number. A float here is therefore a bug in a caller's types rather than input to accommodate, and is reported as one. Admitting floats later is a format decision.
func ParseManifest ¶
ParseManifest decodes and validates a subject manifest.
Decoding is strict. An unknown field in a manifest is either a newer format this build should refuse, or an attempt to smuggle something past a reader that ignores what it does not recognize; neither is a reason to continue.
func WriteTreeRecords ¶
func WriteTreeRecords(w io.Writer, records []FileRecord) error
WriteTreeRecords writes the normative v1 tree record stream to w.
The encoding is fixed by docs/bundle-format.md:
"devproof-tree-v1\x00" uint64be record count per record, in canonical path order: uint32be path byte length bytes canonical UTF-8 path uint32be normalized mode uint64be file byte length [32]byte raw SHA-256 content digest
There is no padding, delimiter, or terminator beyond those fields.
Types ¶
type ContentSource ¶
ContentSource supplies the bytes for a canonical file record.
Packaging reads only through this interface, and only for paths that appear in the inventory it was given. That is what keeps the packager away from a live filesystem: a resolver hands it a frozen snapshot, and there is no API here through which it could reach anything else.
type Digest ¶
Digest and its helpers live in bundle: format v1 mandates SHA-256, which makes a digest a format concept rather than an encoding detail. They are aliased here so that this package's own API reads without a qualifier.
func EncodeConfig ¶
EncodeConfig renders cfg as canonical JSON and returns the bytes with their digest.
func EncodeManifest ¶
EncodeManifest renders a subject manifest as canonical JSON and returns the bytes with their digest.
That digest is the bundle's identity, so the manifest is validated before it is encoded: minting a digest for a structurally invalid subject would give a name to something no consumer can verify.
func JSONDigest ¶
JSONDigest returns the SHA-256 of v's canonical JSON encoding, along with those bytes. Callers that persist a document need both: the digest identifies it and the bytes are what must be written, and recomputing either separately invites the two to disagree.
func ParseDigest ¶
ParseDigest accepts the OCI "sha256:<64 lowercase hex>" form.
func TreeDigest ¶
func TreeDigest(records []FileRecord) (Digest, error)
TreeDigest returns the v1 tree digest for records.
It hashes incrementally rather than materializing the record stream: a bundle at the default file limit would otherwise build a buffer far larger than the inventory it describes.
func VerifyConfigTreeDigest ¶
VerifyConfigTreeDigest recomputes the tree digest from a config's own inventory and compares it against the digest the config states.
The two can disagree only if the config was tampered with, so a mismatch is reported as such rather than as a generic validation failure.
type FileRecord ¶
type FileRecord struct {
// Path is the canonical bundle-relative path.
Path Path
// Mode is the normalized mode: bundle.ModeFile or bundle.ModeExecutable.
Mode uint32
// Size is the file's length in bytes.
Size int64
// Digest is the SHA-256 of the file's exact content bytes.
Digest Digest
}
FileRecord is one entry of the canonical inventory: everything about a file that contributes to identity, and nothing that does not.
Ownership, timestamps, and link targets are absent by construction rather than normalized away later, so there is no code path where one could reach the tree digest.
func ConfigRecords ¶
func ConfigRecords(cfg *bundle.Config) ([]FileRecord, error)
ConfigRecords converts a validated config back into file records.
This is the verification direction: a config fetched from a registry is turned into records so its tree digest can be recomputed and compared against the one the config claims.
type GzipWriter ¶
type GzipWriter struct {
// contains filtered or unexported fields
}
GzipWriter produces the canonical v1 gzip encoding of a stream.
The container is written here rather than by compress/gzip because that package chooses some header bytes itself, and those choices are not part of its compatibility promise. The compressed stream comes from the frozen encoder in the deflate subpackage (DP-016). Between them, every byte of the layer is pinned.
func NewGzipWriter ¶
func NewGzipWriter(w io.Writer) (*GzipWriter, error)
NewGzipWriter returns a writer that emits the canonical v1 gzip encoding to w. The caller must call Close to flush the final block and the trailer.
func (*GzipWriter) Close ¶
func (g *GzipWriter) Close() error
Close flushes the final DEFLATE block and writes the gzip trailer. It is idempotent, and reports the first error the writer saw.
type Path ¶
type Path string
Path is a validated, normalized, relative bundle path. Its zero value is not a valid path; obtain one from NormalizePath.
func NormalizePath ¶
func NormalizePath(p string, lim PathLimits) (Path, error)
NormalizePath validates p and returns its canonical form.
Normalization is NFC only. No segment is ever removed or resolved: a "." or ".." is rejected rather than collapsed, because collapsing would let a source describe one path and a consumer materialize another.
func (Path) FoldKey ¶
FoldKey returns p's case-insensitive collision key.
Simple folding, matching what APFS, HFS+, and NTFS actually merge. Full folding would additionally collapse "ß" onto "ss", which no filesystem does, and would reject bundles that expand cleanly everywhere (DP-017).
type PathLimits ¶
type PathLimits struct {
// MaxBytes bounds the whole path's UTF-8 length.
MaxBytes int
// MaxSegmentBytes bounds one segment's UTF-8 length.
MaxSegmentBytes int
// MaxDepth bounds the number of segments.
MaxDepth int
}
PathLimits bounds a canonical path.
It is a local struct rather than bundle.Limits so that bundle can depend on canonical for its inventory types without a cycle. Callers project the three relevant fields across.
type PathSet ¶
type PathSet struct {
// contains filtered or unexported fields
}
PathSet accumulates canonical paths and rejects any tree a consumer could materialize two different ways.
Three shapes are refused, all for the same reason: the result would depend on the extractor rather than on the bundle.
- Two files at one path, or at paths that fold together. Equal bytes do not help; ownership would still be ambiguous (DP-011).
- A file whose path is a directory in another entry, in either order.
- Any of the above across case, since a case-insensitive filesystem merges them on arrival.
The zero value is ready to use.
func (*PathSet) Add ¶
Add records p, deriving its parent directories.
Callers that need a deterministic *message* when a tree has several collisions should add paths in sorted order. The failure itself is order-independent: a colliding tree is rejected whatever order it arrives in, which is the property DP-011 requires.
func (*PathSet) Directories ¶
Directories returns the derived parent directories sorted by canonical UTF-8 bytes, which places every parent before its children.
func (*PathSet) Files ¶
Files returns every added path sorted by canonical UTF-8 bytes, which is the order the tree digest and the tar stream require.
type Subject ¶
type Subject struct {
// Manifest is the OCI subject. ManifestDigest is the bundle's identity.
Manifest *artifact.Manifest
ManifestBytes []byte
ManifestDigest Digest
// Config is the inventory blob.
Config *bundle.Config
ConfigBytes []byte
ConfigDigest Digest
// LayerDigest and LayerSize describe the compressed archive that was
// written to the packager's output.
LayerDigest Digest
LayerSize int64
// TreeDigest identifies the payload independently of how it was encoded.
// Two bundles with the same tree digest hold the same files even if a
// future format encodes them differently.
TreeDigest Digest
}
Subject is a fully encoded bundle.
The config and manifest are carried as bytes because both are small by construction and a caller that recomputed either would invite the bytes and the digest to disagree. The layer is not: it is streamed to a writer during packaging and identified here by digest and size only.
func Package ¶
func Package(ctx context.Context, records []FileRecord, src ContentSource, layerOut io.Writer) (*Subject, error)
Package encodes records into a complete bundle subject, streaming the compressed layer to layerOut.
The work happens in one pass over the content so that hashing, archiving, and compressing all see the same bytes. Anything else would leave a window in which the layer describes content the config never saw.
func VerifySubject ¶
func VerifySubject(manifestBytes, configBytes []byte, layerSize int64, layerDigest Digest) (*Subject, error)
VerifySubject re-derives a subject's identity from its own blobs.
It is the read-side counterpart of Package: given the bytes a registry served, it establishes that the manifest, config, and layer describe one consistent artifact before any of it is trusted or written to disk.
func (*Subject) ConfigDescriptor ¶
func (s *Subject) ConfigDescriptor() artifact.Descriptor
ConfigDescriptor returns the descriptor naming the config blob.
func (*Subject) Descriptor ¶
func (s *Subject) Descriptor() artifact.Descriptor
Descriptor returns the descriptor naming this subject.
func (*Subject) LayerDescriptor ¶
func (s *Subject) LayerDescriptor() artifact.Descriptor
LayerDescriptor returns the descriptor naming the compressed layer.
type TarWriter ¶
type TarWriter struct {
// contains filtered or unexported fields
}
TarWriter emits the canonical v1 tar stream.
It is written by hand rather than with archive/tar because DP-019 fixes every header byte, including PAX record naming, ordering, and numeric encoding. archive/tar makes several of those choices itself, and none of them are covered by its compatibility promise — the same coupling DP-016 removed for the compressor.
The writer does not sort. It emits what it is given, in the order given, and rejects anything that would produce a stream a consumer could materialize two ways.
func NewTarWriter ¶
NewTarWriter returns a writer emitting the canonical v1 tar stream to w.
func (*TarWriter) Close ¶
Close writes the two zero blocks that terminate the archive. Nothing may follow them.
func (*TarWriter) WriteDirectory ¶
WriteDirectory emits a directory entry.
Directories are derived from file paths, never carried from a source, so this takes a path and nothing else: there is no mode or ownership to get wrong.
func (*TarWriter) WriteFile ¶
func (t *TarWriter) WriteFile(rec FileRecord, content io.Reader) error
WriteFile emits a file entry and copies its content, hashing as it goes.
The observed digest and size are checked against rec before the entry is considered written. Hashing here rather than trusting the inventory is what closes the time-of-check gap: content that changed between snapshot and packaging fails the build instead of producing a bundle whose layer disagrees with its own config.