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