Skip to content

Commit d6124fc

Browse files
Add path handling functions
There are several places throughout the code where we repeat the same normalization patterns for paths. This change adds a couple of helper functions and makes CheckSystemDriveAndRemoveDriveLetter() on all platforms. Additionally, all functions accept a platform flag that indicates the target OS we're building the image for. Decissions are made based on this flag instead of checking the platform on which buildkit is running. Signed-off-by: Gabriel Adrian Samfira <[email protected]>
1 parent 61e6f28 commit d6124fc

File tree

5 files changed

+631
-126
lines changed

5 files changed

+631
-126
lines changed

util/system/path.go

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

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

0 commit comments

Comments
 (0)