Skip to content

Commit 80b91c5

Browse files
authored
Merge pull request moby#3907 from gabriel-samfira/add-path-handling-functions
Add path handling functions
2 parents bd366f8 + feeb3ff commit 80b91c5

File tree

5 files changed

+638
-126
lines changed

5 files changed

+638
-126
lines changed

util/system/path.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package system
22

3+
import (
4+
"path"
5+
"path/filepath"
6+
"strings"
7+
8+
"github.com/pkg/errors"
9+
)
10+
311
// DefaultPathEnvUnix is unix style list of directories to search for
412
// executables. Each directory is separated from the next by a colon
513
// ':' character .
@@ -16,3 +24,202 @@ func DefaultPathEnv(os string) string {
1624
}
1725
return DefaultPathEnvUnix
1826
}
27+
28+
// NormalizePath cleans the path based on the operating system the path is meant for.
29+
// It takes into account a potential parent path, and will join the path to the parent
30+
// if the path is relative. Additionally, it will apply the folliwing rules:
31+
// - always return an absolute path
32+
// - always strip drive letters for Windows paths
33+
// - optionally keep the trailing slashes on paths
34+
// - paths are returned using forward slashes
35+
func NormalizePath(parent, newPath, inputOS string, keepSlash bool) (string, error) {
36+
if inputOS == "" {
37+
inputOS = "linux"
38+
}
39+
40+
newPath = toSlash(newPath, inputOS)
41+
parent = toSlash(parent, inputOS)
42+
origPath := newPath
43+
44+
if parent == "" {
45+
parent = "/"
46+
}
47+
48+
var err error
49+
parent, err = CheckSystemDriveAndRemoveDriveLetter(parent, inputOS)
50+
if err != nil {
51+
return "", errors.Wrap(err, "removing drive letter")
52+
}
53+
54+
if !IsAbs(parent, inputOS) {
55+
parent = path.Join("/", parent)
56+
}
57+
58+
if newPath == "" {
59+
// New workdir is empty. Use the "current" workdir. It should already
60+
// be an absolute path.
61+
newPath = parent
62+
}
63+
64+
newPath, err = CheckSystemDriveAndRemoveDriveLetter(newPath, inputOS)
65+
if err != nil {
66+
return "", errors.Wrap(err, "removing drive letter")
67+
}
68+
69+
if !IsAbs(newPath, inputOS) {
70+
// The new WD is relative. Join it to the previous WD.
71+
newPath = path.Join(parent, newPath)
72+
}
73+
74+
if keepSlash {
75+
if strings.HasSuffix(origPath, "/") && !strings.HasSuffix(newPath, "/") {
76+
newPath += "/"
77+
} else if strings.HasSuffix(origPath, "/.") {
78+
if newPath != "/" {
79+
newPath += "/"
80+
}
81+
newPath += "."
82+
}
83+
}
84+
85+
return toSlash(newPath, inputOS), nil
86+
}
87+
88+
func toSlash(inputPath, inputOS string) string {
89+
separator := "/"
90+
if inputOS == "windows" {
91+
separator = "\\"
92+
}
93+
return strings.Replace(inputPath, separator, "/", -1)
94+
}
95+
96+
func fromSlash(inputPath, inputOS string) string {
97+
separator := "/"
98+
if inputOS == "windows" {
99+
separator = "\\"
100+
}
101+
return strings.Replace(inputPath, "/", separator, -1)
102+
}
103+
104+
// NormalizeWorkdir will return a normalized version of the new workdir, given
105+
// the currently configured workdir and the desired new workdir. When setting a
106+
// new relative workdir, it will be joined to the previous workdir or default to
107+
// the root folder.
108+
// On Windows we remove the drive letter and convert the path delimiter to "\".
109+
// Paths that begin with os.PathSeparator are considered absolute even on Windows.
110+
func NormalizeWorkdir(current, wd string, inputOS string) (string, error) {
111+
if inputOS == "" {
112+
inputOS = "linux"
113+
}
114+
115+
wd, err := NormalizePath(current, wd, inputOS, false)
116+
if err != nil {
117+
return "", errors.Wrap(err, "normalizing working directory")
118+
}
119+
120+
// Make sure we use the platform specific path separator. HCS does not like forward
121+
// slashes in CWD.
122+
return fromSlash(wd, inputOS), nil
123+
}
124+
125+
// IsAbs returns a boolean value indicating whether or not the path
126+
// is absolute. On Linux, this is just a wrapper for filepath.IsAbs().
127+
// On Windows, we strip away the drive letter (if any), clean the path,
128+
// and check whether or not the path starts with a filepath.Separator.
129+
// This function is meant to check if a path is absolute, in the context
130+
// of a COPY, ADD or WORKDIR, which have their root set in the mount point
131+
// of the writable layer we are mutating. The filepath.IsAbs() function on
132+
// Windows will not work in these scenatios, as it will return true for paths
133+
// that:
134+
// - Begin with drive letter (DOS style paths)
135+
// - Are volume paths \\?\Volume{UUID}
136+
// - Are UNC paths
137+
func IsAbs(pth, inputOS string) bool {
138+
if inputOS == "" {
139+
inputOS = "linux"
140+
}
141+
cleanedPath, err := CheckSystemDriveAndRemoveDriveLetter(pth, inputOS)
142+
if err != nil {
143+
return false
144+
}
145+
cleanedPath = toSlash(cleanedPath, inputOS)
146+
// We stripped any potential drive letter and converted any backslashes to
147+
// forward slashes. We can safely use path.IsAbs() for both Windows and Linux.
148+
return path.IsAbs(cleanedPath)
149+
}
150+
151+
// CheckSystemDriveAndRemoveDriveLetter verifies and manipulates a Windows path.
152+
// For linux, this is a no-op.
153+
//
154+
// This is used, for example, when validating a user provided path in docker cp.
155+
// If a drive letter is supplied, it must be the system drive. The drive letter
156+
// is always removed. It also converts any backslash to forward slash. The conversion
157+
// to OS specific separator should happen as late as possible (ie: before passing the
158+
// value to the function that will actually use it). Paths are parsed and code paths are
159+
// triggered starting with the client and all the way down to calling into the runtime
160+
// environment. The client may run on a foreign OS from the one the build will be triggered
161+
// (Windows clients connecting to Linux or vice versa).
162+
// Keeping the file separator consistent until the last moment is desirable.
163+
//
164+
// We need the Windows path without the drive letter so that it can ultimately be concatenated with
165+
// a Windows long-path which doesn't support drive-letters. Examples:
166+
// C: --> Fail
167+
// C:somepath --> somepath // This is a relative path to the CWD set for that drive letter
168+
// C:\ --> \
169+
// a --> a
170+
// /a --> \a
171+
// d:\ --> Fail
172+
//
173+
// UNC paths can refer to multiple types of paths. From local filesystem paths,
174+
// to remote filesystems like SMB or named pipes.
175+
// There is no sane way to support this without adding a lot of complexity
176+
// which I am not sure is worth it.
177+
// \\.\C$\a --> Fail
178+
func CheckSystemDriveAndRemoveDriveLetter(path string, inputOS string) (string, error) {
179+
if inputOS == "" {
180+
inputOS = "linux"
181+
}
182+
183+
if inputOS != "windows" {
184+
return path, nil
185+
}
186+
187+
if len(path) == 2 && string(path[1]) == ":" {
188+
return "", errors.Errorf("No relative path specified in %q", path)
189+
}
190+
191+
// UNC paths should error out
192+
if len(path) >= 2 && toSlash(path[:2], inputOS) == "//" {
193+
return "", errors.Errorf("UNC paths are not supported")
194+
}
195+
196+
parts := strings.SplitN(path, ":", 2)
197+
// Path does not have a drive letter. Just return it.
198+
if len(parts) < 2 {
199+
return toSlash(filepath.Clean(path), inputOS), nil
200+
}
201+
202+
// We expect all paths to be in C:
203+
if !strings.EqualFold(parts[0], "c") {
204+
return "", errors.New("The specified path is not on the system drive (C:)")
205+
}
206+
207+
// A path of the form F:somepath, is a path that is relative CWD set for a particular
208+
// drive letter. See:
209+
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#fully-qualified-vs-relative-paths
210+
//
211+
// C:\>mkdir F:somepath
212+
// C:\>dir F:\
213+
// Volume in drive F is New Volume
214+
// Volume Serial Number is 86E5-AB64
215+
//
216+
// Directory of F:\
217+
//
218+
// 11/27/2022 02:22 PM <DIR> somepath
219+
// 0 File(s) 0 bytes
220+
// 1 Dir(s) 1,052,876,800 bytes free
221+
//
222+
// We must return the second element of the split path, as is, without attempting to convert
223+
// it to an absolute path. We have no knowledge of the CWD; that is treated elsewhere.
224+
return toSlash(filepath.Clean(parts[1]), inputOS), nil
225+
}

0 commit comments

Comments
 (0)