2020-07-28 09:13:15 +00:00
// Copyright (C) 2020 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package fs
import (
"context"
"errors"
"fmt"
"path/filepath"
"sync"
"time"
2021-01-27 18:25:34 +00:00
lru "github.com/hashicorp/golang-lru"
2021-05-17 10:35:03 +00:00
"golang.org/x/text/unicode/norm"
2020-07-28 09:13:15 +00:00
)
const (
2020-09-09 12:38:39 +00:00
// How long to consider cached dirnames valid
2021-01-27 18:25:34 +00:00
caseCacheTimeout = time . Second
caseCacheItemLimit = 4 << 10
2020-07-28 09:13:15 +00:00
)
type ErrCaseConflict struct {
2020-09-09 09:47:14 +00:00
Given , Real string
2020-07-28 09:13:15 +00:00
}
func ( e * ErrCaseConflict ) Error ( ) string {
2021-08-01 20:44:49 +00:00
return fmt . Sprintf ( ` remote "%v" uses different upper or lowercase characters than local "%v"; change the casing on either side to match the other ` , e . Given , e . Real )
2020-07-28 09:13:15 +00:00
}
func IsErrCaseConflict ( err error ) bool {
e := & ErrCaseConflict { }
return errors . As ( err , & e )
}
type realCaser interface {
realCase ( name string ) ( string , error )
dropCache ( )
}
type fskey struct {
2021-02-19 10:06:25 +00:00
fstype FilesystemType
uri , opts string
2020-07-28 09:13:15 +00:00
}
2020-09-09 12:38:39 +00:00
// caseFilesystemRegistry caches caseFilesystems and runs a routine to drop
// their cache every now and then.
type caseFilesystemRegistry struct {
fss map [ fskey ] * caseFilesystem
2021-01-12 15:22:21 +00:00
mut sync . RWMutex
2020-09-09 12:38:39 +00:00
startCleaner sync . Once
}
2021-03-11 14:23:56 +00:00
func newFSKey ( fs Filesystem ) fskey {
2021-02-19 10:06:25 +00:00
k := fskey {
fstype : fs . Type ( ) ,
uri : fs . URI ( ) ,
}
2021-03-11 14:23:56 +00:00
if opts := fs . Options ( ) ; len ( opts ) > 0 {
k . opts = opts [ 0 ] . String ( )
2021-02-19 10:06:25 +00:00
for _ , o := range opts [ 1 : ] {
2021-03-11 14:23:56 +00:00
k . opts += "&" + o . String ( )
2021-02-19 10:06:25 +00:00
}
}
return k
}
2021-03-11 14:23:56 +00:00
func ( r * caseFilesystemRegistry ) get ( fs Filesystem ) Filesystem {
k := newFSKey ( fs )
2021-01-12 15:22:21 +00:00
// Use double locking when getting a caseFs. In the common case it will
// already exist and we take the read lock fast path. If it doesn't, we
// take a write lock and try again.
r . mut . RLock ( )
2020-09-09 12:38:39 +00:00
caseFs , ok := r . fss [ k ]
2021-01-12 15:22:21 +00:00
r . mut . RUnlock ( )
2020-09-09 12:38:39 +00:00
if ! ok {
2021-01-12 15:22:21 +00:00
r . mut . Lock ( )
caseFs , ok = r . fss [ k ]
if ! ok {
caseFs = & caseFilesystem {
Filesystem : fs ,
realCaser : newDefaultRealCaser ( fs ) ,
}
r . fss [ k ] = caseFs
r . startCleaner . Do ( func ( ) {
go r . cleaner ( )
} )
2020-09-09 12:38:39 +00:00
}
2021-01-12 15:22:21 +00:00
r . mut . Unlock ( )
2020-09-09 12:38:39 +00:00
}
return caseFs
}
func ( r * caseFilesystemRegistry ) cleaner ( ) {
for range time . NewTicker ( time . Minute ) . C {
2021-01-13 16:45:29 +00:00
// We need to not hold this lock for a long time, as it blocks
// creating new filesystems in get(), which is needed to do things
// like add new folders. The (*caseFs).dropCache() method can take
// an arbitrarily long time to kick in because it in turn waits for
// locks held by things performing I/O. So we can't call that from
// within the loop.
2021-01-12 15:22:21 +00:00
r . mut . RLock ( )
2021-01-13 16:45:29 +00:00
toProcess := make ( [ ] * caseFilesystem , 0 , len ( r . fss ) )
2020-09-09 12:38:39 +00:00
for _ , caseFs := range r . fss {
2021-01-13 16:45:29 +00:00
toProcess = append ( toProcess , caseFs )
2020-09-09 12:38:39 +00:00
}
2021-01-12 15:22:21 +00:00
r . mut . RUnlock ( )
2021-01-13 16:45:29 +00:00
for _ , caseFs := range toProcess {
caseFs . dropCache ( )
}
2020-09-09 12:38:39 +00:00
}
}
var globalCaseFilesystemRegistry = caseFilesystemRegistry { fss : make ( map [ fskey ] * caseFilesystem ) }
2020-07-28 09:13:15 +00:00
2022-04-10 18:55:05 +00:00
// OptionDetectCaseConflicts ensures that the potentially case-insensitive filesystem
2020-07-28 09:13:15 +00:00
// behaves like a case-sensitive filesystem. Meaning that it takes into account
// the real casing of a path and returns ErrCaseConflict if the given path differs
// from the real path. It is safe to use with any filesystem, i.e. also a
// case-sensitive one. However it will add some overhead and thus shouldn't be
// used if the filesystem is known to already behave case-sensitively.
2022-04-10 18:55:05 +00:00
type OptionDetectCaseConflicts struct { }
func ( o * OptionDetectCaseConflicts ) apply ( fs Filesystem ) Filesystem {
return globalCaseFilesystemRegistry . get ( fs )
}
func ( o * OptionDetectCaseConflicts ) String ( ) string {
return "detectCaseConflicts"
}
// caseFilesystem is a BasicFilesystem with additional checks to make a
// potentially case insensitive underlying FS behave like it's case-sensitive.
type caseFilesystem struct {
Filesystem
realCaser
2020-07-28 09:13:15 +00:00
}
func ( f * caseFilesystem ) Chmod ( name string , mode FileMode ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
return f . Filesystem . Chmod ( name , mode )
}
func ( f * caseFilesystem ) Lchown ( name string , uid , gid int ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
return f . Filesystem . Lchown ( name , uid , gid )
}
func ( f * caseFilesystem ) Chtimes ( name string , atime time . Time , mtime time . Time ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
return f . Filesystem . Chtimes ( name , atime , mtime )
}
func ( f * caseFilesystem ) Mkdir ( name string , perm FileMode ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
if err := f . Filesystem . Mkdir ( name , perm ) ; err != nil {
return err
}
f . dropCache ( )
return nil
}
func ( f * caseFilesystem ) MkdirAll ( path string , perm FileMode ) error {
if err := f . checkCase ( path ) ; err != nil {
return err
}
if err := f . Filesystem . MkdirAll ( path , perm ) ; err != nil {
return err
}
f . dropCache ( )
return nil
}
func ( f * caseFilesystem ) Lstat ( name string ) ( FileInfo , error ) {
var err error
if name , err = Canonicalize ( name ) ; err != nil {
return nil , err
}
stat , err := f . Filesystem . Lstat ( name )
if err != nil {
return nil , err
}
if err = f . checkCaseExisting ( name ) ; err != nil {
return nil , err
}
return stat , nil
}
func ( f * caseFilesystem ) Remove ( name string ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
if err := f . Filesystem . Remove ( name ) ; err != nil {
return err
}
f . dropCache ( )
return nil
}
func ( f * caseFilesystem ) RemoveAll ( name string ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
if err := f . Filesystem . RemoveAll ( name ) ; err != nil {
return err
}
f . dropCache ( )
return nil
}
func ( f * caseFilesystem ) Rename ( oldpath , newpath string ) error {
if err := f . checkCase ( oldpath ) ; err != nil {
return err
}
2021-03-12 20:15:50 +00:00
if err := f . checkCase ( newpath ) ; err != nil {
// Case-only rename is ok
e := & ErrCaseConflict { }
if ! errors . As ( err , & e ) || e . Real != oldpath {
return err
}
}
2020-07-28 09:13:15 +00:00
if err := f . Filesystem . Rename ( oldpath , newpath ) ; err != nil {
return err
}
f . dropCache ( )
return nil
}
func ( f * caseFilesystem ) Stat ( name string ) ( FileInfo , error ) {
var err error
if name , err = Canonicalize ( name ) ; err != nil {
return nil , err
}
stat , err := f . Filesystem . Stat ( name )
if err != nil {
return nil , err
}
if err = f . checkCaseExisting ( name ) ; err != nil {
return nil , err
}
return stat , nil
}
func ( f * caseFilesystem ) DirNames ( name string ) ( [ ] string , error ) {
if err := f . checkCase ( name ) ; err != nil {
return nil , err
}
return f . Filesystem . DirNames ( name )
}
func ( f * caseFilesystem ) Open ( name string ) ( File , error ) {
if err := f . checkCase ( name ) ; err != nil {
return nil , err
}
return f . Filesystem . Open ( name )
}
func ( f * caseFilesystem ) OpenFile ( name string , flags int , mode FileMode ) ( File , error ) {
if err := f . checkCase ( name ) ; err != nil {
return nil , err
}
file , err := f . Filesystem . OpenFile ( name , flags , mode )
if err != nil {
return nil , err
}
f . dropCache ( )
return file , nil
}
func ( f * caseFilesystem ) ReadSymlink ( name string ) ( string , error ) {
if err := f . checkCase ( name ) ; err != nil {
return "" , err
}
return f . Filesystem . ReadSymlink ( name )
}
func ( f * caseFilesystem ) Create ( name string ) ( File , error ) {
if err := f . checkCase ( name ) ; err != nil {
return nil , err
}
file , err := f . Filesystem . Create ( name )
if err != nil {
return nil , err
}
f . dropCache ( )
return file , nil
}
func ( f * caseFilesystem ) CreateSymlink ( target , name string ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
if err := f . Filesystem . CreateSymlink ( target , name ) ; err != nil {
return err
}
f . dropCache ( )
return nil
}
func ( f * caseFilesystem ) Walk ( root string , walkFn WalkFunc ) error {
// Walking the filesystem is likely (in Syncthing's case certainly) done
// to pick up external changes, for which caching is undesirable.
f . dropCache ( )
if err := f . checkCase ( root ) ; err != nil {
return err
}
return f . Filesystem . Walk ( root , walkFn )
}
func ( f * caseFilesystem ) Watch ( path string , ignore Matcher , ctx context . Context , ignorePerms bool ) ( <- chan Event , <- chan error , error ) {
if err := f . checkCase ( path ) ; err != nil {
return nil , nil , err
}
return f . Filesystem . Watch ( path , ignore , ctx , ignorePerms )
}
func ( f * caseFilesystem ) Hide ( name string ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
return f . Filesystem . Hide ( name )
}
func ( f * caseFilesystem ) Unhide ( name string ) error {
if err := f . checkCase ( name ) ; err != nil {
return err
}
return f . Filesystem . Unhide ( name )
}
2021-05-03 10:28:25 +00:00
func ( f * caseFilesystem ) underlying ( ) ( Filesystem , bool ) {
return f . Filesystem , true
}
func ( f * caseFilesystem ) wrapperType ( ) filesystemWrapperType {
return filesystemWrapperTypeCase
}
2020-07-28 09:13:15 +00:00
func ( f * caseFilesystem ) checkCase ( name string ) error {
var err error
if name , err = Canonicalize ( name ) ; err != nil {
return err
}
// Stat is necessary for case sensitive FS, as it's then not a conflict
// if name is e.g. "foo" and on dir there is "Foo".
if _ , err := f . Filesystem . Lstat ( name ) ; err != nil {
if IsNotExist ( err ) {
return nil
}
return err
}
return f . checkCaseExisting ( name )
}
// checkCaseExisting must only be called after successfully canonicalizing and
// stating the file.
func ( f * caseFilesystem ) checkCaseExisting ( name string ) error {
realName , err := f . realCase ( name )
if IsNotExist ( err ) {
// It did exist just before -> cache is outdated, try again
f . dropCache ( )
realName , err = f . realCase ( name )
}
if err != nil {
return err
}
2021-05-17 10:35:03 +00:00
// We normalize the normalization (hah!) of the strings before
// comparing, as we don't want to treat a normalization difference as a
// case conflict.
if norm . NFC . String ( realName ) != norm . NFC . String ( name ) {
2020-07-28 09:13:15 +00:00
return & ErrCaseConflict { name , realName }
}
return nil
}
type defaultRealCaser struct {
2021-01-27 18:25:34 +00:00
cache caseCache
2020-07-28 09:13:15 +00:00
}
func newDefaultRealCaser ( fs Filesystem ) * defaultRealCaser {
2021-01-27 18:25:34 +00:00
cache , err := lru . New2Q ( caseCacheItemLimit )
// New2Q only errors if given invalid parameters, which we don't.
if err != nil {
panic ( err )
}
2021-06-27 06:30:02 +00:00
return & defaultRealCaser {
2021-01-27 18:25:34 +00:00
cache : caseCache {
2021-06-27 06:30:02 +00:00
fs : fs ,
2021-01-27 18:25:34 +00:00
TwoQueueCache : cache ,
} ,
2020-07-28 09:13:15 +00:00
}
}
func ( r * defaultRealCaser ) realCase ( name string ) ( string , error ) {
2021-01-27 18:25:34 +00:00
realName := "."
if name == realName {
return realName , nil
2020-07-28 09:13:15 +00:00
}
2021-04-11 13:29:43 +00:00
for _ , comp := range PathComponents ( name ) {
2021-01-27 18:25:34 +00:00
node := r . cache . getExpireAdd ( realName )
2020-09-09 12:38:39 +00:00
2021-01-27 18:25:34 +00:00
if node . err != nil {
return "" , node . err
2020-07-28 09:13:15 +00:00
}
2020-09-09 12:38:39 +00:00
2021-01-27 18:25:34 +00:00
// Try to find a direct or case match
if _ , ok := node . children [ comp ] ; ! ok {
2021-05-17 10:35:03 +00:00
comp , ok = node . lowerToReal [ UnicodeLowercaseNormalized ( comp ) ]
2021-01-27 18:25:34 +00:00
if ! ok {
return "" , ErrNotExist
2020-09-09 12:38:39 +00:00
}
2020-07-28 09:13:15 +00:00
}
2020-09-09 12:38:39 +00:00
2021-01-27 18:25:34 +00:00
realName = filepath . Join ( realName , comp )
2020-07-28 09:13:15 +00:00
}
2021-01-27 18:25:34 +00:00
return realName , nil
2020-07-28 09:13:15 +00:00
}
func ( r * defaultRealCaser ) dropCache ( ) {
2021-01-27 18:25:34 +00:00
r . cache . Purge ( )
}
2021-06-27 06:30:02 +00:00
type caseCache struct {
* lru . TwoQueueCache
fs Filesystem
mut sync . Mutex
}
// getExpireAdd gets an entry for the given key. If no entry exists, or it is
// expired a new one is created and added to the cache.
func ( c * caseCache ) getExpireAdd ( key string ) * caseNode {
c . mut . Lock ( )
defer c . mut . Unlock ( )
v , ok := c . Get ( key )
if ! ok {
node := newCaseNode ( key , c . fs )
c . Add ( key , node )
return node
}
node := v . ( * caseNode )
if node . expires . Before ( time . Now ( ) ) {
node = newCaseNode ( key , c . fs )
c . Add ( key , node )
2021-01-27 18:25:34 +00:00
}
2021-06-27 06:30:02 +00:00
return node
2020-07-28 09:13:15 +00:00
}
2021-01-27 18:25:34 +00:00
// The keys to children are "real", case resolved names of the path
2020-07-28 09:13:15 +00:00
// component this node represents (i.e. containing no path separator).
2021-01-27 18:25:34 +00:00
// lowerToReal is a map of lowercase path components (as in UnicodeLowercase)
// to their corresponding "real", case resolved names.
// A node is created empty and populated using once. If an error occurs the node
// is removed from cache and the error stored in err, such that anyone that
// already got the node doesn't try to access the nil maps.
2020-07-28 09:13:15 +00:00
type caseNode struct {
2021-01-27 18:25:34 +00:00
expires time . Time
lowerToReal map [ string ] string
children map [ string ] struct { }
err error
}
2021-06-27 06:30:02 +00:00
func newCaseNode ( name string , filesystem Filesystem ) * caseNode {
node := new ( caseNode )
dirNames , err := filesystem . DirNames ( name )
// Set expiry after calling DirNames in case this is super-slow
// (e.g. dirs with many children on android)
node . expires = time . Now ( ) . Add ( caseCacheTimeout )
if err != nil {
node . err = err
2021-01-27 18:25:34 +00:00
return node
2020-07-28 09:13:15 +00:00
}
2021-06-27 06:30:02 +00:00
num := len ( dirNames )
node . children = make ( map [ string ] struct { } , num )
node . lowerToReal = make ( map [ string ] string , num )
lastLower := ""
for _ , n := range dirNames {
node . children [ n ] = struct { } { }
lower := UnicodeLowercaseNormalized ( n )
if lower != lastLower {
node . lowerToReal [ lower ] = n
lastLower = n
}
2020-07-28 09:13:15 +00:00
}
2021-06-27 06:30:02 +00:00
2021-01-27 18:25:34 +00:00
return node
2020-07-28 09:13:15 +00:00
}