Skip to content

Commit

Permalink
fix: util/path: CheckSystemDriveAndRemoveDriveLetter to preserve /
Browse files Browse the repository at this point in the history
The call to CheckSystemDriveAndRemoveDriveLetter() does not preserve
the trailing `/` or `\\`. This happens because `filepath.Clean()`
strips away any trailing slashes. For example `/sample/` will be
`\\sample` on Windows and `/sample` on Linux.
This function was mainly written for Windows scenarios, which
have System Drive Letters like C:/, etc.

This was causing cases like `COPY testfile /testdir/` to
be intepreted as `COPY testfile /testdir`, and if `testdir` is
not explictly created before the call, it ends up being treated
as a destination file other than a directory.

Fix this by checking that if we have a trailing `/` or `\\`, we
preserve it after the call to `filepath.Clean()`.

Fixes #5249

PS. Also fixed for cross-building from Linux scenario, taking care
for paths like `\\sample\\` that are not changed when run
through `filepath.Clean()`.

Signed-off-by: Anthony Nandaa <profnandaa@gmail.com>
  • Loading branch information
profnandaa committed Sep 24, 2024
1 parent 1da789b commit e8ae74c
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 2 deletions.
37 changes: 37 additions & 0 deletions frontend/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ var allTests = integration.TestFuncs(
testNamedMultiplatformInputContext,
testNamedFilteredContext,
testEmptyDestDir,
testPreserveDestDirSlash,
testCopyLinkDotDestDir,
testCopyLinkEmptyDestDir,
testCopyChownCreateDest,
Expand Down Expand Up @@ -547,6 +548,42 @@ RUN cmd /V:on /C "set /p tfcontent=<testfile \
require.NoError(t, err)
}

func testPreserveDestDirSlash(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)

dockerfile := []byte(integration.UnixOrWindows(
`
FROM busybox
COPY testfile /sample/
RUN [ "$(cat /sample/testfile)" == "contents0" ]
`,
`
FROM nanoserver
COPY testfile /sample/
RUN cmd /V:on /C "set /p tfcontent=<\sample\testfile \
& if !tfcontent! NEQ contents0 (exit 1)"
`,
))

dir := integration.Tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("testfile", []byte("contents0"), 0600),
)

c, err := client.New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()

_, err = f.Solve(sb.Context(), c, client.SolveOpt{
LocalMounts: map[string]fsutil.FS{
dockerui.DefaultLocalNameDockerfile: dir,
dockerui.DefaultLocalNameContext: dir,
},
}, nil)
require.NoError(t, err)
}

func testCopyLinkDotDestDir(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
f := getFrontend(t, sb)
Expand Down
18 changes: 16 additions & 2 deletions util/system/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,11 @@ func CheckSystemDriveAndRemoveDriveLetter(path string, inputOS string) (string,
}

parts := strings.SplitN(path, ":", 2)

// Path does not have a drive letter. Just return it.
if len(parts) < 2 {
return ToSlash(filepath.Clean(path), inputOS), nil
cleanedPath := filepath.Clean(path)
return ToSlash(keepTrailingSlash(cleanedPath, path, inputOS), inputOS), nil
}

// We expect all paths to be in C:
Expand All @@ -220,5 +222,17 @@ func CheckSystemDriveAndRemoveDriveLetter(path string, inputOS string) (string,
//
// We must return the second element of the split path, as is, without attempting to convert
// it to an absolute path. We have no knowledge of the CWD; that is treated elsewhere.
return ToSlash(filepath.Clean(parts[1]), inputOS), nil
cleanedPath := filepath.Clean(parts[1])
return ToSlash(keepTrailingSlash(cleanedPath, parts[1], inputOS), inputOS), nil
}

// Checks if a path other than root has a trailing slash
// and retains it. See https://github.com/moby/buildkit/issues/5249
// Expects cleanedPath to have gone through filepath.Clean()
func keepTrailingSlash(cleanedPath, origPath, inputOS string) string {
if len(cleanedPath) > 1 && hasTrailingSlash(origPath) && inputOS == "windows" {
// cleanedPath already comes with platform specific separators
return cleanedPath + string(filepath.Separator)
}
return cleanedPath
}
10 changes: 10 additions & 0 deletions util/system/path_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build !windows
// +build !windows

package system

import "strings"

func hasTrailingSlash(origPath string) bool {
return strings.HasSuffix(origPath, "/")
}
9 changes: 9 additions & 0 deletions util/system/path_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package system

import "strings"

func hasTrailingSlash(origPath string) bool {
// Windows allows for both \\ and / as path separators.
return strings.HasSuffix(origPath, "\\") ||
strings.HasSuffix(origPath, "/")
}

0 comments on commit e8ae74c

Please sign in to comment.