Record file mode in the manifest, 0000 unless asked (closes #161)
check / check (push) Failing after 4s
check / check (push) Failing after 4s
MFFilePath gains mode (field 304): a file's permission bits, 0777 at most, or 0000, meaning none recorded. gen and freshen record real modes only with --include-permissions (ScannerOptions.IncludePermissions); the builder keeps only mode.Perm(), so setuid, setgid and sticky are never written. list -l and export show the mode in octal. check reports MODE_MISMATCH for a recorded mode other than 0000 the file lacks. fetch refuses a manifest with a mode above 0777 before requesting any file, sets only the permission bits of each recorded mode on the files it writes, and downloads again a present file whose mode differs. The decoding-cost bound counts a file entry at 176 bytes, up from 160. Model: opus-5-5
This commit was merged in pull request #163.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -137,12 +138,14 @@ func (b *Builder) SetSeed(seed string) {
|
||||
}
|
||||
|
||||
// AddFile reads file content from reader, computes hashes, and adds to manifest.
|
||||
// Only mode's permission bits (mode.Perm()) are recorded; 0 records none.
|
||||
// Progress updates are sent to the progress channel (if non-nil) without blocking.
|
||||
// Returns the number of bytes read.
|
||||
func (b *Builder) AddFile(
|
||||
path RelFilePath,
|
||||
size FileSize,
|
||||
mtime ModTime,
|
||||
mode fs.FileMode,
|
||||
reader io.Reader,
|
||||
progress chan<- FileHashProgress,
|
||||
) (FileSize, error) {
|
||||
@@ -198,6 +201,7 @@ func (b *Builder) AddFile(
|
||||
{MultiHash: mh},
|
||||
},
|
||||
Mtime: mtime.Timestamp(),
|
||||
Mode: uint32(mode.Perm()),
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
@@ -229,12 +233,14 @@ func (b *Builder) FileCount() int {
|
||||
|
||||
// AddFileWithHash adds a file entry with a pre-computed hash.
|
||||
// This is useful when the hash is already known (e.g., from an existing manifest).
|
||||
// Only mode's permission bits (mode.Perm()) are recorded; 0 records none.
|
||||
// Returns an error if path is invalid, size is negative, or hash is not a
|
||||
// multihash with a digest of at least 32 bytes, as long as SHA-256's.
|
||||
func (b *Builder) AddFileWithHash(
|
||||
path RelFilePath,
|
||||
size FileSize,
|
||||
mtime ModTime,
|
||||
mode fs.FileMode,
|
||||
hash Multihash,
|
||||
) error {
|
||||
err := ValidatePath(string(path))
|
||||
@@ -268,6 +274,7 @@ func (b *Builder) AddFileWithHash(
|
||||
{MultiHash: hash},
|
||||
},
|
||||
Mtime: mtime.Timestamp(),
|
||||
Mode: uint32(mode.Perm()),
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
|
||||
+18
-18
@@ -35,7 +35,7 @@ func TestBuilderAddFile(t *testing.T) {
|
||||
reader := bytes.NewReader(content)
|
||||
|
||||
bytesRead, err := b.AddFile(
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), reader, nil,
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), 0, reader, nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, FileSize(len(content)), bytesRead)
|
||||
@@ -49,7 +49,7 @@ func TestBuilderAddFileWithHash(t *testing.T) {
|
||||
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), hash)
|
||||
err = b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), 0, hash)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, b.FileCount())
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func TestBuilderAddFileWithHashValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := NewBuilder()
|
||||
err := b.AddFileWithHash("", 100, ModTime(time.Now()), sha256Hash)
|
||||
err := b.AddFileWithHash("", 100, ModTime(time.Now()), 0, sha256Hash)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path")
|
||||
})
|
||||
@@ -73,7 +73,7 @@ func TestBuilderAddFileWithHashValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := NewBuilder()
|
||||
err := b.AddFileWithHash("test.txt", -1, ModTime(time.Now()), sha256Hash)
|
||||
err := b.AddFileWithHash("test.txt", -1, ModTime(time.Now()), 0, sha256Hash)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "size")
|
||||
})
|
||||
@@ -82,7 +82,7 @@ func TestBuilderAddFileWithHashValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := NewBuilder()
|
||||
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), sha256Hash)
|
||||
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), 0, sha256Hash)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, b.FileCount())
|
||||
})
|
||||
@@ -118,7 +118,7 @@ func TestBuilderAddFileWithHashRejectsBadHashes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := NewBuilder()
|
||||
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), tt.hash)
|
||||
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), 0, tt.hash)
|
||||
require.ErrorIs(t, err, tt.want)
|
||||
assert.Equal(t, 0, b.FileCount())
|
||||
})
|
||||
@@ -133,7 +133,7 @@ func TestBuilderBuild(t *testing.T) {
|
||||
reader := bytes.NewReader(content)
|
||||
|
||||
_, err := b.AddFile(
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), reader, nil,
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), 0, reader, nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -196,7 +196,7 @@ func TestBuilderDeterministicOutput(t *testing.T) {
|
||||
for _, f := range files {
|
||||
r := bytes.NewReader([]byte(f.content))
|
||||
_, err := b.AddFile(
|
||||
RelFilePath(f.path), FileSize(len(f.content)), mtime, r, nil,
|
||||
RelFilePath(f.path), FileSize(len(f.content)), mtime, 0, r, nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -279,7 +279,7 @@ func TestBuilderAddFileSizeMismatch(t *testing.T) {
|
||||
reader := bytes.NewReader(content)
|
||||
|
||||
// Declare wrong size
|
||||
_, err := b.AddFile("test.txt", FileSize(100), ModTime(time.Now()), reader, nil)
|
||||
_, err := b.AddFile("test.txt", FileSize(100), ModTime(time.Now()), 0, reader, nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "size mismatch")
|
||||
}
|
||||
@@ -291,12 +291,12 @@ func TestBuilderAddFileInvalidPath(t *testing.T) {
|
||||
content := []byte("data")
|
||||
reader := bytes.NewReader(content)
|
||||
|
||||
_, err := b.AddFile("", FileSize(len(content)), ModTime(time.Now()), reader, nil)
|
||||
_, err := b.AddFile("", FileSize(len(content)), ModTime(time.Now()), 0, reader, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
reader.Reset(content)
|
||||
_, err = b.AddFile(
|
||||
"/absolute", FileSize(len(content)), ModTime(time.Now()), reader, nil,
|
||||
"/absolute", FileSize(len(content)), ModTime(time.Now()), 0, reader, nil,
|
||||
)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -310,7 +310,7 @@ func TestBuilderAddFileWithProgress(t *testing.T) {
|
||||
progress := make(chan FileHashProgress, 100)
|
||||
|
||||
bytesRead, err := b.AddFile(
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), reader, progress,
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), 0, reader, progress,
|
||||
)
|
||||
close(progress)
|
||||
require.NoError(t, err)
|
||||
@@ -345,7 +345,7 @@ func TestBuilderBuildRoundTrip(t *testing.T) {
|
||||
for _, f := range files {
|
||||
reader := bytes.NewReader(f.content)
|
||||
_, err := b.AddFile(
|
||||
RelFilePath(f.path), FileSize(len(f.content)), ModTime(now), reader, nil,
|
||||
RelFilePath(f.path), FileSize(len(f.content)), ModTime(now), 0, reader, nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -387,7 +387,7 @@ func TestBuilderBuildRoundTripLargeManifest(t *testing.T) {
|
||||
|
||||
for i := range 4000 {
|
||||
path := RelFilePath(fmt.Sprintf("dir/file-%05d.txt", i))
|
||||
require.NoError(t, b.AddFileWithHash(path, FileSize(i), ModTime{}, hash))
|
||||
require.NoError(t, b.AddFileWithHash(path, FileSize(i), ModTime{}, 0, hash))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -452,7 +452,7 @@ func TestManifestString(t *testing.T) {
|
||||
content := []byte("test")
|
||||
reader := bytes.NewReader(content)
|
||||
_, err := b.AddFile(
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), reader, nil,
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), 0, reader, nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -484,7 +484,7 @@ func TestBuilderOmitsCreatedAtByDefault(t *testing.T) {
|
||||
b := NewBuilder()
|
||||
content := []byte("hello")
|
||||
_, err := b.AddFile(
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()),
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), 0,
|
||||
bytes.NewReader(content), nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -506,7 +506,7 @@ func TestBuilderIncludesCreatedAtWhenRequested(t *testing.T) {
|
||||
|
||||
content := []byte("hello")
|
||||
_, err := b.AddFile(
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()),
|
||||
"test.txt", FileSize(len(content)), ModTime(time.Now()), 0,
|
||||
bytes.NewReader(content), nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -532,7 +532,7 @@ func TestBuilderDeterministicFileOrder(t *testing.T) {
|
||||
content := []byte("content of " + name)
|
||||
_, err := b.AddFile(
|
||||
RelFilePath(name), FileSize(len(content)),
|
||||
ModTime(time.Unix(1000, 0)), bytes.NewReader(content), nil,
|
||||
ModTime(time.Unix(1000, 0)), 0, bytes.NewReader(content), nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
+17
-3
@@ -36,6 +36,7 @@ const (
|
||||
StatusMissing // File not found on disk
|
||||
StatusSizeMismatch // File size differs from manifest
|
||||
StatusHashMismatch // File hash differs from manifest
|
||||
StatusModeMismatch // File permission bits differ from a recorded mode
|
||||
StatusExtra // File exists on disk but not in manifest
|
||||
StatusError // Error occurred during verification
|
||||
)
|
||||
@@ -50,6 +51,8 @@ func (s Status) String() string {
|
||||
return "SIZE_MISMATCH"
|
||||
case StatusHashMismatch:
|
||||
return "HASH_MISMATCH"
|
||||
case StatusModeMismatch:
|
||||
return "MODE_MISMATCH"
|
||||
case StatusExtra:
|
||||
return "EXTRA"
|
||||
case StatusError:
|
||||
@@ -408,11 +411,22 @@ func (c *Checker) checkFile(entry *MFFilePath, checkedBytes *FileSize) Result {
|
||||
return Result{Path: relPath, Status: StatusError, Message: err.Error()}
|
||||
}
|
||||
|
||||
// Check against all hashes in manifest (at least one must match)
|
||||
// Check against all hashes in manifest (at least one must match),
|
||||
// then against the recorded mode, where one is: 0 means none was.
|
||||
for _, hash := range entry.GetHashes() {
|
||||
if bytes.Equal(computed, hash.GetMultiHash()) {
|
||||
return Result{Path: relPath, Status: StatusOK}
|
||||
if !bytes.Equal(computed, hash.GetMultiHash()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.GetMode() != 0 && info.Mode().Perm() != os.FileMode(entry.GetMode()) {
|
||||
return Result{
|
||||
Path: relPath,
|
||||
Status: StatusModeMismatch,
|
||||
Message: "mode mismatch",
|
||||
}
|
||||
}
|
||||
|
||||
return Result{Path: relPath, Status: StatusOK}
|
||||
}
|
||||
|
||||
return Result{
|
||||
|
||||
+52
-2
@@ -34,6 +34,7 @@ func TestStatusString(t *testing.T) {
|
||||
{StatusMissing, "MISSING"},
|
||||
{StatusSizeMismatch, "SIZE_MISMATCH"},
|
||||
{StatusHashMismatch, "HASH_MISMATCH"},
|
||||
{StatusModeMismatch, "MODE_MISMATCH"},
|
||||
{StatusExtra, "EXTRA"},
|
||||
{StatusError, "ERROR"},
|
||||
{Status(99), "UNKNOWN"},
|
||||
@@ -59,7 +60,7 @@ func createTestManifest(
|
||||
for path, content := range files {
|
||||
reader := bytes.NewReader(content)
|
||||
_, err := builder.AddFile(
|
||||
RelFilePath(path), FileSize(len(content)), ModTime(time.Now()), reader, nil,
|
||||
RelFilePath(path), FileSize(len(content)), ModTime(time.Now()), 0, reader, nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -279,7 +280,8 @@ func TestCheckMissingFile(t *testing.T) {
|
||||
missingCount++
|
||||
|
||||
assert.Equal(t, RelFilePath("missing.txt"), r.Path)
|
||||
case StatusSizeMismatch, StatusHashMismatch, StatusExtra, StatusError:
|
||||
case StatusSizeMismatch, StatusHashMismatch, StatusModeMismatch,
|
||||
StatusExtra, StatusError:
|
||||
// Not expected in this test; counted assertions below will fail.
|
||||
}
|
||||
}
|
||||
@@ -349,6 +351,54 @@ func TestCheckHashMismatch(t *testing.T) {
|
||||
assert.Equal(t, RelFilePath(testFileName), r.Path)
|
||||
}
|
||||
|
||||
// A recorded mode other than 0000 that differs from the file's permission
|
||||
// bits fails the check; a recorded 0000 is never checked.
|
||||
func TestCheckMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
recorded os.FileMode
|
||||
onDisk os.FileMode
|
||||
want Status
|
||||
}{
|
||||
{"recorded mode matches", 0o640, 0o640, StatusOK},
|
||||
{"recorded mode differs", 0o640, 0o600, StatusModeMismatch},
|
||||
{"0000 is not checked", 0, 0o600, StatusOK},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
content := []byte("content")
|
||||
|
||||
b := NewBuilder()
|
||||
_, err := b.AddFile(testFileName, FileSize(len(content)), ModTime{},
|
||||
tc.recorded, bytes.NewReader(content), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, b.Build(context.Background(), &buf))
|
||||
require.NoError(t, afero.WriteFile(fs, testManifestPath, buf.Bytes(), 0o644))
|
||||
require.NoError(t, fs.MkdirAll(testDataDir, 0o755))
|
||||
require.NoError(t, afero.WriteFile(fs,
|
||||
testDataDir+"/"+testFileName, content, tc.onDisk))
|
||||
|
||||
chk, err := NewChecker(&CheckerOptions{
|
||||
ManifestPath: testManifestPath,
|
||||
BasePath: testDataDir,
|
||||
Fs: fs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(chan Result, 1)
|
||||
require.NoError(t, chk.Check(context.Background(), results, nil))
|
||||
|
||||
assert.Equal(t, tc.want, (<-results).Status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckWithProgress(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+4
-4
@@ -29,8 +29,8 @@ const (
|
||||
// Bytes decoding sets aside for each file entry, hash, timestamp and
|
||||
// MIME type, however short its encoding. checkDecodedSize refuses an
|
||||
// inner message for which these add up to more than maxDecodedGrowth
|
||||
// times its size.
|
||||
decodedFileEntrySize = 160
|
||||
// times its size. The mode is held in the file entry itself.
|
||||
decodedFileEntrySize = 176
|
||||
decodedHashSize = 112
|
||||
decodedTimestampSize = 64
|
||||
decodedMIMETypeSize = 16
|
||||
@@ -38,7 +38,7 @@ const (
|
||||
// Each file entry mfer writes holds a path of at least one byte, a
|
||||
// multihash at least as long as SHA-256's 34 bytes (AddFileWithHash
|
||||
// refuses shorter ones) and a modification time: at least 47 bytes,
|
||||
// counted at 336. So its manifests add up to at most about 7.15 times
|
||||
// their size, and this limit is about 12% above that.
|
||||
// counted at 352. So its manifests add up to at most about 7.49 times
|
||||
// their size, and this limit is about 7% above that.
|
||||
maxDecodedGrowth = 8
|
||||
)
|
||||
|
||||
@@ -119,11 +119,11 @@ func TestDeserializeRejectsInvalidEntryPaths(t *testing.T) {
|
||||
}
|
||||
|
||||
// Entries of a path, an empty hash, an empty MIME type and empty modification
|
||||
// and change times are counted at 416 bytes each (160 + 112 + 16 + 64 + 64)
|
||||
// and take 16 bytes plus the path to encode. A 35-character path makes that
|
||||
// 51 bytes, about 8.2 times: refused, and leaving any one of the five
|
||||
// uncounted, even the MIME type, brings it under 8. A 37-character path makes
|
||||
// it 53 bytes, about 7.8 times: loaded.
|
||||
// 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.
|
||||
func TestDeserializeRefusesEntriesThatDecodeTooLarge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -131,8 +131,8 @@ func TestDeserializeRefusesEntriesThatDecodeTooLarge(t *testing.T) {
|
||||
pathLen int
|
||||
refused bool
|
||||
}{
|
||||
{35, true},
|
||||
{37, false},
|
||||
{37, true},
|
||||
{39, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -215,7 +215,7 @@ func TestDeserializeLoadsDensestManifest(t *testing.T) {
|
||||
const files = 10000
|
||||
for i := range files {
|
||||
name := RelFilePath(strconv.FormatInt(int64(i), 36))
|
||||
require.NoError(t, b.AddFileWithHash(name, 0, ModTime(time.Unix(0, 0)), hash))
|
||||
require.NoError(t, b.AddFileWithHash(name, 0, ModTime(time.Unix(0, 0)), 0, hash))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -233,7 +233,7 @@ func TestDeserializeValidManifestRoundTrips(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
b := NewBuilder()
|
||||
require.NoError(t, b.AddFileWithHash("dir/file.txt", 123, ModTime{}, hash))
|
||||
require.NoError(t, b.AddFileWithHash("dir/file.txt", 123, ModTime{}, 0, hash))
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, b.Build(context.Background(), &buf))
|
||||
|
||||
+4
-4
@@ -187,7 +187,7 @@ func TestBuilderWithSigning(t *testing.T) {
|
||||
// Add a test file
|
||||
content := []byte("test file content")
|
||||
reader := bytes.NewReader(content)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, reader, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Build the manifest
|
||||
@@ -314,7 +314,7 @@ func TestManifestSignatureVerification(t *testing.T) {
|
||||
// Add a test file
|
||||
content := []byte("test file content for verification")
|
||||
reader := bytes.NewReader(content)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, reader, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Build the manifest
|
||||
@@ -344,7 +344,7 @@ func TestManifestTamperedSignatureFails(t *testing.T) {
|
||||
|
||||
content := []byte("test file content")
|
||||
reader := bytes.NewReader(content)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, reader, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -377,7 +377,7 @@ func TestBuilderWithoutSigning(t *testing.T) {
|
||||
// Add a test file
|
||||
content := []byte("test file content")
|
||||
reader := bytes.NewReader(content)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, reader, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Build the manifest
|
||||
|
||||
+15
-5
@@ -337,9 +337,11 @@ type MFFilePath struct {
|
||||
// gotta have at least one:
|
||||
Hashes []*MFFileChecksum `protobuf:"bytes,3,rep,name=hashes,proto3" json:"hashes,omitempty"`
|
||||
// optional per-file metadata
|
||||
MimeType *string `protobuf:"bytes,301,opt,name=mimeType,proto3,oneof" json:"mimeType,omitempty"`
|
||||
Mtime *Timestamp `protobuf:"bytes,302,opt,name=mtime,proto3,oneof" json:"mtime,omitempty"`
|
||||
Ctime *Timestamp `protobuf:"bytes,303,opt,name=ctime,proto3,oneof" json:"ctime,omitempty"`
|
||||
MimeType *string `protobuf:"bytes,301,opt,name=mimeType,proto3,oneof" json:"mimeType,omitempty"`
|
||||
Mtime *Timestamp `protobuf:"bytes,302,opt,name=mtime,proto3,oneof" json:"mtime,omitempty"`
|
||||
Ctime *Timestamp `protobuf:"bytes,303,opt,name=ctime,proto3,oneof" json:"ctime,omitempty"`
|
||||
// permission bits, at most 0777; 0 when not recorded
|
||||
Mode uint32 `protobuf:"varint,304,opt,name=mode,proto3" json:"mode,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -416,6 +418,13 @@ func (x *MFFilePath) GetCtime() *Timestamp {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MFFilePath) GetMode() uint32 {
|
||||
if x != nil {
|
||||
return x.Mode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type MFFileChecksum struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// 1.0 golang implementation must write a multihash here
|
||||
@@ -561,7 +570,7 @@ const file_mf_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"_signatureB\t\n" +
|
||||
"\a_signerB\x10\n" +
|
||||
"\x0e_signingPubKey\"\xf0\x01\n" +
|
||||
"\x0e_signingPubKey\"\x85\x02\n" +
|
||||
"\n" +
|
||||
"MFFilePath\x12\x12\n" +
|
||||
"\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" +
|
||||
@@ -571,7 +580,8 @@ const file_mf_proto_rawDesc = "" +
|
||||
"\x05mtime\x18\xae\x02 \x01(\v2\n" +
|
||||
".TimestampH\x01R\x05mtime\x88\x01\x01\x12&\n" +
|
||||
"\x05ctime\x18\xaf\x02 \x01(\v2\n" +
|
||||
".TimestampH\x02R\x05ctime\x88\x01\x01B\v\n" +
|
||||
".TimestampH\x02R\x05ctime\x88\x01\x01\x12\x13\n" +
|
||||
"\x04mode\x18\xb0\x02 \x01(\rR\x04modeB\v\n" +
|
||||
"\t_mimeTypeB\b\n" +
|
||||
"\x06_mtimeB\b\n" +
|
||||
"\x06_ctime\".\n" +
|
||||
|
||||
@@ -59,6 +59,8 @@ message MFFilePath {
|
||||
optional string mimeType = 301;
|
||||
optional Timestamp mtime = 302;
|
||||
optional Timestamp ctime = 303;
|
||||
// permission bits, at most 0777; 0 when not recorded
|
||||
uint32 mode = 304;
|
||||
}
|
||||
|
||||
message MFFileChecksum {
|
||||
|
||||
@@ -1 +1 @@
|
||||
fa6fceaba5553c8667c631994535fe5c307c8adb19f3ad289babf79a0f438650 mf.proto
|
||||
3d4dcb0b2f4640dd3ae6bb48b833e9f26ae9771e2d3e88bd646e7f1724dda654 mf.proto
|
||||
|
||||
@@ -41,6 +41,7 @@ func TestFileEntryFieldsMatchSpec(t *testing.T) {
|
||||
"mimeType": 301,
|
||||
"mtime": 302,
|
||||
"ctime": 303,
|
||||
"mode": 304,
|
||||
}
|
||||
|
||||
got := map[string]protoreflect.FieldNumber{}
|
||||
|
||||
@@ -52,6 +52,9 @@ type ScannerOptions struct {
|
||||
// IncludeTimestamps includes a createdAt timestamp in the manifest
|
||||
// (default: omit for determinism).
|
||||
IncludeTimestamps bool
|
||||
// IncludePermissions records each file's permission bits, 0777 at
|
||||
// most, in the manifest (default: record 0000).
|
||||
IncludePermissions bool
|
||||
// Fs is the filesystem to use, defaults to OsFs if nil.
|
||||
Fs afero.Fs
|
||||
// SigningOptions holds GPG signing options (nil = no signing).
|
||||
@@ -69,6 +72,7 @@ type FileEntry struct {
|
||||
Size FileSize // File size in bytes
|
||||
Mtime ModTime // Last modification time
|
||||
Ctime time.Time // Creation time (platform-dependent)
|
||||
Mode fs.FileMode // Permission bits (Perm() of the file's mode)
|
||||
}
|
||||
|
||||
// Scanner accumulates files and generates manifests from them.
|
||||
@@ -353,11 +357,18 @@ func (s *Scanner) scanFile(
|
||||
}(scannedBytes, scannedFiles)
|
||||
}
|
||||
|
||||
// A mode of 0 records 0000, which means none was recorded.
|
||||
var mode fs.FileMode
|
||||
if s.options.IncludePermissions {
|
||||
mode = entry.Mode
|
||||
}
|
||||
|
||||
// Add to manifest with progress channel
|
||||
bytesRead, err := builder.AddFile(
|
||||
entry.Path,
|
||||
entry.Size,
|
||||
entry.Mtime,
|
||||
mode,
|
||||
f,
|
||||
fileProgress,
|
||||
)
|
||||
@@ -461,6 +472,7 @@ func (s *Scanner) enumerateFileWithInfo(
|
||||
AbsPath: AbsFilePath(absPath),
|
||||
Size: FileSize(info.Size()),
|
||||
Mtime: ModTime(info.ModTime()),
|
||||
Mode: info.Mode().Perm(),
|
||||
// Note: Ctime not available from fs.FileInfo on all platforms
|
||||
// Will need platform-specific code to extract it
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package mfer
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -350,6 +351,46 @@ func TestScannerFileEntryFields(t *testing.T) {
|
||||
assert.WithinDuration(t, now, time.Time(entry.Mtime), 2*time.Second)
|
||||
}
|
||||
|
||||
// A manifest records every mode as 0000 unless the creator asks for
|
||||
// permissions; then it records each file's permission bits and never the
|
||||
// setuid, setgid or sticky bits.
|
||||
func TestScannerRecordsModeOnlyWhenAsked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, afero.WriteFile(fs, "/"+testFile1, []byte("a"), 0o640))
|
||||
require.NoError(t, afero.WriteFile(fs, "/run.sh", []byte("b"), 0o755))
|
||||
require.NoError(t, afero.WriteFile(fs, "/su", []byte("c"), 0o755))
|
||||
require.NoError(t, fs.Chmod("/su", 0o755|os.ModeSetuid|os.ModeSetgid|os.ModeSticky))
|
||||
|
||||
for _, tc := range []struct {
|
||||
includePermissions bool
|
||||
want map[string]uint32
|
||||
}{
|
||||
{false, map[string]uint32{testFile1: 0, "run.sh": 0, "su": 0}},
|
||||
{true, map[string]uint32{testFile1: 0o640, "run.sh": 0o755, "su": 0o755}},
|
||||
} {
|
||||
s := NewScannerWithOptions(&ScannerOptions{
|
||||
Fs: fs,
|
||||
IncludePermissions: tc.includePermissions,
|
||||
})
|
||||
require.NoError(t, s.EnumerateFS(fs, "/", nil))
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, s.ToManifest(context.Background(), &buf, nil))
|
||||
|
||||
m, err := NewManifestFromReader(&buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := map[string]uint32{}
|
||||
for _, f := range m.Files() {
|
||||
got[f.GetPath()] = f.GetMode()
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.want, got, "IncludePermissions: %v", tc.includePermissions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScannerLargeFileEnumeration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user