lib/fs: Add case-insensitive fakefs (#6074)

This commit is contained in:
Evgeny Kuznetsov
2019-11-29 11:17:42 +03:00
committed by Jakob Borg
parent c71116ee94
commit 1c277fc096
3 changed files with 859 additions and 32 deletions

View File

@@ -47,12 +47,14 @@ const randomBlockShift = 14 // 128k
// maxsize=n to generate files up to a total of n MiB (default 0)
// sizeavg=n to set the average size of random files, in bytes (default 1<<20)
// seed=n to set the initial random seed (default 0)
// insens=b "true" makes filesystem case-insensitive Windows- or OSX-style (default false)
//
// - Two fakefs:s pointing at the same root path see the same files.
//
type fakefs struct {
mut sync.Mutex
root *fakeEntry
mut sync.Mutex
root *fakeEntry
insens bool
}
var (
@@ -90,6 +92,10 @@ func newFakeFilesystem(root string) *fakefs {
maxsize, _ := strconv.Atoi(params.Get("maxsize"))
sizeavg, _ := strconv.Atoi(params.Get("sizeavg"))
seed, _ := strconv.Atoi(params.Get("seed"))
if params.Get("insens") == "true" {
fs.insens = true
}
if sizeavg == 0 {
sizeavg = 1 << 20
}
@@ -149,6 +155,9 @@ type fakeEntry struct {
func (fs *fakefs) entryForName(name string) *fakeEntry {
// bug: lookup doesn't work through symlinks.
if fs.insens {
name = UnicodeLowercase(name)
}
name = filepath.ToSlash(name)
if name == "." || name == "/" {
@@ -232,6 +241,11 @@ func (fs *fakefs) create(name string) (*fakeEntry, error) {
mode: 0666,
mtime: time.Now(),
}
if fs.insens {
base = UnicodeLowercase(base)
}
entry.children[base] = new
return new, nil
}
@@ -241,6 +255,9 @@ func (fs *fakefs) Create(name string) (File, error) {
if err != nil {
return nil, err
}
if fs.insens {
return &fakeFile{fakeEntry: entry, presentedName: filepath.Base(name)}, nil
}
return &fakeFile{fakeEntry: entry}, nil
}
@@ -264,8 +281,8 @@ func (fs *fakefs) DirNames(name string) ([]string, error) {
}
names := make([]string, 0, len(entry.children))
for name := range entry.children {
names = append(names, name)
for _, child := range entry.children {
names = append(names, child.name)
}
return names, nil
@@ -279,7 +296,13 @@ func (fs *fakefs) Lstat(name string) (FileInfo, error) {
if entry == nil {
return nil, os.ErrNotExist
}
return &fakeFileInfo{*entry}, nil
info := &fakeFileInfo{*entry}
if fs.insens {
info.name = filepath.Base(name)
}
return info, nil
}
func (fs *fakefs) Mkdir(name string, perm FileMode) error {
@@ -289,17 +312,22 @@ func (fs *fakefs) Mkdir(name string, perm FileMode) error {
dir := filepath.Dir(name)
base := filepath.Base(name)
entry := fs.entryForName(dir)
key := base
if entry == nil {
return os.ErrNotExist
}
if entry.entryType != fakeEntryTypeDir {
return os.ErrExist
}
if _, ok := entry.children[base]; ok {
if fs.insens {
key = UnicodeLowercase(key)
}
if _, ok := entry.children[key]; ok {
return os.ErrExist
}
entry.children[base] = &fakeEntry{
entry.children[key] = &fakeEntry{
name: base,
entryType: fakeEntryTypeDir,
mode: perm,
@@ -315,7 +343,12 @@ func (fs *fakefs) MkdirAll(name string, perm FileMode) error {
comps := strings.Split(name, "/")
entry := fs.root
for _, comp := range comps {
next, ok := entry.children[comp]
key := comp
if fs.insens {
key = UnicodeLowercase(key)
}
next, ok := entry.children[key]
if !ok {
new := &fakeEntry{
@@ -325,7 +358,7 @@ func (fs *fakefs) MkdirAll(name string, perm FileMode) error {
mtime: time.Now(),
children: make(map[string]*fakeEntry),
}
entry.children[comp] = new
entry.children[key] = new
next = new
} else if next.entryType != fakeEntryTypeDir {
return errors.New("not a directory")
@@ -344,28 +377,37 @@ func (fs *fakefs) Open(name string) (File, error) {
if entry == nil || entry.entryType != fakeEntryTypeFile {
return nil, os.ErrNotExist
}
if fs.insens {
return &fakeFile{fakeEntry: entry, presentedName: filepath.Base(name)}, nil
}
return &fakeFile{fakeEntry: entry}, nil
}
func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error) {
fs.mut.Lock()
defer fs.mut.Unlock()
if flags&os.O_CREATE == 0 {
return fs.Open(name)
}
fs.mut.Lock()
defer fs.mut.Unlock()
dir := filepath.Dir(name)
base := filepath.Base(name)
entry := fs.entryForName(dir)
key := base
if entry == nil {
return nil, os.ErrNotExist
} else if entry.entryType != fakeEntryTypeDir {
return nil, errors.New("not a directory")
}
if fs.insens {
key = UnicodeLowercase(key)
}
if flags&os.O_EXCL != 0 {
if _, ok := entry.children[base]; ok {
if _, ok := entry.children[key]; ok {
return nil, os.ErrExist
}
}
@@ -376,7 +418,7 @@ func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error)
mtime: time.Now(),
}
entry.children[base] = newEntry
entry.children[key] = newEntry
return &fakeFile{fakeEntry: newEntry}, nil
}
@@ -397,6 +439,10 @@ func (fs *fakefs) Remove(name string) error {
fs.mut.Lock()
defer fs.mut.Unlock()
if fs.insens {
name = UnicodeLowercase(name)
}
entry := fs.entryForName(name)
if entry == nil {
return os.ErrNotExist
@@ -414,9 +460,13 @@ func (fs *fakefs) RemoveAll(name string) error {
fs.mut.Lock()
defer fs.mut.Unlock()
if fs.insens {
name = UnicodeLowercase(name)
}
entry := fs.entryForName(filepath.Dir(name))
if entry == nil {
return os.ErrNotExist
return nil // all tested real systems exibit this behaviour
}
// RemoveAll is easy when the file system uses garbage collection under
@@ -429,12 +479,20 @@ func (fs *fakefs) Rename(oldname, newname string) error {
fs.mut.Lock()
defer fs.mut.Unlock()
oldKey := filepath.Base(oldname)
newKey := filepath.Base(newname)
if fs.insens {
oldKey = UnicodeLowercase(oldKey)
newKey = UnicodeLowercase(newKey)
}
p0 := fs.entryForName(filepath.Dir(oldname))
if p0 == nil {
return os.ErrNotExist
}
entry := p0.children[filepath.Base(oldname)]
entry := p0.children[oldKey]
if entry == nil {
return os.ErrNotExist
}
@@ -444,13 +502,24 @@ func (fs *fakefs) Rename(oldname, newname string) error {
return os.ErrNotExist
}
dst, ok := p1.children[filepath.Base(newname)]
if ok && dst.entryType == fakeEntryTypeDir {
return errors.New("is a directory")
dst, ok := p1.children[newKey]
if ok {
if fs.insens && newKey == oldKey {
// case-only in-place rename
entry.name = filepath.Base(newname)
return nil
}
if dst.entryType == fakeEntryTypeDir {
return errors.New("is a directory")
}
}
p1.children[filepath.Base(newname)] = entry
delete(p0.children, filepath.Base(oldname))
p1.children[newKey] = entry
entry.name = filepath.Base(newname)
delete(p0.children, oldKey)
return nil
}
@@ -500,18 +569,30 @@ func (fs *fakefs) URI() string {
}
func (fs *fakefs) SameFile(fi1, fi2 FileInfo) bool {
return fi1.Name() == fi2.Name()
// BUG: real systems base file sameness on path, inodes, etc
// we try our best, but FileInfo just doesn't have enough data
// so there be false positives, especially on Windows
// where ModTime is not that precise
var ok bool
if fs.insens {
ok = UnicodeLowercase(fi1.Name()) == UnicodeLowercase(fi2.Name())
} else {
ok = fi1.Name() == fi2.Name()
}
return ok && fi1.ModTime().Equal(fi2.ModTime()) && fi1.Mode() == fi2.Mode() && fi1.IsDir() == fi2.IsDir() && fi1.IsRegular() == fi2.IsRegular() && fi1.IsSymlink() == fi2.IsSymlink() && fi1.Owner() == fi2.Owner() && fi1.Group() == fi2.Group()
}
// fakeFile is the representation of an open file. We don't care if it's
// opened for reading or writing, it's all good.
type fakeFile struct {
*fakeEntry
mut sync.Mutex
rng io.Reader
seed int64
offset int64
seedOffs int64
mut sync.Mutex
rng io.Reader
seed int64
offset int64
seedOffs int64
presentedName string // present (i.e. != "") on insensitive fs only
}
func (f *fakeFile) Close() error {
@@ -674,6 +755,9 @@ func (f *fakeFile) WriteAt(p []byte, off int64) (int, error) {
}
func (f *fakeFile) Name() string {
if f.presentedName != "" {
return f.presentedName
}
return f.name
}
@@ -690,7 +774,12 @@ func (f *fakeFile) Truncate(size int64) error {
}
func (f *fakeFile) Stat() (FileInfo, error) {
return &fakeFileInfo{*f.fakeEntry}, nil
info := &fakeFileInfo{*f.fakeEntry}
if f.presentedName != "" {
info.name = f.presentedName
}
return info, nil
}
func (f *fakeFile) Sync() error {