Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions pkg/protocol/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ var DeltaHeader = [...]byte{'t', 'a', 'r', 'd', 'f', '1', '\n', 0}
var DeltaHeaderv2 = [...]byte{'t', 'a', 'r', 'd', 'f', '2', '\n', 0}

// CleanPath cleans up the path lexically and prevents path traversal attacks.
// Any ".." that extends outside the first elements (or the root itself) is invalid and returns "".
// Paths that traverse above the root are sanitized to the corresponding path
Comment thread
miabbott marked this conversation as resolved.
// below the root. An empty result is always invalid and callers must reject it;
// it can result from an empty input or a path containing a Windows volume name.
// Uses filepath.Clean for proper cross-platform path handling (Windows backslashes, drive letters).
// This is a security-critical function used by both tar-diff and tar-patch packages.
func CleanPath(pathName string) string {
Expand All @@ -39,9 +41,6 @@ func CleanPath(pathName string) string {
// Convert back to forward slashes to ensure consistent output
clean = filepath.ToSlash(clean)

// We clean the initial slash, making all result relative (or "" which is error)
if len(clean) > 0 && clean[0] == '/' {
return clean[1:]
}
return ""
// Strip the leading slash (filepath.Join always adds one).
return clean[1:]
}
83 changes: 83 additions & 0 deletions pkg/protocol/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package protocol

import (
"bytes"
"regexp"
"testing"
)

func TestDeltaOperationConstants(t *testing.T) {
tests := []struct {
name string
got int
want int
}{
{name: "data", got: DeltaOpData, want: 0},
{name: "open", got: DeltaOpOpen, want: 1},
{name: "copy", got: DeltaOpCopy, want: 2},
{name: "add data", got: DeltaOpAddData, want: 3},
{name: "seek", got: DeltaOpSeek, want: 4},
{name: "zstd dictionary", got: DeltaOpZstdDict, want: 5},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Errorf("got %d, want %d", tt.got, tt.want)
}
})
}
}

func TestDeltaHeaders(t *testing.T) {
tests := []struct {
name string
got [8]byte
want [8]byte
}{
{name: "v1", got: DeltaHeader, want: [8]byte{'t', 'a', 'r', 'd', 'f', '1', '\n', 0}},
{name: "v2", got: DeltaHeaderv2, want: [8]byte{'t', 'a', 'r', 'd', 'f', '2', '\n', 0}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !bytes.Equal(tt.got[:], tt.want[:]) {
t.Errorf("got %q, want %q", tt.got, tt.want)
}
})
}
}

func TestVersionFormat(t *testing.T) {
versionPattern := regexp.MustCompile(`^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$`)
if !versionPattern.MatchString(VERSION) {
t.Errorf("VERSION %q does not match the vMAJOR.MINOR.PATCH format", VERSION)
}
}

func TestCleanPath(t *testing.T) {
tests := []struct {
name string
path string
want string
}{
{name: "relative", path: "dir/file", want: "dir/file"},
{name: "absolute", path: "/dir/file", want: "dir/file"},
{name: "duplicate separators", path: "dir//file", want: "dir/file"},
{name: "internal traversal", path: "dir/../file", want: "file"},
{name: "leading traversal", path: "../file", want: "file"},
{name: "outside root", path: "../../file", want: "file"},
{name: "empty", path: "", want: ""},
{name: "root", path: "/", want: ""},
{name: "current directory", path: ".", want: ""},
{name: "parent directory", path: "..", want: ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CleanPath(tt.path); got != tt.want {
t.Errorf("CleanPath(%q) = %q, want %q", tt.path, got, tt.want)
}
})
}
}
Loading