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

Commit 2d40f42

Browse files
committed
check cfg filename case on case insensitive systems
- `fs` - Export `IsCaseSensitiveFilesystem`. Add and run test on windows, linux and macOS. - Add function `ReadActualFilenames` to read actual file names of given string slice. Add tests to be run on windows and macOS. - `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 and macOS. - `context` - Call `project.go#checkCfgFilenames` after resolving project root. Add test for invalid manifest file name to be run on windows and macOS.
1 parent 238d8af commit 2d40f42

File tree

6 files changed

+335
-12
lines changed

6 files changed

+335
-12
lines changed

context.go

+5
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

+45
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" && runtime.GOOS != "darwin" {
269+
t.Skip("skip this test on non-Windows, non-macOS")
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

+89-6
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ func HasFilepathPrefix(path, prefix string) (bool, error) {
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))
@@ -82,7 +82,7 @@ func HasFilepathPrefix(path, prefix string) (bool, error) {
8282
// something like ext4 filesystem mounted on FAT
8383
// mountpoint, mounted on ext4 filesystem, i.e. the
8484
// problematic filesystem is not the last one.
85-
caseSensitive, err := isCaseSensitiveFilesystem(filepath.Join(d, dirs[i]))
85+
caseSensitive, err := IsCaseSensitiveFilesystem(filepath.Join(d, dirs[i]))
8686
if err != nil {
8787
return false, errors.Wrap(err, "failed to check filepath prefix")
8888
}
@@ -135,7 +135,7 @@ func EquivalentPaths(p1, p2 string) (bool, error) {
135135
}
136136

137137
if p1Filename != "" || p2Filename != "" {
138-
caseSensitive, err := isCaseSensitiveFilesystem(filepath.Join(p1, p1Filename))
138+
caseSensitive, err := IsCaseSensitiveFilesystem(filepath.Join(p1, p1Filename))
139139
if err != nil {
140140
return false, errors.Wrap(err, "could not check for filesystem case-sensitivity")
141141
}
@@ -193,7 +193,7 @@ func renameByCopy(src, dst string) error {
193193
return errors.Wrapf(os.RemoveAll(src), "cannot delete %s", src)
194194
}
195195

196-
// isCaseSensitiveFilesystem determines if the filesystem where dir
196+
// IsCaseSensitiveFilesystem determines if the filesystem where dir
197197
// exists is case sensitive or not.
198198
//
199199
// CAVEAT: this function works by taking the last component of the given
@@ -212,7 +212,7 @@ func renameByCopy(src, dst string) error {
212212
// If the input directory is such that the last component is composed
213213
// exclusively of case-less codepoints (e.g. numbers), this function will
214214
// return false.
215-
func isCaseSensitiveFilesystem(dir string) (bool, error) {
215+
func IsCaseSensitiveFilesystem(dir string) (bool, error) {
216216
alt := filepath.Join(filepath.Dir(dir), genTestFilename(filepath.Base(dir)))
217217

218218
dInfo, err := os.Stat(dir)
@@ -264,6 +264,89 @@ func genTestFilename(str string) string {
264264
}, str)
265265
}
266266

267+
var errPathNotDir = errors.New("given path is not a directory")
268+
269+
// ReadActualFilenames is used to determine the actual file names in given directory.
270+
//
271+
// On case sensitive file systems like ext4, it will check if those files exist using
272+
// `os.Stat` and return a map with key and value as filenames which exist in the folder.
273+
//
274+
// Otherwise, it reads the contents of the directory
275+
func ReadActualFilenames(dirPath string, names []string) (map[string]string, error) {
276+
actualFilenames := make(map[string]string, len(names))
277+
if len(names) <= 0 {
278+
// This isn't expected to happen for current usage.
279+
// Adding edge case handling, maybe useful in future
280+
return actualFilenames, nil
281+
}
282+
// First, check that the given path is valid and it is a directory
283+
dirStat, err := os.Stat(dirPath)
284+
if err != nil {
285+
return nil, errors.Wrap(err, "failed to read actual filenames")
286+
}
287+
288+
if !dirStat.IsDir() {
289+
return nil, errPathNotDir
290+
}
291+
292+
// Ideally, we would use `os.Stat` for getting the actual file names
293+
// but that returns the name we passed in as an argument and not the actual filename.
294+
// So we are forced to list the directory contents and check
295+
// against that. Since this check is costly, we do it only if absolutely necessary.
296+
caseSensitive, err := IsCaseSensitiveFilesystem(dirPath)
297+
if err != nil {
298+
return nil, errors.Wrap(err, "failed to read actual filenames")
299+
}
300+
if caseSensitive {
301+
// There will be no difference between actual filename and given filename
302+
// So just check if those files exist.
303+
for _, name := range names {
304+
_, err := os.Stat(filepath.Join(dirPath, name))
305+
if err == nil {
306+
actualFilenames[name] = name
307+
} else if !os.IsNotExist(err) {
308+
// Some unexpected err, return it.
309+
return nil, errors.Wrap(err, "failed to read actual filenames")
310+
}
311+
}
312+
return actualFilenames, nil
313+
}
314+
315+
dir, err := os.Open(dirPath)
316+
if err != nil {
317+
return nil, errors.Wrap(err, "failed to read actual filenames")
318+
}
319+
defer dir.Close()
320+
321+
// Pass -1 to read all files in directory
322+
files, err := dir.Readdir(-1)
323+
if err != nil {
324+
return nil, errors.Wrap(err, "failed to read actual filenames")
325+
}
326+
327+
// namesMap holds the mapping from lowercase name to search name.
328+
// Using this, we can avoid repeatedly looping through names.
329+
namesMap := make(map[string]string, len(names))
330+
for _, name := range names {
331+
namesMap[strings.ToLower(name)] = name
332+
}
333+
334+
for _, file := range files {
335+
if file.Mode().IsRegular() {
336+
searchName, ok := namesMap[strings.ToLower(file.Name())]
337+
if ok {
338+
// We are interested in this file, case insensitive match successful
339+
actualFilenames[searchName] = file.Name()
340+
if len(actualFilenames) == len(names) {
341+
// We found all that we were looking for
342+
return actualFilenames, nil
343+
}
344+
}
345+
}
346+
}
347+
return actualFilenames, nil
348+
}
349+
267350
var (
268351
errSrcNotDir = errors.New("source is not a directory")
269352
errDstExist = errors.New("destination already exists")

internal/fs/fs_test.go

+101-6
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"
@@ -169,7 +170,7 @@ func TestEquivalentPaths(t *testing.T) {
169170
{strings.ToLower(h.Path("dir")), strings.ToUpper(h.Path("dir")), false, true, true},
170171
}
171172

172-
caseSensitive, err := isCaseSensitiveFilesystem(h.Path("dir"))
173+
caseSensitive, err := IsCaseSensitiveFilesystem(h.Path("dir"))
173174
if err != nil {
174175
t.Fatal("unexpcted error:", err)
175176
}
@@ -229,6 +230,99 @@ func TestRenameWithFallback(t *testing.T) {
229230
}
230231
}
231232

233+
func TestIsCaseSensitiveFilesystem(t *testing.T) {
234+
isLinux := runtime.GOOS == "linux"
235+
isWindows := runtime.GOOS == "windows"
236+
isMacOS := runtime.GOOS == "darwin"
237+
238+
if !isLinux && !isWindows && !isMacOS {
239+
t.Skip("Run this test on Windows, Linux and macOS only")
240+
}
241+
242+
dir, err := ioutil.TempDir("", "TestCaseSensitivity")
243+
if err != nil {
244+
t.Fatal(err)
245+
}
246+
defer os.RemoveAll(dir)
247+
248+
var want bool
249+
if isLinux {
250+
want = true
251+
} else {
252+
want = false
253+
}
254+
255+
got, err := IsCaseSensitiveFilesystem(dir)
256+
257+
if err != nil {
258+
t.Fatalf("unexpected error message: \n\t(GOT) %+v", err)
259+
}
260+
261+
if want != got {
262+
t.Fatalf("unexpected value returned: \n\t(GOT) %t\n\t(WNT) %t", got, want)
263+
}
264+
}
265+
266+
func TestReadActualFilenames(t *testing.T) {
267+
if runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
268+
t.Skip("skip this test on non-Windows, non-macOS")
269+
}
270+
271+
h := test.NewHelper(t)
272+
defer h.Cleanup()
273+
274+
h.TempDir("")
275+
tmpPath := h.Path(".")
276+
277+
_, err := ReadActualFilenames(filepath.Join(tmpPath, "does_not_exists"), []string{""})
278+
switch {
279+
case err == nil:
280+
t.Fatal("expected err for non-existing folder")
281+
case !os.IsNotExist(err):
282+
t.Fatalf("unexpected error: %+v", err)
283+
}
284+
h.TempFile("tmpFile", "")
285+
_, err = ReadActualFilenames(h.Path("tmpFile"), []string{""})
286+
switch {
287+
case err == nil:
288+
t.Fatal("expected err for passing file instead of directory")
289+
case err != errPathNotDir:
290+
t.Fatalf("unexpected error: %+v", err)
291+
}
292+
293+
cases := []struct {
294+
createFiles []string
295+
names []string
296+
want map[string]string
297+
}{
298+
{nil, nil, map[string]string{}}, {
299+
[]string{"test1.txt"},
300+
[]string{"Test1.txt"},
301+
map[string]string{"Test1.txt": "test1.txt"},
302+
}, {
303+
[]string{"test2.txt", "test3.TXT"},
304+
[]string{"test2.txt", "Test3.txt", "Test4.txt"},
305+
map[string]string{
306+
"test2.txt": "test2.txt",
307+
"Test3.txt": "test3.TXT",
308+
"Test4.txt": "",
309+
},
310+
},
311+
}
312+
for _, c := range cases {
313+
for _, file := range c.createFiles {
314+
h.TempFile(file, "")
315+
}
316+
got, err := ReadActualFilenames(tmpPath, c.names)
317+
if err != nil {
318+
t.Fatalf("unexpected error: %+v", err)
319+
}
320+
if !reflect.DeepEqual(c.want, got) {
321+
t.Fatalf("returned value does not match expected: \n\t(GOT) %v\n\t(WNT) %v", got, c.want)
322+
}
323+
}
324+
}
325+
232326
func TestGenTestFilename(t *testing.T) {
233327
cases := []struct {
234328
str string
@@ -254,11 +348,11 @@ func TestGenTestFilename(t *testing.T) {
254348

255349
func BenchmarkGenTestFilename(b *testing.B) {
256350
cases := []string{
257-
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
258-
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
259-
"αααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααα",
260-
"11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
261-
"⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘",
351+
strings.Repeat("a", 128),
352+
strings.Repeat("A", 128),
353+
strings.Repeat("α", 128),
354+
strings.Repeat("1", 128),
355+
strings.Repeat("⌘", 128),
262356
}
263357

264358
for i := 0; i < b.N; i++ {
@@ -613,6 +707,7 @@ func TestCopyFileLongFilePath(t *testing.T) {
613707

614708
h := test.NewHelper(t)
615709
h.TempDir(".")
710+
defer h.Cleanup()
616711

617712
tmpPath := h.Path(".")
618713

0 commit comments

Comments
 (0)