| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021 |
- // Copyright (C) 2018 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"
- "hash/fnv"
- "io"
- "math/rand"
- "net/url"
- "os"
- "os/user"
- "path/filepath"
- "strconv"
- "strings"
- "sync"
- "testing"
- "time"
- "github.com/syncthing/syncthing/lib/protocol"
- )
- // see readShortAt()
- const randomBlockShift = 14 // 128k
- // fakeFS is a fake filesystem for testing and benchmarking. It has the
- // following properties:
- //
- // - File metadata is kept in RAM. Specifically, we remember which files and
- // directories exist, their dates, permissions and sizes. Symlinks are
- // not supported.
- //
- // - File contents are generated pseudorandomly with just the file name as
- // seed. Writes are discarded, other than having the effect of increasing
- // the file size. If you only write data that you've read from a file with
- // the same name on a different fakeFS, you'll never know the difference...
- //
- // - We totally ignore permissions - pretend you are root.
- //
- // - The root path can contain URL query-style parameters that pre populate
- // the filesystem at creation with a certain amount of random data:
- //
- // files=n to generate n random files (default 0)
- // 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)
- // latency=d to set the amount of time each "disk" operation takes, where d is time.ParseDuration format
- // content=true to save actual file contents instead of generating pseudorandomly; n.b. memory usage
- // nostfolder=true skip the creation of .stfolder
- // timeprecisionsecond=true Modification times are stored with only second precision
- //
- // - Two fakeFS:s pointing at the same root path see the same files.
- type fakeFS struct {
- counters fakeFSCounters
- uri string
- mut sync.Mutex
- root *fakeEntry
- insens bool
- withContent bool
- timePrecisionSecond bool
- latency time.Duration
- userCache *userCache
- groupCache *groupCache
- }
- type fakeFSCounters struct {
- Chmod int64
- Lchown int64
- Chtimes int64
- Create int64
- DirNames int64
- Lstat int64
- Mkdir int64
- MkdirAll int64
- Open int64
- OpenFile int64
- ReadSymlink int64
- Remove int64
- RemoveAll int64
- Rename int64
- }
- var (
- fakeFSMut sync.Mutex
- fakeFSCache = make(map[string]*fakeFS)
- )
- func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
- fakeFSMut.Lock()
- defer fakeFSMut.Unlock()
- var params url.Values
- uri, err := url.Parse(rootURI)
- if err == nil {
- params = uri.Query()
- }
- if fs, ok := fakeFSCache[rootURI]; ok {
- // Already have an fs at this path
- return fs
- }
- fs := &fakeFS{
- uri: "fake://" + rootURI,
- root: &fakeEntry{
- name: "/",
- entryType: fakeEntryTypeDir,
- mode: 0o700,
- mtime: time.Now(),
- children: make(map[string]*fakeEntry),
- },
- userCache: newValueCache(time.Hour, user.LookupId),
- groupCache: newValueCache(time.Hour, user.LookupGroupId),
- }
- files, _ := strconv.Atoi(params.Get("files"))
- maxsize, _ := strconv.Atoi(params.Get("maxsize"))
- sizeavg, _ := strconv.Atoi(params.Get("sizeavg"))
- seed, _ := strconv.Atoi(params.Get("seed"))
- fs.insens = params.Get("insens") == "true"
- fs.withContent = params.Get("content") == "true"
- nostfolder := params.Get("nostfolder") == "true"
- fs.timePrecisionSecond = params.Get("timeprecisionsecond") == "true"
- if sizeavg == 0 {
- sizeavg = 1 << 20
- }
- if files > 0 || maxsize > 0 {
- // Generate initial data according to specs. Operations in here
- // *look* like file I/O, but they are not. Do not worry that they
- // might fail.
- rng := rand.New(rand.NewSource(int64(seed)))
- var createdFiles int
- var writtenData int64
- for (files == 0 || createdFiles < files) && (maxsize == 0 || writtenData>>20 < int64(maxsize)) {
- dir := filepath.Join(fmt.Sprintf("%02x", rng.Intn(255)), fmt.Sprintf("%02x", rng.Intn(255)))
- file := fmt.Sprintf("%016x", rng.Int63())
- fs.MkdirAll(dir, 0o755)
- fd, _ := fs.Create(filepath.Join(dir, file))
- createdFiles++
- fsize := int64(sizeavg/2 + rng.Intn(sizeavg))
- fd.Truncate(fsize)
- writtenData += fsize
- ftime := time.Unix(1000000000+rng.Int63n(10*365*86400), 0)
- fs.Chtimes(filepath.Join(dir, file), ftime, ftime)
- }
- }
- if !nostfolder {
- // Also create a default folder marker for good measure
- fs.Mkdir(".stfolder", 0o700)
- }
- // We only set the latency after doing the operations required to create
- // the filesystem initially.
- fs.latency, _ = time.ParseDuration(params.Get("latency"))
- fakeFSCache[rootURI] = fs
- return fs
- }
- type fakeEntryType int
- const (
- fakeEntryTypeFile fakeEntryType = iota
- fakeEntryTypeDir
- fakeEntryTypeSymlink
- )
- // fakeEntry is an entry (file or directory) in the fake filesystem
- type fakeEntry struct {
- name string
- entryType fakeEntryType
- dest string // for symlinks
- size int64
- mode FileMode
- uid int
- gid int
- mtime time.Time
- children map[string]*fakeEntry
- content []byte
- }
- func (fs *fakeFS) entryForName(name string) *fakeEntry {
- if fs.insens {
- name = UnicodeLowercaseNormalized(name)
- }
- name = filepath.ToSlash(name)
- if name == "." || name == "/" {
- return fs.root
- }
- name = strings.Trim(name, "/")
- comps := strings.Split(name, "/")
- entry := fs.root
- for i, comp := range comps {
- if entry.entryType != fakeEntryTypeDir {
- return nil
- }
- var ok bool
- entry, ok = entry.children[comp]
- if !ok {
- return nil
- }
- if i < len(comps)-1 && entry.entryType == fakeEntryTypeSymlink {
- // only absolute link targets are supported, and we assume
- // lookup is Lstat-kind so we only resolve symlinks when they
- // are not the last path component.
- return fs.entryForName(entry.dest)
- }
- }
- return entry
- }
- func (fs *fakeFS) Chmod(name string, mode FileMode) error {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Chmod++
- time.Sleep(fs.latency)
- entry := fs.entryForName(name)
- if entry == nil {
- return os.ErrNotExist
- }
- entry.mode = mode
- return nil
- }
- func (fs *fakeFS) Lchown(name, uid, gid string) error {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Lchown++
- time.Sleep(fs.latency)
- entry := fs.entryForName(name)
- if entry == nil {
- return os.ErrNotExist
- }
- entry.uid, _ = strconv.Atoi(uid)
- entry.gid, _ = strconv.Atoi(gid)
- return nil
- }
- func (fs *fakeFS) Chtimes(name string, _ time.Time, mtime time.Time) error {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Chtimes++
- time.Sleep(fs.latency)
- entry := fs.entryForName(name)
- if entry == nil {
- return os.ErrNotExist
- }
- if fs.timePrecisionSecond {
- mtime = mtime.Truncate(time.Second)
- }
- entry.mtime = mtime
- return nil
- }
- func (fs *fakeFS) create(name string) (*fakeEntry, error) {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Create++
- time.Sleep(fs.latency)
- if entry := fs.entryForName(name); entry != nil {
- if entry.entryType == fakeEntryTypeDir {
- return nil, os.ErrExist
- } else if entry.entryType == fakeEntryTypeSymlink {
- return nil, errors.New("following symlink not supported")
- }
- entry.size = 0
- entry.mtime = time.Now()
- entry.mode = 0o666
- entry.content = nil
- if fs.withContent {
- entry.content = make([]byte, 0)
- }
- return entry, nil
- }
- dir := filepath.Dir(name)
- base := filepath.Base(name)
- entry := fs.entryForName(dir)
- if entry == nil {
- return nil, os.ErrNotExist
- }
- new := &fakeEntry{
- name: base,
- mode: 0o666,
- mtime: time.Now(),
- }
- if fs.insens {
- base = UnicodeLowercaseNormalized(base)
- }
- if fs.withContent {
- new.content = make([]byte, 0)
- }
- entry.children[base] = new
- return new, nil
- }
- func (fs *fakeFS) Create(name string) (File, error) {
- entry, err := fs.create(name)
- if err != nil {
- return nil, err
- }
- if fs.insens {
- return &fakeFile{fakeEntry: entry, presentedName: filepath.Base(name), mut: &fs.mut}, nil
- }
- return &fakeFile{fakeEntry: entry, mut: &fs.mut}, nil
- }
- func (fs *fakeFS) CreateSymlink(target, name string) error {
- entry, err := fs.create(name)
- if err != nil {
- return err
- }
- entry.entryType = fakeEntryTypeSymlink
- entry.dest = target
- return nil
- }
- func (fs *fakeFS) DirNames(name string) ([]string, error) {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.DirNames++
- time.Sleep(fs.latency)
- entry := fs.entryForName(name)
- if entry == nil {
- return nil, os.ErrNotExist
- }
- names := make([]string, 0, len(entry.children))
- for _, child := range entry.children {
- names = append(names, child.name)
- }
- return names, nil
- }
- func (fs *fakeFS) Lstat(name string) (FileInfo, error) {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Lstat++
- time.Sleep(fs.latency)
- entry := fs.entryForName(name)
- if entry == nil {
- return nil, os.ErrNotExist
- }
- info := &fakeFileInfo{*entry}
- if fs.insens {
- info.name = filepath.Base(name)
- }
- return info, nil
- }
- func (fs *fakeFS) Mkdir(name string, perm FileMode) error {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Mkdir++
- time.Sleep(fs.latency)
- 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 fs.insens {
- key = UnicodeLowercaseNormalized(key)
- }
- if _, ok := entry.children[key]; ok {
- return os.ErrExist
- }
- entry.children[key] = &fakeEntry{
- name: base,
- entryType: fakeEntryTypeDir,
- mode: perm,
- mtime: time.Now(),
- children: make(map[string]*fakeEntry),
- }
- return nil
- }
- func (fs *fakeFS) MkdirAll(name string, perm FileMode) error {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.MkdirAll++
- time.Sleep(fs.latency)
- name = filepath.ToSlash(name)
- name = strings.Trim(name, "/")
- comps := strings.Split(name, "/")
- entry := fs.root
- for _, comp := range comps {
- key := comp
- if fs.insens {
- key = UnicodeLowercaseNormalized(key)
- }
- next, ok := entry.children[key]
- if !ok {
- new := &fakeEntry{
- name: comp,
- entryType: fakeEntryTypeDir,
- mode: perm,
- mtime: time.Now(),
- children: make(map[string]*fakeEntry),
- }
- entry.children[key] = new
- next = new
- } else if next.entryType != fakeEntryTypeDir {
- return errors.New("not a directory")
- }
- entry = next
- }
- return nil
- }
- func (fs *fakeFS) Open(name string) (File, error) {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Open++
- time.Sleep(fs.latency)
- entry := fs.entryForName(name)
- if entry == nil || entry.entryType != fakeEntryTypeFile {
- return nil, os.ErrNotExist
- }
- if fs.insens {
- return &fakeFile{fakeEntry: entry, presentedName: filepath.Base(name), mut: &fs.mut}, nil
- }
- return &fakeFile{fakeEntry: entry, mut: &fs.mut}, nil
- }
- func (fs *fakeFS) OpenFile(name string, flags int, mode FileMode) (File, error) {
- if flags&os.O_CREATE == 0 {
- return fs.Open(name)
- }
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.OpenFile++
- time.Sleep(fs.latency)
- 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 = UnicodeLowercaseNormalized(key)
- }
- if flags&os.O_EXCL != 0 {
- if _, ok := entry.children[key]; ok {
- return nil, os.ErrExist
- }
- }
- newEntry := &fakeEntry{
- name: base,
- mode: mode,
- mtime: time.Now(),
- }
- if fs.withContent {
- newEntry.content = make([]byte, 0)
- }
- entry.children[key] = newEntry
- return &fakeFile{fakeEntry: newEntry, mut: &fs.mut}, nil
- }
- func (fs *fakeFS) ReadSymlink(name string) (string, error) {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.ReadSymlink++
- time.Sleep(fs.latency)
- entry := fs.entryForName(name)
- if entry == nil {
- return "", os.ErrNotExist
- } else if entry.entryType != fakeEntryTypeSymlink {
- return "", errors.New("not a symlink")
- }
- return entry.dest, nil
- }
- func (fs *fakeFS) Remove(name string) error {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Remove++
- time.Sleep(fs.latency)
- if fs.insens {
- name = UnicodeLowercaseNormalized(name)
- }
- entry := fs.entryForName(name)
- if entry == nil {
- return os.ErrNotExist
- }
- if len(entry.children) != 0 {
- return errors.New("not empty")
- }
- entry = fs.entryForName(filepath.Dir(name))
- delete(entry.children, filepath.Base(name))
- return nil
- }
- func (fs *fakeFS) RemoveAll(name string) error {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.RemoveAll++
- time.Sleep(fs.latency)
- if fs.insens {
- name = UnicodeLowercaseNormalized(name)
- }
- entry := fs.entryForName(filepath.Dir(name))
- if entry == nil {
- return nil // all tested real systems exhibit this behaviour
- }
- // RemoveAll is easy when the file system uses garbage collection under
- // the hood... We even get the correct semantics for open fd:s for free.
- delete(entry.children, filepath.Base(name))
- return nil
- }
- func (fs *fakeFS) Rename(oldname, newname string) error {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- fs.counters.Rename++
- time.Sleep(fs.latency)
- oldKey := filepath.Base(oldname)
- newKey := filepath.Base(newname)
- if fs.insens {
- oldKey = UnicodeLowercaseNormalized(oldKey)
- newKey = UnicodeLowercaseNormalized(newKey)
- }
- p0 := fs.entryForName(filepath.Dir(oldname))
- if p0 == nil {
- return os.ErrNotExist
- }
- entry := p0.children[oldKey]
- if entry == nil {
- return os.ErrNotExist
- }
- p1 := fs.entryForName(filepath.Dir(newname))
- if p1 == nil {
- return os.ErrNotExist
- }
- 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[newKey] = entry
- entry.name = filepath.Base(newname)
- delete(p0.children, oldKey)
- return nil
- }
- func (fs *fakeFS) Stat(name string) (FileInfo, error) {
- return fs.Lstat(name)
- }
- func (*fakeFS) SymlinksSupported() bool {
- return false
- }
- func (*fakeFS) Walk(_ string, _ WalkFunc) error {
- return errors.New("not implemented")
- }
- func (*fakeFS) Watch(_ string, _ Matcher, _ context.Context, _ bool) (<-chan Event, <-chan error, error) {
- return nil, nil, ErrWatchNotSupported
- }
- func (*fakeFS) Hide(_ string) error {
- return nil
- }
- func (*fakeFS) Unhide(_ string) error {
- return nil
- }
- func (*fakeFS) GetXattr(_ string, _ XattrFilter) ([]protocol.Xattr, error) {
- return nil, nil
- }
- func (*fakeFS) SetXattr(_ string, _ []protocol.Xattr, _ XattrFilter) error {
- return nil
- }
- // A basic glob-impelementation that should be able to handle
- // simple test cases.
- func (fs *fakeFS) Glob(pattern string) ([]string, error) {
- dir := filepath.Dir(pattern)
- file := filepath.Base(pattern)
- if _, err := fs.Lstat(dir); err != nil {
- return nil, errPathInvalid
- }
- var matches []string
- names, err := fs.DirNames(dir)
- if err != nil {
- return nil, err
- }
- for _, n := range names {
- matched, err := filepath.Match(file, n)
- if err != nil {
- return nil, err
- }
- if matched {
- matches = append(matches, filepath.Join(dir, n))
- }
- }
- return matches, err
- }
- func (*fakeFS) Roots() ([]string, error) {
- return []string{"/"}, nil
- }
- func (*fakeFS) Usage(_ string) (Usage, error) {
- return Usage{}, errors.New("not implemented")
- }
- func (*fakeFS) Type() FilesystemType {
- return FilesystemTypeFake
- }
- func (fs *fakeFS) URI() string {
- return fs.uri
- }
- func (*fakeFS) Options() []Option {
- return nil
- }
- func (fs *fakeFS) SameFile(fi1, fi2 FileInfo) bool {
- // 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 = UnicodeLowercaseNormalized(fi1.Name()) == UnicodeLowercaseNormalized(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()
- }
- func (fs *fakeFS) PlatformData(name string, scanOwnership, scanXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error) {
- return unixPlatformData(fs, name, fs.userCache, fs.groupCache, scanOwnership, scanXattrs, xattrFilter)
- }
- func (*fakeFS) underlying() (Filesystem, bool) {
- return nil, false
- }
- func (*fakeFS) wrapperType() filesystemWrapperType {
- return filesystemWrapperTypeNone
- }
- func (fs *fakeFS) resetCounters() {
- fs.mut.Lock()
- fs.counters = fakeFSCounters{}
- fs.mut.Unlock()
- }
- func (fs *fakeFS) reportMetricsPerOp(b *testing.B) {
- fs.reportMetricsPer(b, 1, "op")
- }
- func (fs *fakeFS) reportMetricsPer(b *testing.B, divisor float64, unit string) {
- fs.mut.Lock()
- defer fs.mut.Unlock()
- b.ReportMetric(float64(fs.counters.Lstat)/divisor/float64(b.N), "Lstat/"+unit)
- b.ReportMetric(float64(fs.counters.DirNames)/divisor/float64(b.N), "DirNames/"+unit)
- }
- // 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
- presentedName string // present (i.e. != "") on insensitive fs only
- }
- func (*fakeFile) Close() error {
- return nil
- }
- func (f *fakeFile) Read(p []byte) (int, error) {
- f.mut.Lock()
- defer f.mut.Unlock()
- return f.readShortAt(p, f.offset)
- }
- func (f *fakeFile) ReadAt(p []byte, offs int64) (int, error) {
- f.mut.Lock()
- defer f.mut.Unlock()
- // ReadAt is spec:ed to always read a full block unless EOF or failure,
- // so we must loop. It's also not supposed to affect the seek position,
- // but that would make things annoying or inefficient in terms of
- // generating the appropriate RNG etc so I ignore that. In practice we
- // currently don't depend on that aspect of it...
- var read int
- for {
- n, err := f.readShortAt(p[read:], offs+int64(read))
- read += n
- if err != nil {
- return read, err
- }
- if read == len(p) {
- return read, nil
- }
- }
- }
- func (f *fakeFile) readShortAt(p []byte, offs int64) (int, error) {
- // Here be a certain amount of magic... We want to return pseudorandom,
- // predictable data so that a read from the same offset in the same file
- // always returns the same data. But the RNG is a stream, and reads can
- // be random.
- //
- // We split the file into "blocks" numbered by "seedNo", where each
- // block becomes an instantiation of the RNG, seeded with the hash of
- // the file number plus the seedNo (block number). We keep the RNG
- // around in the hope that the next read will be sequential to this one
- // and we can continue reading from the same RNG.
- //
- // When that's not the case we create a new RNG for the block we are in,
- // read as many bytes from it as necessary to get to the right offset,
- // and then serve the read from there. We limit the length of the read
- // to the end of the block, as another RNG needs to be created to serve
- // the next block.
- //
- // The size of the blocks are a matter of taste... Larger blocks give
- // better performance for sequential reads, but worse for random reads
- // as we often need to generate and throw away a lot of data at the
- // start of the block to serve a given read. 128 KiB blocks fit
- // reasonably well with the type of IO Syncthing tends to do.
- if f.entryType == fakeEntryTypeDir {
- return 0, errors.New("is a directory")
- }
- if offs >= f.size {
- return 0, io.EOF
- }
- if f.content != nil {
- n := copy(p, f.content[int(offs):])
- f.offset = offs + int64(n)
- return n, nil
- }
- // Lazily calculate our main seed, a simple 64 bit FNV hash our file
- // name.
- if f.seed == 0 {
- hf := fnv.New64()
- hf.Write([]byte(f.name))
- f.seed = int64(hf.Sum64())
- }
- // Check whether the read is a continuation of an RNG we already have or
- // we need to set up a new one.
- seedNo := offs >> randomBlockShift
- minOffs := seedNo << randomBlockShift
- nextBlockOffs := (seedNo + 1) << randomBlockShift
- if f.rng == nil || f.offset != offs || seedNo != f.seedOffs {
- // This is not a straight read continuing from a previous one
- f.rng = rand.New(rand.NewSource(f.seed + seedNo))
- // If the read is not at the start of the block, discard data
- // accordingly.
- diff := offs - minOffs
- if diff > 0 {
- lr := io.LimitReader(f.rng, diff)
- io.Copy(io.Discard, lr)
- }
- f.offset = offs
- f.seedOffs = seedNo
- }
- size := len(p)
- // Don't read past the end of the file
- if offs+int64(size) > f.size {
- size = int(f.size - offs)
- }
- // Don't read across the block boundary
- if offs+int64(size) > nextBlockOffs {
- size = int(nextBlockOffs - offs)
- }
- f.offset += int64(size)
- return f.rng.Read(p[:size])
- }
- func (f *fakeFile) Seek(offset int64, whence int) (int64, error) {
- f.mut.Lock()
- defer f.mut.Unlock()
- if f.entryType == fakeEntryTypeDir {
- return 0, errors.New("is a directory")
- }
- f.rng = nil
- switch whence {
- case io.SeekCurrent:
- f.offset += offset
- case io.SeekEnd:
- f.offset = f.size - offset
- case io.SeekStart:
- f.offset = offset
- }
- if f.offset < 0 {
- f.offset = 0
- return f.offset, errors.New("seek before start")
- }
- if f.offset > f.size {
- f.offset = f.size
- return f.offset, io.EOF
- }
- return f.offset, nil
- }
- func (f *fakeFile) Write(p []byte) (int, error) {
- f.mut.Lock()
- offs := f.offset
- f.mut.Unlock()
- return f.WriteAt(p, offs)
- }
- func (f *fakeFile) WriteAt(p []byte, off int64) (int, error) {
- f.mut.Lock()
- defer f.mut.Unlock()
- if f.entryType == fakeEntryTypeDir {
- return 0, errors.New("is a directory")
- }
- if f.content != nil {
- if len(f.content) < int(off)+len(p) {
- newc := make([]byte, int(off)+len(p))
- copy(newc, f.content)
- f.content = newc
- }
- copy(f.content[int(off):], p)
- }
- f.rng = nil
- f.offset = off + int64(len(p))
- if f.offset > f.size {
- f.size = f.offset
- }
- return len(p), nil
- }
- func (f *fakeFile) Name() string {
- if f.presentedName != "" {
- return f.presentedName
- }
- f.mut.Lock()
- defer f.mut.Unlock()
- return f.name
- }
- func (f *fakeFile) Truncate(size int64) error {
- f.mut.Lock()
- defer f.mut.Unlock()
- if f.content != nil {
- if int64(cap(f.content)) < size {
- c := make([]byte, size)
- copy(c[:len(f.content)], f.content)
- f.content = c
- } else {
- f.content = f.content[:int(size)]
- }
- }
- f.rng = nil
- f.size = size
- if f.offset > size {
- f.offset = size
- }
- return nil
- }
- func (f *fakeFile) Stat() (FileInfo, error) {
- f.mut.Lock()
- info := &fakeFileInfo{*f.fakeEntry}
- f.mut.Unlock()
- if f.presentedName != "" {
- info.name = f.presentedName
- }
- return info, nil
- }
- func (*fakeFile) Sync() error {
- return nil
- }
- // fakeFileInfo is the stat result.
- type fakeFileInfo struct {
- fakeEntry // intentionally a copy of the struct
- }
- func (f *fakeFileInfo) Name() string {
- return f.name
- }
- func (f *fakeFileInfo) Mode() FileMode {
- return f.mode
- }
- func (f *fakeFileInfo) Size() int64 {
- return f.size
- }
- func (f *fakeFileInfo) ModTime() time.Time {
- return f.mtime
- }
- func (f *fakeFileInfo) IsDir() bool {
- return f.entryType == fakeEntryTypeDir
- }
- func (f *fakeFileInfo) IsRegular() bool {
- return f.entryType == fakeEntryTypeFile
- }
- func (f *fakeFileInfo) IsSymlink() bool {
- return f.entryType == fakeEntryTypeSymlink
- }
- func (f *fakeFileInfo) Owner() int {
- return f.uid
- }
- func (f *fakeFileInfo) Group() int {
- return f.gid
- }
- func (*fakeFileInfo) Sys() interface{} {
- return nil
- }
- func (*fakeFileInfo) InodeChangeTime() time.Time {
- return time.Time{}
- }
|