Skip to content

Commit a346728

Browse files
committed
Update locator functions to handle Windows file names
Signed-off-by: Jan Dubois <[email protected]>
1 parent 9c977a8 commit a346728

File tree

3 files changed

+188
-91
lines changed

3 files changed

+188
-91
lines changed

pkg/limatmpl/abs.go

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"errors"
55
"fmt"
66
"net/url"
7+
"path"
78
"path/filepath"
9+
"runtime"
810
"strings"
911
)
1012

@@ -57,33 +59,62 @@ func (tmpl *Template) useAbsLocators() error {
5759
return tmpl.evalExpr()
5860
}
5961

60-
// basePath returns the locator without the filename part.
62+
// withVolume adds the volume name of the current working directory to a path without volume name.
63+
// On Windows filepath.Abs() only returns a "rooted" name, but does not add the volume name.
64+
// withVolume also normalizes all path separators to the platform native one.
65+
func withVolume(path string) (string, error) {
66+
if runtime.GOOS == "windows" && len(filepath.VolumeName(path)) == 0 {
67+
root, err := filepath.Abs("/")
68+
if err != nil {
69+
return "", err
70+
}
71+
path = filepath.VolumeName(root) + path
72+
}
73+
return filepath.Clean(path), nil
74+
}
75+
76+
// basePath returns the locator in absolute format, but without the filename part.
6177
func basePath(locator string) (string, error) {
6278
u, err := url.Parse(locator)
63-
if err != nil || u.Scheme == "" {
64-
return filepath.Abs(filepath.Dir(locator))
79+
// Single-letter schemes will be drive names on Windows, e.g. "c:/foo"
80+
if err == nil && len(u.Scheme) > 1 {
81+
// path.Dir("") returns ".", which must be removed for url.JoinPath() to do the right thing later
82+
return u.Scheme + "://" + strings.TrimSuffix(path.Dir(path.Join(u.Host, u.Path)), "."), nil
83+
}
84+
base, err := filepath.Abs(filepath.Dir(locator))
85+
if err != nil {
86+
return "", err
6587
}
66-
// filepath.Dir("") returns ".", which must be removed for url.JoinPath() to do the right thing later
67-
return u.Scheme + "://" + strings.TrimSuffix(filepath.Dir(filepath.Join(u.Host, u.Path)), "."), nil
88+
return withVolume(base)
6889
}
6990

7091
// absPath either returns the locator directly, or combines it with the basePath if the locator is a relative path.
7192
func absPath(locator, basePath string) (string, error) {
7293
u, err := url.Parse(locator)
73-
if (err == nil && u.Scheme != "") || filepath.IsAbs(locator) {
94+
if err == nil && len(u.Scheme) > 1 {
7495
return locator, nil
7596
}
76-
switch {
77-
case basePath == "":
78-
return "", errors.New("basePath is empty")
79-
case basePath == "-":
80-
return "", errors.New("can't use relative paths when reading template from STDIN")
81-
case strings.Contains(locator, "../"):
82-
return "", fmt.Errorf("relative locator path %q must not contain '../' segments", locator)
83-
}
84-
u, err = url.Parse(basePath)
85-
if err != nil {
86-
return "", err
97+
// Check for rooted locator; filepath.IsAbs() returns false on Windows when the volume name is missing
98+
volumeLen := len(filepath.VolumeName(locator))
99+
if locator[volumeLen] != '/' && locator[volumeLen] != filepath.Separator {
100+
switch {
101+
case basePath == "":
102+
return "", errors.New("basePath is empty")
103+
case basePath == "-":
104+
return "", errors.New("can't use relative paths when reading template from STDIN")
105+
case strings.Contains(locator, "../"):
106+
return "", fmt.Errorf("relative locator path %q must not contain '../' segments", locator)
107+
case volumeLen != 0:
108+
return "", fmt.Errorf("relative locator path %q must not include a volume name", locator)
109+
}
110+
u, err = url.Parse(basePath)
111+
if err != nil {
112+
return "", err
113+
}
114+
if len(u.Scheme) > 1 {
115+
return u.JoinPath(locator).String(), nil
116+
}
117+
locator = filepath.Join(basePath, locator)
87118
}
88-
return u.JoinPath(locator).String(), nil
119+
return withVolume(locator)
89120
}

pkg/limatmpl/abs_test.go

Lines changed: 135 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package limatmpl
22

33
import (
4+
"path/filepath"
5+
"runtime"
46
"strings"
57
"testing"
68

@@ -18,8 +20,8 @@ var useAbsLocatorsTestCases = []useAbsLocatorsTestCase{
1820
{
1921
"Template without basedOn or script file",
2022
"template://foo",
21-
`foo: bar`,
22-
`foo: bar`,
23+
`arch: aarch64`,
24+
`arch: aarch64`,
2325
},
2426
{
2527
"Single string base template",
@@ -102,98 +104,161 @@ func RunUseAbsLocatorTest(t *testing.T, tc useAbsLocatorsTestCase) {
102104
}
103105

104106
func TestBasePath(t *testing.T) {
105-
actual, err := basePath("/foo")
107+
// On Windows the root will be something like "C:\"
108+
root, err := filepath.Abs("/")
106109
assert.NilError(t, err)
107-
assert.Equal(t, actual, "/")
110+
volume := filepath.VolumeName(root)
108111

109-
actual, err = basePath("/foo/bar")
110-
assert.NilError(t, err)
111-
assert.Equal(t, actual, "/foo")
112+
t.Run("", func(t *testing.T) {
113+
actual, err := basePath("/foo")
114+
assert.NilError(t, err)
115+
assert.Equal(t, actual, root)
116+
})
112117

113-
actual, err = basePath("template://foo")
114-
assert.NilError(t, err)
115-
assert.Equal(t, actual, "template://")
118+
t.Run("", func(t *testing.T) {
119+
actual, err := basePath("/foo/bar")
120+
assert.NilError(t, err)
121+
assert.Equal(t, actual, filepath.Clean(volume+"/foo"))
122+
})
116123

117-
actual, err = basePath("template://foo/bar")
118-
assert.NilError(t, err)
119-
assert.Equal(t, actual, "template://foo")
124+
t.Run("", func(t *testing.T) {
125+
actual, err := basePath("template://foo")
126+
assert.NilError(t, err)
127+
assert.Equal(t, actual, "template://")
128+
})
120129

121-
actual, err = basePath("http://host/foo")
122-
assert.NilError(t, err)
123-
assert.Equal(t, actual, "http://host")
130+
t.Run("", func(t *testing.T) {
131+
actual, err := basePath("template://foo/bar")
132+
assert.NilError(t, err)
133+
assert.Equal(t, actual, "template://foo")
134+
})
124135

125-
actual, err = basePath("http://host/foo/bar")
126-
assert.NilError(t, err)
127-
assert.Equal(t, actual, "http://host/foo")
136+
t.Run("", func(t *testing.T) {
137+
actual, err := basePath("http://host/foo")
138+
assert.NilError(t, err)
139+
assert.Equal(t, actual, "http://host")
140+
})
128141

129-
actual, err = basePath("file:///foo")
130-
assert.NilError(t, err)
131-
assert.Equal(t, actual, "file:///")
142+
t.Run("", func(t *testing.T) {
143+
actual, err := basePath("http://host/foo/bar")
144+
assert.NilError(t, err)
145+
assert.Equal(t, actual, "http://host/foo")
146+
})
132147

133-
actual, err = basePath("file:///foo/bar")
134-
assert.NilError(t, err)
135-
assert.Equal(t, actual, "file:///foo")
148+
t.Run("", func(t *testing.T) {
149+
actual, err := basePath("file:///foo")
150+
assert.NilError(t, err)
151+
assert.Equal(t, actual, "file:///")
152+
})
153+
154+
t.Run("", func(t *testing.T) {
155+
actual, err := basePath("file:///foo/bar")
156+
assert.NilError(t, err)
157+
assert.Equal(t, actual, "file:///foo")
158+
})
136159
}
137160

138161
func TestAbsPath(t *testing.T) {
139-
// If the locator is already an absolute path, it is returned unchanged (no extension appended either)
140-
actual, err := absPath("/foo", "/root")
162+
root, err := filepath.Abs("/")
141163
assert.NilError(t, err)
142-
assert.Equal(t, actual, "/foo")
164+
volume := filepath.VolumeName(root)
143165

144-
actual, err = absPath("template://foo", "/root")
145-
assert.NilError(t, err)
146-
assert.Equal(t, actual, "template://foo")
166+
t.Run("If the locator is already an absolute path, it is returned unchanged", func(t *testing.T) {
167+
actual, err := absPath(volume+"/foo", volume+"/root")
168+
assert.NilError(t, err)
169+
assert.Equal(t, actual, filepath.Clean(volume+"/foo"))
170+
})
147171

148-
actual, err = absPath("http://host/foo", "/root")
149-
assert.NilError(t, err)
150-
assert.Equal(t, actual, "http://host/foo")
172+
t.Run("If the locator is a rooted path without volume name, then the volume will be added", func(t *testing.T) {
173+
actual, err := absPath("/foo", volume+"/root")
174+
assert.NilError(t, err)
175+
assert.Equal(t, actual, filepath.Clean(volume+"/foo"))
176+
})
151177

152-
actual, err = absPath("file:///foo", "/root")
153-
assert.NilError(t, err)
154-
assert.Equal(t, actual, "file:///foo")
178+
t.Run("", func(t *testing.T) {
179+
actual, err := absPath("template://foo", volume+"/root")
180+
assert.NilError(t, err)
181+
assert.Equal(t, actual, "template://foo")
182+
})
155183

156-
// Can't have relative path when reading from STDIN
157-
_, err = absPath("foo", "-")
158-
assert.ErrorContains(t, err, "STDIN")
184+
t.Run("", func(t *testing.T) {
185+
actual, err := absPath("http://host/foo", volume+"/root")
186+
assert.NilError(t, err)
187+
assert.Equal(t, actual, "http://host/foo")
188+
})
159189

160-
// Relative paths must be underneath the basePath
161-
_, err = absPath("../foo", "/root")
162-
assert.ErrorContains(t, err, "'../'")
190+
t.Run("", func(t *testing.T) {
191+
actual, err := absPath("file:///foo", volume+"/root")
192+
assert.NilError(t, err)
193+
assert.Equal(t, actual, "file:///foo")
194+
})
163195

164-
// basePath must not be empty
165-
_, err = absPath("foo", "")
166-
assert.ErrorContains(t, err, "empty")
196+
t.Run("Can't have relative path when reading from STDIN", func(t *testing.T) {
197+
_, err = absPath("foo", "-")
198+
assert.ErrorContains(t, err, "STDIN")
199+
})
167200

168-
_, err = absPath("./foo", "")
169-
assert.ErrorContains(t, err, "empty")
201+
t.Run("Relative paths must be underneath the basePath", func(t *testing.T) {
202+
_, err = absPath("../foo", volume+"/root")
203+
assert.ErrorContains(t, err, "'../'")
204+
})
170205

171-
// Check relative paths with all the supported schemes
172-
actual, err = absPath("./foo", "/root")
173-
assert.NilError(t, err)
174-
assert.Equal(t, actual, "/root/foo")
206+
t.Run("basePath must not be empty", func(t *testing.T) {
207+
_, err = absPath("foo", "")
208+
assert.ErrorContains(t, err, "empty")
209+
})
175210

176-
actual, err = absPath("foo", "template://")
177-
assert.NilError(t, err)
178-
assert.Equal(t, actual, "template://foo")
211+
t.Run("", func(t *testing.T) {
212+
_, err = absPath("./foo", "")
213+
assert.ErrorContains(t, err, "empty")
214+
})
179215

180-
actual, err = absPath("bar", "template://foo")
181-
assert.NilError(t, err)
182-
assert.Equal(t, actual, "template://foo/bar")
216+
t.Run("", func(t *testing.T) {
217+
actual, err := absPath("./foo", volume+"/root")
218+
assert.NilError(t, err)
219+
assert.Equal(t, actual, filepath.Clean(volume+"/root/foo"))
220+
})
183221

184-
actual, err = absPath("foo", "http://host")
185-
assert.NilError(t, err)
186-
assert.Equal(t, actual, "http://host/foo")
222+
if runtime.GOOS == "windows" {
223+
t.Run("Relative locators must not include volume names", func(t *testing.T) {
224+
_, err := absPath(volume+"foo", volume+"/root")
225+
assert.ErrorContains(t, err, "volume")
226+
})
227+
}
228+
229+
t.Run("", func(t *testing.T) {
230+
actual, err := absPath("foo", "template://")
231+
assert.NilError(t, err)
232+
assert.Equal(t, actual, "template://foo")
233+
})
187234

188-
actual, err = absPath("bar", "http://host/foo")
189-
assert.NilError(t, err)
190-
assert.Equal(t, actual, "http://host/foo/bar")
235+
t.Run("", func(t *testing.T) {
236+
actual, err := absPath("bar", "template://foo")
237+
assert.NilError(t, err)
238+
assert.Equal(t, actual, "template://foo/bar")
239+
})
191240

192-
actual, err = absPath("foo", "file:///")
193-
assert.NilError(t, err)
194-
assert.Equal(t, actual, "file:///foo")
241+
t.Run("", func(t *testing.T) {
242+
actual, err := absPath("foo", "http://host")
243+
assert.NilError(t, err)
244+
assert.Equal(t, actual, "http://host/foo")
245+
})
195246

196-
actual, err = absPath("bar", "file:///foo")
197-
assert.NilError(t, err)
198-
assert.Equal(t, actual, "file:///foo/bar")
247+
t.Run("", func(t *testing.T) {
248+
actual, err := absPath("bar", "http://host/foo")
249+
assert.NilError(t, err)
250+
assert.Equal(t, actual, "http://host/foo/bar")
251+
})
252+
253+
t.Run("", func(t *testing.T) {
254+
actual, err := absPath("foo", "file:///")
255+
assert.NilError(t, err)
256+
assert.Equal(t, actual, "file:///foo")
257+
})
258+
259+
t.Run("", func(t *testing.T) {
260+
actual, err := absPath("bar", "file:///foo")
261+
assert.NilError(t, err)
262+
assert.Equal(t, actual, "file:///foo/bar")
263+
})
199264
}

pkg/limatmpl/locator.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,14 @@ func SeemsYAMLPath(arg string) bool {
153153
return strings.HasSuffix(lower, ".yml") || strings.HasSuffix(lower, ".yaml")
154154
}
155155

156-
// SeemsFilePath returns true if arg either contains a slash or has a file extension that
156+
// SeemsFilePath returns true if arg either contains a path separator or has a file extension that
157157
// does not start with a digit. `my.yaml` is a file path, `ubuntu-20.10` is not.
158158
func SeemsFilePath(arg string) bool {
159-
if u, err := url.Parse(arg); err == nil && u.Scheme != "" {
159+
// Single-letter schemes will be drive names on Windows, e.g. "c:/foo.yaml"
160+
if u, err := url.Parse(arg); err == nil && len(u.Scheme) > 1 {
160161
return false
161162
}
162-
if strings.Contains(arg, "/") {
163+
if strings.ContainsRune(arg, '/') || strings.ContainsRune(arg, filepath.Separator) {
163164
return true
164165
}
165166
ext := filepath.Ext(arg)

0 commit comments

Comments
 (0)