Compare commits
1 Commits
fix/issue-
...
ac25e0638c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac25e0638c |
@@ -3,47 +3,13 @@ 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
|
||||
|
||||
@@ -108,10 +74,6 @@ 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()
|
||||
|
||||
@@ -134,11 +96,6 @@ 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 {
|
||||
@@ -184,8 +141,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 err := ValidatePath(string(path)); err != nil {
|
||||
return err
|
||||
if path == "" {
|
||||
return errors.New("path cannot be empty")
|
||||
}
|
||||
if size < 0 {
|
||||
return errors.New("size cannot be negative")
|
||||
|
||||
@@ -92,6 +92,29 @@ func TestBuilderBuild(t *testing.T) {
|
||||
assert.True(t, strings.HasPrefix(buf.String(), MAGIC))
|
||||
}
|
||||
|
||||
func TestNewTimestampFromTimeExtremeDate(t *testing.T) {
|
||||
// Regression test: newTimestampFromTime used UnixNano() which panics
|
||||
// for dates outside ~1678-2262. Now uses Nanosecond() which is safe.
|
||||
tests := []struct {
|
||||
name string
|
||||
time time.Time
|
||||
}{
|
||||
{"zero time", time.Time{}},
|
||||
{"year 1000", time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
{"year 3000", time.Date(3000, 1, 1, 0, 0, 0, 123456789, time.UTC)},
|
||||
{"unix epoch", time.Unix(0, 0)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Should not panic
|
||||
ts := newTimestampFromTime(tt.time)
|
||||
assert.Equal(t, tt.time.Unix(), ts.Seconds)
|
||||
assert.Equal(t, int32(tt.time.Nanosecond()), ts.Nanos)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderBuildEmpty(t *testing.T) {
|
||||
b := NewBuilder()
|
||||
|
||||
|
||||
@@ -46,9 +46,6 @@ 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;
|
||||
|
||||
|
||||
@@ -16,11 +16,10 @@ import (
|
||||
const MAGIC string = "ZNAVSRFG"
|
||||
|
||||
func newTimestampFromTime(t time.Time) *Timestamp {
|
||||
out := &Timestamp{
|
||||
return &Timestamp{
|
||||
Seconds: t.Unix(),
|
||||
Nanos: int32(t.UnixNano() - (t.Unix() * 1000000000)),
|
||||
Nanos: int32(t.Nanosecond()),
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *manifest) generate() error {
|
||||
|
||||
Reference in New Issue
Block a user