1
1
package system
2
2
3
+ import (
4
+ "path"
5
+ "path/filepath"
6
+ "runtime"
7
+ "strings"
8
+
9
+ "github.com/pkg/errors"
10
+ )
11
+
3
12
// DefaultPathEnvUnix is unix style list of directories to search for
4
13
// executables. Each directory is separated from the next by a colon
5
14
// ':' character .
@@ -16,3 +25,194 @@ func DefaultPathEnv(os string) string {
16
25
}
17
26
return DefaultPathEnvUnix
18
27
}
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