Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Commit e24eef4

Browse files
committed
check cfg filename case on case insensitive systems
- `fs` - Export `IsCaseSensitiveFilesystem`. Add and run test on windows, linux. - Add function `ReadActualFilenames` to read actual file names of given string slice. Add tests to be run on windows. - `project` - Add function `checkCfgFilenames` to check the filenames for manifest and lock have the expected case. Use `fs#IsCaseSensitiveFilesystem` for an early return as the check is costly. Add test to be run on windows. - `context` - Call `project#checkCfgFilenames` after resolving project root. Add test for invalid manifest file name to be run on windows.
1 parent 876083e commit e24eef4

File tree

6 files changed

+307
-10
lines changed

6 files changed

+307
-10
lines changed

context.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ func (c *Ctx) LoadProject() (*Project, error) {
103103
return nil, err
104104
}
105105

106+
err = checkCfgFilenames(root)
107+
if err != nil {
108+
return nil, err
109+
}
110+
106111
p := new(Project)
107112

108113
if err = p.SetRoot(root); err != nil {

context_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package dep
66

77
import (
8+
"fmt"
89
"io/ioutil"
910
"log"
1011
"os"
@@ -263,6 +264,50 @@ func TestLoadProjectNoSrcDir(t *testing.T) {
263264
}
264265
}
265266

267+
func TestLoadProjectCfgFileCase(t *testing.T) {
268+
if runtime.GOOS != "windows" {
269+
t.Skip("skip this test on non-Windows")
270+
}
271+
272+
// Here we test that a manifest filename with incorrect case
273+
// throws an error. Similar error will also be thrown for the
274+
// lock file as well which has been tested in
275+
// `project_test.go#TestCheckCfgFilenames`. So not repeating here.
276+
277+
h := test.NewHelper(t)
278+
defer h.Cleanup()
279+
280+
invalidMfName := strings.ToLower(ManifestName)
281+
282+
wd := filepath.Join("src", "test")
283+
h.TempFile(filepath.Join(wd, invalidMfName), "")
284+
285+
ctx := &Ctx{
286+
Out: discardLogger,
287+
Err: discardLogger,
288+
}
289+
290+
err := ctx.SetPaths(h.Path(wd), h.Path("."))
291+
if err != nil {
292+
t.Fatalf("%+v", err)
293+
}
294+
295+
_, err = ctx.LoadProject()
296+
297+
if err == nil {
298+
t.Fatal("should have returned 'Manifest Filename' error")
299+
}
300+
301+
expectedErrMsg := fmt.Sprintf(
302+
"manifest filename '%s' does not match '%s'",
303+
invalidMfName, ManifestName,
304+
)
305+
306+
if err.Error() != expectedErrMsg {
307+
t.Fatalf("unexpected error: %+v", err)
308+
}
309+
}
310+
266311
// TestCaseInsentitive is test for Windows. This should work even though set
267312
// difference letter cases in GOPATH.
268313
func TestCaseInsentitiveGOPATH(t *testing.T) {

internal/fs/fs.go

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ func HasFilepathPrefix(path, prefix string) bool {
3535
// handling of volume name/drive letter on Windows. vnPath and vnPrefix
3636
// are first compared, and then used to initialize initial values of p and
3737
// d which will be appended to for incremental checks using
38-
// isCaseSensitiveFilesystem and then equality.
38+
// IsCaseSensitiveFilesystem and then equality.
3939

40-
// no need to check isCaseSensitiveFilesystem because VolumeName return
40+
// no need to check IsCaseSensitiveFilesystem because VolumeName return
4141
// empty string on all non-Windows machines
4242
vnPath := strings.ToLower(filepath.VolumeName(path))
4343
vnPrefix := strings.ToLower(filepath.VolumeName(prefix))
@@ -84,7 +84,7 @@ func HasFilepathPrefix(path, prefix string) bool {
8484
// something like ext4 filesystem mounted on FAT
8585
// mountpoint, mounted on ext4 filesystem, i.e. the
8686
// problematic filesystem is not the last one.
87-
if isCaseSensitiveFilesystem(filepath.Join(d, dirs[i])) {
87+
if IsCaseSensitiveFilesystem(filepath.Join(d, dirs[i])) {
8888
d = filepath.Join(d, dirs[i])
8989
p = filepath.Join(p, prefixes[i])
9090
} else {
@@ -140,7 +140,7 @@ func renameByCopy(src, dst string) error {
140140
return errors.Wrapf(os.RemoveAll(src), "cannot delete %s", src)
141141
}
142142

143-
// isCaseSensitiveFilesystem determines if the filesystem where dir
143+
// IsCaseSensitiveFilesystem determines if the filesystem where dir
144144
// exists is case sensitive or not.
145145
//
146146
// CAVEAT: this function works by taking the last component of the given
@@ -159,7 +159,7 @@ func renameByCopy(src, dst string) error {
159159
// If the input directory is such that the last component is composed
160160
// exclusively of case-less codepoints (e.g. numbers), this function will
161161
// return false.
162-
func isCaseSensitiveFilesystem(dir string) bool {
162+
func IsCaseSensitiveFilesystem(dir string) bool {
163163
alt := filepath.Join(filepath.Dir(dir),
164164
genTestFilename(filepath.Base(dir)))
165165

@@ -176,6 +176,85 @@ func isCaseSensitiveFilesystem(dir string) bool {
176176
return !os.SameFile(dInfo, aInfo)
177177
}
178178

179+
var errPathNotDir = errors.New("given path is not a directory")
180+
181+
// ReadActualFilenames is used to determine the actual file names in given directory.
182+
//
183+
// On case sensitive file systems like ext4, it will check if those files exist using
184+
// `os#Stat` and return a map with key and value as filenames which exist in the folder.
185+
//
186+
// Otherwise, it reads the contents of the directory
187+
func ReadActualFilenames(dirPath string, names []string) (map[string]string, error) {
188+
actualFilenames := make(map[string]string, len(names))
189+
if len(names) <= 0 {
190+
// This isn't expected to happen for current usage.
191+
// Adding edge case handling, maybe useful in future
192+
return actualFilenames, nil
193+
}
194+
// First, check that the given path is valid and it is a directory
195+
dirStat, err := os.Stat(dirPath)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
if !dirStat.IsDir() {
201+
return nil, errPathNotDir
202+
}
203+
204+
// Ideally, we would use `os#Stat` for getting the actual file names
205+
// but that returns the name we passed in as an argument and not the actual filename.
206+
// So we are forced to list the directory contents and check
207+
// against that. Since this check is costly, we do it only if absolutely necessary.
208+
if IsCaseSensitiveFilesystem(dirPath) {
209+
// There will be no difference between actual filename and given filename
210+
// So just check if those files exist.
211+
for _, name := range names {
212+
_, err := os.Stat(filepath.Join(dirPath, name))
213+
if err == nil {
214+
actualFilenames[name] = name
215+
} else if !os.IsNotExist(err) {
216+
// Some unexpected err, return it.
217+
return nil, err
218+
}
219+
}
220+
return actualFilenames, nil
221+
}
222+
223+
dir, err := os.Open(dirPath)
224+
if err != nil {
225+
return nil, err
226+
}
227+
defer dir.Close()
228+
229+
// Pass -1 to read all files in directory
230+
files, err := dir.Readdir(-1)
231+
if err != nil {
232+
return nil, err
233+
}
234+
235+
// namesMap holds the mapping from lowercase name to search name.
236+
// Using this, we can avoid repeatedly looping through names.
237+
namesMap := make(map[string]string, len(names))
238+
for _, name := range names {
239+
namesMap[strings.ToLower(name)] = name
240+
}
241+
242+
for _, file := range files {
243+
if file.Mode().IsRegular() {
244+
searchName, ok := namesMap[strings.ToLower(file.Name())]
245+
if ok {
246+
// We are interested in this file, case insensitive match successful
247+
actualFilenames[searchName] = file.Name()
248+
if len(actualFilenames) == len(names) {
249+
// We found all that we were looking for
250+
return actualFilenames, nil
251+
}
252+
}
253+
}
254+
}
255+
return actualFilenames, nil
256+
}
257+
179258
// genTestFilename returns a string with at most one rune case-flipped.
180259
//
181260
// The transformation is applied only to the first rune that can be

internal/fs/fs_test.go

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io/ioutil"
99
"os"
1010
"path/filepath"
11+
"reflect"
1112
"runtime"
1213
"strings"
1314
"testing"
@@ -172,6 +173,85 @@ func TestRenameWithFallback(t *testing.T) {
172173
}
173174
}
174175

176+
func TestIsCaseSensitiveFilesystem(t *testing.T) {
177+
if runtime.GOOS != "windows" && runtime.GOOS != "linux" {
178+
t.Skip("skip this test on non-Windows, non-Linux")
179+
}
180+
181+
dir, err := ioutil.TempDir("", "TestCaseSensitivity")
182+
if err != nil {
183+
t.Fatal(err)
184+
}
185+
defer os.RemoveAll(dir)
186+
187+
isSensitive := IsCaseSensitiveFilesystem(dir)
188+
isWindows := runtime.GOOS == "windows"
189+
190+
if !isSensitive && !isWindows {
191+
t.Fatal("expected isSensitive to be true on linux")
192+
} else if isSensitive && isWindows {
193+
t.Fatal("expected isSensitive to be false on windows")
194+
}
195+
}
196+
197+
func TestReadActualFilenames(t *testing.T) {
198+
if runtime.GOOS != "windows" {
199+
t.Skip("skip this test on non-Windows")
200+
}
201+
202+
h := test.NewHelper(t)
203+
defer h.Cleanup()
204+
205+
h.TempDir("")
206+
tmpPath := h.Path(".")
207+
208+
_, err := ReadActualFilenames(filepath.Join(tmpPath, "does_not_exists"), []string{""})
209+
switch {
210+
case err == nil:
211+
t.Fatal("expected err for non-existing folder")
212+
case !os.IsNotExist(err):
213+
t.Fatalf("unexpected error: %+v", err)
214+
}
215+
h.TempFile("tmpFile", "")
216+
_, err = ReadActualFilenames(h.Path("tmpFile"), []string{""})
217+
switch {
218+
case err == nil:
219+
t.Fatal("expected err for passing file instead of directory")
220+
case err != errPathNotDir:
221+
t.Fatalf("unexpected error: %+v", err)
222+
}
223+
224+
cases := []struct {
225+
createFiles []string
226+
names []string
227+
want map[string]string
228+
}{
229+
{nil, nil, map[string]string{}}, {
230+
[]string{"test1.txt"},
231+
[]string{"Test1.txt"},
232+
map[string]string{"Test1.txt": "test1.txt"},
233+
}, {
234+
[]string{"test2.txt", "test3.TXT"},
235+
[]string{"test2.txt", "Test3.txt", "Test4.txt"},
236+
map[string]string{
237+
"test2.txt": "test2.txt",
238+
"Test3.txt": "test3.TXT",
239+
"Test4.txt": "",
240+
},
241+
},
242+
}
243+
for _, c := range cases {
244+
for _, file := range c.createFiles {
245+
h.TempFile(file, "")
246+
}
247+
got, err := ReadActualFilenames(tmpPath, c.names)
248+
if err != nil {
249+
t.Fatalf("unexpected error: %+v", err)
250+
}
251+
reflect.DeepEqual(c.want, got)
252+
}
253+
}
254+
175255
func TestGenTestFilename(t *testing.T) {
176256
cases := []struct {
177257
str string
@@ -197,11 +277,11 @@ func TestGenTestFilename(t *testing.T) {
197277

198278
func BenchmarkGenTestFilename(b *testing.B) {
199279
cases := []string{
200-
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
201-
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
202-
"αααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααα",
203-
"11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
204-
"⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘",
280+
strings.Repeat("a", 128),
281+
strings.Repeat("A", 128),
282+
strings.Repeat("α", 128),
283+
strings.Repeat("1", 128),
284+
strings.Repeat("⌘", 128),
205285
}
206286

207287
for i := 0; i < b.N; i++ {
@@ -556,6 +636,7 @@ func TestCopyFileLongFilePath(t *testing.T) {
556636

557637
h := test.NewHelper(t)
558638
h.TempDir(".")
639+
defer h.Cleanup()
559640

560641
tmpPath := h.Path(".")
561642

project.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@ func findProjectRoot(from string) (string, error) {
4141
}
4242
}
4343

44+
// checkCfgFilenames validates filename case for the manifest and lock files.
45+
// This is relevant on case-insensitive systems like Windows.
46+
func checkCfgFilenames(projectRoot string) error {
47+
// ReadActualFilenames is actually costly. Since this check is not relevant
48+
// to case-sensitive filesystems like ext4, try for an early return.
49+
if fs.IsCaseSensitiveFilesystem(projectRoot) {
50+
return nil
51+
}
52+
53+
actualFilenames, err := fs.ReadActualFilenames(projectRoot, []string{ManifestName, LockName})
54+
55+
if err != nil {
56+
return err
57+
}
58+
59+
// Since this check is done after `findProjectRoot`, we can assume that
60+
// manifest file will be present. Even if it is not, error will still be thrown.
61+
// But error message will be a tad inaccurate.
62+
actualMfName := actualFilenames[ManifestName]
63+
if actualMfName != ManifestName {
64+
return fmt.Errorf("manifest filename '%s' does not match '%s'", actualMfName, ManifestName)
65+
}
66+
67+
actualLfName := actualFilenames[LockName]
68+
if len(actualLfName) > 0 && actualLfName != LockName {
69+
return fmt.Errorf("lock filename '%s' does not match '%s'", actualLfName, LockName)
70+
}
71+
72+
return nil
73+
}
74+
4475
// A Project holds a Manifest and optional Lock for a project.
4576
type Project struct {
4677
// AbsRoot is the absolute path to the root directory of the project.

0 commit comments

Comments
 (0)