diff --git a/cmd/oras/internal/fileref/unix.go b/cmd/oras/internal/fileref/unix.go index 82494f0e9..ea3d9c6fd 100644 --- a/cmd/oras/internal/fileref/unix.go +++ b/cmd/oras/internal/fileref/unix.go @@ -24,14 +24,50 @@ import ( // Parse parses file reference on unix. func Parse(reference string, defaultMediaType string) (filePath, mediaType string, err error) { - i := strings.LastIndex(reference, ":") - if i < 0 { - filePath, mediaType = reference, defaultMediaType + i := len(reference) + for { + // found the right most colon which is not escaped + i = strings.LastIndex(reference[:i], ":") + if i < 0 || !isEscaped(reference, i) { + break + } + } + if i < 0 || isEscaped(reference, i) { + filePath, mediaType = unescape(reference), defaultMediaType } else { - filePath, mediaType = reference[:i], reference[i+1:] + filePath, mediaType = unescape(reference[:i]), reference[i+1:] } if filePath == "" { return "", "", fmt.Errorf("found empty file path in %q", reference) } return filePath, mediaType, nil } + +func isEscaped(path string, offset int) bool { + cnt := 0 + for i := offset - 1; i >= 0; i-- { + if path[i] != '\\' { + break + } + cnt++ + } + return cnt%2 != 0 +} + +func unescape(path string) string { + len := len(path) + ret := "" + i := 0 + for i < len { + if path[i] == '\\' { + if i < len-1 { + ret += string(path[i+1]) + } + i += 2 + } else { + ret += string(path[i]) + i += 1 + } + } + return ret +} diff --git a/cmd/oras/internal/fileref/unix_test.go b/cmd/oras/internal/fileref/unix_test.go index 5bf255c45..f0df4e534 100644 --- a/cmd/oras/internal/fileref/unix_test.go +++ b/cmd/oras/internal/fileref/unix_test.go @@ -19,7 +19,7 @@ package fileref import "testing" -func Test_ParseFileReference(t *testing.T) { +func Test_Parse_fileReference(t *testing.T) { type args struct { reference string mediaType string @@ -35,7 +35,10 @@ func Test_ParseFileReference(t *testing.T) { {"file name and default media type", args{"az", "c"}, "az", "c"}, {"file name and media type, default type ignored", args{"az:b", "c"}, "az", "b"}, {"file name and empty media type, default type ignored", args{"az:", "c"}, "az", ""}, - {"colon file name and media type", args{"az:b:c", "d"}, "az:b", "c"}, + {"colon file name and media type", args{`az\:b:c`, "d"}, "az:b", "c"}, + {"colon file name and default media type", args{`az\:`, "b"}, "az:", "b"}, + {"colon file name with backslash and media type1", args{`az\\\:b:c`, "d"}, `az\:b`, `c`}, + {"colon file name with backslash and media type2", args{`az\\\\:b`, "c"}, `az\\`, `b`}, {"colon file name and empty media type", args{"az:b:", "c"}, "az:b", ""}, {"colon-prefix file name and media type", args{":az:b:c", "d"}, ":az:b", "c"}, @@ -55,7 +58,7 @@ func Test_ParseFileReference(t *testing.T) { } } -func TestParse(t *testing.T) { +func Test_Parse_err(t *testing.T) { type args struct { reference string mediaType string @@ -88,3 +91,26 @@ func TestParse(t *testing.T) { }) } } + +func Test_unescape(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {"empty string", "", ""}, + {"only backslash", "\\", ""}, + {"double backslash", `\\`, `\`}, + {"escaping t", `\t\t`, "tt"}, + {"not escaping 1st t", `\\t\t`, `\tt`}, + {"not escaping 2nd t", `\t\\t`, `t\t`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := unescape(tt.input) + if got != tt.want { + t.Errorf("unescape() gotFilePath = %v, want %v", got, tt.want) + } + }) + } +}