Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib/fs,model: Clarify errors for Android filenames (fixes #9395) #9397

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/fs/basicfs.go
Expand Up @@ -24,6 +24,7 @@ var (
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")
errInvalidFilenameWindowsReservedChar = errors.New("name is invalid, contains Windows reserved character")
errInvalidFilenameAndroidReservedChar = errors.New("name is invalid, contains Android reserved character")
)

type OptionJunctionsAsDirs struct{}
Expand Down
54 changes: 33 additions & 21 deletions lib/fs/util.go
Expand Up @@ -48,32 +48,44 @@ func getHomeDir() (string, error) {
return os.UserHomeDir()
}

const windowsDisallowedCharacters = (`<>:"|?*` +
// On Android, the following characters are disallowed on user storage
// even if the filesystem is not FAT. On Windows, they apply to both FAT
// and NTFS.
// Ref: https://cs.android.com/android/platform/superproject/main/+/main:packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java;drc=9d054a9eb6a8d8c3d79e007ea6d2f89c94a52f06;l=485
const fatDisallowedCharacters = (`/\<>:"|?*` +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

androidDisallowedCharacters?

Copy link
Contributor Author

@tomasz1986 tomasz1986 Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't fat better in this case? It's more generic and that's essentially where these characters come from (even though they are applied on a wider scale than just on FAT).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can imagine someone reading the code later on thinking "what the hell, you can't blindly assume that the user has a fat filesystem. *reads comment* Ah, that's why".

"\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 {
func OsInvalidFilename(name string) error {
// The path must not contain any disallowed characters.
if idx := strings.IndexAny(name, windowsDisallowedCharacters); idx != -1 {
return fmt.Errorf("%w: %q", errInvalidFilenameWindowsReservedChar, name[idx:idx+1])
var errMsg error
if build.IsAndroid {
errMsg = errInvalidFilenameAndroidReservedChar
} else if build.IsWindows {
errMsg = errInvalidFilenameWindowsReservedChar
}
if idx := strings.IndexAny(name, fatDisallowedCharacters); idx != -1 && errMsg != nil {
return fmt.Errorf("%w: %q", errMsg, name[idx:idx+1])
}

// None of the path components should end in space or period, or be a
// reserved name.
for len(name) > 0 {
part, rest, _ := strings.Cut(name, `\`)
name = rest

if part == "" {
continue
}
switch part[len(part)-1] {
case ' ', '.':
// Names ending in space or period are not valid.
return errInvalidFilenameWindowsSpacePeriod
}
if reserved := windowsReservedNamePart(part); reserved != "" {
return fmt.Errorf("%w: %q", errInvalidFilenameWindowsReservedName, reserved)
if build.IsWindows {
// None of the path components should end in space or period, or be a
// reserved name.
for len(name) > 0 {
part, rest, _ := strings.Cut(name, `\`)
name = rest

if part == "" {
continue
}
switch part[len(part)-1] {
case ' ', '.':
// Names ending in space or period are not valid.
return errInvalidFilenameWindowsSpacePeriod
}
if reserved := windowsReservedNamePart(part); reserved != "" {
return fmt.Errorf("%w: %q", errInvalidFilenameWindowsReservedName, reserved)
}
}
}

Expand Down Expand Up @@ -102,7 +114,7 @@ func WindowsInvalidFilename(name string) error {
func SanitizePath(path string) string {
var b strings.Builder

const disallowed = `'/\[]{};:!@$%&^#` + windowsDisallowedCharacters
const disallowed = `'[]{};!@$%&^#` + fatDisallowedCharacters
prev := ' '
for _, c := range path {
if !unicode.IsPrint(c) || c == unicode.ReplacementChar ||
Expand Down
16 changes: 8 additions & 8 deletions lib/fs/util_test.go
Expand Up @@ -50,7 +50,7 @@ func TestCommonPrefix(t *testing.T) {
test(`.`, `.`, `.`)
}

func TestWindowsInvalidFilename(t *testing.T) {
func TestOsInvalidFilename(t *testing.T) {
cases := []struct {
name string
err error
Expand All @@ -69,7 +69,7 @@ func TestWindowsInvalidFilename(t *testing.T) {
}

for _, tc := range cases {
err := WindowsInvalidFilename(tc.name)
err := OsInvalidFilename(tc.name)
if !errors.Is(err, tc.err) {
t.Errorf("For %q, got %v, expected %v", tc.name, err, tc.err)
}
Expand Down Expand Up @@ -121,16 +121,16 @@ func TestSanitizePathFuzz(t *testing.T) {
}
}

func benchmarkWindowsInvalidFilename(b *testing.B, name string) {
func benchmarkOsInvalidFilename(b *testing.B, name string) {
for i := 0; i < b.N; i++ {
WindowsInvalidFilename(name)
OsInvalidFilename(name)
}
}

func BenchmarkWindowsInvalidFilenameValid(b *testing.B) {
benchmarkWindowsInvalidFilename(b, "License.txt.gz")
func BenchmarkOsInvalidFilenameValid(b *testing.B) {
benchmarkOsInvalidFilename(b, "License.txt.gz")
}

func BenchmarkWindowsInvalidFilenameNUL(b *testing.B) {
benchmarkWindowsInvalidFilename(b, "nul.txt.gz")
func BenchmarkOsInvalidFilenameNUL(b *testing.B) {
benchmarkOsInvalidFilename(b, "nul.txt.gz")
}
4 changes: 2 additions & 2 deletions lib/model/folder_sendrecv.go
Expand Up @@ -348,7 +348,7 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
l.Debugln(f, "Handling ignored file", file)
dbUpdateChan <- dbUpdateJob{file, dbUpdateInvalidate}

case build.IsWindows && fs.WindowsInvalidFilename(file.Name) != nil:
case (build.IsAndroid || build.IsWindows) && fs.OsInvalidFilename(file.Name) != nil:
if file.IsDeleted() {
// Just pretend we deleted it, no reason to create an error
// about a deleted file that we can't have anyway.
Expand All @@ -358,7 +358,7 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
} else {
// We can't pull an invalid file. Grab the error again since
// we couldn't assign it directly in the case clause.
f.newPullError(file.Name, fs.WindowsInvalidFilename(file.Name))
f.newPullError(file.Name, fs.OsInvalidFilename(file.Name))
// No reason to retry for this
changed--
}
Expand Down