fix(s3): complete flexible checksums across every upload path
Summary
S3 flexible checksums, completed across every upload path and verified as S3 does.
All ten algorithms, real digests. The model's ChecksumAlgorithm enum has CRC32, CRC32C, CRC64NVME, SHA1, SHA256, SHA512, MD5, XXHASH64, XXHASH3, XXHASH128; fakecloud only computed the first five and returned an empty checksum value for the rest. New crates/fakecloud-s3/src/checksum.rs implements all of them (incremental hashers, base64 of the big-endian digest; xxHash with seed 0 as aws-c-s3 does), plus composite checksums and a CRC combine (zlib construction, as aws-checksums) for CRC32/CRC32C/CRC64NVME. New dependency: xxhash-rust (xxh64 + xxh3), as a workspace dependency; SHA512/MD5 reuse sha2 / md-5.
Default CRC64NVME. An upload that names no algorithm gets a CRC64NVME full-object checksum, as S3 has done since Dec 2024; listings and checksum-mode reads report it.
One verification path (ChecksumHeaders): Content-MD5, x-amz-checksum-* values or trailers and x-amz-content-sha256 are verified the same way on every op. Mismatch -> BadDigest; malformed value, more than one checksum header, or an unknown algorithm -> InvalidRequest; malformed Content-MD5 -> InvalidDigest; declared trailer missing -> MalformedTrailerError. Empty header values are treated as absent, as before. Streaming uploads hash in the same pass that spools to disk (no second read), and a rejected upload no longer leaks its spool file. Small XML bodies (PutBucketTagging, DeleteObjects, ...) are verified too, including aws-chunked bodies with trailers, which the core decoder now keeps instead of discarding.
Error order: NoSuchBucket / NoSuchKey / 412 preconditions come before digest errors (PutObject, UploadPart, CopyObject, annotations, buffered bodies); on CompleteMultipartUpload, InvalidPart for a bad ETag comes before checksum errors.
Multipart: COMPOSITE and FULL_OBJECT per the user guide's per-algorithm table; a part in a different algorithm is rejected; CompleteMultipartUpload checks every listed part checksum (InvalidRequest for an algorithm mismatch, InvalidPart for a value mismatch), requires them for COMPOSITE, and validates the whole-object value's shape (-N exactly for COMPOSITE, none for FULL_OBJECT), x-amz-checksum-type and x-amz-mp-object-size. FULL_OBJECT CRCs are combined from stored part CRCs instead of re-reading the object. Part checksums persist and show up on part-number / single-part-range reads.
Object annotations: PutObjectAnnotation is a streaming-payload op; its aws-chunked body is de-framed and its trailer verified (it was stored with the framing). GetObjectAnnotation returns the stored algorithm's real digest (it returned a base64 MD5 under that algorithm's header) and both return x-amz-checksum-type. Limits follow the model docs: 1,000 annotations, 512-byte names, payloads 1 byte to 1 MiB, UTF-8 only. Annotations are now stored one per record (a put or delete touches only that annotation, no object copy under the write lock) and persisted; previously they were lost on restart. Older builds' embedded annotations are migrated on load.
Other fixes found on the way: GetObjectAttributes read only the first x-amz-object-attributes header (SDKs send one per attribute) and emitted TotalPartsCount where the model's XML name is PartsCount; ObjectParts paging added. Algorithm names are validated against the enum, case-insensitive, stored uppercase; persisted lowercase names still load.
Not verified against live S3: the exact message text for an unknown algorithm and some multipart messages (the error codes match the docs).
Test plan
- Unit: every algorithm against published vectors (CRC catalogue "123456789" check values, FIPS 180 / RFC 1321 empty-string digests, xxHash reference values), verification failures, trailers, spool cleanup, CRC combine == rehash across cut points, multipart COMPOSITE/FULL_OBJECT and part checks, error order, annotation storage (a put doesn't rewrite other annotations), legacy migration, persisted lowercase names.
- E2E
s3_checksum_integrity.rs: SDK put/get/head with checksum mode for the five algorithms the pinned SDK knows, raw requests for the other five, trailing-checksum uploads, composite and full-object multipart, annotation trailer round trip, buffered-body check.s3_persistence.rs: checksums and annotations survive a restart. Everys3*.rsE2E file passes. - Conformance
--test s3(51) and--test checksum_annotations; S3 probe 4068/4068.
Overlaps #2725 (shared version-commit path for PutObject / CopyObject / multipart); whichever merges second is rebased onto the other.
Sources: checking object integrity, PutObjectAnnotation, aws-c-s3 s3_checksums.c, S3 default data integrity protections.
Summary by cubic
Completes S3 flexible checksums across every upload path: all ten ChecksumAlgorithm values now produce real digests, uploads without an algorithm default to CRC64NVME, and Content-MD5, x-amz-checksum-*, trailer, and x-amz-content-sha256 values are verified consistently on every operation. Multipart uploads support COMPOSITE and FULL_OBJECT checksums per the S3 user guide, and checksum-mode reads return stored values, including per-part checksums.
Bug fixes
GetObjectAttributesnow reads allx-amz-object-attributesheaders and emitsPartsCountinstead ofTotalPartsCount; addedObjectPartspaging.PutObjectAnnotationnow de-frames aws-chunked bodies and verifies the trailer; annotations are stored one per record, survive restarts, and enforce the documented limits (1,000 per object, 512-byte names, 1 byte to 1 MiB payloads).- Rejected streaming uploads no longer leak their spool files; checksum mismatch errors respect S3's error ordering (
NoSuchBucket,NoSuchKey, 412 preconditions before digest errors).
Migration
- Existing persisted objects with lowercase or legacy checksum algorithm names still load; annotations embedded in older object snapshots are migrated on load.