From 3e032c4da671874cccaac7a7a533a7b250266499 Mon Sep 17 00:00:00 2001 From: greatroar <61184462+greatroar@users.noreply.github.com> Date: Sat, 6 Nov 2021 13:07:09 +0100 Subject: [PATCH] lib/fs: optimize Windows path checking/sanitizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit name old time/op new time/op delta WindowsInvalidFilenameValid-8 875ns ± 1% 150ns ± 1% -82.84% (p=0.000 n=9+9) WindowsInvalidFilenameNUL-8 276ns ± 4% 121ns ± 3% -56.26% (p=0.000 n=10+10) name old alloc/op new alloc/op delta WindowsInvalidFilenameValid-8 32.0B ± 0% 16.0B ± 0% -50.00% (p=0.000 n=10+10) WindowsInvalidFilenameNUL-8 32.0B ± 0% 19.0B ± 0% -40.62% (p=0.000 n=10+10) name old allocs/op new allocs/op delta WindowsInvalidFilenameValid-8 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=10+10) WindowsInvalidFilenameNUL-8 2.00 ± 0% 2.00 ± 0% ~ (all equal) --- lib/fs/util.go | 52 ++++++++++++++++++++++----------------------- lib/fs/util_test.go | 12 +++++++++++ 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/lib/fs/util.go b/lib/fs/util.go index 348124dcf..2ffd6cc22 100644 --- a/lib/fs/util.go +++ b/lib/fs/util.go @@ -47,25 +47,13 @@ func getHomeDir() (string, error) { return os.UserHomeDir() } -var ( - windowsDisallowedCharacters = string([]rune{ - '<', '>', ':', '"', '|', '?', '*', - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, - 31, - }) - windowsDisallowedNames = []string{"CON", "PRN", "AUX", "NUL", - "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", - "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", - } -) +const windowsDisallowedCharacters = (`<>:"|?*` + + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f") func WindowsInvalidFilename(name string) error { // None of the path components should end in space or period, or be a - // reserved name. COM0 and LPT0 are missing from the Microsoft docs, - // but Windows Explorer treats them as invalid too. - // (https://docs.microsoft.com/windows/win32/fileio/naming-a-file) + // reserved name. for _, part := range strings.Split(name, `\`) { if len(part) == 0 { continue @@ -110,7 +98,7 @@ func WindowsInvalidFilename(name string) error { func SanitizePath(path string) string { var b strings.Builder - disallowed := `<>:"'/\|?*[]{};:!@$%&^#` + windowsDisallowedCharacters + const disallowed = `'/\[]{};:!@$%&^#` + windowsDisallowedCharacters prev := ' ' for _, c := range path { if !unicode.IsPrint(c) || c == unicode.ReplacementChar || @@ -132,15 +120,27 @@ func SanitizePath(path string) string { } func windowsIsReserved(part string) bool { - upperCased := strings.ToUpper(part) - for _, disallowed := range windowsDisallowedNames { - if upperCased == disallowed { - return true - } - if strings.HasPrefix(upperCased, disallowed+".") { - // nul.txt.jpg is also disallowed - return true - } + // nul.txt.jpg is also disallowed. + dot := strings.IndexByte(part, '.') + if dot != -1 { + part = part[:dot] + } + + // Check length to skip allocating ToUpper. + if len(part) != 3 && len(part) != 4 { + return false + } + + // COM0 and LPT0 are missing from the Microsoft docs, + // but Windows Explorer treats them as invalid too. + // (https://docs.microsoft.com/windows/win32/fileio/naming-a-file) + switch strings.ToUpper(part) { + case "CON", "PRN", "AUX", "NUL", + "COM0", "COM1", "COM2", "COM3", "COM4", + "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", + "LPT5", "LPT6", "LPT7", "LPT8", "LPT9": + return true } return false } diff --git a/lib/fs/util_test.go b/lib/fs/util_test.go index 0b934308a..a263b5ef9 100644 --- a/lib/fs/util_test.go +++ b/lib/fs/util_test.go @@ -117,3 +117,15 @@ func TestSanitizePathFuzz(t *testing.T) { } } } + +func benchmarkWindowsInvalidFilename(b *testing.B, name string) { + for i := 0; i < b.N; i++ { + WindowsInvalidFilename(name) + } +} +func BenchmarkWindowsInvalidFilenameValid(b *testing.B) { + benchmarkWindowsInvalidFilename(b, "License.txt.gz") +} +func BenchmarkWindowsInvalidFilenameNUL(b *testing.B) { + benchmarkWindowsInvalidFilename(b, "nul.txt.gz") +}