diff --git a/mfer/builder.go b/mfer/builder.go index 22d4d4a..7864897 100644 --- a/mfer/builder.go +++ b/mfer/builder.go @@ -3,13 +3,47 @@ package mfer import ( "crypto/sha256" "errors" + "fmt" "io" + "strings" "sync" "time" + "unicode/utf8" "github.com/multiformats/go-multihash" ) +// ValidatePath checks that a file path conforms to manifest path invariants: +// - Must be valid UTF-8 +// - Must use forward slashes only (no backslashes) +// - Must be relative (no leading /) +// - Must not contain ".." segments +// - Must not contain empty segments (no "//") +// - Must not be empty +func ValidatePath(p string) error { + if p == "" { + return errors.New("path cannot be empty") + } + if !utf8.ValidString(p) { + return fmt.Errorf("path %q is not valid UTF-8", p) + } + if strings.ContainsRune(p, '\\') { + return fmt.Errorf("path %q contains backslash; use forward slashes only", p) + } + if strings.HasPrefix(p, "/") { + return fmt.Errorf("path %q is absolute; must be relative", p) + } + for _, seg := range strings.Split(p, "/") { + if seg == "" { + return fmt.Errorf("path %q contains empty segment", p) + } + if seg == ".." { + return fmt.Errorf("path %q contains '..' segment", p) + } + } + return nil +} + // RelFilePath represents a relative file path within a manifest. type RelFilePath string @@ -74,6 +108,10 @@ func (b *Builder) AddFile( reader io.Reader, progress chan<- FileHashProgress, ) (FileSize, error) { + if err := ValidatePath(string(path)); err != nil { + return 0, err + } + // Create hash writer h := sha256.New() @@ -96,6 +134,11 @@ 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) + } + // Encode hash as multihash (SHA2-256) mh, err := multihash.Encode(h.Sum(nil), multihash.SHA2_256) if err != nil { @@ -141,8 +184,8 @@ func (b *Builder) FileCount() int { // 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 path == "" { - return errors.New("path cannot be empty") + if err := ValidatePath(string(path)); err != nil { + return err } if size < 0 { return errors.New("size cannot be negative") diff --git a/mfer/mf.proto b/mfer/mf.proto index d8a5bac..66748e0 100644 --- a/mfer/mf.proto +++ b/mfer/mf.proto @@ -46,6 +46,9 @@ message MFFileOuter { message MFFilePath { // required attributes: + // Path invariants: must be valid UTF-8, use forward slashes only, + // be relative (no leading /), contain no ".." segments, and no + // empty segments (no "//"). string path = 1; int64 size = 2;