Classes | |
| struct | MemberInfo |
| class | CapsuleArchive |
| A reader over an admitted archive. More... | |
| struct | WriteMember |
| One member to write. path must already be normalized. More... | |
| struct | CanonicalPcm |
| struct | AdmissionOptions |
| struct | ExportItem |
| One member of an export inventory. More... | |
| struct | ExportRequest |
| class | Redistribution |
| Permission to pass a component's bytes on. More... | |
| struct | ComponentPolicy |
| struct | FileEntry |
| One row of the archive closure. More... | |
| struct | DependencyEntry |
| A component whose bytes are not in the archive. More... | |
| struct | ComponentRef |
| A reference to one component row, for a result that must name components it does not own. More... | |
| struct | CapsuleLimits |
| struct | Compatibility |
| Version floors the consuming runtime must satisfy. More... | |
| struct | Manifest |
| struct | CapabilityRequirement |
| struct | DependencySummary |
| struct | RightsSummary |
| Rights facts, machine-derived. More... | |
| struct | CapsulePreview |
| class | ProfileValidator |
| Semantic validation of an extracted staging tree. More... | |
| class | ProfileRegistry |
| struct | SignatureEnvelope |
| struct | VersionFloor |
| Minimum version the signer will let this capsule be admitted at. More... | |
| class | SignatureVerifier |
| class | StagingArea |
| A private directory that is removed when it goes out of scope unless it was published. More... | |
| struct | CapsuleError |
| A rejection with the exact subject that caused it. More... | |
Typedefs | |
| using | ExtractionProgress = std::function<bool(std::size_t member_index, std::size_t member_count)> |
| Progress and cancellation. | |
Functions | |
| runtime::Result< CapsuleArchive, CapsuleError > | open_archive (const std::filesystem::path &path, const CapsuleLimits &limits=kCapsuleLimitsV1) |
| runtime::Result< std::uint64_t, CapsuleError > | write_archive_no_replace (const std::vector< WriteMember > &members, const std::filesystem::path &destination, const CapsuleLimits &limits=kCapsuleLimitsV1) |
| Write deterministically to a path that must not already exist. | |
| runtime::Result< CanonicalPcm, CapsuleError > | decode_to_canonical (std::span< const std::uint8_t > source) |
| Decode a source audio file into the canonical form. | |
| std::vector< std::uint8_t > | to_canonical_bytes (const CanonicalPcm &pcm) |
| Serialize to the exact member bytes a capsule stores: the fixed header documented at the top of this file, then the interleaved little-endian binary32 samples. | |
| runtime::Result< CanonicalPcm, CapsuleError > | from_canonical_bytes (std::span< const std::uint8_t > bytes) |
| Parse a canonical member from its bytes alone; no external metadata exists. | |
| std::string | canonical_pcm_digest (const CanonicalPcm &pcm) |
| SHA-256, bare lowercase hex, over to_canonical_bytes(pcm). | |
| runtime::Result< void, CapsuleError > | validate_canonical (const CanonicalPcm &pcm) |
| Reject NaN, infinity, and any frame count that disagrees with the buffer length. | |
| runtime::Result< CapsulePreview, CapsuleError > | preview_capsule (const CapsuleArchive &archive, const ProfileRegistry ®istry, const AdmissionOptions &options) |
| Steps 1-4 of the import state machine: bounded manifest read, structural and closure validation, optional trust envelope, and a non-executing preview. | |
| runtime::Result< void, CapsuleError > | admit_to_staging (const CapsuleArchive &archive, const CapsulePreview &preview, const ProfileRegistry ®istry, const StagingArea &staging, const ExtractionProgress &progress={}) |
| Steps 6-7: extract only declared members into staging, verifying digests, then run the profile's semantic validation. | |
| runtime::Result< std::uint64_t, CapsuleError > | export_capsule (ExportRequest request, const std::filesystem::path &destination, const CapsuleLimits &limits=kCapsuleLimitsV1) |
| Fill in the closure — sorted files, digests, sizes, and revision_id — and write one deterministic archive to a destination that must not exist. | |
| runtime::Result< std::string, CapsuleError > | to_canonical_json (const Manifest &manifest) |
| Serialize to canonical JSON: UTF-8 without BOM, LF, keys sorted by code point, no insignificant whitespace, shortest round-tripping numbers, no NaN or infinity, files sorted by path and dependencies by id in byte order. | |
| runtime::Result< Manifest, CapsuleError > | parse_manifest (std::string_view json) |
| Parse and structurally validate. | |
| runtime::Result< std::string, CapsuleError > | revision_digest (const Manifest &manifest) |
| sha256 over the canonical JSON of the whole manifest with exactly three fields removed: revision_id (self-referential), exported_at (the one value that changes when nothing else did), and attestations (signatures are computed over this digest). | |
| Completeness | derive_completeness (const Manifest &manifest) |
| Recomputed from the component rows. | |
| runtime::Result< std::string, CapsuleError > | admit_member_path (std::string_view raw, const CapsuleLimits &limits=kCapsuleLimitsV1) |
| Verify a member path and return it unchanged. | |
| std::string | collision_key (std::string_view normalized_path) |
| Fold a normalized path to the key used for collision detection: ASCII case-folded, NFC, with confusable characters mapped to a representative. | |
| runtime::Result< void, CapsuleError > | check_collisions (const std::vector< std::string > &normalized) |
| Detect a collision across a member set. Returns the first offending pair. | |
| runtime::Result< void, CapsuleError > | extract_declared (const CapsuleArchive &archive, const Manifest &manifest, const StagingArea &staging, const ExtractionProgress &progress={}) |
| Extract only the members the manifest declares, verifying each digest as it lands. | |
| runtime::Result< std::vector< std::uint8_t >, CapsuleError > | read_staged_member (const StagingArea &staging, const FileEntry &entry) |
| Read one member back out of a staged tree. | |
| runtime::Result< std::vector< std::uint8_t >, CapsuleError > | read_staged_member (const std::filesystem::path &staging_root, const FileEntry &entry) |
| Reads a declared member from a staging root the caller does not own. | |
| std::string_view | status_token (CapsuleStatus status) noexcept |
| Stable machine-readable token, e.g. | |
Variables | |
| constexpr std::uint32_t | kMinCanonicalSampleRate = 8000 |
| constexpr std::uint32_t | kMaxCanonicalSampleRate = 192000 |
| constexpr std::uint32_t | kMaxCanonicalChannels = 2 |
| constexpr std::uint32_t | kCanonicalPcmDecoderVersion = 1 |
| The decoder's contract version, stored in every canonical member's header. | |
| constexpr char | kCanonicalPcmMagic [8] = {'p', 'u', 'l', 'p', '.', 'p', 'c', 'm'} |
| First eight bytes of every canonical member. | |
| constexpr std::size_t | kCanonicalPcmHeaderBytes = 28 |
| Fixed header size; sample data begins here. | |
| constexpr std::string_view | kManifestPath = "capsule.json" |
| Fixed path of the manifest, which must also be the archive's first member. | |
| constexpr CapsuleLimits | kCapsuleLimitsV1 {} |
| The frozen v1 budgets. | |
| constexpr std::string_view | kFormatId = "org.pulp.audio-authoring-capsule" |
| The vendor-neutral format identifier. Constant for format version 1. | |
| constexpr std::uint32_t | kFormatVersion = 1 |
| using pulp::authoring_capsule::ExtractionProgress = std::function<bool(std::size_t member_index, std::size_t member_count)> |
Progress and cancellation.
Returning false cancels; the staging area is discarded and nothing outside it was ever touched.
|
strong |
|
strong |
|
strong |
What the local runtime can offer for this capsule.
|
strong |
Derived from the component rows.
A capsule that declares its own completeness is ignored: the reader recomputes it.
|
strong |
|
strong |
|
strong |
|
strong |
|
strong |
| runtime::Result< std::string, CapsuleError > pulp::authoring_capsule::admit_member_path | ( | std::string_view | raw, |
| const CapsuleLimits & | limits = kCapsuleLimitsV1 ) |
Verify a member path and return it unchanged.
It does not rewrite bytes toward NFC: with no Unicode character database linked, the only sound options are prove already-NFC or reject, and quietly returning a half-normalized path would be worse than either. A path this cannot prove is NFC is rejected.
Also rejects absolute paths, drive letters, UNC prefixes, any . or .. component, leading or trailing separators, empty components, backslashes, NUL and C0 controls, trailing dots or spaces, and anything past the depth or byte budget.
The accepted subset is narrow, which is affordable because a member path is generated by the exporter and is never the user's filename — a sample is content-addressed and the author's chosen name travels as metadata, where arbitrary text is fine.
| runtime::Result< void, CapsuleError > pulp::authoring_capsule::admit_to_staging | ( | const CapsuleArchive & | archive, |
| const CapsulePreview & | preview, | ||
| const ProfileRegistry & | registry, | ||
| const StagingArea & | staging, | ||
| const ExtractionProgress & | progress = {} ) |
Steps 6-7: extract only declared members into staging, verifying digests, then run the profile's semantic validation.
Does not publish and does not compile. The caller has consent before calling this.
| std::string pulp::authoring_capsule::canonical_pcm_digest | ( | const CanonicalPcm & | pcm | ) |
SHA-256, bare lowercase hex, over to_canonical_bytes(pcm).
This is the identity a files[] row carries for an audio member — and because it is the digest of the member bytes themselves, it is the same digest the exporter's uniform member hashing and the importer's extraction check compute, with no audio-specific exception.
| runtime::Result< void, CapsuleError > pulp::authoring_capsule::check_collisions | ( | const std::vector< std::string > & | normalized | ) |
Detect a collision across a member set. Returns the first offending pair.
| std::string pulp::authoring_capsule::collision_key | ( | std::string_view | normalized_path | ) |
Fold a normalized path to the key used for collision detection: ASCII case-folded, NFC, with confusable characters mapped to a representative.
Two members whose keys match are a collision even when their bytes differ — on a case-insensitive filesystem one would silently overwrite the other.
| runtime::Result< CanonicalPcm, CapsuleError > pulp::authoring_capsule::decode_to_canonical | ( | std::span< const std::uint8_t > | source | ) |
Decode a source audio file into the canonical form.
Admitted formats are exactly the ones that decode deterministically — identical bytes in, identical samples out, on every ISA:
Anything else — MP3, OGG, AAC, CAF, an unknown container, a codec inside a supported container — returns decode_unsupported naming what was found: an approximation the user did not ask for is worse than a refusal they can act on.
| Completeness pulp::authoring_capsule::derive_completeness | ( | const Manifest & | manifest | ) |
Recomputed from the component rows.
A declared value in the capsule is ignored.
| runtime::Result< std::uint64_t, CapsuleError > pulp::authoring_capsule::export_capsule | ( | ExportRequest | request, |
| const std::filesystem::path & | destination, | ||
| const CapsuleLimits & | limits = kCapsuleLimitsV1 ) |
Fill in the closure — sorted files, digests, sizes, and revision_id — and write one deterministic archive to a destination that must not exist.
Repeating this for unchanged content reproduces the file byte for byte.
What it validates, exactly. Each item's path is re-admitted and may not claim capsule.json; each digest and size is measured from the bytes that travel rather than taken from what the caller declared; the item set is checked for case and confusable collisions; and the finished manifest is read back through parse_manifest() before the archive is created. That last step is what applies the structural rules — a files[] row must declare source_availability: included, a dependency's provider must be https:// or capsule-library:, format/profile/product/ project_id must be present — to a Manifest that was assembled in memory and never parsed. to_canonical_json() alone applies none of them, so without the read-back a caller passing its own dependency rows straight through could mint a capsule this same code refuses to read. The refusal carries parse_manifest()'s status and JSON-pointer subject, and nothing is written.
dependencies[] is otherwise carried through as given: this layer resolves nothing, contacts no provider, and cannot confirm that a dependency's digest names the bytes a recipient will eventually fetch.
| runtime::Result< void, CapsuleError > pulp::authoring_capsule::extract_declared | ( | const CapsuleArchive & | archive, |
| const Manifest & | manifest, | ||
| const StagingArea & | staging, | ||
| const ExtractionProgress & | progress = {} ) |
Extract only the members the manifest declares, verifying each digest as it lands.
An undeclared member is never written, so a capsule cannot smuggle a file past the closure it published.
| runtime::Result< CanonicalPcm, CapsuleError > pulp::authoring_capsule::from_canonical_bytes | ( | std::span< const std::uint8_t > | bytes | ) |
Parse a canonical member from its bytes alone; no external metadata exists.
The header is self-checking: a wrong magic, a decoder_version this build does not implement, a channel count outside [1, 2], or a payload whose length disagrees with the declared geometry is refused. There is no separate media struct: a successful parse returns the header's fields in the CanonicalPcm itself, and the decoder version is not surfaced because success proves it equals kCanonicalPcmDecoderVersion.
| runtime::Result< CapsuleArchive, CapsuleError > pulp::authoring_capsule::open_archive | ( | const std::filesystem::path & | path, |
| const CapsuleLimits & | limits = kCapsuleLimitsV1 ) |
| runtime::Result< Manifest, CapsuleError > pulp::authoring_capsule::parse_manifest | ( | std::string_view | json | ) |
Parse and structurally validate.
Does not check the archive closure — that needs the archive; see admit().
| runtime::Result< CapsulePreview, CapsuleError > pulp::authoring_capsule::preview_capsule | ( | const CapsuleArchive & | archive, |
| const ProfileRegistry & | registry, | ||
| const AdmissionOptions & | options ) |
Steps 1-4 of the import state machine: bounded manifest read, structural and closure validation, optional trust envelope, and a non-executing preview.
Nothing is written outside the process.
This is also where the profile's required_roles() is applied — a missing role surfaces as missing_required_role in unmet with an unsupported verdict, which admit_to_staging() then refuses on. A consumer does not repeat that check.
| runtime::Result< std::vector< std::uint8_t >, CapsuleError > pulp::authoring_capsule::read_staged_member | ( | const StagingArea & | staging, |
| const FileEntry & | entry ) |
Read one member back out of a staged tree.
This exists so a consumer never joins a manifest path to a staging root itself. The path rules and the join belong to the same layer: a consumer doing staging.root() / entry.path by hand is a path-admission decision made outside the module that owns path admission, and it silently loses the NFC, depth, byte-budget, reserved-name, and containment checks. So the row is re-admitted here — this is a public entry point taking an arbitrary FileEntry, and it cannot assume a caller ran preview or extraction first.
Every hop is opened relative to the staging area's pinned root refusing to follow links, and only a plain file is read: nothing in this module creates a symlink, a device, or a directory where a member belongs, so one standing there means the tree is not the one extraction wrote.
The size on disk must equal entry.bytes. That is checked before the bytes are copied out, so the read is bounded by what the owner-private tree actually holds rather than by a size the row asserted, and a truncated or replaced member is refused rather than returned as if it were whole. The content digest is not recomputed: extract_declared() verified it before the member was allowed to land, and the staging directory is readable only by its owner, so re-hashing on every read would charge a repeated cost for a property already established. A caller that wants the stronger check can hash the returned bytes against entry.sha256 itself.
Failures are exact. A path that fails admission reports whatever admit_member_path() decided — path_rejected for a path that could escape or that the grammar refuses. Everything else is staging_failed, with the declared and found sizes in required and found when the disagreement is a size.
| runtime::Result< std::vector< std::uint8_t >, CapsuleError > pulp::authoring_capsule::read_staged_member | ( | const std::filesystem::path & | staging_root, |
| const FileEntry & | entry ) |
Reads a declared member from a staging root the caller does not own.
ProfileValidator::validate_staged() receives the staging root as a path rather than as a StagingArea, and a StagingArea cannot be adopted from an existing tree — create() makes a new one. Without this overload the entry point that exists so a consumer never joins an untrusted member path to a directory by hand is unreachable from the one place every consumer needs exactly that, and each profile hand-rolls the join instead.
Identical to the overload above in every respect: the same admission, the same directory-identity checks before and after the read, the same errors. The StagingArea is only ever read for its root.
| runtime::Result< std::string, CapsuleError > pulp::authoring_capsule::revision_digest | ( | const Manifest & | manifest | ) |
sha256 over the canonical JSON of the whole manifest with exactly three fields removed: revision_id (self-referential), exported_at (the one value that changes when nothing else did), and attestations (signatures are computed over this digest).
Everything else is covered — title, created_at, provenance, distribution, each file row's executable_data, and any unknown optional key. Those fields are not inert: distribution is what makes a capsule Play-only and executable_data is what tells the user a payload contains code, so leaving either outside the digest would let outer metadata be edited on a validly signed capsule and still verify.
None of the covered fields depends on the machine or the moment, so the digest remains independent of compression level, archive member order, filesystem timestamps, export time, and the exporting machine's paths. Two exports of an unchanged project agree.
Fallible for the same reason to_canonical_json() is: the digest is taken over canonical bytes, so it cannot exist when those bytes cannot.
|
noexcept |
Stable machine-readable token, e.g.
"digest_mismatch". Stable across releases: products key their explanatory copy off these.
| std::vector< std::uint8_t > pulp::authoring_capsule::to_canonical_bytes | ( | const CanonicalPcm & | pcm | ) |
Serialize to the exact member bytes a capsule stores: the fixed header documented at the top of this file, then the interleaved little-endian binary32 samples.
canonical_pcm_digest() is the SHA-256 of exactly this buffer. The caller validates first; the header carries the fields as declared.
| runtime::Result< std::string, CapsuleError > pulp::authoring_capsule::to_canonical_json | ( | const Manifest & | manifest | ) |
Serialize to canonical JSON: UTF-8 without BOM, LF, keys sorted by code point, no insignificant whitespace, shortest round-tripping numbers, no NaN or infinity, files sorted by path and dependencies by id in byte order.
Fallible because a hand-assembled Manifest can hold material no reader ever validated — a non-UTF-8 string, or a preserved subtree past the depth limit. Returning an empty string instead would hand the exporter a capsule that fails admission later, with nothing to tell the user why.
| runtime::Result< void, CapsuleError > pulp::authoring_capsule::validate_canonical | ( | const CanonicalPcm & | pcm | ) |
Reject NaN, infinity, and any frame count that disagrees with the buffer length.
A capsule that admitted a NaN would hand machine B silence or a blown output the author never heard.
| runtime::Result< std::uint64_t, CapsuleError > pulp::authoring_capsule::write_archive_no_replace | ( | const std::vector< WriteMember > & | members, |
| const std::filesystem::path & | destination, | ||
| const CapsuleLimits & | limits = kCapsuleLimitsV1 ) |
Write deterministically to a path that must not already exist.
Members are emitted in the given order with fixed metadata, so repeating an export of unchanged content reproduces the file byte for byte. The destination appears only once it is complete: a cancelled or failed write leaves no partial file for a person to mistake for a good one.
|
inlineconstexpr |
The decoder's contract version, stored in every canonical member's header.
A decode that is deterministic today must stay comparable tomorrow: a change to the decode arithmetic is a change to every digest it produced, so it must advance this number.
|
inlineconstexpr |
Fixed header size; sample data begins here.
|
inlineconstexpr |
First eight bytes of every canonical member.
|
inlineconstexpr |
The frozen v1 budgets.
A consumer may tighten a field; loosening one is a format change, not a configuration choice.
|
inlineconstexpr |
The vendor-neutral format identifier. Constant for format version 1.
|
inlineconstexpr |
|
inlineconstexpr |
Fixed path of the manifest, which must also be the archive's first member.
It lives beside the budgets because it is a container fact: the reader requires it and the writer holds itself to the same rule, so neither may learn it from the envelope layer above.
|
inlineconstexpr |
|
inlineconstexpr |
|
inlineconstexpr |