Skip to content

Commit 9a723ff

Browse files
committed
Fix windows path handling in podman cp
Fixes: #14862 Signed-off-by: David Negstad <[email protected]>
1 parent 40d7ab1 commit 9a723ff

File tree

5 files changed

+272
-120
lines changed

5 files changed

+272
-120
lines changed

cmd/podman/containers/cp.go

+29-21
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"os"
77
"os/user"
8+
"path"
89
"path/filepath"
910
"strconv"
1011
"strings"
@@ -147,7 +148,7 @@ func copyContainerToContainer(sourceContainer string, sourcePath string, destCon
147148
sourceContainerTarget := sourceContainerInfo.LinkTarget
148149
destContainerTarget := destContainerInfo.LinkTarget
149150
if !destContainerInfo.IsDir {
150-
destContainerTarget = filepath.Dir(destPath)
151+
destContainerTarget = path.Dir(destPath)
151152
}
152153

153154
// If we copy a directory via the "." notation and the container path
@@ -158,8 +159,8 @@ func copyContainerToContainer(sourceContainer string, sourcePath string, destCon
158159
// Hence, whenever "." is the source and the destination does not
159160
// exist, we copy the source's parent and let the copier package create
160161
// the destination via the Rename option.
161-
if destResolvedToParentDir && sourceContainerInfo.IsDir && filepath.Base(sourcePath) == "." {
162-
sourceContainerTarget = filepath.Dir(sourceContainerTarget)
162+
if destResolvedToParentDir && sourceContainerInfo.IsDir && path.Base(sourcePath) == "." {
163+
sourceContainerTarget = path.Dir(sourceContainerTarget)
163164
}
164165

165166
reader, writer := io.Pipe()
@@ -183,7 +184,7 @@ func copyContainerToContainer(sourceContainer string, sourcePath string, destCon
183184
if (!sourceContainerInfo.IsDir && !destContainerInfo.IsDir) || destResolvedToParentDir {
184185
// If we're having a file-to-file copy, make sure to
185186
// rename accordingly.
186-
copyOptions.Rename = map[string]string{filepath.Base(sourceContainerTarget): destContainerBaseName}
187+
copyOptions.Rename = map[string]string{path.Base(sourceContainerTarget): destContainerBaseName}
187188
}
188189

189190
copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.Context(), destContainer, destContainerTarget, reader, copyOptions)
@@ -261,8 +262,8 @@ func copyFromContainer(container string, containerPath string, hostPath string)
261262
// we copy the source's parent and let the copier package create the
262263
// destination via the Rename option.
263264
containerTarget := containerInfo.LinkTarget
264-
if resolvedToHostParentDir && containerInfo.IsDir && filepath.Base(containerTarget) == "." {
265-
containerTarget = filepath.Dir(containerTarget)
265+
if resolvedToHostParentDir && containerInfo.IsDir && path.Base(containerTarget) == "." {
266+
containerTarget = path.Dir(containerTarget)
266267
}
267268

268269
if !isStdout && containerInfo.IsDir && !hostInfo.IsDir {
@@ -307,7 +308,7 @@ func copyFromContainer(container string, containerPath string, hostPath string)
307308
if (!containerInfo.IsDir && !hostInfo.IsDir) || resolvedToHostParentDir {
308309
// If we're having a file-to-file copy, make sure to
309310
// rename accordingly.
310-
putOptions.Rename = map[string]string{filepath.Base(containerTarget): hostBaseName}
311+
putOptions.Rename = map[string]string{path.Base(containerTarget): hostBaseName}
311312
}
312313
dir := hostInfo.LinkTarget
313314
if !hostInfo.IsDir {
@@ -428,7 +429,11 @@ func copyToContainer(container string, containerPath string, hostPath string) er
428429
// rename accordingly.
429430
getOptions.Rename = map[string]string{filepath.Base(hostTarget): containerBaseName}
430431
}
431-
if err := buildahCopiah.Get("/", "", getOptions, []string{hostTarget}, writer); err != nil {
432+
433+
// On Windows, the root path needs to be <drive>:\, while otherwise
434+
// it needs to be /. Combining filepath.VolumeName() + string(os.PathSeparator)
435+
// gives us the correct path for the current OS.
436+
if err := buildahCopiah.Get(filepath.VolumeName(hostTarget)+string(os.PathSeparator), "", getOptions, []string{hostTarget}, writer); err != nil {
432437
return fmt.Errorf("copying from host: %w", err)
433438
}
434439
return nil
@@ -438,7 +443,7 @@ func copyToContainer(container string, containerPath string, hostPath string) er
438443
defer reader.Close()
439444
target := containerInfo.FileInfo.LinkTarget
440445
if !containerInfo.IsDir {
441-
target = filepath.Dir(target)
446+
target = path.Dir(target)
442447
}
443448

444449
copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.Context(), container, target, reader, entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir})
@@ -460,7 +465,7 @@ func copyToContainer(container string, containerPath string, hostPath string) er
460465
func resolvePathOnDestinationContainer(container string, containerPath string, isStdin bool) (baseName string, containerInfo *entities.ContainerStatReport, resolvedToParentDir bool, err error) {
461466
containerInfo, err = registry.ContainerEngine().ContainerStat(registry.Context(), container, containerPath)
462467
if err == nil {
463-
baseName = filepath.Base(containerInfo.LinkTarget)
468+
baseName = path.Base(containerInfo.LinkTarget)
464469
return //nolint: nilerr
465470
}
466471

@@ -476,17 +481,17 @@ func resolvePathOnDestinationContainer(container string, containerPath string, i
476481
// NOTE: containerInfo may actually be set. That happens when
477482
// the container path is a symlink into nirvana. In that case,
478483
// we must use the symlinked path instead.
479-
path := containerPath
484+
parentPath := containerPath
480485
if containerInfo != nil {
481-
baseName = filepath.Base(containerInfo.LinkTarget)
482-
path = containerInfo.LinkTarget
486+
baseName = path.Base(containerInfo.LinkTarget)
487+
parentPath = containerInfo.LinkTarget
483488
} else {
484-
baseName = filepath.Base(containerPath)
489+
baseName = path.Base(containerPath)
485490
}
486491

487-
parentDir, err := containerParentDir(container, path)
492+
parentDir, err := containerParentDir(container, parentPath)
488493
if err != nil {
489-
err = fmt.Errorf("could not determine parent dir of %q on container %s: %w", path, container, err)
494+
err = fmt.Errorf("could not determine parent dir of %q on container %s: %w", parentPath, container, err)
490495
return
491496
}
492497

@@ -504,8 +509,11 @@ func resolvePathOnDestinationContainer(container string, containerPath string, i
504509
// container. If the path is relative, it will be resolved relative to the
505510
// container's working directory (or "/" if the work dir isn't set).
506511
func containerParentDir(container string, containerPath string) (string, error) {
507-
if filepath.IsAbs(containerPath) {
508-
return filepath.Dir(containerPath), nil
512+
// This is specifically a path in the (linux) container, so we need to intentionally use
513+
// path instead of filepath to ensure we don't try to parse container paths using the
514+
// host OS conventions.
515+
if path.IsAbs(containerPath) {
516+
return path.Dir(containerPath), nil
509517
}
510518
inspectData, _, err := registry.ContainerEngine().ContainerInspect(registry.Context(), []string{container}, entities.InspectOptions{})
511519
if err != nil {
@@ -514,9 +522,9 @@ func containerParentDir(container string, containerPath string) (string, error)
514522
if len(inspectData) != 1 {
515523
return "", fmt.Errorf("inspecting container %q: expected 1 data item but got %d", container, len(inspectData))
516524
}
517-
workDir := filepath.Join("/", inspectData[0].Config.WorkingDir)
518-
workDir = filepath.Join(workDir, containerPath)
519-
return filepath.Dir(workDir), nil
525+
workDir := path.Join("/", inspectData[0].Config.WorkingDir)
526+
workDir = path.Join(workDir, containerPath)
527+
return path.Dir(workDir), nil
520528
}
521529

522530
// validateFileInfo returns an error if the specified FileInfo doesn't point to

pkg/copy/fileinfo.go

+18-13
Original file line numberDiff line numberDiff line change
@@ -83,23 +83,28 @@ func ResolveHostPath(path string) (*FileInfo, error) {
8383
// is preserved. The filepath API among tends to clean up a bit too much but
8484
// we *must* preserve this data by all means.
8585
func PreserveBasePath(original, resolved string) string {
86-
// Handle "/"
87-
if strings.HasSuffix(original, "/") {
88-
if !strings.HasSuffix(resolved, "/") {
89-
resolved += "/"
86+
// Ensure paths are in platform semantics (replace / with \ on Windows)
87+
resolved = filepath.FromSlash(resolved)
88+
original = filepath.FromSlash(original)
89+
90+
if filepath.Base(resolved) != "." && filepath.Base(original) == "." {
91+
if !hasTrailingPathSeparator(resolved) {
92+
// Add a separator if it doesn't already end with one (a cleaned
93+
// path would only end in a separator if it is the root).
94+
resolved += string(filepath.Separator)
9095
}
91-
return resolved
96+
resolved += "."
9297
}
9398

94-
// Handle "/."
95-
if strings.HasSuffix(original, "/.") {
96-
if strings.HasSuffix(resolved, "/") { // could be root!
97-
resolved += "."
98-
} else if !strings.HasSuffix(resolved, "/.") {
99-
resolved += "/."
100-
}
101-
return resolved
99+
if !hasTrailingPathSeparator(resolved) && hasTrailingPathSeparator(original) {
100+
resolved += string(filepath.Separator)
102101
}
103102

104103
return resolved
105104
}
105+
106+
// hasTrailingPathSeparator returns whether the given
107+
// path ends with the system's path separator character.
108+
func hasTrailingPathSeparator(path string) bool {
109+
return len(path) > 0 && os.IsPathSeparator(path[len(path)-1])
110+
}

pkg/copy/parse.go

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package copy
22

33
import (
44
"fmt"
5+
"path/filepath"
56
"strings"
67
)
78

@@ -40,6 +41,14 @@ func parseUserInput(input string) (container string, path string) {
4041
return
4142
}
4243

44+
// If the input is an absolute path, it cannot refer to a container.
45+
// This is necessary because absolute paths on Windows will include
46+
// a colon, which would cause the drive letter to be parsed as a
47+
// container name.
48+
if filepath.IsAbs(input) {
49+
return
50+
}
51+
4352
if parsedContainer, parsedPath, ok := strings.Cut(path, ":"); ok {
4453
container = parsedContainer
4554
path = parsedPath

pkg/machine/e2e/basic_test.go

-84
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package e2e_test
22

33
import (
4-
"archive/tar"
5-
"bytes"
64
"fmt"
75
"io"
8-
"io/fs"
96
"net"
107
"net/http"
118
"net/url"
@@ -288,87 +285,6 @@ var _ = Describe("run basic podman commands", func() {
288285
Expect(run).To(Exit(0))
289286
Expect(build.outputToString()).To(ContainSubstring(name))
290287
})
291-
292-
It("Copy ops", func() {
293-
var (
294-
stdinDirectory = "stdin-dir"
295-
stdinFile = "file.txt"
296-
)
297-
298-
now := time.Now()
299-
300-
tarBuffer := &bytes.Buffer{}
301-
tw := tar.NewWriter(tarBuffer)
302-
303-
// Write a directory header to the tar
304-
err := tw.WriteHeader(&tar.Header{
305-
Name: stdinDirectory,
306-
Mode: int64(0640 | fs.ModeDir),
307-
Gid: 1000,
308-
ModTime: now,
309-
ChangeTime: now,
310-
AccessTime: now,
311-
Typeflag: tar.TypeDir,
312-
})
313-
Expect(err).ToNot(HaveOccurred())
314-
315-
// Write a file header to the tar
316-
err = tw.WriteHeader(&tar.Header{
317-
Name: path.Join(stdinDirectory, stdinFile),
318-
Mode: 0755,
319-
Uid: 1000,
320-
ModTime: now,
321-
ChangeTime: now,
322-
AccessTime: now,
323-
})
324-
Expect(err).ToNot(HaveOccurred())
325-
326-
err = tw.Close()
327-
Expect(err).ToNot(HaveOccurred())
328-
329-
name := randomString()
330-
i := new(initMachine)
331-
session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withNow()).run()
332-
Expect(err).ToNot(HaveOccurred())
333-
Expect(session).To(Exit(0))
334-
335-
bm := basicMachine{}
336-
newImgs, err := mb.setCmd(bm.withPodmanCommand([]string{"pull", TESTIMAGE})).run()
337-
Expect(err).ToNot(HaveOccurred())
338-
Expect(newImgs).To(Exit(0))
339-
Expect(newImgs.outputToStringSlice()).To(HaveLen(1))
340-
341-
createAlp, err := mb.setCmd(bm.withPodmanCommand([]string{"create", TESTIMAGE, "top"})).run()
342-
Expect(err).ToNot(HaveOccurred())
343-
Expect(createAlp).To(Exit(0))
344-
Expect(createAlp.outputToStringSlice()).To(HaveLen(1))
345-
346-
// Testing stdin copy with archive mode disabled (ownership will be determined by the tar file)
347-
containerID := createAlp.outputToStringSlice()[0]
348-
cpTar, err := mb.setCmd(bm.withPodmanCommand([]string{"cp", "-a=false", "-", containerID + ":/tmp"})).setStdin(tarBuffer).run()
349-
Expect(err).ToNot(HaveOccurred())
350-
Expect(cpTar).To(Exit(0))
351-
352-
start, err := mb.setCmd(bm.withPodmanCommand([]string{"start", containerID})).run()
353-
Expect(err).ToNot(HaveOccurred())
354-
Expect(start).To(Exit(0))
355-
356-
// Check the directory is created with the appropriate mode, uid, gid
357-
exec, err := mb.setCmd(bm.withPodmanCommand([]string{"exec", containerID, "stat", "-c", "%a %u %g", "/tmp/stdin-dir"})).run()
358-
Expect(err).ToNot(HaveOccurred())
359-
Expect(exec).To(Exit(0))
360-
execStdOut := exec.outputToStringSlice()
361-
Expect(execStdOut).To(HaveLen(1))
362-
Expect(execStdOut[0]).To(Equal("640 0 1000"))
363-
364-
// Check the file is created with the appropriate mode, uid, gid
365-
exec, err = mb.setCmd(bm.withPodmanCommand([]string{"exec", containerID, "stat", "-c", "%a %u %g", "/tmp/stdin-dir/file.txt"})).run()
366-
Expect(err).ToNot(HaveOccurred())
367-
Expect(exec).To(Exit(0))
368-
execStdOut = exec.outputToStringSlice()
369-
Expect(execStdOut).To(HaveLen(1))
370-
Expect(execStdOut[0]).To(Equal("755 1000 0"))
371-
})
372288
})
373289

374290
func testHTTPServer(port string, shouldErr bool, expectedResponse string) {

0 commit comments

Comments
 (0)