Require a SHA-256 hash in every manifest entry (closes #224)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Loading accepted an entry whose hashes held no SHA-256 multihash, and check and fetch, which compute only SHA-256, then reported the intact file as a hash mismatch. Loading now refuses, naming the path, an entry that does not list exactly one SHA-256 multihash; other hashes beside it are allowed. AddFileWithHash accepts only a SHA-256 multihash, and freshen carries an unchanged entry's SHA-256 hash over instead of its first hash, found with SHA256Multihash, new in package mfer. docs/FORMAT.md states the rule for writers and readers. Two loading tests whose entries held an empty hash now give each entry a SHA-256 hash; the decoding-size test's path lengths change with it. Model: opus-5-5
This commit was merged in pull request #228.
This commit is contained in:
@@ -5,8 +5,10 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"uuid"
|
||||
@@ -167,40 +169,94 @@ func TestDeserializeRefusesPathListedTwice(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A manifest holding a file entry with no hash is refused as it is loaded,
|
||||
// naming the path: there is nothing to check the file against.
|
||||
func TestDeserializeRefusesEntryWithNoHash(t *testing.T) {
|
||||
// A file entry must list exactly one SHA-256 hash, and may list hashes of
|
||||
// other algorithms beside it. An entry with none, even one that lists a
|
||||
// SHA-512 hash, is refused as it is loaded, and so is an entry with two;
|
||||
// each refusal names the path. An entry listing a SHA-512 hash and then a
|
||||
// SHA-256 one loads with both.
|
||||
func TestDeserializeRequiresExactlyOneSHA256Hash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
id := uuid.NewV4()
|
||||
inner, err := proto.Marshal(&MFFile{
|
||||
Version: MFFile_VERSION_ONE,
|
||||
Files: []*MFFilePath{{Path: "x", Size: 1}},
|
||||
Uuid: id[:],
|
||||
})
|
||||
sha256Hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = NewManifestFromReader(bytes.NewReader(wrapInner(t, id, inner)))
|
||||
require.ErrorIs(t, err, errEntryMissingHash)
|
||||
require.EqualError(t, err, `file entry has no hash: "x"`)
|
||||
otherSHA256Hash, err := multihash.Encode(
|
||||
bytes.Repeat([]byte{1}, sha256.Size), multihash.SHA2_256)
|
||||
require.NoError(t, err)
|
||||
|
||||
sha512Hash, err := multihash.Encode(make([]byte, sha512.Size), multihash.SHA2_512)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hashes [][]byte
|
||||
want error // nil for an entry that loads
|
||||
}{
|
||||
{"no hash", nil, errEntryMissingHash},
|
||||
{"SHA-512 hash only", [][]byte{sha512Hash}, errEntryMissingHash},
|
||||
{
|
||||
"two SHA-256 hashes",
|
||||
[][]byte{sha256Hash, otherSHA256Hash},
|
||||
errEntryExtraSHA256Hash,
|
||||
},
|
||||
{"SHA-512 hash then SHA-256 hash", [][]byte{sha512Hash, sha256Hash}, nil},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
entry := &MFFilePath{Path: "x", Size: 1}
|
||||
for _, h := range tt.hashes {
|
||||
entry.Hashes = append(entry.Hashes, &MFFileChecksum{MultiHash: h})
|
||||
}
|
||||
|
||||
id := uuid.NewV4()
|
||||
inner, err := proto.Marshal(&MFFile{
|
||||
Version: MFFile_VERSION_ONE,
|
||||
Files: []*MFFilePath{entry},
|
||||
Uuid: id[:],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := NewManifestFromReader(bytes.NewReader(wrapInner(t, id, inner)))
|
||||
if tt.want == nil {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, m.Files(), 1)
|
||||
assert.Len(t, m.Files()[0].GetHashes(), len(tt.hashes))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.ErrorIs(t, err, tt.want)
|
||||
assert.Contains(t, err.Error(), `"x"`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Entries of a path, an empty hash, an empty MIME type and empty modification
|
||||
// and change times are counted at 432 bytes each (176 + 112 + 16 + 64 + 64)
|
||||
// and take 16 bytes plus the path to encode. A 37-character path makes that
|
||||
// 53 bytes, about 8.2 times: refused, and leaving any one of the five
|
||||
// uncounted, even the MIME type, brings it under 8. A 39-character path makes
|
||||
// it 55 bytes, about 7.9 times: loaded. Each entry's path is its number,
|
||||
// padded with zeros to that length, since a manifest lists a path only once.
|
||||
// Entries of a path, a SHA-256 hash, an empty MIME type and empty
|
||||
// modification and change times are counted at 432 bytes each (176 + 112 +
|
||||
// 16 + 64 + 64) and take 52 bytes plus the path to encode. A one-character
|
||||
// path makes that 53 bytes, about 8.2 times: refused, and leaving any one of
|
||||
// the five uncounted, even the MIME type, brings it under 8. A two-character
|
||||
// path makes it 54 bytes, 8 times exactly: loaded. There is one entry for
|
||||
// each base-36 digit, and each entry's path is its number in base 36, padded
|
||||
// with zeros to that length, since a manifest lists a path only once.
|
||||
func TestDeserializeRefusesEntriesThatDecodeTooLarge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
||||
require.NoError(t, err)
|
||||
|
||||
checksum, err := proto.Marshal(&MFFileChecksum{MultiHash: hash})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
pathLen int
|
||||
refused bool
|
||||
}{
|
||||
{37, true},
|
||||
{39, false},
|
||||
{1, true},
|
||||
{2, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -213,11 +269,14 @@ func TestDeserializeRefusesEntriesThatDecodeTooLarge(t *testing.T) {
|
||||
inner = protowire.AppendTag(inner, 102, protowire.BytesType) // MFFile.uuid
|
||||
inner = protowire.AppendBytes(inner, id[:])
|
||||
|
||||
for i := range 1000 {
|
||||
for i := range 36 {
|
||||
path := strconv.FormatInt(int64(i), 36)
|
||||
path = strings.Repeat("0", tt.pathLen-len(path)) + path
|
||||
|
||||
entry := protowire.AppendTag(nil, 1, protowire.BytesType) // MFFilePath.path
|
||||
entry = protowire.AppendString(entry, fmt.Sprintf("%0*d", tt.pathLen, i))
|
||||
entry = protowire.AppendString(entry, path)
|
||||
entry = protowire.AppendTag(entry, 3, protowire.BytesType) // MFFilePath.hashes
|
||||
entry = protowire.AppendBytes(entry, nil)
|
||||
entry = protowire.AppendBytes(entry, checksum)
|
||||
entry = protowire.AppendTag(entry, 301, protowire.BytesType) // MFFilePath.mimeType
|
||||
entry = protowire.AppendBytes(entry, nil)
|
||||
entry = protowire.AppendTag(entry, 302, protowire.BytesType) // MFFilePath.mtime
|
||||
@@ -247,10 +306,16 @@ func TestDeserializeDropsUnknownFields(t *testing.T) {
|
||||
unknown := protowire.AppendTag(nil, 99, protowire.BytesType) // in no message
|
||||
unknown = protowire.AppendBytes(unknown, []byte("not known"))
|
||||
|
||||
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
||||
require.NoError(t, err)
|
||||
|
||||
checksum, err := proto.Marshal(&MFFileChecksum{MultiHash: hash})
|
||||
require.NoError(t, err)
|
||||
|
||||
entry := protowire.AppendTag(nil, 1, protowire.BytesType) // MFFilePath.path
|
||||
entry = protowire.AppendString(entry, "a")
|
||||
entry = protowire.AppendTag(entry, 3, protowire.BytesType) // MFFilePath.hashes
|
||||
entry = protowire.AppendBytes(entry, nil)
|
||||
entry = protowire.AppendBytes(entry, checksum)
|
||||
entry = append(entry, unknown...)
|
||||
|
||||
id := uuid.NewV4()
|
||||
|
||||
Reference in New Issue
Block a user