diff --git a/build/opt.go b/build/opt.go index 1c6d429b8..845a1937e 100644 --- a/build/opt.go +++ b/build/opt.go @@ -558,12 +558,8 @@ func loadInputs(ctx context.Context, d *driver.DriverHandle, inp *Inputs, pw pro // handle OCI layout if localPath, ok := strings.CutPrefix(v.Path, "oci-layout://"); ok { - localPath, dig, hasDigest := strings.Cut(localPath, "@") - localPath, tag, hasTag := strings.Cut(localPath, ":") - if !hasTag { - tag = "latest" - } - if !hasDigest { + localPath, dig, tag := parseOCILayoutPath(localPath) + if dig == "" { dig, err = resolveDigest(localPath, tag) if err != nil { return nil, errors.Wrapf(err, "oci-layout reference %q could not be resolved", v.Path) @@ -899,7 +895,7 @@ type lazyFileWriter struct { func (w *lazyFileWriter) Write(p []byte) (int, error) { if w.file == nil { - if err := os.MkdirAll(filepath.Dir(w.path), 0755); err != nil { + if err := os.MkdirAll(filepath.Dir(w.path), 0o755); err != nil { return 0, err } f, err := os.Create(w.path) @@ -1017,3 +1013,35 @@ func isActive(ce *client.CacheOptionsEntry) bool { } return ce.Attrs["token"] != "" && (ce.Attrs["url"] != "" || ce.Attrs["url_v2"] != "") } + +// parseOCILayoutPath handles the oci-layout url accepted by buildx. +func parseOCILayoutPath(s string) (localPath, dgst, tag string) { + localPath = s + + // Look for the digest reference. There might be multiple @ symbols + // in the path and the @ symbol may be part of the path or part of + // the digest. If we find the @ symbol, verify that it's a valid + // digest reference instead of just assuming it is because it + // might be part of the file path. + if i := strings.LastIndex(localPath, "@"); i >= 0 { + after := localPath[i+1:] + if reference.DigestRegexp.MatchString(after) { + localPath, dgst = localPath[:i], after + } + } + + // Do the same with the tag. This isn't as necessary since colons + // aren't valid as file paths on Linux/Unix systems, but they are valid + // on Windows systems so we might as well just be safe. + if i := strings.LastIndex(localPath, ":"); i >= 0 { + after := localPath[i+1:] + if reference.TagRegexp.MatchString(after) { + localPath, tag = localPath[:i], after + } + } + + if tag == "" { + tag = "latest" + } + return +} diff --git a/build/opt_test.go b/build/opt_test.go index c57660139..44865e107 100644 --- a/build/opt_test.go +++ b/build/opt_test.go @@ -5,6 +5,7 @@ import ( "github.com/docker/buildx/util/buildflags" "github.com/moby/buildkit/client" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,3 +39,45 @@ func TestCacheOptions_DerivedVars(t *testing.T) { }, }, CreateCaches(cacheFrom)) } + +func TestParseOCILayoutPath(t *testing.T) { + for _, tt := range []struct { + s string + path string + dgst string + tag string + }{ + { + s: "/path/to/oci/layout", + path: "/path/to/oci/layout", + tag: "latest", + }, + { + s: "/path/to/oci/layout:1.3", + path: "/path/to/oci/layout", + tag: "1.3", + }, + { + s: "/path/to/oci/layout@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + path: "/path/to/oci/layout", + dgst: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + tag: "latest", + }, + { + s: "/path/to/oci/@/layout@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + path: "/path/to/oci/@/layout", + dgst: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + tag: "latest", + }, + { + s: "/path/to/oci/@/layout", + path: "/path/to/oci/@/layout", + tag: "latest", + }, + } { + path, dgst, tag := parseOCILayoutPath(tt.s) + assert.Equal(t, tt.path, path, "comparing path: %s", tt.s) + assert.Equal(t, tt.dgst, dgst, "comparing digest: %s", tt.s) + assert.Equal(t, tt.tag, tag, "comparing tag: %s", tt.s) + } +}