protondrive: don't list a file as empty when its recorded size is missing or invalid
A file whose extended attributes had a SHA-1 but a missing, negative or malformed Size was listed with size 0, so copying it failed with a size mismatch. Its size is now the sum of its recorded block sizes, if they are usable, and otherwise unknown, as for a file without extended attributes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f89be42eaf
commit
18b4f7d1d1
6 files changed
+40
-14
No files matched your search
@@ -750,6 +750,7 @@ type xattrs struct {
|
||||
BlockSizes []int64
|
||||
SHA1 string // hex; empty for a folder
|
||||
Extra map[string]json.RawMessage // top level members other than Common
|
||||
NoSize bool // set if read attributes record no usable size
|
||||
}
|
||||
|
||||
// xattrsTimeLayout is the layout of ModificationTime, as JavaScript's
|
||||
@@ -808,6 +809,7 @@ func parseXAttrs(data []byte) *xattrs {
|
||||
x := &xattrs{}
|
||||
var members map[string]json.RawMessage
|
||||
if json.Unmarshal(data, &members) != nil {
|
||||
x.NoSize = true
|
||||
return x
|
||||
}
|
||||
var common struct {
|
||||
@@ -831,9 +833,6 @@ func parseXAttrs(data []byte) *xattrs {
|
||||
}
|
||||
}
|
||||
}
|
||||
if json.Unmarshal(common.Size, &x.Size) != nil || x.Size < 0 {
|
||||
x.Size = 0
|
||||
}
|
||||
var blockSizes []int64
|
||||
if json.Unmarshal(common.BlockSizes, &blockSizes) == nil && len(blockSizes) > 0 {
|
||||
x.BlockSizes = blockSizes
|
||||
@@ -844,6 +843,19 @@ func parseXAttrs(data []byte) *xattrs {
|
||||
}
|
||||
}
|
||||
}
|
||||
if json.Unmarshal(common.Size, &x.Size) != nil || x.Size < 0 {
|
||||
// Without a valid size, block sizes no larger than a block
|
||||
// add up to it.
|
||||
x.Size = 0
|
||||
x.NoSize = x.BlockSizes == nil
|
||||
for _, size := range x.BlockSizes {
|
||||
if size > maxBlockSize {
|
||||
x.Size, x.NoSize = 0, true
|
||||
break
|
||||
}
|
||||
x.Size += size
|
||||
}
|
||||
}
|
||||
if common.Digests != nil {
|
||||
_ = json.Unmarshal(common.Digests.SHA1, &x.SHA1)
|
||||
}
|
||||
|
||||
@@ -746,7 +746,7 @@ func TestXAttrs(t *testing.T) {
|
||||
got, verifyErr, err := decryptXAttrs(encryptSigned(t, []byte(`{}`), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, verifyErr)
|
||||
assert.Equal(t, &xattrs{}, got)
|
||||
assert.Equal(t, &xattrs{NoSize: true}, got)
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
got, verifyErr, err := decryptXAttrs("", nodeKR, publicOf(t, signer), now)
|
||||
@@ -759,17 +759,25 @@ func TestXAttrs(t *testing.T) {
|
||||
in := `{"Common":{"ModificationTime":"yesterday","Size":"big","BlockSizes":[1,0],"Digests":{"SHA1":7}}}`
|
||||
got, _, err := decryptXAttrs(encryptSigned(t, []byte(in), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &xattrs{}, got)
|
||||
assert.Equal(t, &xattrs{NoSize: true}, got)
|
||||
|
||||
got, _, err = decryptXAttrs(encryptSigned(t, []byte(`not json`), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &xattrs{}, got)
|
||||
assert.Equal(t, &xattrs{NoSize: true}, got)
|
||||
})
|
||||
t.Run("NegativeSize", func(t *testing.T) {
|
||||
in := `{"Common":{"Size":-5,"BlockSizes":[5],"Digests":{"SHA1":"abc"}}}`
|
||||
got, _, err := decryptXAttrs(encryptSigned(t, []byte(in), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &xattrs{BlockSizes: []int64{5}, SHA1: "abc"}, got)
|
||||
// The size is taken from the block sizes, if they are usable.
|
||||
for in, want := range map[string]*xattrs{
|
||||
`{"Common":{"Size":-5,"BlockSizes":[5],"Digests":{"SHA1":"abc"}}}`: {Size: 5, BlockSizes: []int64{5}, SHA1: "abc"},
|
||||
`{"Common":{"BlockSizes":[4194304,3],"Digests":{"SHA1":"abc"}}}`: {Size: 4194307, BlockSizes: []int64{4194304, 3}, SHA1: "abc"},
|
||||
`{"Common":{"Size":"big","BlockSizes":[4194304,3],"Digests":{"SHA1":"abc"}}}`: {Size: 4194307, BlockSizes: []int64{4194304, 3}, SHA1: "abc"},
|
||||
`{"Common":{"Size":-5,"Digests":{"SHA1":"abc"}}}`: {SHA1: "abc", NoSize: true},
|
||||
`{"Common":{"Size":-5,"BlockSizes":[1152921504606846976],"Digests":{"SHA1":"abc"}}}`: {BlockSizes: []int64{1 << 60}, SHA1: "abc", NoSize: true},
|
||||
} {
|
||||
got, _, err := decryptXAttrs(encryptSigned(t, []byte(in), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, got, in)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,9 @@ func cloneFile(file *api.File) *api.File {
|
||||
func checkXAttrs(t *testing.T, x *xattrs) {
|
||||
t.Helper()
|
||||
assert.GreaterOrEqual(t, x.Size, int64(0))
|
||||
if x.NoSize {
|
||||
assert.Zero(t, x.Size)
|
||||
}
|
||||
if x.BlockSizes != nil {
|
||||
assert.NotEmpty(t, x.BlockSizes)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func (n *node) size(original bool) int64 {
|
||||
// plaintextSize returns the size of the file's content, and whether
|
||||
// the extended attributes record it.
|
||||
func (n *node) plaintextSize() (int64, bool) {
|
||||
if n.X != nil && (n.X.SHA1 != "" || n.X.Size > 0 || len(n.X.BlockSizes) > 0) {
|
||||
if n.X != nil && !n.X.NoSize && (n.X.SHA1 != "" || n.X.Size > 0 || len(n.X.BlockSizes) > 0) {
|
||||
return n.X.Size, true
|
||||
}
|
||||
return 0, false
|
||||
|
||||
@@ -213,6 +213,12 @@ func TestNodeWithoutXAttrs(t *testing.T) {
|
||||
assert.Equal(t, "abcdef", n.sha1())
|
||||
assert.Equal(t, int64(0), n.size(true))
|
||||
assert.Equal(t, int64(42), n.size(false))
|
||||
|
||||
// Attributes which record no usable size leave it unknown.
|
||||
n.X = &xattrs{SHA1: "ABCDEF", NoSize: true}
|
||||
_, known := n.plaintextSize()
|
||||
assert.False(t, known)
|
||||
assert.Equal(t, int64(42), n.size(true))
|
||||
}
|
||||
|
||||
func TestGetNode(t *testing.T) {
|
||||
|
||||
@@ -147,8 +147,5 @@ Small, specific, independently actionable. Remove only when actually done.
|
||||
existing files in one folder lists it again each time (`sync` is not
|
||||
affected: it calls `Update`). New files and folders are added to the
|
||||
listing instead since Task 17.
|
||||
- [ ] protondrive: extended attributes with a SHA-1 but no `Size` make a
|
||||
file report size 0; the sum of `BlockSizes`, or the encrypted size,
|
||||
would be a better fallback. Seen in Task 17.
|
||||
- [ ] protondrive: spec 9.5 asks for a fuzz target for event decoding;
|
||||
add it with events (fuzz_internal_test.go).
|
||||
Reference in new issue
Block a user