lib/fs: Clarify errors for Windows filenames (fixes #8968) (#8969)

With this change, error messages include the offending characters or
name parts. Examples:

    nul.txt: name is invalid, contains Windows reserved name: "nul"
    foo>bar.txt: name is invalid, contains Windows reserved character: ">"
    foo \bar.txt: name is invalid, must not end in space or period on Windows
This commit is contained in:
Jakob Borg 2023-07-07 13:00:40 +02:00 committed by GitHub
parent 6ff5ed6d23
commit c44de2cd58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 16 additions and 12 deletions

View File

@ -22,8 +22,8 @@ import (
var ( var (
errInvalidFilenameEmpty = errors.New("name is invalid, must not be empty") errInvalidFilenameEmpty = errors.New("name is invalid, must not be empty")
errInvalidFilenameWindowsSpacePeriod = errors.New("name is invalid, must not end in space or period on Windows") errInvalidFilenameWindowsSpacePeriod = errors.New("name is invalid, must not end in space or period on Windows")
errInvalidFilenameWindowsReservedName = errors.New("name is invalid, contains Windows reserved name (NUL, COM1, etc.)") errInvalidFilenameWindowsReservedName = errors.New("name is invalid, contains Windows reserved name")
errInvalidFilenameWindowsReservedChar = errors.New("name is invalid, contains Windows reserved character (?, *, etc.)") errInvalidFilenameWindowsReservedChar = errors.New("name is invalid, contains Windows reserved character")
) )
type OptionJunctionsAsDirs struct{} type OptionJunctionsAsDirs struct{}

View File

@ -54,8 +54,8 @@ const windowsDisallowedCharacters = (`<>:"|?*` +
func WindowsInvalidFilename(name string) error { func WindowsInvalidFilename(name string) error {
// The path must not contain any disallowed characters. // The path must not contain any disallowed characters.
if strings.ContainsAny(name, windowsDisallowedCharacters) { if idx := strings.IndexAny(name, windowsDisallowedCharacters); idx != -1 {
return errInvalidFilenameWindowsReservedChar return fmt.Errorf("%w: %q", errInvalidFilenameWindowsReservedChar, name[idx:idx+1])
} }
// None of the path components should end in space or period, or be a // None of the path components should end in space or period, or be a
@ -72,8 +72,8 @@ func WindowsInvalidFilename(name string) error {
// Names ending in space or period are not valid. // Names ending in space or period are not valid.
return errInvalidFilenameWindowsSpacePeriod return errInvalidFilenameWindowsSpacePeriod
} }
if windowsIsReserved(part) { if reserved := windowsReservedNamePart(part); reserved != "" {
return errInvalidFilenameWindowsReservedName return fmt.Errorf("%w: %q", errInvalidFilenameWindowsReservedName, reserved)
} }
} }
@ -117,13 +117,13 @@ func SanitizePath(path string) string {
} }
path = strings.TrimSpace(b.String()) path = strings.TrimSpace(b.String())
if windowsIsReserved(path) { if reserved := windowsReservedNamePart(path); reserved != "" {
path = "-" + path path = "-" + path
} }
return path return path
} }
func windowsIsReserved(part string) bool { func windowsReservedNamePart(part string) string {
// nul.txt.jpg is also disallowed. // nul.txt.jpg is also disallowed.
dot := strings.IndexByte(part, '.') dot := strings.IndexByte(part, '.')
if dot != -1 { if dot != -1 {
@ -132,7 +132,7 @@ func windowsIsReserved(part string) bool {
// Check length to skip allocating ToUpper. // Check length to skip allocating ToUpper.
if len(part) != 3 && len(part) != 4 { if len(part) != 3 && len(part) != 4 {
return false return ""
} }
// COM0 and LPT0 are missing from the Microsoft docs, // COM0 and LPT0 are missing from the Microsoft docs,
@ -144,9 +144,9 @@ func windowsIsReserved(part string) bool {
"COM5", "COM6", "COM7", "COM8", "COM9", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4",
"LPT5", "LPT6", "LPT7", "LPT8", "LPT9": "LPT5", "LPT6", "LPT7", "LPT8", "LPT9":
return true return part
} }
return false return ""
} }
// IsParent compares paths purely lexicographically, meaning it returns false // IsParent compares paths purely lexicographically, meaning it returns false

View File

@ -7,6 +7,7 @@
package fs package fs
import ( import (
"errors"
"math/rand" "math/rand"
"testing" "testing"
"unicode" "unicode"
@ -69,9 +70,10 @@ func TestWindowsInvalidFilename(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
err := WindowsInvalidFilename(tc.name) err := WindowsInvalidFilename(tc.name)
if err != tc.err { if !errors.Is(err, tc.err) {
t.Errorf("For %q, got %v, expected %v", tc.name, err, tc.err) t.Errorf("For %q, got %v, expected %v", tc.name, err, tc.err)
} }
t.Logf("%s: %v", tc.name, err)
} }
} }
@ -124,9 +126,11 @@ func benchmarkWindowsInvalidFilename(b *testing.B, name string) {
WindowsInvalidFilename(name) WindowsInvalidFilename(name)
} }
} }
func BenchmarkWindowsInvalidFilenameValid(b *testing.B) { func BenchmarkWindowsInvalidFilenameValid(b *testing.B) {
benchmarkWindowsInvalidFilename(b, "License.txt.gz") benchmarkWindowsInvalidFilename(b, "License.txt.gz")
} }
func BenchmarkWindowsInvalidFilenameNUL(b *testing.B) { func BenchmarkWindowsInvalidFilenameNUL(b *testing.B) {
benchmarkWindowsInvalidFilename(b, "nul.txt.gz") benchmarkWindowsInvalidFilename(b, "nul.txt.gz")
} }