Expose a read container's raw blocks for hashing (#131) - #134
Merged
Merged
Conversation
bootler binds each recorded install attempt to a fingerprint over the container's version and its raw manifest, archive and envelope blocks, and must take those blocks from this crate's reader rather than grow a second footer parser. UnparsedContainer now hands them out: the footer version, the manifest block exactly as written, and a bounded reader over the still-compressed archive block that never leaves the block and reports a source ending inside it as UnexpectedEof rather than a short stream. The manifest accessor is documented as unauthenticated and points decoding at the crate's own parsers, keeping the concern that kept these bytes private. A test-support writer for version-1 containers lets a dependent test version-1 input through the real reader without hand-building footers. Closes #131
raw_archive_block documents PayloadError::Io when its seek fails, and nothing exercised that path. Part of #131
Contributor
Author
|
[Reviewer Round 1] Review verdict: Approve. I found no blocking issues. The change gives a caller the exact footer version and manifest bytes, plus a bounded reader for the compressed archive block. The reader seeks once, stops at the recorded length, and reports an early end as The PR body has the required |
Contributor
Author
|
[Review Verdict Round 1: APPROVED] |
Contributor
Author
Suggested squash commitTitle Body |
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR makes the container that
read_package_containerreturns hand out its raw blocks, so a caller can hash them. bootler#412 needs this to fingerprint the exact installer payload an install attempt ran from. With these accessors, bootler doesn't need a second footer parser of its own. The blocks are only hashed, never parsed. #72's rule that manifest decoding must go through this crate's parse still holds.New public API on
payload::UnparsedContainercontainer_version()returns the version recorded in the footer:1, orFORMAT_VERSION(currently2). This is not the manifest'sformat_version, and no manifest parse is needed to get it. It replaces the crate-internalfooter_version().raw_manifest_block()returns the manifest block exactly as it sits in the container. It is unparsed, unauthenticated and never re-serialized. It replaces the crate-internalmanifest_bytes(). Its rustdoc says to decode throughparse_unverified_manifestorverify::verify_package, never withserde_jsondirectly.archive_block_len()returns the length of the compressed archive block as the validated footer records it.raw_archive_block()seeks the source to the start of the block once and returns aRawArchiveBlock.RawArchiveBlockis new and public. It implementsReadonly, with noSeek, and borrows the container's source. It yields exactlyarchive_block_len()bytes and then EOF. It never reads outside the block, never holds the block in memory and never decompresses it. If the source ends before the recorded length,readreturnsio::ErrorKind::UnexpectedEofinstead of a short stream that looks successful. If the source reports reading more bytes than it was asked for,readreturns an error rather than trusting it.The accessors work whatever state the envelope blocks are in (
Absent,PresentorWrongLength) and whether or not the manifest would parse. Rejecting either case is left to the caller.The rustdoc of
read_package_containernow states that a file with no trailer is reported asPayloadError::NoTrailer. An installer-side caller uses that error to recognise an empty payload.Test support
payload::append_version_1_trailerwrites a version-1 container fixture:base ‖ manifest ‖ archive ‖ footer, using the 41-byte version-1 footer. It is gated with#[cfg(any(test, feature = "test-support"))], the same aswiden_envelope_blocks. With it, dependent crates can test version-1 input through the real reader without building footers by hand. The README and CHANGELOG mention it.Unchanged
No existing public signature, verdict or verdict order changes. The crate-internal callers in
verify.rs, including two of its tests, now call the renamed accessors, and nothing else there changes.verification_never_reads_the_archive_blockpasses unchanged, because onlyraw_archive_blockreads that block.Tests
The unit tests are in
src/payload/raw_block_tests.rs:1, andappend_traileroutput reports2.append_trailer_signed, it is also exactly what the signer was handed.rewrap_traileronto a base of a different length.Read + Seeksource shows one seek to the start of the block, then reads of exactlyarchive_block_len()bytes, with nothing read before or after the block.UnexpectedEof.PayloadError::Io.WrongLengthand when the manifest does not parse.read_package_container.NoTrailer.tests/raw_container_blocks.rsrepeats the rewrap and version-1 cases through the public API only, the way a dependent crate would use it.Locally, the full CI matrix passes: fmt, clippy with and without
test-support, and the tests with and withouttest-support.Closes #131
Deviations from the issue
None
Test plan
cargo fmt -- --check --config group_imports=StdExternalCratepassescargo clippy --all-targets -- -D warningspassescargo clippy --all-targets --features test-support -- -D warningspassescargo testpassescargo test --features test-supportpassesa_version_1_container_reports_1_and_a_current_one_reports_2: a version-1 fixture reportscontainer_version() == 1, andappend_traileroutput reports2the_raw_manifest_block_is_the_one_the_unsigned_writer_emitted:raw_manifest_block()equals the unsigned writer's manifest bytes, byte for bytethe_raw_manifest_block_is_the_one_the_signer_was_handed: forappend_trailer_signedoutput,raw_manifest_block()equals the bytes the signer was handedthe_raw_archive_block_is_the_one_the_writer_emitted_and_survives_a_rewrap:raw_archive_block()yields the writer's archive bytes, and the same bytes afterrewrap_traileronto a base of a different lengththe_archive_reader_reads_exactly_the_block_and_nothing_around_it: a recordingRead + Seeksource logs one seek to the block's offset, then reads that cover exactlyarchive_block_len()bytes and nothing before or after the blocka_source_that_ends_inside_the_block_is_unexpected_eof: a source cut short after validation, either at the block's start or inside it, returnsio::ErrorKind::UnexpectedEofafter delivering only the bytes before the cuta_failed_seek_to_the_archive_block_is_an_io_error: a failing seek returnsPayloadError::Iofromraw_archive_block()the_accessors_answer_whatever_the_envelope_state: all four accessors work when the envelope blocks areWrongLengththe_accessors_answer_for_a_manifest_that_does_not_parse: all four accessors work when the manifest does not parsethe_version_1_writer_frames_a_container_the_real_readers_accept:append_version_1_traileroutput is accepted byread_package_containera_file_without_a_trailer_is_no_trailer:read_package_containerreports a file with no trailer asPayloadError::NoTrailertests/raw_container_blocks.rs, which use the public API only: the raw blocks stay the same across a rewrap, and a version-1 fixture reads through the real readerverify.rstests pass;verification_never_reads_the_archive_blockis unchanged, and the only edits to the others renamefooter_version()/manifest_bytes()calls tocontainer_version()/raw_manifest_block()