diff --git a/cmd/notation/manifest.go b/cmd/notation/manifest.go index 7e3da1c79..1c9e08f2c 100644 --- a/cmd/notation/manifest.go +++ b/cmd/notation/manifest.go @@ -115,7 +115,7 @@ func parseOCILayoutReference(raw string) (string, string, error) { } else { // find `tag` idx := strings.LastIndex(raw, ":") - if idx == -1 || (idx == 1 && len(raw) > 2 && unicode.IsLetter(rune(raw[0])) && raw[2] == '\\') { + if idx == -1 || (idx == 1 && len(raw) > 2 && unicode.IsLetter(rune(raw[0])) && (raw[2] == '\\' || raw[2] == '/')) { return "", "", notationerrors.ErrorOCILayoutMissingReference{Msg: fmt.Sprintf("%q: invalid reference: missing tag or digest. Expecting : or @", raw)} } else { path, ref = raw[:idx], raw[idx+1:] diff --git a/cmd/notation/manifest_test.go b/cmd/notation/manifest_test.go new file mode 100644 index 000000000..1c1398b5b --- /dev/null +++ b/cmd/notation/manifest_test.go @@ -0,0 +1,89 @@ +// Copyright The Notary Project Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "errors" + "testing" + + notationerrors "github.com/notaryproject/notation/v2/cmd/notation/internal/errors" +) + +func TestParseOCILayoutReference(t *testing.T) { + tests := []struct { + name string + raw string + wantPath string + wantRef string + wantErr bool + }{ + { + name: "tag", + raw: "layouts/foo:v1", + wantPath: "layouts/foo", + wantRef: "v1", + }, + { + name: "digest", + raw: "layouts/foo@" + zeroDigest, + wantPath: "layouts/foo", + wantRef: zeroDigest, + }, + { + name: "windows path with backslashes and a tag", + raw: `C:\layouts\foo:v1`, + wantPath: `C:\layouts\foo`, + wantRef: "v1", + }, + { + name: "windows path with forward slashes and a tag", + raw: "C:/layouts/foo:v1", + wantPath: "C:/layouts/foo", + wantRef: "v1", + }, + { + name: "windows path with backslashes and no reference", + raw: `C:\layouts\foo`, + wantErr: true, + }, + { + name: "windows path with forward slashes and no reference", + raw: "C:/layouts/foo", + wantErr: true, + }, + { + name: "no reference", + raw: "layouts/foo", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path, ref, err := parseOCILayoutReference(tt.raw) + if tt.wantErr { + var missingRef notationerrors.ErrorOCILayoutMissingReference + if !errors.As(err, &missingRef) { + t.Fatalf("parseOCILayoutReference(%q) = (%q, %q, %v), want ErrorOCILayoutMissingReference", tt.raw, path, ref, err) + } + return + } + if err != nil { + t.Fatalf("parseOCILayoutReference(%q) returned unexpected error: %v", tt.raw, err) + } + if path != tt.wantPath || ref != tt.wantRef { + t.Errorf("parseOCILayoutReference(%q) = (%q, %q), want (%q, %q)", tt.raw, path, ref, tt.wantPath, tt.wantRef) + } + }) + } +}