fix: report logical part sizes in GetObjectAttributes

GetObjectAttributes filled ObjectPart.Size from the on-disk part length
(cmd/object-handlers.go:715), so every compressed or encrypted multipart
object reported transformed sizes that do not sum to the logical
ObjectSize the same response returns from objInfo.GetActualSize().
Report each part's uploaded plaintext length instead: a compressed part
uses its recorded ActualSize, and a separately encrypted part derives the
plaintext length with sio.DecryptedSize, because ActualSize is the
ciphertext length for a replicated SSE-C part and zero for parts written
before actualSize existed.

Only parts of an encrypted multipart object are streams of their own. A
legacy encrypted object carries no multipart marker and is one continuous
stream that the erasure writer split into storage fragments, so those
fragments keep their stored size. Where a part is a stream, one whose
length cannot be a valid encrypted stream has no logical length, and the
request now fails with XMinioObjectTampered rather than reporting the
ciphertext length; DecryptObjectInfo does not catch that case, because
ObjectInfo.isMultipart gives up on the first bad part and only the object
total is then validated.

Tests: TestAPIGetObjectAttributesMultipartLogicalPartSize (plain,
compressed, SSE-C and compressed+SSE-C, consecutive and sparse part
numbers), TestAPIGetObjectAttributesCompressedEmptyTrailingPart,
TestAPIGetObjectAttributesEncryptedPartLengths, and a part-size assertion
added to TestAPISSECMultipartReplicationTrust. Compatibility: the XML
shape is unchanged and nothing is written to disk, only the value of the
existing Size element is corrected.

Fixes pgsty/silo#114

Signed-off-by: Feng Ruohang <rh@vonng.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7qJqWwy8oFA6aCXWRzXQe
This commit is contained in:
Feng Ruohang
2026-09-05 15:38:33 +08:00
parent f0bd164b92
commit b5409ca112
3 changed files with 450 additions and 1 deletions
+19
View File
@@ -829,6 +829,25 @@ func testAPISSECMultipartReplicationTrust(obj ObjectLayer, instanceType, bucketN
if !bytes.Equal(getRec.Body.Bytes(), data) {
t.Fatal("replicated SSE-C multipart object did not decrypt to source plaintext")
}
// A replicated SSE-C part is written as raw ciphertext, so its stored
// ActualSize is the ciphertext length. GetObjectAttributes must still
// report the part's plaintext length, which for this single-part object
// is the whole object size.
attributes := attributesPartsFetch(t, apiRouter, credentials, bucketName, object, sseHeaders)
if len(attributes.ObjectParts.Parts) != 1 {
t.Fatalf("replica attributes part count %d, want 1", len(attributes.ObjectParts.Parts))
}
if got := attributes.ObjectParts.Parts[0].Size; got != int64(len(data)) {
t.Errorf("replica attributes part 1 size=%d, want plaintext size %d", got, len(data))
}
if attributes.ObjectSize != int64(len(data)) {
t.Errorf("replica attributes ObjectSize=%d, want %d", attributes.ObjectSize, len(data))
}
if attributes.ObjectParts.Parts[0].Size != attributes.ObjectSize {
t.Errorf("replica attributes part 1 size=%d does not match ObjectSize=%d",
attributes.ObjectParts.Parts[0].Size, attributes.ObjectSize)
}
}
// TestAPIStreamingTrailerWithUntrustedReplicationHeaders verifies that a