Update golangci-lint to v2.12.2 with canonical config (closes #60)
check / check (push) Has been cancelled
check / check (push) Has been cancelled
Adopts golangci-lint v2.12.2 and the canonical .golangci.yml (default: all), and fixes all resulting findings across the tree. Two intended behavior changes: absent MFFilePath.Mtime is handled explicitly in freshen, list and export rather than dereferenced (main panicked); gpg positional key IDs now follow an explicit -- end-of-options marker. All twelve reworded user-visible error messages restored to byte-identical parity with main and pinned by tests.
This commit was merged in pull request #59.
This commit is contained in:
+78
-30
@@ -1,3 +1,5 @@
|
||||
// Package mfer implements the mfer manifest file format: building,
|
||||
// serializing, verifying, and checking manifests of file trees.
|
||||
package mfer
|
||||
|
||||
import (
|
||||
@@ -14,6 +16,27 @@ import (
|
||||
"github.com/multiformats/go-multihash"
|
||||
)
|
||||
|
||||
// readChunkSize is the buffer size used when reading file contents for
|
||||
// hashing.
|
||||
const readChunkSize = 64 * 1024
|
||||
|
||||
// The errPath* sentinels below are worded as the trailing fragment of the
|
||||
// message ValidatePath renders, because the offending path is quoted
|
||||
// before them (`path %q ...`). Wrapping them mid-sentence keeps the
|
||||
// rendered text exactly as mfer has always printed it. Match them with
|
||||
// errors.Is rather than by reading their messages.
|
||||
var (
|
||||
errPathEmpty = errors.New("path cannot be empty")
|
||||
errPathNotUTF8 = errors.New("is not valid UTF-8")
|
||||
errPathBackslash = errors.New("contains backslash; use forward slashes only")
|
||||
errPathAbsolute = errors.New("is absolute; must be relative")
|
||||
errPathEmptySegment = errors.New("contains empty segment")
|
||||
errPathDotDot = errors.New("contains '..' segment")
|
||||
errSizeMismatch = errors.New("size mismatch")
|
||||
errNegativeSize = errors.New("size cannot be negative")
|
||||
errEmptyHash = errors.New("hash cannot be nil or empty")
|
||||
)
|
||||
|
||||
// ValidatePath checks that a file path conforms to manifest path invariants:
|
||||
// - Must be valid UTF-8
|
||||
// - Must use forward slashes only (no backslashes)
|
||||
@@ -23,25 +46,31 @@ import (
|
||||
// - Must not be empty
|
||||
func ValidatePath(p string) error {
|
||||
if p == "" {
|
||||
return errors.New("path cannot be empty")
|
||||
return errPathEmpty
|
||||
}
|
||||
|
||||
if !utf8.ValidString(p) {
|
||||
return fmt.Errorf("path %q is not valid UTF-8", p)
|
||||
return fmt.Errorf("path %q %w", p, errPathNotUTF8)
|
||||
}
|
||||
|
||||
if strings.ContainsRune(p, '\\') {
|
||||
return fmt.Errorf("path %q contains backslash; use forward slashes only", p)
|
||||
return fmt.Errorf("path %q %w", p, errPathBackslash)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(p, "/") {
|
||||
return fmt.Errorf("path %q is absolute; must be relative", p)
|
||||
return fmt.Errorf("path %q %w", p, errPathAbsolute)
|
||||
}
|
||||
|
||||
for _, seg := range strings.Split(p, "/") {
|
||||
if seg == "" {
|
||||
return fmt.Errorf("path %q contains empty segment", p)
|
||||
return fmt.Errorf("path %q %w", p, errPathEmptySegment)
|
||||
}
|
||||
|
||||
if seg == ".." {
|
||||
return fmt.Errorf("path %q contains '..' segment", p)
|
||||
return fmt.Errorf("path %q %w", p, errPathDotDot)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -68,11 +97,7 @@ type UnixNanos int32
|
||||
|
||||
// Timestamp converts ModTime to a protobuf Timestamp.
|
||||
func (m ModTime) Timestamp() *Timestamp {
|
||||
t := time.Time(m)
|
||||
return &Timestamp{
|
||||
Seconds: t.Unix(),
|
||||
Nanos: int32(t.Nanosecond()),
|
||||
}
|
||||
return newTimestampFromTime(time.Time(m))
|
||||
}
|
||||
|
||||
// Multihash represents a multihash-encoded file hash (typically SHA2-256).
|
||||
@@ -93,14 +118,6 @@ type Builder struct {
|
||||
fixedUUID []byte // if set, use this UUID instead of generating one
|
||||
}
|
||||
|
||||
// SetSeed derives a deterministic UUID from the given seed string.
|
||||
// The seed is hashed once with SHA-256 and the first 16 bytes are used
|
||||
// as a fixed UUID for the manifest.
|
||||
func (b *Builder) SetSeed(seed string) {
|
||||
hash := sha256.Sum256([]byte(seed))
|
||||
b.fixedUUID = hash[:16]
|
||||
}
|
||||
|
||||
// NewBuilder creates a new Builder.
|
||||
func NewBuilder() *Builder {
|
||||
return &Builder{
|
||||
@@ -109,6 +126,14 @@ func NewBuilder() *Builder {
|
||||
}
|
||||
}
|
||||
|
||||
// SetSeed derives a deterministic UUID from the given seed string.
|
||||
// The seed is hashed once with SHA-256 and the first 16 bytes are used
|
||||
// as a fixed UUID for the manifest.
|
||||
func (b *Builder) SetSeed(seed string) {
|
||||
hash := sha256.Sum256([]byte(seed))
|
||||
b.fixedUUID = hash[:uuidLength]
|
||||
}
|
||||
|
||||
// AddFile reads file content from reader, computes hashes, and adds to manifest.
|
||||
// Progress updates are sent to the progress channel (if non-nil) without blocking.
|
||||
// Returns the number of bytes read.
|
||||
@@ -119,7 +144,8 @@ func (b *Builder) AddFile(
|
||||
reader io.Reader,
|
||||
progress chan<- FileHashProgress,
|
||||
) (FileSize, error) {
|
||||
if err := ValidatePath(string(path)); err != nil {
|
||||
err := ValidatePath(string(path))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -128,7 +154,8 @@ func (b *Builder) AddFile(
|
||||
|
||||
// Read file in chunks, updating hash and progress
|
||||
var totalRead FileSize
|
||||
buf := make([]byte, 64*1024) // 64KB chunks
|
||||
|
||||
buf := make([]byte, readChunkSize)
|
||||
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
@@ -137,9 +164,11 @@ func (b *Builder) AddFile(
|
||||
totalRead += FileSize(n)
|
||||
sendFileHashProgress(progress, FileHashProgress{BytesRead: totalRead})
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return totalRead, err
|
||||
}
|
||||
@@ -147,7 +176,10 @@ func (b *Builder) AddFile(
|
||||
|
||||
// Verify actual bytes read matches declared size
|
||||
if totalRead != size {
|
||||
return totalRead, fmt.Errorf("size mismatch for %q: declared %d bytes but read %d bytes", path, size, totalRead)
|
||||
return totalRead, fmt.Errorf(
|
||||
"%w for %q: declared %d bytes but read %d bytes",
|
||||
errSizeMismatch, path, size, totalRead,
|
||||
)
|
||||
}
|
||||
|
||||
// Encode hash as multihash (SHA2-256)
|
||||
@@ -178,6 +210,7 @@ func sendFileHashProgress(ch chan<- FileHashProgress, p FileHashProgress) {
|
||||
if ch == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- p:
|
||||
default:
|
||||
@@ -188,21 +221,30 @@ func sendFileHashProgress(ch chan<- FileHashProgress, p FileHashProgress) {
|
||||
func (b *Builder) FileCount() int {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
return len(b.files)
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Returns an error if path is empty, size is negative, or hash is nil/empty.
|
||||
func (b *Builder) AddFileWithHash(path RelFilePath, size FileSize, mtime ModTime, hash Multihash) error {
|
||||
if err := ValidatePath(string(path)); err != nil {
|
||||
func (b *Builder) AddFileWithHash(
|
||||
path RelFilePath,
|
||||
size FileSize,
|
||||
mtime ModTime,
|
||||
hash Multihash,
|
||||
) error {
|
||||
err := ValidatePath(string(path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("add file: %w", err)
|
||||
}
|
||||
|
||||
if size < 0 {
|
||||
return errors.New("size cannot be negative")
|
||||
return errNegativeSize
|
||||
}
|
||||
|
||||
if len(hash) == 0 {
|
||||
return errors.New("hash cannot be nil or empty")
|
||||
return errEmptyHash
|
||||
}
|
||||
|
||||
entry := &MFFilePath{
|
||||
@@ -217,6 +259,7 @@ func (b *Builder) AddFileWithHash(path RelFilePath, size FileSize, mtime ModTime
|
||||
b.mu.Lock()
|
||||
b.files = append(b.files, entry)
|
||||
b.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -225,6 +268,7 @@ func (b *Builder) AddFileWithHash(path RelFilePath, size FileSize, mtime ModTime
|
||||
func (b *Builder) SetIncludeTimestamps(include bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.includeTimestamps = include
|
||||
}
|
||||
|
||||
@@ -233,6 +277,7 @@ func (b *Builder) SetIncludeTimestamps(include bool) {
|
||||
func (b *Builder) SetSigningOptions(opts *SigningOptions) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.signingOptions = opts
|
||||
}
|
||||
|
||||
@@ -243,7 +288,7 @@ func (b *Builder) Build(w io.Writer) error {
|
||||
|
||||
// Sort files by path for deterministic output
|
||||
sort.Slice(b.files, func(i, j int) bool {
|
||||
return b.files[i].Path < b.files[j].Path
|
||||
return b.files[i].GetPath() < b.files[j].GetPath()
|
||||
})
|
||||
|
||||
// Create inner manifest
|
||||
@@ -263,19 +308,22 @@ func (b *Builder) Build(w io.Writer) error {
|
||||
}
|
||||
|
||||
// Generate outer wrapper
|
||||
if err := m.generateOuter(); err != nil {
|
||||
err := m.generateOuter()
|
||||
if err != nil {
|
||||
return fmt.Errorf("build: generate outer: %w", err)
|
||||
}
|
||||
|
||||
// Generate final output
|
||||
if err := m.generate(); err != nil {
|
||||
err = m.generate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("build: generate: %w", err)
|
||||
}
|
||||
|
||||
// Write to output
|
||||
_, err := w.Write(m.output.Bytes())
|
||||
_, err = w.Write(m.output.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("build: write output: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user