diff --git a/path_windows.go b/path_windows.go index b5a6de0..f1ba8cb 100644 --- a/path_windows.go +++ b/path_windows.go @@ -5,12 +5,32 @@ package fsnotify import ( "strings" "syscall" + "unicode" + "unicode/utf8" ) // pathKey returns a comparison key for p. NTFS is case-insensitive, so // fold to lowercase before using a path as a map key. func pathKey(p string) string { - return strings.ToLower(p) + isASCII := true + for i := 0; i < len(p); i++ { + if p[i] >= utf8.RuneSelf { + isASCII = false + break + } + } + if isASCII { // optimize for ASCII-only strings. + return strings.ToLower(p) + } + + return strings.Map(func(r rune) rune { + if r == 'ß' || r == 'ẞ' { + // NTFS is case-insensitive, but as an exception, + // it distinguishes between uppercase and lowercase Sharp S. + return r + } + return unicode.ToLower(r) + }, p) } // canonicalizeOS expands an 8.3 short-form path (e.g. C:\PROGRA~1) to diff --git a/watcher_test.go b/watcher_test.go index 6a36a71..5c81b27 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -519,43 +519,70 @@ func TestAddCaseInsensitive(t *testing.T) { } func TestAddSharpS(t *testing.T) { - if runtime.GOOS != "darwin" { - t.Skip("This test is only applicable on macOS") - } + switch runtime.GOOS { + case "darwin": + // APFS is case-insensitive. + // Uppercase and lowercase Sharp S are considered the same path on APFS. + // Furthermore, "ss" and "SS" are also considered the same path as "ß" and "ẞ". + + parent := tempDir(t) + w := newWatcher(t) + + dir := filepath.Join(parent, "ß") // LATIN SMALL LETTER SHARP S + if err := os.Mkdir(dir, 0o755); err != nil { + t.Fatalf("Mkdir: %v", err) + } + if err := w.Add(dir, All); err != nil { + t.Fatalf("Add: %v", err) + } - parent := tempDir(t) - w := newWatcher(t) + dir = filepath.Join(parent, "ẞ") // LATIN CAPITAL LETTER SHARP S + if err := os.Mkdir(dir, 0o755); !errors.Is(err, os.ErrExist) { + t.Errorf("Mkdir(ẞ) = %v, want os.ErrExist", err) + } + if err := w.Add(dir, All); !errors.Is(err, ErrAlreadyAdded) { + t.Errorf("Add(ẞ) = %v, want ErrAlreadyAdded", err) + } - dir := filepath.Join(parent, "ß") // LATIN SMALL LETTER SHARP S - if err := os.Mkdir(dir, 0o755); err != nil { - t.Fatalf("Mkdir: %v", err) - } - if err := w.Add(dir, All); err != nil { - t.Fatalf("Add: %v", err) - } + dir = filepath.Join(parent, "ss") + if err := os.Mkdir(dir, 0o755); !errors.Is(err, os.ErrExist) { + t.Errorf("Mkdir(ss) = %v, want os.ErrExist", err) + } + if err := w.Add(dir, All); !errors.Is(err, ErrAlreadyAdded) { + t.Errorf("Add(ss) = %v, want ErrAlreadyAdded", err) + } - dir = filepath.Join(parent, "ẞ") // LATIN CAPITAL LETTER SHARP S - if err := os.Mkdir(dir, 0o755); !errors.Is(err, os.ErrExist) { - t.Errorf("Mkdir(ẞ) = %v, want os.ErrExist", err) - } - if err := w.Add(dir, All); !errors.Is(err, ErrAlreadyAdded) { - t.Errorf("Add(ẞ) = %v, want ErrAlreadyAdded", err) - } + dir = filepath.Join(parent, "SS") + if err := os.Mkdir(dir, 0o755); !errors.Is(err, os.ErrExist) { + t.Errorf("Mkdir(SS) = %v, want os.ErrExist", err) + } + if err := w.Add(dir, All); !errors.Is(err, ErrAlreadyAdded) { + t.Errorf("Add(SS) = %v, want ErrAlreadyAdded", err) + } - dir = filepath.Join(parent, "ss") - if err := os.Mkdir(dir, 0o755); !errors.Is(err, os.ErrExist) { - t.Errorf("Mkdir(ss) = %v, want os.ErrExist", err) - } - if err := w.Add(dir, All); !errors.Is(err, ErrAlreadyAdded) { - t.Errorf("Add(ss) = %v, want ErrAlreadyAdded", err) - } + default: + // On other platforms, including Windows, "ß" and "ẞ" are considered different paths. + // NTFS is case-insensitive, but as an exception, + // it distinguishes between uppercase and lowercase Sharp S. - dir = filepath.Join(parent, "SS") - if err := os.Mkdir(dir, 0o755); !errors.Is(err, os.ErrExist) { - t.Errorf("Mkdir(SS) = %v, want os.ErrExist", err) - } - if err := w.Add(dir, All); !errors.Is(err, ErrAlreadyAdded) { - t.Errorf("Add(SS) = %v, want ErrAlreadyAdded", err) + parent := tempDir(t) + w := newWatcher(t) + + dir1 := filepath.Join(parent, "ß") // LATIN SMALL LETTER SHARP S + if err := os.Mkdir(dir1, 0o755); err != nil { + t.Fatalf("Mkdir(ß): %v", err) + } + if err := w.Add(dir1, All); err != nil { + t.Fatalf("Add(ß): %v", err) + } + + dir2 := filepath.Join(parent, "ẞ") // LATIN CAPITAL LETTER SHARP S + if err := os.Mkdir(dir2, 0o755); err != nil { + t.Fatalf("Mkdir(ẞ): %v", err) + } + if err := w.Add(dir2, All); err != nil { + t.Fatalf("Add(ẞ): %v", err) + } } }