├── vfs ├── testdata │ └── fuzz │ │ └── FuzzVFSRace │ │ ├── 07c3f73e80412f46 │ │ ├── 47a7136ed37e9f86 │ │ ├── fbd4a3219a81bf21 │ │ ├── 0ba03ef4ab18265a │ │ ├── fe2665663d88a916 │ │ ├── 2fd462465208836e │ │ ├── 602e38610cb413da │ │ ├── d6dd1d9f5c0a9eed │ │ ├── 662e0f2aed8b339d │ │ ├── 91a1910747cf76cd │ │ ├── fcbfec04a73cf845 │ │ ├── 0ba57e99b0b6a33b │ │ ├── 8f24c32945b817ea │ │ └── 66f803482d989053 ├── utils.go ├── walk_test.go ├── vfs_fuzz_test.go ├── vfsfile.go ├── vfs.go └── vfs_test.go ├── osfs ├── utils.go └── osfs.go ├── .gitignore ├── .github ├── dependabot.yml ├── workflows │ ├── lint-actions.yml │ ├── lint-go.yml │ ├── test.yml │ ├── vuln.yml │ └── codeql.yml └── actionlint-matcher.json ├── go.mod ├── inode ├── pathutils.go ├── inode.go └── inode_test.go ├── LICENSE ├── .goreleaser.yml ├── utils.go ├── .golangci.yml ├── go.sum ├── filepath.go ├── pandoras_box.go ├── absfs ├── file.go └── filesystem.go ├── ioutil └── tempfile.go ├── box.go └── README.md /vfs/testdata/fuzz/FuzzVFSRace/07c3f73e80412f46: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("&&77") 3 | []byte("0") 4 | uint64(0) 5 | uint64(96) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/47a7136ed37e9f86: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("#0cc") 3 | []byte("0") 4 | uint64(183) 5 | uint64(1) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/fbd4a3219a81bf21: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("NNNNNN") 3 | []byte("S") 4 | uint64(0) 5 | uint64(18) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/0ba03ef4ab18265a: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("97777777") 3 | []byte("0") 4 | uint64(17) 5 | uint64(5) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/fe2665663d88a916: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("C\xdc128") 3 | []byte("B7.") 4 | uint64(0) 5 | uint64(75) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/2fd462465208836e: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("d\x87µ=\xfe\x8b") 3 | []byte("d\x87") 4 | uint64(23) 5 | uint64(0) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/602e38610cb413da: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("B\xec^u\x87") 3 | []byte("B\xec^u\x87\xe2") 4 | uint64(0) 5 | uint64(0) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/d6dd1d9f5c0a9eed: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("BBBCBBBBCCCBCBCCCCCCCCCCCEB22") 3 | []byte("0") 4 | uint64(113) 5 | uint64(7) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/662e0f2aed8b339d: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("B\xec^uݟ\xd0([\xf4;\xc5LJ") 3 | []byte("B\xec^uݟ\xd0(\\\x12") 4 | uint64(0) 5 | uint64(0) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/91a1910747cf76cd: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("H\x93\xe3\x10\xcb") 3 | []byte("H\x93\xe3\x10ˏ\x8b\xa2") 4 | uint64(136) 5 | uint64(75) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/fcbfec04a73cf845: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("B\xec^uݟ\xd0(\\\x12;\xc5LJ") 3 | []byte("B\xec^uݟ\xd0(\\\x12") 4 | uint64(0) 5 | uint64(0) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/0ba57e99b0b6a33b: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("B\xec^uݟ\xd0(\\\x12;\xc5LJ\x96\r\xb0") 3 | []byte("B\xec^uݟ\xd0(\\\x12") 4 | uint64(0) 5 | uint64(0) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/8f24c32945b817ea: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("B\xec^uݟ\xd0(\\\x12;\xc5LJ\x96ss") 3 | []byte("B\xec^uݟ\xd0(\\\x12") 4 | uint64(0) 5 | uint64(0) 6 | -------------------------------------------------------------------------------- /vfs/testdata/fuzz/FuzzVFSRace/66f803482d989053: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("B\xec^uݟ\xd0(\xf5*\xd0\xdc\x1f3l\\ݟ.\x1a'\xc5LJy\r\xb0") 3 | []byte("B\xec^uݟ\xd0(\\\x12") 4 | uint64(0) 5 | uint64(0) 6 | -------------------------------------------------------------------------------- /vfs/utils.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | func IsPathSeparator(c uint8) bool { 4 | return PathSeparator == c 5 | } 6 | 7 | func SameFile(fi1, fi2 *FileInfo) bool { 8 | return fi1.node.Ino == fi2.node.Ino 9 | } 10 | -------------------------------------------------------------------------------- /osfs/utils.go: -------------------------------------------------------------------------------- 1 | package osfs 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func IsPathSeparator(c uint8) bool { 8 | return os.IsPathSeparator(c) 9 | } 10 | 11 | func SameFile(fi1, fi2 os.FileInfo) bool { 12 | return os.SameFile(fi1, fi2) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: gomod 8 | open-pull-requests-limit: 5 9 | directory: / 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/capnspacehook/pandorasbox 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/awnumar/fastrand v0.0.0-20210315215012-30ee0990fa2d 7 | github.com/awnumar/memguard v0.23.0 8 | github.com/matryer/is v1.4.1 9 | ) 10 | 11 | require ( 12 | github.com/awnumar/memcall v0.5.0 // indirect 13 | golang.org/x/crypto v0.43.0 // indirect 14 | golang.org/x/sys v0.37.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.github/workflows/lint-actions.yml: -------------------------------------------------------------------------------- 1 | name: Lint workflows 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - ".github/workflows/*" 9 | pull_request: 10 | branches: 11 | - "*" 12 | paths: 13 | - ".github/workflows/*" 14 | 15 | workflow_dispatch: {} 16 | 17 | jobs: 18 | lint-workflows: 19 | permissions: 20 | contents: read 21 | uses: capnspacehook/go-workflows/.github/workflows/lint-actions.yml@master 22 | -------------------------------------------------------------------------------- /.github/workflows/lint-go.yml: -------------------------------------------------------------------------------- 1 | name: Lint Go 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "**.go" 9 | - "go.mod" 10 | - "go.sum" 11 | - ".github/workflows/lint-go.yml" 12 | pull_request: 13 | branches: 14 | - "*" 15 | paths: 16 | - "**.go" 17 | - "go.mod" 18 | - "go.sum" 19 | - ".github/workflows/lint-go.yml" 20 | 21 | workflow_dispatch: {} 22 | 23 | jobs: 24 | lint-go: 25 | permissions: 26 | contents: read 27 | uses: capnspacehook/go-workflows/.github/workflows/lint-go.yml@master 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "**.go" 9 | - "go.mod" 10 | - "go.sum" 11 | - ".github/workflows/test.yml" 12 | pull_request: 13 | branches: 14 | - "*" 15 | paths: 16 | - "**.go" 17 | - "go.mod" 18 | - "go.sum" 19 | - ".github/workflows/test.yml" 20 | 21 | workflow_dispatch: {} 22 | 23 | jobs: 24 | test: 25 | permissions: 26 | contents: read 27 | uses: capnspacehook/go-workflows/.github/workflows/test.yml@master 28 | with: 29 | fuzz-with-race: true 30 | -------------------------------------------------------------------------------- /.github/actionlint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "actionlint", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3, 11 | "message": 4, 12 | "code": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/vuln.yml: -------------------------------------------------------------------------------- 1 | name: Vulnerability scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "**.go" 9 | - "go.mod" 10 | - "go.sum" 11 | - ".github/workflows/vuln.yml" 12 | pull_request: 13 | branches: 14 | - "*" 15 | paths: 16 | - "**.go" 17 | - "go.mod" 18 | - "go.sum" 19 | - ".github/workflows/vuln.yml" 20 | schedule: 21 | - cron: "00 13 * * 1" 22 | 23 | workflow_dispatch: {} 24 | 25 | jobs: 26 | vuln-check: 27 | permissions: 28 | contents: read 29 | uses: capnspacehook/go-workflows/.github/workflows/vuln.yml@master 30 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: Run CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "**.go" 9 | - "go.mod" 10 | - "go.sum" 11 | - ".github/workflows/codeql.yml" 12 | pull_request: 13 | branches: 14 | - "*" 15 | paths: 16 | - "**.go" 17 | - "go.mod" 18 | - "go.sum" 19 | - ".github/workflows/codeql.yml" 20 | schedule: 21 | - cron: "00 13 * * 1" 22 | 23 | workflow_dispatch: {} 24 | 25 | jobs: 26 | codeql: 27 | permissions: 28 | actions: write 29 | contents: read 30 | security-events: write 31 | uses: capnspacehook/go-workflows/.github/workflows/codeql.yml@master 32 | -------------------------------------------------------------------------------- /inode/pathutils.go: -------------------------------------------------------------------------------- 1 | package inode 2 | 3 | import ( 4 | "path" // force forward slash separators on all OSs 5 | "strings" 6 | ) 7 | 8 | // Abs returns name if name is an absolute path. If name is a relative 9 | // path then an absolute path is constructed by using cwd as the current 10 | // working directory. 11 | func Abs(cwd, name string) string { 12 | if path.IsAbs(name) { 13 | return name 14 | } 15 | 16 | return path.Join(cwd, name) 17 | } 18 | 19 | // PopPath returns the first name in `path` and the rest of the `path` string. 20 | // The path provided must use forward slashes ("/"). 21 | func PopPath(path string) (string, string) { 22 | if path == "" { 23 | return "", "" // 1 24 | } 25 | if path == "/" { 26 | return "/", "" // 2 27 | } 28 | 29 | x := strings.Index(path, "/") 30 | if x == -1 { 31 | return path, "" // 6 32 | } else if x == 0 { 33 | return "/", strings.TrimLeft(path, "/") // 3 34 | } 35 | 36 | return path[:x], path[x+1:] // 4, 5 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrew LeFevre, The AbsFs Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | use: github-native 3 | sort: asc 4 | 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | - GO111MODULE=on 9 | goos: 10 | - linux 11 | goarch: 12 | - amd64 13 | flags: 14 | - -buildmode=pie 15 | - -buildvcs=true 16 | - -trimpath 17 | mod_timestamp: '{{ .CommitTimestamp }}' 18 | ldflags: 19 | - '-s -w -X main.version={{ if eq .Tag "v0.0.0" }}devel{{ else }}{{ .Tag }}{{ end }}' 20 | 21 | archives: 22 | - id: binary-archive 23 | name_template: "{{ .ProjectName }}" 24 | format: binary 25 | - id: tar-archive 26 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 27 | format: tar.gz 28 | # hack to only add binary to archive 29 | files: 30 | - none* 31 | 32 | checksum: 33 | name_template: "checksums.txt" 34 | ids: 35 | - binary-archive 36 | - tar-archive 37 | 38 | signs: 39 | - id: checksum-signature 40 | cmd: cosign 41 | certificate: "${artifact}.crt" 42 | args: ["sign-blob", "--output-signature", "${signature}", "--output-certificate", "${certificate}", "${artifact}", "--yes"] 43 | artifacts: checksum 44 | 45 | release: 46 | ids: 47 | - checksum-signature 48 | - tar-archive 49 | prerelease: auto 50 | name_template: "{{ .Tag }}" 51 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package pandorasbox 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "strings" 7 | 8 | "github.com/capnspacehook/pandorasbox/osfs" 9 | "github.com/capnspacehook/pandorasbox/vfs" 10 | ) 11 | 12 | const VFSPrefix = "vfs://" 13 | 14 | func ConvertVFSPath(path string) (string, bool) { 15 | if IsVFSPath(path) { 16 | return convertVFSPath(path), true 17 | } 18 | 19 | return path, false 20 | } 21 | 22 | func convertVFSPath(path string) string { 23 | return strings.Replace(path, VFSPrefix, "/", 1) 24 | } 25 | 26 | func IsVFSPath(path string) bool { 27 | return strings.HasPrefix(path, VFSPrefix) 28 | } 29 | 30 | func MakeVFSPath(path string) string { 31 | if IsVFSPath(path) { 32 | return path 33 | } 34 | 35 | if len(path) > 0 && path[0] != '/' { 36 | return VFSPrefix + path 37 | } 38 | 39 | return strings.Replace(path, "/", VFSPrefix, 1) 40 | } 41 | 42 | func IsVFS(fi fs.FileInfo) bool { 43 | _, fivfs := fi.(*vfs.FileInfo) 44 | 45 | return fivfs 46 | } 47 | 48 | func SameFile(fi1, fi2 os.FileInfo) bool { 49 | vfsfi1, fi1vfs := fi1.(*vfs.FileInfo) 50 | vfsfi2, fi2vfs := fi2.(*vfs.FileInfo) 51 | 52 | switch { 53 | case fi1vfs && fi2vfs: 54 | return vfs.SameFile(vfsfi1, vfsfi2) 55 | case !fi1vfs && !fi2vfs: 56 | return osfs.SameFile(fi1, fi2) 57 | default: 58 | return false 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - asasalint 6 | - bidichk 7 | - bodyclose 8 | - copyloopvar 9 | - durationcheck 10 | - errcheck 11 | - errchkjson 12 | - errorlint 13 | - exptostd 14 | - fatcontext 15 | - gocheckcompilerdirectives 16 | - goconst 17 | - gocritic 18 | - govet 19 | - ineffassign 20 | - intrange 21 | - loggercheck 22 | - mirror 23 | - misspell 24 | - nilerr 25 | - nolintlint 26 | - nilnesserr 27 | - nilnil 28 | - paralleltest 29 | - perfsprint 30 | - prealloc 31 | - predeclared 32 | - reassign 33 | - revive 34 | - rowserrcheck 35 | - sloglint 36 | - sqlclosecheck 37 | - thelper 38 | - tparallel 39 | - unconvert 40 | - unparam 41 | - unused 42 | - usestdlibvars 43 | - wastedassign 44 | settings: 45 | gocritic: 46 | disabled-checks: 47 | - ifElseChain 48 | misspell: 49 | locale: US 50 | paralleltest: 51 | ignore-missing: true 52 | revive: 53 | rules: 54 | - name: blank-imports 55 | disabled: true 56 | exclusions: 57 | generated: lax 58 | presets: 59 | - comments 60 | - common-false-positives 61 | - legacy 62 | - std-error-handling 63 | paths: 64 | - third_party$ 65 | - builtin$ 66 | - examples$ 67 | issues: 68 | max-issues-per-linter: 0 69 | max-same-issues: 0 70 | formatters: 71 | enable: 72 | - gci 73 | - gofumpt 74 | settings: 75 | gci: 76 | sections: 77 | - standard 78 | - default 79 | - prefix(github.com/capnspacehook/pandorasbox) 80 | exclusions: 81 | generated: lax 82 | paths: 83 | - third_party$ 84 | - builtin$ 85 | - examples$ 86 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/awnumar/fastrand v0.0.0-20210315215012-30ee0990fa2d h1:NkqtWyrOjr0QK1FSCmXS6Whbwh100Qt74SaRn92PemU= 2 | github.com/awnumar/fastrand v0.0.0-20210315215012-30ee0990fa2d/go.mod h1:TO59kqNCiDBKS0qjRYUI8qJtkFL6SkP2EKqeOQ6xg/o= 3 | github.com/awnumar/memcall v0.0.0-20190811121346-2affb857f00a/go.mod h1:sbEXyqNZZ3Cebk+6zOUmFNN8OuHHlugjiUmqn2tfiiM= 4 | github.com/awnumar/memcall v0.0.0-20190816154910-db5ea08008a3/go.mod h1:CszzLMKGwNr15cNA+0SuWkZLnPXGgUw+9kxRNbwUVnE= 5 | github.com/awnumar/memcall v0.5.0 h1:31zYqzH08fM1UBzr53ywXFvqVP4grhAIFFd1Pfd7Gtk= 6 | github.com/awnumar/memcall v0.5.0/go.mod h1:5q5zKsL4XfYgqzCQEvUt9Dou4fEXWsn+tNrm1z1oYgQ= 7 | github.com/awnumar/memguard v0.19.1/go.mod h1:tewJ+MrJ12cFtR5gH5zNJs8A6BjBv8709binaV+1pws= 8 | github.com/awnumar/memguard v0.23.0 h1:sJ3a1/SWlcuKIQ7MV+R9p0Pvo9CWsMbGZvcZQtmc68A= 9 | github.com/awnumar/memguard v0.23.0/go.mod h1:olVofBrsPdITtJ2HgxQKrEYEMyIBAIciVG4wNnZhW9M= 10 | github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= 11 | github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 14 | golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= 15 | golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 16 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 18 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 20 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 21 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 22 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 23 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 24 | -------------------------------------------------------------------------------- /filepath.go: -------------------------------------------------------------------------------- 1 | package pandorasbox 2 | 3 | import ( 4 | stdpath "path" 5 | "path/filepath" 6 | 7 | "github.com/capnspacehook/pandorasbox/absfs" 8 | ) 9 | 10 | type File = absfs.File 11 | 12 | func IsAbs(path string) bool { 13 | if _, ok := ConvertVFSPath(path); ok { 14 | return stdpath.IsAbs(path) 15 | } 16 | 17 | return filepath.IsAbs(path) 18 | } 19 | 20 | func Clean(path string) string { 21 | if vfsPath, ok := ConvertVFSPath(path); ok { 22 | path = vfsPath 23 | return MakeVFSPath(stdpath.Clean(path)) 24 | } 25 | 26 | return filepath.Clean(path) 27 | } 28 | 29 | func ToSlash(path string) string { 30 | if vfsPath, ok := ConvertVFSPath(path); ok { 31 | path = vfsPath 32 | return MakeVFSPath(filepath.ToSlash(path)) 33 | } 34 | 35 | return filepath.ToSlash(path) 36 | } 37 | 38 | func FromSlash(path string) string { 39 | if vfsPath, ok := ConvertVFSPath(path); ok { 40 | path = vfsPath 41 | return MakeVFSPath(filepath.FromSlash(path)) 42 | } 43 | 44 | return filepath.FromSlash(path) 45 | } 46 | 47 | func Split(path string) (string, string) { 48 | if vfsPath, ok := ConvertVFSPath(path); ok { 49 | path = vfsPath 50 | dir, file := stdpath.Split(path) 51 | dir = MakeVFSPath(dir) 52 | return dir, file 53 | } 54 | 55 | return filepath.Split(path) 56 | } 57 | 58 | func Join(elem ...string) string { 59 | var isVFS bool 60 | for i := range elem { 61 | vfsPath, ok := ConvertVFSPath(elem[i]) 62 | if ok { 63 | elem[i] = vfsPath 64 | } 65 | 66 | if i == 0 { 67 | isVFS = ok 68 | } 69 | } 70 | 71 | if isVFS { 72 | return MakeVFSPath(stdpath.Join(elem...)) 73 | } 74 | 75 | return filepath.Join(elem...) 76 | } 77 | 78 | func Ext(path string) string { 79 | if vfsPath, ok := ConvertVFSPath(path); ok { 80 | path = vfsPath 81 | return MakeVFSPath(stdpath.Ext(path)) 82 | } 83 | 84 | return filepath.Ext(path) 85 | } 86 | 87 | func Base(path string) string { 88 | if vfsPath, ok := ConvertVFSPath(path); ok { 89 | path = vfsPath 90 | return MakeVFSPath(stdpath.Base(path)) 91 | } 92 | 93 | return filepath.Base(path) 94 | } 95 | 96 | func Dir(path string) string { 97 | if vfsPath, ok := ConvertVFSPath(path); ok { 98 | path = vfsPath 99 | return MakeVFSPath(stdpath.Dir(path)) 100 | } 101 | 102 | return filepath.Dir(path) 103 | } 104 | -------------------------------------------------------------------------------- /pandoras_box.go: -------------------------------------------------------------------------------- 1 | package pandorasbox 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | 7 | "github.com/capnspacehook/pandorasbox/absfs" 8 | ) 9 | 10 | var box *Box 11 | 12 | func init() { 13 | box = NewBox() 14 | } 15 | 16 | func GlobalOSFS() absfs.FileSystem { 17 | return box.osfs 18 | } 19 | 20 | func GlobalVFS() absfs.FileSystem { 21 | return box.vfs 22 | } 23 | 24 | func Open(name string) (absfs.File, error) { 25 | return box.Open(name) 26 | } 27 | 28 | func OpenFile(name string, flag int, perm fs.FileMode) (absfs.File, error) { 29 | return box.OpenFile(name, flag, perm) 30 | } 31 | 32 | func Create(name string) (absfs.File, error) { 33 | return box.Create(name) 34 | } 35 | 36 | func ReadFile(filename string) ([]byte, error) { 37 | return box.ReadFile(filename) 38 | } 39 | 40 | func ReadDir(dirname string) ([]os.DirEntry, error) { 41 | return box.ReadDir(dirname) 42 | } 43 | 44 | func WriteFile(filename string, data []byte, perm fs.FileMode) error { 45 | return box.WriteFile(filename, data, perm) 46 | } 47 | 48 | func Mkdir(name string, perm fs.FileMode) error { 49 | return box.Mkdir(name, perm) 50 | } 51 | 52 | func MkdirAll(name string, perm fs.FileMode) error { 53 | return box.MkdirAll(name, perm) 54 | } 55 | 56 | func Stat(name string) (fs.FileInfo, error) { 57 | return box.Stat(name) 58 | } 59 | 60 | func Lstat(name string) (fs.FileInfo, error) { 61 | return box.Lstat(name) 62 | } 63 | 64 | func Rename(oldpath, newpath string) error { 65 | return box.Rename(oldpath, newpath) 66 | } 67 | 68 | func Remove(name string) error { 69 | return box.Remove(name) 70 | } 71 | 72 | func RemoveAll(path string) error { 73 | return box.RemoveAll(path) 74 | } 75 | 76 | func Truncate(name string, size int64) error { 77 | return box.Truncate(name, size) 78 | } 79 | 80 | func WalkDir(root string, fn fs.WalkDirFunc) error { 81 | return box.WalkDir(root, fn) 82 | } 83 | 84 | func Abs(path string) (string, error) { 85 | return box.Abs(path) 86 | } 87 | 88 | func Separator(vfs bool) uint8 { 89 | return box.Separator(vfs) 90 | } 91 | 92 | func ListSeparator(vfs bool) uint8 { 93 | return box.ListSeparator(vfs) 94 | } 95 | 96 | func IsPathSeparator(c uint8, vfs bool) bool { 97 | return box.IsPathSeparator(c, vfs) 98 | } 99 | 100 | func Chdir(dir string, vfs bool) error { 101 | return box.Chdir(dir, vfs) 102 | } 103 | 104 | func Getwd(vfs bool) (string, error) { 105 | return box.Getwd(vfs) 106 | } 107 | 108 | func GetTempDir(vfs bool) string { 109 | return box.GetTempDir(vfs) 110 | } 111 | -------------------------------------------------------------------------------- /osfs/osfs.go: -------------------------------------------------------------------------------- 1 | package osfs 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/capnspacehook/pandorasbox/absfs" 9 | ) 10 | 11 | type stdFS struct { 12 | pbFS 13 | } 14 | 15 | func (stdFS) Open(name string) (fs.File, error) { 16 | return os.Open(name) 17 | } 18 | 19 | func (stdFS) Sub(dir string) (fs.FS, error) { 20 | return os.DirFS(dir), nil 21 | } 22 | 23 | type pbFS struct{} 24 | 25 | func NewFS() absfs.FileSystem { 26 | return pbFS{} 27 | } 28 | 29 | func (pbFS) FS() fs.FS { 30 | return stdFS{} 31 | } 32 | 33 | func (pbFS) Open(name string) (absfs.File, error) { 34 | f, err := os.Open(name) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return f, nil 40 | } 41 | 42 | func (pbFS) OpenFile(name string, flag int, perm fs.FileMode) (absfs.File, error) { 43 | f, err := os.OpenFile(name, flag, perm) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return f, err 49 | } 50 | 51 | func (pbFS) Create(name string) (absfs.File, error) { 52 | f, err := os.Create(name) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return f, nil 58 | } 59 | 60 | func (pbFS) ReadFile(name string) ([]byte, error) { 61 | return os.ReadFile(name) 62 | } 63 | 64 | func (pbFS) ReadDir(name string) ([]fs.DirEntry, error) { 65 | return os.ReadDir(name) 66 | } 67 | 68 | func (pbFS) WriteFile(name string, data []byte, perm fs.FileMode) error { 69 | return os.WriteFile(name, data, perm) 70 | } 71 | 72 | func (pbFS) Mkdir(name string, perm fs.FileMode) error { 73 | return os.Mkdir(name, perm) 74 | } 75 | 76 | func (pbFS) MkdirAll(name string, perm fs.FileMode) error { 77 | return os.MkdirAll(name, perm) 78 | } 79 | 80 | func (pbFS) Stat(name string) (fs.FileInfo, error) { 81 | return os.Stat(name) 82 | } 83 | 84 | func (pbFS) Lstat(name string) (fs.FileInfo, error) { 85 | return os.Lstat(name) 86 | } 87 | 88 | func (pbFS) Rename(oldpath, newpath string) error { 89 | return os.Rename(oldpath, newpath) 90 | } 91 | 92 | func (pbFS) Remove(name string) error { 93 | return os.Remove(name) 94 | } 95 | 96 | func (pbFS) RemoveAll(name string) error { 97 | return os.RemoveAll(name) 98 | } 99 | 100 | func (pbFS) Truncate(name string, size int64) error { 101 | return os.Truncate(name, size) 102 | } 103 | 104 | func (pbFS) WalkDir(root string, fn fs.WalkDirFunc) error { 105 | return filepath.WalkDir(root, fn) 106 | } 107 | 108 | func (pbFS) Abs(path string) (string, error) { 109 | return filepath.Abs(path) 110 | } 111 | 112 | func (pbFS) Separator() uint8 { 113 | return filepath.Separator 114 | } 115 | 116 | func (pbFS) ListSeparator() uint8 { 117 | return filepath.ListSeparator 118 | } 119 | 120 | func (pbFS) Chdir(name string) error { 121 | return os.Chdir(name) 122 | } 123 | 124 | func (pbFS) Getwd() (dir string, err error) { 125 | return os.Getwd() 126 | } 127 | 128 | func (pbFS) TempDir() string { 129 | return os.TempDir() 130 | } 131 | -------------------------------------------------------------------------------- /vfs/walk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package vfs 6 | 7 | import ( 8 | "io/fs" 9 | "os" 10 | pathpkg "path" 11 | "testing" 12 | ) 13 | 14 | type Node struct { 15 | name string 16 | entries []*Node // nil if the entry is a file 17 | mark int 18 | } 19 | 20 | var tree = &Node{ 21 | "testdata", 22 | []*Node{ 23 | {"a", nil, 0}, 24 | {"b", []*Node{}, 0}, 25 | {"c", nil, 0}, 26 | { 27 | "d", 28 | []*Node{ 29 | {"x", nil, 0}, 30 | {"y", []*Node{}, 0}, 31 | { 32 | "z", 33 | []*Node{ 34 | {"u", nil, 0}, 35 | {"v", nil, 0}, 36 | }, 37 | 0, 38 | }, 39 | }, 40 | 0, 41 | }, 42 | }, 43 | 0, 44 | } 45 | 46 | func walkTree(n *Node, path string, f func(path string, n *Node)) { 47 | f(path, n) 48 | for _, e := range n.entries { 49 | walkTree(e, pathpkg.Join(path, e.name), f) 50 | } 51 | } 52 | 53 | func makeTree(t *testing.T) fs.FS { 54 | t.Helper() 55 | 56 | fsys := NewFS() 57 | walkTree(tree, tree.name, func(path string, n *Node) { 58 | if n.entries == nil { 59 | f, err := fsys.Create(path) 60 | if err != nil { 61 | t.Fatalf("error creating file %s: %v", path, err) 62 | } 63 | if err := f.Close(); err != nil { 64 | t.Fatalf("error closing file %s: %v", path, err) 65 | } 66 | } else { 67 | if err := fsys.Mkdir(path, 0o755); err != nil { 68 | t.Fatalf("error creating dir %s: %v", path, err) 69 | } 70 | } 71 | }) 72 | return fsys.FS() 73 | } 74 | 75 | func checkMarks(t *testing.T, report bool) { 76 | t.Helper() 77 | 78 | walkTree(tree, tree.name, func(path string, n *Node) { 79 | if n.mark != 1 && report { 80 | t.Errorf("node %s mark = %d; expected 1", path, n.mark) 81 | } 82 | n.mark = 0 83 | }) 84 | } 85 | 86 | // Assumes that each node name is unique. Good enough for a test. 87 | // If err if not nil, it is appended to errors. 88 | func mark(entry fs.DirEntry, err error, errors *[]error) error { 89 | name := entry.Name() 90 | walkTree(tree, tree.name, func(path string, n *Node) { 91 | if n.name == name { 92 | n.mark++ 93 | } 94 | }) 95 | if err != nil { 96 | *errors = append(*errors, err) 97 | return nil 98 | } 99 | return nil 100 | } 101 | 102 | func TestWalkDir(t *testing.T) { 103 | tmpDir := t.TempDir() 104 | 105 | origDir, err := os.Getwd() 106 | if err != nil { 107 | t.Fatal("finding working dir:", err) 108 | } 109 | if err = os.Chdir(tmpDir); err != nil { 110 | t.Fatal("entering temp dir:", err) 111 | } 112 | defer func() { 113 | err := os.Chdir(origDir) 114 | if err != nil { 115 | t.Error(err) 116 | } 117 | }() 118 | 119 | fsys := makeTree(t) 120 | errors := make([]error, 0, 10) 121 | markFn := func(path string, entry fs.DirEntry, err error) error { 122 | return mark(entry, err, &errors) 123 | } 124 | // Expect no errors. 125 | err = fs.WalkDir(fsys, ".", markFn) 126 | if err != nil { 127 | t.Fatalf("no error expected, found: %s", err) 128 | } 129 | if len(errors) != 0 { 130 | t.Fatalf("unexpected errors: %s", errors) 131 | } 132 | checkMarks(t, true) 133 | } 134 | -------------------------------------------------------------------------------- /absfs/file.go: -------------------------------------------------------------------------------- 1 | package absfs 2 | 3 | import ( 4 | "io/fs" 5 | ) 6 | 7 | type File interface { 8 | // Name returns the name of the file as presented to Open. 9 | Name() string 10 | 11 | // Read reads up to len(b) bytes from the File. It returns the number of bytes 12 | // read and any error encountered. At end of file, Read returns 0, io.EOF. 13 | Read(p []byte) (int, error) 14 | 15 | // ReadAt reads len(b) bytes from the File starting at byte offset off. It 16 | // returns the number of bytes read and the error, if any. ReadAt always 17 | // returns a non-nil error when n < len(b). At end of file, that error is 18 | // io.EOF. 19 | ReadAt(b []byte, off int64) (n int, err error) 20 | 21 | // Readdir reads the contents of the directory associated with file and 22 | // returns a slice of up to n FileInfo values, as would be returned by Lstat, 23 | // in directory order. Subsequent calls on the same file will yield further 24 | // FileInfos. 25 | 26 | // If n > 0, Readdir returns at most n FileInfo structures. In this case, if 27 | // Readdir returns an empty slice, it will return a non-nil error explaining 28 | // why. At the end of a directory, the error is io.EOF. 29 | 30 | // If n <= 0, Readdir returns all the FileInfo from the directory in a single 31 | // slice. In this case, if Readdir succeeds (reads all the way to the end of 32 | // the directory), it returns the slice and a nil error. If it encounters an 33 | // error before the end of the directory, Readdir returns the FileInfo read 34 | // until that point and a non-nil error. 35 | ReadDir(int) ([]fs.DirEntry, error) 36 | 37 | // Write writes len(b) bytes to the File. It returns the number of bytes 38 | // written and an error, if any. Write returns a non-nil error when 39 | // n != len(b). 40 | Write(p []byte) (int, error) 41 | 42 | // WriteAt writes len(b) bytes to the File starting at byte offset off. It 43 | // returns the number of bytes written and an error, if any. WriteAt returns 44 | // a non-nil error when n != len(b). 45 | WriteAt(b []byte, off int64) (n int, err error) 46 | 47 | // WriteString is like Write, but writes the contents of string s rather than 48 | // a slice of bytes. 49 | WriteString(s string) (n int, err error) 50 | 51 | // Stat returns the FileInfo structure describing file. If there is an error, 52 | // it will be of type *PathError. 53 | Stat() (fs.FileInfo, error) 54 | 55 | // Seek sets the offset for the next Read or Write on file to offset, 56 | // interpreted according to whence: 0 means relative to the origin of the 57 | // file, 1 means relative to the current offset, and 2 means relative to the 58 | // end. It returns the new offset and an error, if any. The behavior of Seek 59 | // on a file opened with O_APPEND is not specified. 60 | Seek(offset int64, whence int) (ret int64, err error) 61 | 62 | // Sync commits the current contents of the file to stable storage. Typically, 63 | // this means flushing the file system's in-memory copy of recently written 64 | // data to disk. 65 | Sync() error 66 | 67 | // Truncate changes the size of the file. It does not change the I/O offset. 68 | // If there is an error, it will be of type *fs.PathError. 69 | Truncate(size int64) error 70 | 71 | // Close closes the File, rendering it unusable for I/O. It returns an error, 72 | // if any. 73 | Close() error 74 | } 75 | -------------------------------------------------------------------------------- /ioutil/tempfile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ioutil 6 | 7 | import ( 8 | "errors" 9 | stdfs "io/fs" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "sync" 14 | "time" 15 | 16 | "github.com/capnspacehook/pandorasbox/absfs" 17 | ) 18 | 19 | // Random number state. 20 | // We generate random temporary file names so that there's a good 21 | // chance the file doesn't exist yet - keeps the number of tries in 22 | // TempFile to a minimum. 23 | var ( 24 | rand uint32 25 | randmu sync.Mutex 26 | ) 27 | 28 | func reseed() uint32 { 29 | return uint32(time.Now().UnixNano() + int64(os.Getpid())) 30 | } 31 | 32 | func nextSuffix() string { 33 | randmu.Lock() 34 | r := rand 35 | if r == 0 { 36 | r = reseed() 37 | } 38 | r = r*1664525 + 1013904223 // constants from Numerical Recipes 39 | rand = r 40 | randmu.Unlock() 41 | return strconv.Itoa(int(1e9 + r%1e9))[1:] 42 | } 43 | 44 | // TempFile creates a new temporary file in the directory dir of the 45 | // absfs.FileSystem fs with a name beginning with prefix, opens the file for 46 | // reading and writing, and returns the resulting absfs.File. 47 | // If dir is the empty string, TempFile uses the default directory 48 | // for temporary files for the given FileSystem (see absfs.TempDir). 49 | // Multiple programs calling TempFile simultaneously 50 | // will not choose the same file. The caller can use f.Name() 51 | // to find the pathname of the file. It is the caller's responsibility 52 | // to remove the file when no longer needed. 53 | func TempFile(fs absfs.FileSystem, dir, prefix string) (f absfs.File, err error) { 54 | if dir == "" || dir == fs.TempDir() { 55 | dir = fs.TempDir() 56 | if _, err := fs.Stat(dir); errors.Is(err, stdfs.ErrNotExist) { 57 | err = fs.Mkdir(dir, 0o755) 58 | if err != nil { 59 | return nil, err 60 | } 61 | } 62 | } 63 | 64 | nconflict := 0 65 | for range 10000 { 66 | name := filepath.Join(dir, prefix+nextSuffix()) 67 | f, err = fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600) 68 | if errors.Is(err, stdfs.ErrExist) { 69 | if nconflict++; nconflict > 10 { 70 | randmu.Lock() 71 | rand = reseed() 72 | randmu.Unlock() 73 | } 74 | continue 75 | } 76 | break 77 | } 78 | return 79 | } 80 | 81 | // TempDir creates a new temporary directory in the directory dir of the 82 | // absfs.FileSystem fs with a name beginning with prefix and returns the 83 | // path of the new directory. If dir is the empty string, TempDir uses the 84 | // default directory for temporary files (see os.TempDir). 85 | // Multiple programs calling TempDir simultaneously 86 | // will not choose the same directory. It is the caller's responsibility 87 | // to remove the directory when no longer needed. 88 | func TempDir(fs absfs.FileSystem, dir, prefix string) (name string, err error) { 89 | if dir == "" || dir == fs.TempDir() { 90 | dir = fs.TempDir() 91 | if _, err := fs.Stat(dir); errors.Is(err, stdfs.ErrNotExist) { 92 | err = fs.Mkdir(dir, 0o700) 93 | if err != nil { 94 | return "", err 95 | } 96 | } 97 | } 98 | 99 | nconflict := 0 100 | for range 10000 { 101 | try := filepath.Join(dir, prefix+nextSuffix()) 102 | err = fs.Mkdir(try, 0o700) 103 | if errors.Is(err, stdfs.ErrExist) { 104 | if nconflict++; nconflict > 10 { 105 | randmu.Lock() 106 | rand = reseed() 107 | randmu.Unlock() 108 | } 109 | continue 110 | } 111 | if errors.Is(err, stdfs.ErrNotExist) { 112 | if _, err := fs.Stat(dir); errors.Is(err, stdfs.ErrNotExist) { 113 | return "", err 114 | } 115 | } 116 | if err == nil { 117 | name = try 118 | } 119 | break 120 | } 121 | return 122 | } 123 | -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | package pandorasbox 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "os" 7 | 8 | "github.com/capnspacehook/pandorasbox/absfs" 9 | "github.com/capnspacehook/pandorasbox/osfs" 10 | "github.com/capnspacehook/pandorasbox/vfs" 11 | ) 12 | 13 | type Box struct { 14 | osfs absfs.FileSystem 15 | vfs absfs.FileSystem 16 | } 17 | 18 | func NewBox() *Box { 19 | box := new(Box) 20 | box.osfs = osfs.NewFS() 21 | box.vfs = vfs.NewFS() 22 | 23 | return box 24 | } 25 | 26 | func (b *Box) OSFS() absfs.FileSystem { 27 | return b.osfs 28 | } 29 | 30 | func (b *Box) VFS() absfs.FileSystem { 31 | return b.vfs 32 | } 33 | 34 | func (b *Box) Open(name string) (absfs.File, error) { 35 | if vfsName, ok := ConvertVFSPath(name); ok { 36 | return b.vfs.Open(vfsName) 37 | } 38 | 39 | return b.osfs.Open(name) 40 | } 41 | 42 | func (b *Box) OpenFile(name string, flag int, perm fs.FileMode) (absfs.File, error) { 43 | if vfsName, ok := ConvertVFSPath(name); ok { 44 | return b.vfs.OpenFile(vfsName, flag, perm) 45 | } 46 | 47 | return b.osfs.OpenFile(name, flag, perm) 48 | } 49 | 50 | func (b *Box) Create(name string) (absfs.File, error) { 51 | if vfsName, ok := ConvertVFSPath(name); ok { 52 | return b.vfs.Create(vfsName) 53 | } 54 | 55 | return b.osfs.Create(name) 56 | } 57 | 58 | func (b *Box) ReadFile(filename string) ([]byte, error) { 59 | if vfsFilename, ok := ConvertVFSPath(filename); ok { 60 | return b.vfs.ReadFile(vfsFilename) 61 | } 62 | 63 | return b.osfs.ReadFile(filename) 64 | } 65 | 66 | func (b *Box) ReadDir(dirname string) ([]fs.DirEntry, error) { 67 | if vfsDirname, ok := ConvertVFSPath(dirname); ok { 68 | return b.vfs.ReadDir(vfsDirname) 69 | } 70 | 71 | return b.osfs.ReadDir(dirname) 72 | } 73 | 74 | func (b *Box) WriteFile(filename string, data []byte, perm fs.FileMode) error { 75 | if vfsFilename, ok := ConvertVFSPath(filename); ok { 76 | return b.vfs.WriteFile(vfsFilename, data, perm) 77 | } 78 | 79 | return os.WriteFile(filename, data, perm) 80 | } 81 | 82 | func (b *Box) Mkdir(name string, perm fs.FileMode) error { 83 | if vfsName, ok := ConvertVFSPath(name); ok { 84 | return b.vfs.Mkdir(vfsName, perm) 85 | } 86 | 87 | return b.osfs.Mkdir(name, perm) 88 | } 89 | 90 | func (b *Box) MkdirAll(name string, perm fs.FileMode) error { 91 | if vfsName, ok := ConvertVFSPath(name); ok { 92 | return b.vfs.MkdirAll(vfsName, perm) 93 | } 94 | 95 | return b.osfs.MkdirAll(name, perm) 96 | } 97 | 98 | func (b *Box) Stat(name string) (fs.FileInfo, error) { 99 | if vfsName, ok := ConvertVFSPath(name); ok { 100 | return b.vfs.Stat(vfsName) 101 | } 102 | 103 | return b.osfs.Stat(name) 104 | } 105 | 106 | func (b *Box) Lstat(name string) (fs.FileInfo, error) { 107 | if vfsName, ok := ConvertVFSPath(name); ok { 108 | return b.vfs.Lstat(vfsName) 109 | } 110 | 111 | return b.osfs.Lstat(name) 112 | } 113 | 114 | func (b *Box) Rename(oldpath, newpath string) error { 115 | vfsOldPath, oldPathVFS := ConvertVFSPath(oldpath) 116 | vfsNewPath, newPathVFS := ConvertVFSPath(newpath) 117 | if oldPathVFS && newPathVFS { 118 | return b.vfs.Rename(vfsOldPath, vfsNewPath) 119 | } else if (oldPathVFS && !newPathVFS) || (!oldPathVFS && newPathVFS) { 120 | return &os.LinkError{Op: "rename", Old: oldpath, New: newpath, Err: errors.New("oldpath and newpath must both either be a VFS path, or normal path")} 121 | } 122 | 123 | return b.osfs.Rename(oldpath, newpath) 124 | } 125 | 126 | func (b *Box) Remove(name string) error { 127 | if vfsName, ok := ConvertVFSPath(name); ok { 128 | return b.vfs.Remove(vfsName) 129 | } 130 | 131 | return b.osfs.Remove(name) 132 | } 133 | 134 | func (b *Box) RemoveAll(path string) error { 135 | if vfsPath, ok := ConvertVFSPath(path); ok { 136 | return b.vfs.RemoveAll(vfsPath) 137 | } 138 | 139 | return b.osfs.RemoveAll(path) 140 | } 141 | 142 | func (b *Box) Truncate(name string, size int64) error { 143 | if vfsName, ok := ConvertVFSPath(name); ok { 144 | return b.vfs.Truncate(vfsName, size) 145 | } 146 | 147 | return b.osfs.Truncate(name, size) 148 | } 149 | 150 | func (b *Box) WalkDir(root string, fn fs.WalkDirFunc) error { 151 | if vfsName, ok := ConvertVFSPath(root); ok { 152 | return b.vfs.WalkDir(vfsName, fn) 153 | } 154 | 155 | return b.osfs.WalkDir(root, fn) 156 | } 157 | 158 | func (b *Box) Abs(path string) (string, error) { 159 | if vfsPath, ok := ConvertVFSPath(path); ok { 160 | absPath, err := b.vfs.Abs(vfsPath) 161 | if err != nil { 162 | return "", err 163 | } 164 | 165 | return MakeVFSPath(absPath), nil 166 | } 167 | 168 | return b.osfs.Abs(path) 169 | } 170 | 171 | func (b *Box) Separator(vfsMode bool) uint8 { 172 | if vfsMode { 173 | return b.vfs.Separator() 174 | } 175 | 176 | return b.osfs.Separator() 177 | } 178 | 179 | func (b *Box) ListSeparator(vfsMode bool) uint8 { 180 | if vfsMode { 181 | return b.vfs.ListSeparator() 182 | } 183 | 184 | return b.osfs.ListSeparator() 185 | } 186 | 187 | func (b *Box) IsPathSeparator(c uint8, vfsMode bool) bool { 188 | if vfsMode { 189 | return vfs.IsPathSeparator(c) 190 | } 191 | 192 | return osfs.IsPathSeparator(c) 193 | } 194 | 195 | func (b *Box) Chdir(dir string, vfsMode bool) error { 196 | if vfsMode { 197 | return b.vfs.Chdir(dir) 198 | } 199 | 200 | return b.osfs.Chdir(dir) 201 | } 202 | 203 | func (b *Box) Getwd(vfsMode bool) (string, error) { 204 | if vfsMode { 205 | return b.vfs.Getwd() 206 | } 207 | 208 | return b.osfs.Getwd() 209 | } 210 | 211 | func (b *Box) GetTempDir(vfsMode bool) string { 212 | if vfsMode { 213 | return b.vfs.TempDir() 214 | } 215 | 216 | return b.osfs.TempDir() 217 | } 218 | -------------------------------------------------------------------------------- /absfs/filesystem.go: -------------------------------------------------------------------------------- 1 | package absfs 2 | 3 | import ( 4 | "io/fs" 5 | ) 6 | 7 | type FileSystem interface { 8 | FS() fs.FS 9 | 10 | // Open opens the named file for reading. If successful, methods on the returned 11 | // file can be used for reading; the associated file descriptor has mode O_RDONLY. 12 | // If there is an error, it will be of type *fs.PathError. 13 | Open(name string) (File, error) 14 | 15 | // OpenFile is the generalized open call; most users will use Open or 16 | // Create instead. It opens the named file with specified flag (os.O_RDONLY etc.). 17 | // If the file does not exist, and the os.O_CREATE flag is passed, it is created 18 | // with mode perm (before umask). If successful, methods on the returned File 19 | // can be used for I/O. If there is an error, it will be of type *fs.PathError. 20 | OpenFile(name string, flag int, perm fs.FileMode) (File, error) 21 | 22 | // Create creates or truncates the named file. If the file already exists, 23 | // it is truncated. If the file does not exist, it is created with mode 0666 24 | // (before umask). If successful, methods on the returned File can be used for 25 | // I/O; the associated file descriptor has mode os.O_RDWR. If there is an error, 26 | // it will be of type *fs.PathError. 27 | Create(name string) (File, error) 28 | 29 | // ReadFile reads the named file and returns its contents. 30 | // A successful call returns a nil error, not io.EOF. 31 | // (Because ReadFile reads the whole file, the expected EOF 32 | // from the final Read is not treated as an error to be reported.) 33 | ReadFile(name string) ([]byte, error) 34 | 35 | // ReadDir reads the named directory 36 | // and returns a list of directory entries sorted by filename. 37 | ReadDir(name string) ([]fs.DirEntry, error) 38 | 39 | // WriteFile writes data to the named file, creating it if necessary. If the 40 | // file does not exist, WriteFile creates it with permissions perm (before umask); 41 | // otherwise WriteFile truncates it before writing, without changing permissions. 42 | WriteFile(name string, data []byte, perm fs.FileMode) error 43 | 44 | // Mkdir creates a new directory with the specified name and permission bits 45 | // (before umask). If there is an error, it will be of type *fs.PathError. 46 | Mkdir(name string, perm fs.FileMode) error 47 | 48 | // MkdirAll creates a directory named path, along with any necessary parents, 49 | // and returns nil, or else returns an error. The permission bits perm (before umask) 50 | // are used for all directories that MkdirAll creates. If path is already a 51 | // directory, MkdirAll does nothing and returns nil. 52 | MkdirAll(name string, perm fs.FileMode) error 53 | 54 | // Stat returns the FileInfo structure describing file. If there is an error, 55 | // it will be of type *fs.PathError. 56 | Stat(name string) (fs.FileInfo, error) 57 | 58 | // Lstat returns a FileInfo describing the named file. 59 | // If the file is a symbolic link, the returned FileInfo 60 | // describes the symbolic link. Lstat makes no attempt to follow the link. 61 | // If there is an error, it will be of type *fs.PathError. 62 | Lstat(name string) (fs.FileInfo, error) 63 | 64 | // Rename renames (moves) oldpath to newpath. If newpath already exists and 65 | // is not a directory, Rename replaces it. OS-specific restrictions may apply 66 | // when oldpath and newpath are in different directories. If there is an 67 | // error, it will be of type *os.LinkError. 68 | Rename(oldpath, newpath string) error 69 | 70 | // Remove removes the named file or (empty) directory. If there is an error, 71 | // it will be of type *fs.PathError. 72 | Remove(name string) error 73 | 74 | // RemoveAll removes path and any children it contains. It removes everything 75 | // it can but returns the first error it encounters. If the path does not exist, 76 | // RemoveAll returns nil (no error). If there is an error, it will be of type 77 | // *fs.PathError. 78 | RemoveAll(path string) error 79 | 80 | // Truncate changes the size of the named file. If the file is a symbolic link, 81 | // it changes the size of the link's target. If there is an error, it will be 82 | // of type *fs.PathError. 83 | Truncate(name string, size int64) error 84 | 85 | // WalkDir walks the file tree rooted at root, calling fn for each file or directory 86 | // in the tree, including root. All errors that arise visiting files and directories 87 | // are filtered by fn: see the fs.WalkDirFunc documentation for details. The files may 88 | // or may not be walked in lexical order. 89 | WalkDir(root string, fn fs.WalkDirFunc) error 90 | 91 | // Abs returns an absolute representation of path. 92 | // If the path is not absolute it will be joined with the current 93 | // working directory to turn it into an absolute path. The absolute 94 | // path name for a given file is not guaranteed to be unique. 95 | // Abs calls pandorasbox.Clean on the result. 96 | Abs(path string) (string, error) 97 | 98 | Separator() uint8 99 | ListSeparator() uint8 100 | 101 | // Chdir changes the current working directory to the named directory. 102 | // If there is an error, it will be of type *fs.PathError. 103 | Chdir(dir string) error 104 | 105 | // Getwd returns a rooted path name corresponding to the 106 | // current directory. If the current directory can be 107 | // reached via multiple paths (due to symbolic links), 108 | // Getwd may return any one of them. 109 | Getwd() (dir string, err error) 110 | 111 | // TempDir returns the default directory to use for temporary files. 112 | // The directory is neither guaranteed to exist nor have accessible 113 | // permissions. 114 | TempDir() string 115 | 116 | // TODO: add all *Temp functions 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pandoras Box 2 | 3 | [![GoDoc](https://godoc.org/github.com/capnspacehook/pandorasbox?status.svg)](https://godoc.org/github.com/capnspacehook/pandorasbox) 4 | 5 | `pandorasbox` is a Go package that allows for simple use of both a host's filesystem, and a virtual filesystem. 6 | 7 | The design goal of Pandora's Box is to easily facilitate the use of a transparently-encrypted VFS (virtual filesystem) and the host's filesystem. It does this by providing functions and methods that operate and look the same as the Go standard library `os` package. If you want to interact with the VFS, pass in a path that starts with `vfs://`, and Pandora's Box will automatically use the VFS. Otherwise, the host's filesystem will be used. 8 | 9 | ## Using Pandora's Box 10 | 11 | Because Pandora's Box has the same interface as the `os` package, giving your code access to a VFS is often as easy as importing `pandorasbox` and replacing `os` calls to `box` calls. Take this super simple function that copies files: 12 | 13 | ```go 14 | import "os" 15 | 16 | func CopyFile(srcFile, dstFile string) error { 17 | out, err := os.Create(dstFile) 18 | defer out.Close() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | in, err := os.Open(srcFile) 24 | defer in.Close() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | _, err = io.Copy(out, in) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return nil 35 | } 36 | ``` 37 | 38 | All it takes to make this function VFS-friendly is switching from using `os` to `pandorasbox`: 39 | 40 | ```go 41 | import box "github.com/capnspacehook/pandorasbox" 42 | 43 | func CopyFile(srcFile, dstFile string) error { 44 | out, err := box.Create(dstFile) 45 | if err != nil { 46 | return err 47 | } 48 | defer out.Close() 49 | 50 | in, err := box.Open(srcFile) 51 | if err != nil { 52 | return err 53 | } 54 | defer in.Close() 55 | 56 | _, err = io.Copy(out, in) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | ``` 64 | 65 | ### Global vs. Local VFS 66 | 67 | For ease of use, Pandora's box provides a global `Box` that is easily accessible, but in some cases a local `Box` may be desired. If you don't wish to use the global `Box`, don't call `box.InitGlobalBox()`, instead create a locally scoped `Box` by calling `box.NewBox()`. This allows you to easily pass a `Box` into functions or methods or embed a `Box` in a struct. 68 | 69 | ### `io/ioutil` and `path/filepath` Functions 70 | 71 | Pandora's Box also provides helper functions that are identical to functions from `io/ioutil` and `path/filepath`. These should be used of the Go standard library packages when using a `Box`. The Pandora's Box versions are VFS-friendly, and will work seamlessly with a VFS, while the Go standard library packages will not. If you're using the global `Box`, the `io/ioutil` functions can be called from the main import: `github.com/capnspacehook/pandorasbox`. If you're using a local `Box`, you'll need to import `github.com/capnspacehook/pandorasbox/ioutil` and pass in your `Box` to those functions. 72 | 73 | Example (error handling omitted): 74 | 75 | ```go 76 | import box "github.com/capnspacehook/pandorasbox" 77 | 78 | func WriteFileGlobalBox() { 79 | box.WriteFile("vfs://file.txt", []byte("Testing testing 1 2 3"), 0o644) 80 | data, _ := box.ReadFile("vfs://file.txt") 81 | fmt.Println(string(data)) 82 | } 83 | 84 | func WriteFileLocalBox() { 85 | myBox := box.NewBox() 86 | 87 | myBox.WriteFile("vfs://file.txt", []byte("Testing testing 1 2 3"), 0o644) 88 | data, _ := myBox.ReadFile("vfs://file.txt") 89 | fmt.Println(string(data)) 90 | } 91 | ``` 92 | 93 | ### Forcing use of Host FS/VFS 94 | 95 | If for some reason you need to force the usage of either the host's filesystem or the VFS, Pandora's box has you covered. All of `pandorasbox`'s functions that are in also in `os` have 3 variants: normal, OS, and VFS. The normal variant auto-detirmines what to use based off the input path, as described earlier. The OS and VFS variants force the usage of a specific filesystem. For instance, `pandorasbox.Mkdir()` will auto-detirmine which filesystem to use, while `pandorasbox.OSMkdir()` will always use the host's filesystem, and `pandorasbox.VFSMkdir()` will always use the VFS. 96 | 97 | ### Memory Safety 98 | 99 | All files in the VFS are encrypted when not in use. When files from the VFS are opened, they are decrypted for the duration of the call that opened them. VFS files are then re-encrypted with a different random key when reading or writing from them is finished. That is, files in the VFS are only decrypted in memory for a brief time while the underlying data needs to be accessed. In other words, calling `Open()` on a VFS file **will not** decrypt it until `Close()` is called on it. It will only be decrypted in memory when it is internally opened by methods like `Read()`, `Write()`, `Truncate()`, etc. Internal decrypted buffers are wiped as soon as possible. So opening a VFS file and calling `Read()` on it 3 times will decrypt and re-encrypt it 3 times. This is to make sure data is encrypted in memory whenever possible. 100 | 101 | `pandorasbox` never wipes buffers that are owned by the user, so you will need to wipe buffers yourself when calling `Write()` 102 | for example if you don't want the buffer to stick around in memory unencrypted. 103 | 104 | For more information about the exact cryptographic code and algorithms used, refer to this repo: https://github.com/awnumar/memguard. 105 | 106 | ## Acknowledgements 107 | 108 | Thanks to AbsFs contributors for the amazing repos, 70% of the code is from repos from [this organization](https://github.com/absfs). 109 | 110 | Took some VFS specific tests from [this repo](https://github.com/blang/vfs), thanks to [blang](https://github.com/blang) for some good VFS tests. 111 | 112 | Thanks to [awnumar](https://github.com/awnumar) for [memguard](https://github.com/awnumar/memguard), he created a great repo that is very easy to use safely. 113 | -------------------------------------------------------------------------------- /inode/inode.go: -------------------------------------------------------------------------------- 1 | package inode 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/fs" 7 | "os" 8 | filepath "path" // force forward slash separators on all OSs 9 | "sort" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | "unsafe" 14 | ) 15 | 16 | // An Inode represents the basic metadata of a file. 17 | type Inode struct { 18 | sync.RWMutex 19 | 20 | Ino uint64 // should never change 21 | Mode fs.FileMode // should never change 22 | Nlink uint64 23 | Size int64 24 | 25 | Ctime time.Time // creation time 26 | Atime time.Time // access time 27 | Mtime time.Time // modification time 28 | 29 | Dir Directory 30 | } 31 | 32 | type DirEntry struct { 33 | Name string 34 | Inode *Inode 35 | } 36 | 37 | func (e *DirEntry) IsDir() bool { 38 | if e.Inode == nil { 39 | return false 40 | } 41 | 42 | return e.Inode.IsDir() 43 | } 44 | 45 | type Directory []*DirEntry 46 | 47 | func (d Directory) Len() int { return len(d) } 48 | func (d Directory) Swap(i, j int) { d[i], d[j] = d[j], d[i] } 49 | func (d Directory) Less(i, j int) bool { return d[i].Name < d[j].Name } 50 | 51 | type Ino uint64 52 | 53 | func (n *Ino) New(mode os.FileMode) *Inode { 54 | atomic.AddUint64((*uint64)(unsafe.Pointer(n)), 1) 55 | now := time.Now() 56 | 57 | return &Inode{ 58 | Ino: uint64(*n), 59 | Atime: now, 60 | Mtime: now, 61 | Ctime: now, 62 | Mode: mode, 63 | } 64 | } 65 | 66 | func (n *Ino) SubIno() { 67 | atomic.AddUint64((*uint64)(unsafe.Pointer(n)), ^uint64(0)) 68 | } 69 | 70 | func (n *Ino) NewDir(mode os.FileMode) *Inode { 71 | dir := n.New(mode) 72 | dir.Mode = os.ModeDir | mode 73 | 74 | if err := dir.Link(".", dir); err != nil { 75 | panic(err) 76 | } 77 | if err := dir.Link("..", dir); err != nil { 78 | panic(err) 79 | } 80 | 81 | return dir 82 | } 83 | 84 | // Link - link adds a directory entry (DirEntry) for the given node (assumed to be a directory) to the provided child Inode. 85 | func (n *Inode) Link(name string, child *Inode) error { 86 | // Return an error if a regular file is used as a link target 87 | if !n.IsDir() { 88 | return errors.New("not a directory") 89 | } 90 | 91 | n.Lock() 92 | defer n.Unlock() 93 | 94 | x := n.find(name) 95 | 96 | entry := &DirEntry{name, child} 97 | 98 | if x < len(n.Dir) && n.Dir[x].Name == name { 99 | n.linkSwapi(x, entry) 100 | return nil 101 | } 102 | n.linki(x, entry) 103 | 104 | return nil 105 | } 106 | 107 | // Unlink - removes the directory entry (DirEntry). 108 | func (n *Inode) Unlink(name string) error { 109 | // It is an error to unlink an Inode that is not a directory 110 | if !n.IsDir() { 111 | return errors.New("not a directory") 112 | } 113 | 114 | n.Lock() 115 | defer n.Unlock() 116 | 117 | x := n.find(name) 118 | 119 | if x == n.Dir.Len() || n.Dir[x].Name != name { 120 | return fs.ErrNotExist 121 | } 122 | 123 | n.unlinki(x) 124 | 125 | return nil 126 | } 127 | 128 | func (n *Inode) UnlinkAll() { 129 | n.Lock() 130 | 131 | for _, e := range n.Dir { 132 | if e.Name == ".." { 133 | continue 134 | } 135 | if e.Inode.Ino == n.Ino { 136 | e.Inode.countDown() 137 | continue 138 | } 139 | 140 | n.Unlock() 141 | e.Inode.UnlinkAll() 142 | n.Lock() 143 | e.Inode.countDown() 144 | } 145 | 146 | n.Dir = n.Dir[:0] 147 | n.Unlock() 148 | } 149 | 150 | func (n *Inode) IsDir() bool { 151 | return n.Mode&fs.ModeDir != 0 152 | } 153 | 154 | func (n *Inode) Rename(oldpath, newpath string) error { 155 | dir, name := filepath.Split(oldpath) 156 | dir = filepath.Clean(dir) 157 | 158 | snode, err := n.Resolve(oldpath) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | p, err := n.Resolve(dir) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | var rename string 169 | tnode, err := n.Resolve(newpath) 170 | if err == nil && tnode.IsDir() { 171 | return fs.ErrExist 172 | } 173 | if (err == nil && !tnode.IsDir()) || (err != nil && errors.Is(err, fs.ErrNotExist)) { 174 | var tdir string 175 | tdir, rename = filepath.Split(newpath) 176 | tdir = filepath.Clean(tdir) 177 | tnode, err = n.Resolve(tdir) 178 | } 179 | if err != nil { 180 | return err 181 | } 182 | 183 | if len(rename) > 0 { 184 | name, rename = rename, name 185 | } 186 | err = tnode.Link(name, snode) 187 | if err != nil { 188 | return err 189 | } 190 | if len(rename) > 0 { 191 | name = rename 192 | } 193 | err = p.Unlink(name) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | return nil 199 | } 200 | 201 | func (n *Inode) Resolve(path string) (*Inode, error) { 202 | n.RLock() 203 | defer n.RUnlock() 204 | 205 | name, trim := PopPath(path) 206 | if name == "/" { 207 | if trim == "" { 208 | return n, nil 209 | } 210 | nn, err := n.Resolve(trim) 211 | if err != nil { 212 | return nil, err 213 | } 214 | if nn == nil { 215 | return n, nil 216 | } 217 | return nn, err 218 | } 219 | 220 | x := n.find(name) 221 | if x < len(n.Dir) && n.Dir[x].Name == name { 222 | nn := n.Dir[x].Inode 223 | if len(trim) == 0 { 224 | return nn, nil 225 | } 226 | return nn.Resolve(trim) 227 | } 228 | 229 | return nil, fs.ErrNotExist 230 | } 231 | 232 | func (n *Inode) accessed() { 233 | n.Atime = time.Now() 234 | } 235 | 236 | func (n *Inode) modified() { 237 | now := time.Now() 238 | n.Atime = now 239 | n.Mtime = now 240 | } 241 | 242 | func (n *Inode) countUp() { 243 | n.Nlink++ 244 | n.accessed() // (I don't think link count mod counts as node mod ) 245 | } 246 | 247 | func (n *Inode) countDown() { 248 | if n.Nlink == 0 { 249 | panic(fmt.Sprintf("inode %d negative link count", n.Ino)) 250 | } 251 | n.Nlink-- 252 | n.accessed() // (I don't think link count mod counts as node mod ) 253 | } 254 | 255 | func (n *Inode) unlinki(i int) { 256 | n.Dir[i].Inode.countDown() 257 | copy(n.Dir[i:], n.Dir[i+1:]) 258 | n.Dir = n.Dir[:len(n.Dir)-1] 259 | 260 | n.modified() 261 | } 262 | 263 | func (n *Inode) linkSwapi(i int, entry *DirEntry) { 264 | n.Dir[i].Inode.countDown() 265 | n.Dir[i] = entry 266 | n.Dir[i].Inode.countUp() 267 | 268 | n.modified() 269 | } 270 | 271 | func (n *Inode) linki(i int, entry *DirEntry) { 272 | n.Dir = append(n.Dir, nil) 273 | copy(n.Dir[i+1:], n.Dir[i:]) 274 | 275 | n.Dir[i] = entry 276 | n.Dir[i].Inode.countUp() 277 | 278 | n.modified() 279 | } 280 | 281 | func (n *Inode) find(name string) int { 282 | return sort.Search(len(n.Dir), func(i int) bool { 283 | return n.Dir[i].Name >= name 284 | }) 285 | } 286 | -------------------------------------------------------------------------------- /vfs/vfs_fuzz_test.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | stdfs "io/fs" 7 | "math/rand/v2" 8 | "os" 9 | "path" 10 | "strconv" 11 | "testing" 12 | "time" 13 | 14 | "github.com/matryer/is" 15 | ) 16 | 17 | type fsOp uint8 18 | 19 | const ( 20 | openFileFS fsOp = iota 21 | createFS 22 | readFileFS 23 | readDirFS 24 | writeFileFS 25 | mkDirFS 26 | mkdirAllFS 27 | statFS 28 | lstatFS 29 | renameFS 30 | removeFS 31 | removeAllFS 32 | truncateFS 33 | walkDirFS 34 | chdirFS 35 | 36 | read 37 | readAt 38 | readDir 39 | write 40 | writeAt 41 | stat 42 | seek 43 | truncate 44 | closeFile 45 | 46 | endOp 47 | ) 48 | 49 | const ( 50 | initialDirname = "dir" 51 | initialFilename = "file.txt" 52 | 53 | maxOps = 50 54 | totalMaxOps = 75 55 | maxRandBytes = 8096 56 | ) 57 | 58 | // FuzzVFSRace preforms random operations on a virtual filesystem in 59 | // two different goroutines and checks that operations don't fail 60 | // unexpectedly. It also tests that operations preform as expected 61 | // when multiple operations are preformed concurrently. It should be 62 | // run with the race detector enabled for best results. 63 | func FuzzVFSRace(f *testing.F) { 64 | f.Fuzz(func(t *testing.T, ops1 []uint8, ops2 []uint8, seed1, seed2 uint64) { 65 | // Skip if the number of operations is too large, it can 66 | // exceed the 1 second timeout. 67 | if len(ops1) == 0 || len(ops2) == 0 { 68 | t.Skip() 69 | } else if len(ops1) > maxOps || len(ops2) > maxOps || len(ops1)+len(ops2) > totalMaxOps { 70 | t.Skip() 71 | } 72 | 73 | is := is.New(t) 74 | 75 | fs := NewFS().(*virtualFS) 76 | rand.Int() 77 | 78 | is.NoErr(fs.Mkdir(initialDirname, 0o777)) 79 | initialFile, err := fs.OpenFile(initialFilename, os.O_RDWR|os.O_CREATE, 0o777) 80 | is.NoErr(err) 81 | 82 | performOps := func(ops []uint8, done chan struct{}, num uint64) { 83 | defer func() { done <- struct{}{} }() 84 | 85 | f := initialFile.(*vfsFile) 86 | dirname := initialDirname 87 | filename := initialFilename 88 | r := rand.New(rand.NewPCG(seed1+num, seed2+num)) 89 | 90 | checkErr := checkErrFunc(t, num) 91 | 92 | for _, o := range ops { 93 | op := fsOp(o) % endOp 94 | 95 | switch op { 96 | // filesystem operations 97 | case openFileFS: 98 | t.Logf("%d: openFileFS(%s)", num, filename) 99 | 100 | openedFile, err := fs.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0o777) 101 | checkErr("openFileFS", err) 102 | f = openedFile.(*vfsFile) 103 | 104 | t.Logf("%d: openFileFS finished", num) 105 | case createFS: 106 | filename := strconv.FormatUint(r.Uint64(), 10) 107 | t.Logf("%d: createFS(%s)", num, filename) 108 | 109 | createdFile, err := fs.Create(filename) 110 | checkErr("createFS", err) 111 | f = createdFile.(*vfsFile) 112 | 113 | t.Logf("%d: createFS finished", num) 114 | case readFileFS: 115 | t.Logf("%d: readFileFS(%s)", num, filename) 116 | 117 | _, err := fs.ReadFile(filename) 118 | checkErr("readFileFS", err, io.EOF, stdfs.ErrNotExist) 119 | 120 | t.Logf("%d: readFileFS finished", num) 121 | case readDirFS: 122 | t.Logf("%d: readDirFS(%s)", num, dirname) 123 | 124 | _, err := fs.ReadDir(dirname) 125 | checkErr("readDirFS", err, stdfs.ErrNotExist) 126 | 127 | t.Logf("%d: readDirFS finished", num) 128 | case writeFileFS: 129 | data := randBytes(t, r) 130 | t.Logf("%d: writeFileFS(%s [%d bytes])", num, filename, len(data)) 131 | 132 | err := fs.WriteFile(filename, data, 0o666) 133 | checkErr("writeFileFS", err) 134 | 135 | t.Logf("%d: writeFileFS finished", num) 136 | case mkDirFS: 137 | dirname = strconv.FormatUint(r.Uint64(), 10) 138 | t.Logf("%d: mkDirFS(%s)", num, dirname) 139 | 140 | err := fs.Mkdir(dirname, 0o777) 141 | checkErr("mkDirFS", err) 142 | 143 | t.Logf("%d: mkDirFS finished", num) 144 | case mkdirAllFS: 145 | dirname = path.Join(dirname, strconv.FormatUint(r.Uint64(), 10)) 146 | t.Logf("%d: mkdirAllFS(%s)", num, dirname) 147 | 148 | err := fs.MkdirAll(dirname, 0o777) 149 | checkErr("mkdirAllFS", err) 150 | 151 | t.Logf("%d: mkdirAllFS finished", num) 152 | case statFS: 153 | t.Logf("%d: statFS(%s)", num, filename) 154 | 155 | _, err := fs.Stat(filename) 156 | checkErr("statFS", err, stdfs.ErrNotExist) 157 | 158 | t.Logf("%d: statFS finished", num) 159 | case lstatFS: 160 | t.Logf("%d: lstatFS(%s)", num, filename) 161 | 162 | _, err := fs.Lstat(filename) 163 | checkErr("lstatFS", err, stdfs.ErrNotExist) 164 | 165 | t.Logf("%d: lstatFS finished", num) 166 | case renameFS: 167 | newFilename := strconv.FormatUint(r.Uint64(), 10) 168 | t.Logf("%d: renameFS(%s, %s)", num, filename, newFilename) 169 | 170 | err := fs.Rename(filename, newFilename) 171 | if checkErr("renameFS", err, stdfs.ErrNotExist) { 172 | filename = newFilename 173 | } 174 | 175 | t.Logf("%d: renameFS finished", num) 176 | case removeFS: 177 | t.Logf("%d: removeFS(%s)", num, filename) 178 | 179 | err := fs.Remove(filename) 180 | if checkErr("removeFS", err, stdfs.ErrNotExist) { 181 | f = initialFile.(*vfsFile) 182 | filename = initialFilename 183 | } 184 | 185 | t.Logf("%d: removeFS finished", num) 186 | case removeAllFS: 187 | t.Logf("%d: removeAllFS(%s)", num, dirname) 188 | 189 | err := fs.RemoveAll(dirname) 190 | if checkErr("removeAllFS", err, stdfs.ErrNotExist) { 191 | dirname = initialDirname 192 | } 193 | 194 | t.Logf("%d: removeAllFS finished", num) 195 | case truncateFS: 196 | newSize := r.Int64N(maxRandBytes) 197 | t.Logf("%d: truncateFS(%s, %d)", num, filename, newSize) 198 | 199 | err := fs.Truncate(filename, newSize) 200 | checkErr("truncateFS", err, stdfs.ErrNotExist) 201 | 202 | t.Logf("%d: truncateFS finished", num) 203 | case chdirFS: 204 | t.Logf("%d: chdirFS(%s)", num, dirname) 205 | 206 | err := fs.Chdir(dirname) 207 | checkErr("chdirFS", err, stdfs.ErrNotExist) 208 | 209 | t.Logf("%d: chdirFS finished", num) 210 | 211 | // file operations 212 | case read: 213 | buf := make([]byte, r.IntN(maxRandBytes)) 214 | t.Logf("%d: (%s) read([%d bytes])", num, f.name, len(buf)) 215 | 216 | _, err := f.Read(buf) 217 | checkErr("read", err, io.EOF, stdfs.ErrClosed) 218 | 219 | t.Logf("%d: (%s) read finished", num, f.name) 220 | case readAt: 221 | fi, err := f.Stat() 222 | if checkErr("stat", err, stdfs.ErrClosed) { 223 | off := r.Int64N(fi.Size() + 1) 224 | buf := make([]byte, r.IntN(maxRandBytes)) 225 | t.Logf("%d: (%s) readAt([%d bytes], %d)", num, f.name, len(buf), off) 226 | 227 | _, err = f.ReadAt(buf, off) 228 | checkErr("readAt", err, io.EOF, stdfs.ErrClosed) 229 | } 230 | 231 | t.Logf("%d: (%s) readAt finished", num, f.name) 232 | case write: 233 | data := randBytes(t, r) 234 | t.Logf("%d: (%s) write([%d bytes])", num, f.name, len(data)) 235 | 236 | _, err := f.Write(data) 237 | checkErr("write", err, stdfs.ErrClosed) 238 | 239 | t.Logf("%d: (%s) write finished", num, f.name) 240 | case writeAt: 241 | fi, err := f.Stat() 242 | if checkErr("stat", err, stdfs.ErrClosed) { 243 | off := r.Int64N(fi.Size() + 1) 244 | data := randBytes(t, r) 245 | t.Logf("%d: (%s) writeAt([%d bytes], %d)", num, f.name, len(data), off) 246 | 247 | _, err = f.WriteAt(data, off) 248 | checkErr("writeAt", err, stdfs.ErrClosed) 249 | } 250 | 251 | t.Logf("%d: (%s) writeAt finished", num, f.name) 252 | case stat: 253 | t.Logf("%d: (%s) stat()", num, f.name) 254 | _, err := f.Stat() 255 | checkErr("stat", err, stdfs.ErrClosed) 256 | 257 | t.Logf("%d: (%s) stat finished", num, f.name) 258 | case seek: 259 | fi, err := f.Stat() 260 | if checkErr("stat", err, stdfs.ErrClosed) { 261 | off := r.Int64N(fi.Size() + 1) 262 | whence := r.IntN(3) 263 | t.Logf("%d: (%s) seek(%d, %d)", num, f.name, off, whence) 264 | 265 | _, err = f.Seek(off, whence) 266 | checkErr("seek", err, stdfs.ErrClosed) 267 | } 268 | 269 | t.Logf("%d: (%s) seek finished", num, f.name) 270 | case truncate: 271 | newSize := r.Int64N(maxRandBytes) 272 | t.Logf("%d: (%s) truncate(%d)", num, f.name, newSize) 273 | 274 | err := f.Truncate(newSize) 275 | checkErr("truncate", err, stdfs.ErrClosed) 276 | 277 | t.Logf("%d: (%s) truncate finished", num, f.name) 278 | case closeFile: 279 | t.Logf("%d: (%s) close()", num, f.name) 280 | 281 | err := f.Close() 282 | checkErr("close", err, stdfs.ErrClosed) 283 | f = initialFile.(*vfsFile) 284 | 285 | t.Logf("%d: (%s) close finished", num, f.name) 286 | } 287 | } 288 | } 289 | 290 | done1 := make(chan struct{}) 291 | done2 := make(chan struct{}) 292 | go performOps(ops1, done1, 1) 293 | go performOps(ops2, done2, 2) 294 | 295 | for range 2 { 296 | select { 297 | case <-done1: 298 | case <-done2: 299 | case <-time.After(time.Second): 300 | t.Fatal("timed out") 301 | } 302 | } 303 | }) 304 | } 305 | 306 | // checkErrFunc returns a function that checks if an error is nil or 307 | // of an expected type. Otherwise it will log the error and fail the 308 | // test. Because we are testing random operations concurrently, some 309 | // errors are expected to occur. 310 | func checkErrFunc(t *testing.T, num uint64) func(string, error, ...error) bool { 311 | t.Helper() 312 | 313 | return func(op string, err error, allowedErrs ...error) bool { 314 | t.Helper() 315 | 316 | if err == nil { 317 | return true 318 | } 319 | if len(allowedErrs) == 0 { 320 | t.Fatalf("%d: %s: expected no errors, got: %v", num, op, err) 321 | return false 322 | } 323 | 324 | for _, allowedErr := range allowedErrs { 325 | if errors.Is(err, allowedErr) { 326 | return false 327 | } 328 | } 329 | 330 | t.Fatalf("%d: %s: expected one of these errors: %v got: %v", num, op, allowedErrs, err) 331 | return false 332 | } 333 | } 334 | 335 | func randBytes(t *testing.T, r *rand.Rand) []byte { 336 | t.Helper() 337 | 338 | data := make([]byte, r.IntN(maxRandBytes)) 339 | //nolint:intrange 340 | for i := range len(data) { 341 | data[i] = byte(r.UintN(256)) 342 | } 343 | 344 | return data 345 | } 346 | -------------------------------------------------------------------------------- /vfs/vfsfile.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "sync" 10 | "sync/atomic" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/awnumar/fastrand" 15 | "github.com/awnumar/memguard" 16 | "github.com/awnumar/memguard/core" 17 | 18 | "github.com/capnspacehook/pandorasbox/inode" 19 | ) 20 | 21 | const keySize = 32 22 | 23 | type vfsFile struct { 24 | fs *virtualFS 25 | 26 | // protects dirOffset 27 | sync.Mutex 28 | 29 | name string 30 | flags int 31 | node *inode.Inode 32 | closed atomic.Bool 33 | 34 | sfile *sealedFile 35 | 36 | offset atomic.Int64 37 | dirOffset int 38 | } 39 | 40 | // sealedFile contains authenticated and encrypted file contents, as 41 | // well as a key used to decrypt the file contents 42 | type sealedFile struct { 43 | // protects ciphertext and sealedKey; since sealedFiles are shared 44 | // between multiple files this ensures read/write operations 45 | // don't race 46 | sync.RWMutex 47 | 48 | ciphertext []byte 49 | sealedKey *memguard.Enclave 50 | } 51 | 52 | func (s *sealedFile) size() int { 53 | return max(len(s.ciphertext)-core.Overhead, 0) 54 | } 55 | 56 | func (f *vfsFile) Name() string { 57 | return f.name 58 | } 59 | 60 | func (f *vfsFile) setOffset(offset int64) { 61 | if offset < 0 { 62 | panic(fmt.Sprintf("%s: negative offset: %d", f.name, offset)) 63 | } 64 | f.offset.Store(offset) 65 | } 66 | 67 | func (f *vfsFile) addOffset(offset int64) int64 { 68 | newOffset := f.offset.Add(offset) 69 | if newOffset < 0 { 70 | panic(fmt.Sprintf("%s: negative offset: %d", f.name, newOffset)) 71 | } 72 | return newOffset 73 | } 74 | 75 | // decrypt returns the plaintext of the sealed file. It must be called 76 | // under lock. 77 | func (f *vfsFile) decrypt(plaintext []byte) error { 78 | key, err := f.sfile.sealedKey.Open() 79 | if err != nil { 80 | return err 81 | } 82 | _, err = core.Decrypt(f.sfile.ciphertext, key.Bytes(), plaintext) 83 | key.Destroy() 84 | if err != nil { 85 | return fmt.Errorf("failed to decrypt: %w", err) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // encrypt encrypts plaintext, stores the ciphertext in the sealed file 92 | // and updates the file size. It must be called under lock. 93 | func (f *vfsFile) encrypt(plaintext []byte) error { 94 | var err error 95 | 96 | newKey := memguard.NewBufferFromBytes(fastrand.Bytes(keySize)) 97 | f.sfile.ciphertext, err = core.Encrypt(plaintext, newKey.Bytes()) 98 | core.Wipe(plaintext) 99 | if err != nil { 100 | return fmt.Errorf("failed to enrypt: %w", err) 101 | } 102 | 103 | newKey.Freeze() 104 | f.sfile.sealedKey = newKey.Seal() 105 | f.node.Size = int64(len(plaintext)) 106 | 107 | return nil 108 | } 109 | 110 | func (f *vfsFile) Read(p []byte) (int, error) { 111 | n, err := f.read(p, f.offset.Load()) 112 | f.addOffset(int64(n)) 113 | 114 | return n, err 115 | } 116 | 117 | func (f *vfsFile) read(p []byte, offset int64) (int, error) { 118 | if offset < 0 { 119 | return 0, &fs.PathError{Op: "read", Path: f.name, Err: fs.ErrInvalid} 120 | } 121 | if f.closed.Load() { 122 | return 0, &fs.PathError{Op: "read", Path: f.name, Err: fs.ErrClosed} 123 | } 124 | if len(p) == 0 { 125 | return 0, nil 126 | } 127 | if f.flags&_O_ACCESS == os.O_WRONLY { 128 | return 0, &fs.PathError{Op: "read", Path: f.name, Err: fs.ErrPermission} 129 | } 130 | if f.node.IsDir() { 131 | return 0, &fs.PathError{Op: "read", Path: f.name, Err: syscall.EISDIR} 132 | } 133 | 134 | f.node.RLock() 135 | defer f.node.RUnlock() 136 | 137 | if offset >= f.node.Size { 138 | return 0, io.EOF 139 | } 140 | if f.node.Size == 0 { 141 | return 0, io.EOF 142 | } 143 | 144 | f.sfile.RLock() 145 | defer f.sfile.RUnlock() 146 | 147 | plaintext := make([]byte, f.sfile.size()) 148 | if err := f.decrypt(plaintext); err != nil { 149 | return 0, &fs.PathError{Op: "read", Path: f.name, Err: err} 150 | } 151 | 152 | core.Copy(p, plaintext[offset:]) 153 | core.Wipe(plaintext) 154 | 155 | var n int 156 | if len(p) < len(plaintext[offset:]) { 157 | n = len(p) 158 | } else { 159 | n = len(plaintext[offset:]) 160 | } 161 | 162 | if len(p) > n { 163 | return n, io.EOF 164 | } 165 | 166 | return n, nil 167 | } 168 | 169 | func (f *vfsFile) ReadAt(p []byte, off int64) (n int, err error) { 170 | return f.read(p, off) 171 | } 172 | 173 | func (f *vfsFile) ReadDir(n int) ([]fs.DirEntry, error) { 174 | if f.closed.Load() { 175 | return nil, &fs.PathError{Op: "readdir", Path: f.name, Err: fs.ErrClosed} 176 | } 177 | if f.flags&_O_ACCESS == os.O_WRONLY { 178 | return nil, &fs.PathError{Op: "readat", Path: f.name, Err: fs.ErrPermission} 179 | } 180 | if !f.node.IsDir() { 181 | return nil, &fs.PathError{Op: "readdir", Path: f.Name(), Err: syscall.ENOTDIR} 182 | } 183 | 184 | // protect f.dirOffset 185 | f.Lock() 186 | defer f.Unlock() 187 | 188 | // protect f.node.Dir 189 | f.node.RLock() 190 | defer f.node.RUnlock() 191 | 192 | dirs := f.node.Dir 193 | if f.dirOffset >= len(dirs) { 194 | if n <= 0 { 195 | return nil, nil 196 | } 197 | return nil, io.EOF 198 | } 199 | 200 | if n <= 0 { 201 | // if there are only 2 dirs ('.' and '..'), return 202 | // since we are skipping them below 203 | if len(dirs) == 2 { 204 | return nil, nil 205 | } 206 | n = len(dirs) 207 | } 208 | // skip '.' and '..' to retain compatibility with os.ReadDir 209 | if f.dirOffset == 0 { 210 | f.dirOffset = 2 211 | } 212 | 213 | infosLen := n - f.dirOffset 214 | if infosLen <= 0 { 215 | infosLen = n 216 | } 217 | 218 | infos := make([]fs.DirEntry, infosLen) 219 | for i, entry := range dirs[f.dirOffset:] { 220 | if i == n { 221 | break 222 | } 223 | 224 | infos[i] = &DirEntry{entry.Name, entry.Inode} 225 | } 226 | f.dirOffset += n 227 | 228 | return infos, nil 229 | } 230 | 231 | func (f *vfsFile) Write(p []byte) (int, error) { 232 | n, err := f.write(p, f.offset.Load()) 233 | f.addOffset(int64(n)) 234 | 235 | return n, err 236 | } 237 | 238 | func (f *vfsFile) write(p []byte, offset int64) (int, error) { 239 | if offset < 0 { 240 | return 0, &fs.PathError{Op: "write", Path: f.name, Err: fs.ErrInvalid} 241 | } 242 | if f.closed.Load() { 243 | return 0, &fs.PathError{Op: "write", Path: f.name, Err: fs.ErrClosed} 244 | } 245 | if f.flags&_O_ACCESS == os.O_RDONLY { 246 | return 0, &fs.PathError{Op: "write", Path: f.name, Err: fs.ErrPermission} 247 | } 248 | if f.node.IsDir() { 249 | return 0, &fs.PathError{Op: "write", Path: f.name, Err: syscall.EISDIR} 250 | } 251 | // writing past the end of the file is allowed as part of the POSIX spec 252 | // and we want to be roughly compatible with that, so we allow it too 253 | 254 | f.node.Lock() 255 | defer f.node.Unlock() 256 | 257 | f.sfile.Lock() 258 | defer f.sfile.Unlock() 259 | 260 | size := f.sfile.size() 261 | if writeSize := len(p) + int(offset); writeSize > size { 262 | size = writeSize 263 | } 264 | plaintext := make([]byte, size) 265 | if len(f.sfile.ciphertext) > 0 { 266 | if err := f.decrypt(plaintext); err != nil { 267 | return 0, &fs.PathError{Op: "write", Path: f.name, Err: err} 268 | } 269 | } 270 | 271 | core.Copy(plaintext[offset:], p) 272 | err := f.encrypt(plaintext) 273 | if err != nil { 274 | return 0, &fs.PathError{Op: "write", Path: f.name, Err: err} 275 | } 276 | 277 | var n int 278 | if len(p) < len(plaintext[offset:]) { 279 | n = len(p) 280 | } else { 281 | n = len(plaintext[offset:]) 282 | } 283 | 284 | return n, nil 285 | } 286 | 287 | func (f *vfsFile) WriteAt(b []byte, off int64) (n int, err error) { 288 | return f.write(b, off) 289 | } 290 | 291 | func (f *vfsFile) WriteString(s string) (n int, err error) { 292 | return f.Write([]byte(s)) 293 | } 294 | 295 | func (f *vfsFile) Stat() (os.FileInfo, error) { 296 | if f.closed.Load() { 297 | return nil, &fs.PathError{Op: "stat", Path: f.name, Err: fs.ErrClosed} 298 | } 299 | 300 | return &FileInfo{filepath.Base(f.name), f.node}, nil 301 | } 302 | 303 | func (f *vfsFile) Seek(offset int64, whence int) (int64, error) { 304 | if f.closed.Load() { 305 | return 0, &fs.PathError{Op: "seek", Path: f.name, Err: fs.ErrClosed} 306 | } 307 | if f.node.IsDir() { 308 | return 0, &fs.PathError{Op: "seek", Path: f.name, Err: syscall.EISDIR} 309 | } 310 | 311 | var ret int64 312 | switch whence { 313 | case io.SeekStart: 314 | if offset < 0 { 315 | return 0, &fs.PathError{Op: "seek", Path: f.name, Err: fs.ErrInvalid} 316 | } 317 | 318 | f.setOffset(offset) 319 | ret = offset 320 | case io.SeekCurrent: 321 | f.node.RLock() 322 | if offset < 0 && (f.node.Size+offset) < 0 { 323 | f.node.RUnlock() 324 | return 0, &fs.PathError{Op: "seek", Path: f.name, Err: fs.ErrInvalid} 325 | } 326 | f.node.RUnlock() 327 | 328 | ret = f.addOffset(offset) 329 | case io.SeekEnd: 330 | f.node.RLock() 331 | ret = f.node.Size + offset 332 | f.node.RUnlock() 333 | f.setOffset(ret) 334 | } 335 | 336 | return ret, nil 337 | } 338 | 339 | func (f *vfsFile) Sync() error { 340 | return nil 341 | } 342 | 343 | func (f *vfsFile) Truncate(size int64) error { 344 | if size < 0 { 345 | return &fs.PathError{Op: "truncate", Path: f.name, Err: fs.ErrInvalid} 346 | } 347 | if f.closed.Load() { 348 | return &fs.PathError{Op: "truncate", Path: f.name, Err: fs.ErrClosed} 349 | } 350 | if f.flags&_O_ACCESS == os.O_RDONLY { 351 | return &fs.PathError{Op: "truncate", Path: f.name, Err: fs.ErrPermission} 352 | } 353 | if f.node.IsDir() { 354 | return &fs.PathError{Op: "truncate", Path: f.name, Err: syscall.EISDIR} 355 | } 356 | 357 | // protect f.node.Size 358 | f.node.Lock() 359 | defer f.node.Unlock() 360 | 361 | if f.node.Size == size { 362 | return nil 363 | } 364 | if f.node.Size == 0 && size == 0 { 365 | // file is already empty, no-op 366 | return nil 367 | } 368 | 369 | f.sfile.Lock() 370 | defer f.sfile.Unlock() 371 | 372 | if f.node.Size == 0 { 373 | // the file is empty and we are extending the file 374 | data := make([]byte, size) 375 | if err := f.encrypt(data); err != nil { 376 | return &fs.PathError{Op: "truncate", Path: f.name, Err: err} 377 | } 378 | return nil 379 | } else if size == 0 { 380 | // the file is not empty and we are making it empty 381 | f.sfile.ciphertext = nil 382 | f.sfile.sealedKey = nil 383 | f.node.Size = 0 384 | return nil 385 | } 386 | 387 | // shrink or extend the file 388 | plaintext := make([]byte, f.sfile.size()) 389 | if err := f.decrypt(plaintext); err != nil { 390 | return &fs.PathError{Op: "truncate", Path: f.name, Err: err} 391 | } 392 | 393 | data := make([]byte, size) 394 | core.Move(data, plaintext) 395 | 396 | if err := f.encrypt(data); err != nil { 397 | return &fs.PathError{Op: "truncate", Path: f.name, Err: err} 398 | } 399 | 400 | return nil 401 | } 402 | 403 | func (f *vfsFile) Close() error { 404 | if f.closed.Load() { 405 | return fs.ErrClosed 406 | } 407 | 408 | f.closed.Store(true) 409 | 410 | return nil 411 | } 412 | 413 | type DirEntry struct { 414 | name string 415 | node *inode.Inode 416 | } 417 | 418 | func (d *DirEntry) Name() string { 419 | return d.name 420 | } 421 | 422 | func (d *DirEntry) IsDir() bool { 423 | return d.node.Mode.IsDir() 424 | } 425 | 426 | func (d *DirEntry) Type() fs.FileMode { 427 | return d.node.Mode.Type() 428 | } 429 | 430 | func (d *DirEntry) Info() (fs.FileInfo, error) { 431 | return &FileInfo{name: d.name, node: d.node}, nil 432 | } 433 | 434 | type FileInfo struct { 435 | name string 436 | node *inode.Inode 437 | } 438 | 439 | func (i *FileInfo) Name() string { 440 | return i.name 441 | } 442 | 443 | func (i *FileInfo) Size() int64 { 444 | i.node.RLock() 445 | defer i.node.RUnlock() 446 | 447 | return i.node.Size 448 | } 449 | 450 | func (i *FileInfo) Mode() os.FileMode { 451 | return i.node.Mode 452 | } 453 | 454 | func (i *FileInfo) ModTime() time.Time { 455 | i.node.RLock() 456 | defer i.node.RUnlock() 457 | 458 | return i.node.Mtime 459 | } 460 | 461 | func (i *FileInfo) IsDir() bool { 462 | return i.node.IsDir() 463 | } 464 | 465 | func (i *FileInfo) Sys() interface{} { 466 | return i.node 467 | } 468 | -------------------------------------------------------------------------------- /vfs/vfs.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | stdfs "io/fs" 7 | "os" 8 | "path" 9 | "slices" 10 | "strings" 11 | "sync" 12 | "syscall" 13 | 14 | "github.com/capnspacehook/pandorasbox/absfs" 15 | "github.com/capnspacehook/pandorasbox/inode" 16 | ) 17 | 18 | const ( 19 | PathSeparator = '/' 20 | PathListSeparator = ':' 21 | 22 | tempDir = "/tmp" 23 | 24 | _O_ACCESS = 0x3 // masks the access mode (os.O_RDONLY, os.O_WRONLY, or os.O_RDWR) 25 | ) 26 | 27 | type stdFS struct { 28 | *virtualFS 29 | } 30 | 31 | func (fs stdFS) Open(name string) (stdfs.File, error) { 32 | if err := checkPath(name, "open"); err != nil { 33 | return nil, err 34 | } 35 | 36 | return fs.virtualFS.Open(name) 37 | } 38 | 39 | func (fs stdFS) ReadDir(name string) ([]stdfs.DirEntry, error) { 40 | if err := checkPath(name, "open"); err != nil { 41 | return nil, err 42 | } 43 | 44 | return fs.virtualFS.ReadDir(name) 45 | } 46 | 47 | func (fs stdFS) ReadFile(name string) ([]byte, error) { 48 | if err := checkPath(name, "open"); err != nil { 49 | return nil, err 50 | } 51 | 52 | return fs.virtualFS.ReadFile(name) 53 | } 54 | 55 | func (fs stdFS) StatFS(name string) (stdfs.FileInfo, error) { 56 | if err := checkPath(name, "stat"); err != nil { 57 | return nil, err 58 | } 59 | 60 | return fs.virtualFS.Stat(name) 61 | } 62 | 63 | func checkPath(name, op string) error { 64 | if path.IsAbs(name) { 65 | // if the name starts with a slash, return an error 66 | // to remain compatible with io/fs 67 | return &stdfs.PathError{Op: op, Path: name, Err: stdfs.ErrInvalid} 68 | } 69 | 70 | return nil 71 | } 72 | 73 | type virtualFS struct { 74 | mtx *sync.RWMutex 75 | 76 | root *inode.Inode 77 | cwd string 78 | dir *inode.Inode 79 | ino *inode.Ino 80 | 81 | sfiles []*sealedFile 82 | } 83 | 84 | func NewFS() absfs.FileSystem { 85 | fs := new(virtualFS) 86 | fs.mtx = new(sync.RWMutex) 87 | fs.ino = new(inode.Ino) 88 | 89 | fs.root = fs.ino.NewDir(0o755) 90 | fs.cwd = "/" 91 | fs.dir = fs.root 92 | fs.sfiles = make([]*sealedFile, 2) 93 | 94 | return fs 95 | } 96 | 97 | func (fs *virtualFS) FS() stdfs.FS { 98 | fs.mtx.RLock() 99 | defer fs.mtx.RUnlock() 100 | 101 | // set cwd to root, as paths are not allowed to start with a slash 102 | // in io/fs filesystems 103 | return stdFS{virtualFS: &virtualFS{ 104 | mtx: fs.mtx, 105 | root: fs.root, 106 | cwd: "/", 107 | dir: fs.dir, 108 | ino: fs.ino, 109 | sfiles: fs.sfiles, 110 | }} 111 | } 112 | 113 | func (fs *virtualFS) Open(name string) (absfs.File, error) { 114 | return fs.OpenFile(name, os.O_RDONLY, 0) 115 | } 116 | 117 | func (fs *virtualFS) OpenFile(name string, flag int, perm stdfs.FileMode) (absfs.File, error) { 118 | if name == "/" { 119 | fs.mtx.RLock() 120 | sfile := fs.sfiles[int(fs.root.Ino)] 121 | fs.mtx.RUnlock() 122 | return &vfsFile{ 123 | fs: fs, 124 | name: name, 125 | flags: flag, 126 | node: fs.root, 127 | sfile: sfile, 128 | }, nil 129 | } 130 | 131 | // check that the path is valid 132 | var validPath bool 133 | if len(name) > 1 && name[0] == '/' { 134 | // if the path starts with a slash, don't call io/fs.ValidPath 135 | // with the leading slash, as we accept that but io/fs doesn't 136 | validPath = stdfs.ValidPath(name[1:]) 137 | } else { 138 | validPath = stdfs.ValidPath(name) 139 | } 140 | if !validPath { 141 | return nil, &stdfs.PathError{Op: "open", Path: name, Err: stdfs.ErrInvalid} 142 | } 143 | 144 | appendFile := flag&os.O_APPEND != 0 145 | if name == "." { 146 | fs.mtx.Lock() 147 | sfile := fs.sfiles[int(fs.dir.Ino)] 148 | fs.mtx.Unlock() 149 | 150 | file := &vfsFile{ 151 | fs: fs, 152 | name: name, 153 | flags: flag, 154 | node: fs.dir, 155 | sfile: sfile, 156 | } 157 | if sfile != nil { 158 | if appendFile { 159 | fs.dir.RLock() 160 | file.offset.Store(fs.dir.Size) 161 | fs.dir.RUnlock() 162 | } 163 | } 164 | 165 | return file, nil 166 | } 167 | 168 | fs.mtx.Lock() 169 | defer fs.mtx.Unlock() 170 | 171 | wd := fs.root 172 | if !path.IsAbs(name) { 173 | wd = fs.dir 174 | } 175 | 176 | var exists bool 177 | node, err := wd.Resolve(name) 178 | if err == nil { 179 | exists = true 180 | } 181 | 182 | dir, filename := path.Split(name) 183 | dir = path.Clean(dir) 184 | parent, err := wd.Resolve(dir) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | access := flag & _O_ACCESS 190 | create := flag&os.O_CREATE != 0 191 | truncate := flag&os.O_TRUNC != 0 192 | 193 | // error if it does not exist, and we are not allowed to create it. 194 | if !exists && !create { 195 | return nil, &stdfs.PathError{Op: "open", Path: name, Err: stdfs.ErrNotExist} 196 | } 197 | if exists { 198 | // err if exclusive create is required 199 | if create && flag&os.O_EXCL != 0 { 200 | return nil, &stdfs.PathError{Op: "open", Path: name, Err: stdfs.ErrExist} 201 | } 202 | if node.IsDir() { 203 | if access != os.O_RDONLY || truncate { 204 | return nil, &stdfs.PathError{Op: "open", Path: name, Err: syscall.EISDIR} 205 | } 206 | } 207 | } else { 208 | // Create write-able file 209 | node = fs.ino.New(perm) 210 | err := parent.Link(filename, node) 211 | if err != nil { 212 | fs.ino.SubIno() 213 | return nil, &stdfs.PathError{Op: "open", Path: name, Err: err} 214 | } 215 | 216 | file := sealedFile{} 217 | fs.sfiles = append(fs.sfiles, &file) 218 | } 219 | sfile := fs.sfiles[int(node.Ino)] 220 | 221 | file := &vfsFile{ 222 | fs: fs, 223 | name: name, 224 | flags: flag, 225 | node: node, 226 | sfile: sfile, 227 | } 228 | if sfile != nil && (truncate || appendFile) { 229 | if truncate { 230 | if err := file.Truncate(0); err != nil { 231 | return nil, &stdfs.PathError{Op: "open", Path: name, Err: err} 232 | } 233 | } 234 | if appendFile { 235 | file.offset.Store(node.Size) 236 | } 237 | } 238 | 239 | return file, nil 240 | } 241 | 242 | func (fs *virtualFS) Create(name string) (absfs.File, error) { 243 | return fs.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644) 244 | } 245 | 246 | func (fs *virtualFS) ReadFile(name string) ([]byte, error) { 247 | f, err := fs.Open(name) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | vf := f.(*vfsFile) 253 | vf.node.RLock() 254 | size := vf.node.Size 255 | vf.node.RUnlock() 256 | 257 | data := make([]byte, size) 258 | n, err := f.Read(data) 259 | if err == nil && n < len(data) { 260 | err = io.ErrUnexpectedEOF 261 | } 262 | if closeErr := f.Close(); closeErr != nil { 263 | err = errors.Join(err, closeErr) 264 | } 265 | 266 | return data, err 267 | } 268 | 269 | func (fs *virtualFS) ReadDir(name string) ([]stdfs.DirEntry, error) { 270 | f, err := fs.Open(name) 271 | if err != nil { 272 | return nil, err 273 | } 274 | defer f.Close() 275 | 276 | dirs, err := f.ReadDir(-1) 277 | if err != nil { 278 | return nil, err 279 | } 280 | slices.SortFunc(dirs, func(de1, de2 stdfs.DirEntry) int { 281 | return strings.Compare(de1.Name(), de2.Name()) 282 | }) 283 | 284 | return dirs, nil 285 | } 286 | 287 | func (fs *virtualFS) WriteFile(name string, data []byte, perm os.FileMode) error { 288 | f, err := fs.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 289 | if err != nil { 290 | return err 291 | } 292 | n, err := f.Write(data) 293 | if err == nil && n < len(data) { 294 | err = io.ErrShortWrite 295 | } 296 | if err1 := f.Close(); err == nil { 297 | err = err1 298 | } 299 | 300 | return err 301 | } 302 | 303 | func (fs *virtualFS) Mkdir(name string, perm stdfs.FileMode) error { 304 | fs.mtx.Lock() 305 | defer fs.mtx.Unlock() 306 | 307 | return fs.mkdir(name, perm) 308 | } 309 | 310 | func (fs *virtualFS) mkdir(name string, perm stdfs.FileMode) error { 311 | wd := fs.root 312 | abs := name 313 | if !path.IsAbs(abs) { 314 | abs = path.Join(fs.cwd, abs) 315 | wd = fs.dir 316 | } 317 | _, err := wd.Resolve(name) 318 | if err == nil { 319 | return &stdfs.PathError{Op: "mkdir", Path: name, Err: stdfs.ErrExist} 320 | } 321 | 322 | parent := fs.root 323 | dir, filename := path.Split(abs) 324 | dir = path.Clean(dir) 325 | if dir != "/" { 326 | parent, err = fs.root.Resolve(strings.TrimLeft(dir, "/")) 327 | if err != nil { 328 | return &stdfs.PathError{Op: "mkdir", Path: dir, Err: err} 329 | } 330 | } 331 | 332 | child := fs.ino.NewDir(perm) 333 | if err := parent.Link(filename, child); err != nil { 334 | return &stdfs.PathError{Op: "mkdir", Path: filename, Err: err} 335 | } 336 | if err := child.Link("..", parent); err != nil { 337 | return &stdfs.PathError{Op: "mkdir", Path: "..", Err: err} 338 | } 339 | fs.sfiles = append(fs.sfiles, new(sealedFile)) 340 | 341 | return nil 342 | } 343 | 344 | func (fs *virtualFS) MkdirAll(name string, perm stdfs.FileMode) error { 345 | fs.mtx.Lock() 346 | defer fs.mtx.Unlock() 347 | 348 | name = inode.Abs(fs.cwd, name) 349 | 350 | dirpath := "" 351 | for _, p := range strings.Split(name, string(fs.Separator())) { 352 | if p == "" { 353 | p = "/" 354 | } 355 | 356 | dirpath = path.Join(dirpath, p) 357 | if err := fs.mkdir(dirpath, perm); err != nil { 358 | if !errors.Is(err, stdfs.ErrExist) { 359 | return err 360 | } 361 | } 362 | } 363 | 364 | return nil 365 | } 366 | 367 | func (fs *virtualFS) Stat(name string) (stdfs.FileInfo, error) { 368 | if name == "/" { 369 | return &FileInfo{"/", fs.root}, nil 370 | } 371 | 372 | fs.mtx.RLock() 373 | node, err := fs.fileStat(fs.cwd, name) 374 | fs.mtx.RUnlock() 375 | if err != nil { 376 | return nil, err 377 | } 378 | 379 | return &FileInfo{path.Base(name), node}, nil 380 | } 381 | 382 | func (fs *virtualFS) fileStat(cwd, name string) (*inode.Inode, error) { 383 | name = inode.Abs(cwd, name) 384 | if name != "/" { 385 | name = strings.TrimLeft(name, "/") 386 | } 387 | node, err := fs.root.Resolve(name) 388 | if err != nil { 389 | return nil, &stdfs.PathError{Op: "stat", Path: name, Err: err} 390 | } 391 | 392 | return node, nil 393 | } 394 | 395 | func (fs *virtualFS) Lstat(name string) (stdfs.FileInfo, error) { 396 | return fs.Stat(name) 397 | } 398 | 399 | func (fs *virtualFS) Rename(oldpath, newpath string) error { 400 | linkErr := os.LinkError{ 401 | Op: "rename", 402 | Old: oldpath, 403 | New: newpath, 404 | } 405 | 406 | if oldpath == "/" { 407 | linkErr.Err = errors.New("the root folder may not be moved or renamed") 408 | return &linkErr 409 | } 410 | 411 | fs.mtx.Lock() 412 | defer fs.mtx.Unlock() 413 | 414 | if !path.IsAbs(oldpath) { 415 | oldpath = path.Join(fs.cwd, oldpath) 416 | } 417 | 418 | if !path.IsAbs(newpath) { 419 | newpath = path.Join(fs.cwd, newpath) 420 | } 421 | 422 | err := fs.root.Rename(oldpath, newpath) 423 | if err != nil { 424 | linkErr.Err = err 425 | return &linkErr 426 | } 427 | 428 | return nil 429 | } 430 | 431 | func (fs *virtualFS) Remove(name string) (err error) { 432 | fs.mtx.Lock() 433 | defer fs.mtx.Unlock() 434 | 435 | wd := fs.root 436 | abs := name 437 | if !path.IsAbs(abs) { 438 | abs = path.Join(fs.cwd, abs) 439 | wd = fs.dir 440 | } 441 | 442 | child, err := wd.Resolve(name) 443 | if err != nil { 444 | return &stdfs.PathError{Op: "remove", Path: name, Err: err} 445 | } 446 | 447 | if child.IsDir() { 448 | if len(child.Dir) > 2 { 449 | return &stdfs.PathError{Op: "remove", Path: name, Err: errors.New("directory not empty")} 450 | } 451 | } 452 | 453 | parent := fs.root 454 | dir, filename := path.Split(abs) 455 | dir = path.Clean(dir) 456 | if dir != "/" { 457 | parent, err = fs.root.Resolve(strings.TrimLeft(dir, "/")) 458 | if err != nil { 459 | return &stdfs.PathError{Op: "remove", Path: dir, Err: err} 460 | } 461 | } 462 | 463 | ino := parent.Ino 464 | if err := parent.Unlink(filename); err != nil { 465 | return &stdfs.PathError{Op: "remove", Path: name, Err: err} 466 | } 467 | fs.sfiles[int(ino)] = nil 468 | 469 | return nil 470 | } 471 | 472 | func (fs *virtualFS) RemoveAll(name string) error { 473 | fs.mtx.Lock() 474 | defer fs.mtx.Unlock() 475 | 476 | wd := fs.root 477 | abs := name 478 | if !path.IsAbs(abs) { 479 | abs = path.Join(fs.cwd, abs) 480 | wd = fs.dir 481 | } 482 | 483 | child, err := wd.Resolve(name) 484 | if err != nil { 485 | return &stdfs.PathError{Op: "remove", Path: name, Err: err} 486 | } 487 | 488 | parent := fs.root 489 | dir, filename := path.Split(abs) 490 | dir = path.Clean(dir) 491 | if dir != "/" { 492 | parent, err = fs.root.Resolve(strings.TrimLeft(dir, "/")) 493 | if err != nil { 494 | return &stdfs.PathError{Op: "remove", Path: dir, Err: err} 495 | } 496 | } 497 | 498 | child.UnlinkAll() 499 | 500 | return parent.Unlink(filename) 501 | } 502 | 503 | func (fs *virtualFS) Truncate(name string, size int64) error { 504 | if size < 0 { 505 | return &stdfs.PathError{Op: "truncate", Path: name, Err: stdfs.ErrInvalid} 506 | } 507 | 508 | fs.mtx.RLock() 509 | path := inode.Abs(fs.cwd, name) 510 | child, err := fs.root.Resolve(path) 511 | if err != nil { 512 | fs.mtx.RUnlock() 513 | return err 514 | } 515 | 516 | sfile := fs.sfiles[child.Ino] 517 | fs.mtx.RUnlock() 518 | 519 | file := vfsFile{ 520 | fs: fs, 521 | name: name, 522 | flags: os.O_WRONLY, 523 | node: child, 524 | sfile: sfile, 525 | } 526 | 527 | return file.Truncate(size) 528 | } 529 | 530 | func (fs *virtualFS) WalkDir(root string, fn stdfs.WalkDirFunc) error { 531 | if path.IsAbs(root) { 532 | if root == "/" { 533 | root = "." 534 | } else { 535 | root = root[1:] 536 | } 537 | } 538 | 539 | return stdfs.WalkDir(fs.FS(), root, fn) 540 | } 541 | 542 | func (fs *virtualFS) Abs(p string) (string, error) { 543 | if strings.HasPrefix(p, string(PathSeparator)) { 544 | return path.Clean(p), nil 545 | } 546 | 547 | wd, err := fs.Getwd() 548 | if err != nil { 549 | return "", err 550 | } 551 | 552 | return path.Join(wd, p), nil 553 | } 554 | 555 | func (fs *virtualFS) Separator() uint8 { 556 | return PathSeparator 557 | } 558 | 559 | func (fs *virtualFS) ListSeparator() uint8 { 560 | return PathListSeparator 561 | } 562 | 563 | func (fs *virtualFS) Chdir(name string) (err error) { 564 | fs.mtx.Lock() 565 | defer fs.mtx.Unlock() 566 | 567 | if name == "/" { 568 | fs.cwd = "/" 569 | fs.dir = fs.root 570 | return nil 571 | } 572 | 573 | wd := fs.root 574 | cwd := name 575 | if !path.IsAbs(name) { 576 | cwd = path.Join(fs.cwd, name) 577 | wd = fs.dir 578 | } 579 | 580 | node, err := wd.Resolve(name) 581 | if err != nil { 582 | return &stdfs.PathError{Op: "chdir", Path: name, Err: err} 583 | } 584 | 585 | if !node.IsDir() { 586 | return &stdfs.PathError{Op: "chdir", Path: name, Err: syscall.ENOTDIR} 587 | } 588 | 589 | fs.cwd = cwd 590 | fs.dir = node 591 | 592 | return nil 593 | } 594 | 595 | func (fs *virtualFS) Getwd() (dir string, err error) { 596 | fs.mtx.RLock() 597 | defer fs.mtx.RUnlock() 598 | 599 | return fs.cwd, nil 600 | } 601 | 602 | func (fs *virtualFS) TempDir() string { 603 | return tempDir 604 | } 605 | -------------------------------------------------------------------------------- /inode/inode_test.go: -------------------------------------------------------------------------------- 1 | package inode 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | filepath "path" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/matryer/is" 12 | ) 13 | 14 | func TestPopPath(t *testing.T) { 15 | tests := []struct { 16 | Input string 17 | Name string 18 | Trim string 19 | }{ 20 | {"", "", ""}, 21 | {"/", "/", ""}, 22 | {"/foo/bar/bat", "/", "foo/bar/bat"}, 23 | {"foo/bar/bat", "foo", "bar/bat"}, 24 | {"bar/bat", "bar", "bat"}, 25 | {"bat", "bat", ""}, 26 | } 27 | 28 | for i, test := range tests { 29 | name, trim := PopPath(test.Input) 30 | t.Logf("%q, %q := popPath(%q)", name, trim, test.Input) 31 | if name != test.Name { 32 | t.Fatalf("%d: %s != %s", i, name, test.Name) 33 | } 34 | if trim != test.Trim { 35 | t.Fatalf("%d: %s != %s", i, trim, test.Trim) 36 | } 37 | } 38 | } 39 | 40 | func TestInode(t *testing.T) { 41 | is := is.New(t) 42 | 43 | var ino Ino 44 | root := ino.NewDir(0o777) 45 | children := make([]*Inode, 100) 46 | for i := range children { 47 | ino++ 48 | children[i] = ino.New(0o666) 49 | } 50 | 51 | nLinkTest := func(t *testing.T, location string, count int) { 52 | t.Helper() 53 | 54 | for _, n := range children { 55 | if n.Nlink != uint64(count) { 56 | t.Fatalf("%s: incorrect link count %d != %d", location, n.Nlink, count) 57 | } 58 | } 59 | } 60 | nLinkTest(t, "NLT 1", 0) 61 | 62 | paths := make(map[string]*Inode) 63 | paths["/"] = root 64 | 65 | for i, n := range children { 66 | name := fmt.Sprintf("file.%04d.txt", i+2) 67 | 68 | err := root.Link(name, n) 69 | name = filepath.Join("/", name) 70 | paths[name] = n 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | } 75 | 76 | nLinkTest(t, "NLT 2", 1) 77 | 78 | CWD := "/" 79 | cwd := &CWD 80 | Mkdir := func(path string, _ os.FileMode) error { 81 | if !filepath.IsAbs(path) { 82 | path = filepath.Join(*cwd, path) 83 | } 84 | 85 | // does this path already exist? 86 | _, ok := paths[path] 87 | if ok { // if so, error 88 | return os.ErrExist 89 | } 90 | 91 | // find the parent directory 92 | dir, name := filepath.Split(path) 93 | dir = filepath.Clean(dir) 94 | parent, ok := paths[dir] 95 | if !ok { 96 | return os.ErrNotExist 97 | } 98 | 99 | // build the node 100 | dirnode := ino.NewDir(0o777) 101 | is.NoErr(dirnode.Link("..", parent)) 102 | // add a link to the parent directory 103 | is.NoErr(parent.Link(name, dirnode)) 104 | 105 | paths[path] = dirnode 106 | 107 | if dirnode.Nlink != 2 { 108 | return fmt.Errorf("incorrect link count for %q", path) 109 | } 110 | return nil // done? 111 | } 112 | 113 | is.NoErr(Mkdir("dir0001", 0o777)) 114 | 115 | CWD = "/dir0001" 116 | is.NoErr(Mkdir("dir0002", 0o777)) 117 | 118 | dirnode, ok := paths["/dir0001/dir0002"] 119 | if !ok { 120 | t.Fatal("broken path") 121 | } 122 | 123 | // dirnode.link(name, child) 124 | for path, n := range paths { 125 | name := filepath.Base(path) 126 | if !strings.HasPrefix(name, "file") { 127 | continue 128 | } 129 | is.NoErr(dirnode.Link(name, n)) 130 | name = filepath.Join("/dir0001/dir0002", name) 131 | paths[name] = n 132 | } 133 | 134 | nLinkTest(t, "NLT 3", 2) 135 | 136 | for path := range paths { 137 | if !strings.HasPrefix(path, "/file") { 138 | continue 139 | } 140 | 141 | name := filepath.Base(path) 142 | err := root.Unlink(name) 143 | if err != nil { 144 | t.Fatalf("%s %s", name, err) 145 | } 146 | delete(paths, path) 147 | } 148 | 149 | nLinkTest(t, "NLT 4", 1) 150 | 151 | type testcase struct { 152 | Path string 153 | Node *Inode 154 | } 155 | testoutput := make(chan *testcase) 156 | var walk func(node *Inode, path string) error 157 | walk = func(node *Inode, path string) error { 158 | testoutput <- &testcase{path, node} 159 | 160 | if !node.IsDir() { 161 | if node.Dir.Len() != 0 { 162 | return errors.New("is directory") 163 | } 164 | return nil 165 | } 166 | for _, suffix := range []string{"/.", "/.."} { 167 | if strings.HasSuffix(path, suffix) { 168 | return nil 169 | } 170 | } 171 | 172 | if path == "/" { 173 | path = "" 174 | } 175 | for _, entry := range node.Dir { 176 | err := walk(entry.Inode, path+"/"+entry.Name) 177 | if err != nil { 178 | return err 179 | } 180 | } 181 | return nil 182 | } 183 | 184 | go func() { 185 | defer close(testoutput) 186 | is.NoErr(walk(root, "/")) 187 | }() 188 | 189 | tests := []struct { 190 | Path string 191 | Ino uint64 192 | }{ 193 | {"/", 1}, 194 | {"/.", 1}, 195 | {"/..", 1}, 196 | {"/dir0001", 202}, 197 | {"/dir0001/.", 202}, 198 | {"/dir0001/..", 1}, 199 | {"/dir0001/dir0002", 203}, 200 | {"/dir0001/dir0002/.", 203}, 201 | {"/dir0001/dir0002/..", 202}, 202 | {"/dir0001/dir0002/file.0002.txt", 3}, 203 | {"/dir0001/dir0002/file.0003.txt", 5}, 204 | {"/dir0001/dir0002/file.0004.txt", 7}, 205 | {"/dir0001/dir0002/file.0005.txt", 9}, 206 | {"/dir0001/dir0002/file.0006.txt", 11}, 207 | {"/dir0001/dir0002/file.0007.txt", 13}, 208 | {"/dir0001/dir0002/file.0008.txt", 15}, 209 | {"/dir0001/dir0002/file.0009.txt", 17}, 210 | {"/dir0001/dir0002/file.0010.txt", 19}, 211 | {"/dir0001/dir0002/file.0011.txt", 21}, 212 | {"/dir0001/dir0002/file.0012.txt", 23}, 213 | {"/dir0001/dir0002/file.0013.txt", 25}, 214 | {"/dir0001/dir0002/file.0014.txt", 27}, 215 | {"/dir0001/dir0002/file.0015.txt", 29}, 216 | {"/dir0001/dir0002/file.0016.txt", 31}, 217 | {"/dir0001/dir0002/file.0017.txt", 33}, 218 | {"/dir0001/dir0002/file.0018.txt", 35}, 219 | {"/dir0001/dir0002/file.0019.txt", 37}, 220 | {"/dir0001/dir0002/file.0020.txt", 39}, 221 | {"/dir0001/dir0002/file.0021.txt", 41}, 222 | {"/dir0001/dir0002/file.0022.txt", 43}, 223 | {"/dir0001/dir0002/file.0023.txt", 45}, 224 | {"/dir0001/dir0002/file.0024.txt", 47}, 225 | {"/dir0001/dir0002/file.0025.txt", 49}, 226 | {"/dir0001/dir0002/file.0026.txt", 51}, 227 | {"/dir0001/dir0002/file.0027.txt", 53}, 228 | {"/dir0001/dir0002/file.0028.txt", 55}, 229 | {"/dir0001/dir0002/file.0029.txt", 57}, 230 | {"/dir0001/dir0002/file.0030.txt", 59}, 231 | {"/dir0001/dir0002/file.0031.txt", 61}, 232 | {"/dir0001/dir0002/file.0032.txt", 63}, 233 | {"/dir0001/dir0002/file.0033.txt", 65}, 234 | {"/dir0001/dir0002/file.0034.txt", 67}, 235 | {"/dir0001/dir0002/file.0035.txt", 69}, 236 | {"/dir0001/dir0002/file.0036.txt", 71}, 237 | {"/dir0001/dir0002/file.0037.txt", 73}, 238 | {"/dir0001/dir0002/file.0038.txt", 75}, 239 | {"/dir0001/dir0002/file.0039.txt", 77}, 240 | {"/dir0001/dir0002/file.0040.txt", 79}, 241 | {"/dir0001/dir0002/file.0041.txt", 81}, 242 | {"/dir0001/dir0002/file.0042.txt", 83}, 243 | {"/dir0001/dir0002/file.0043.txt", 85}, 244 | {"/dir0001/dir0002/file.0044.txt", 87}, 245 | {"/dir0001/dir0002/file.0045.txt", 89}, 246 | {"/dir0001/dir0002/file.0046.txt", 91}, 247 | {"/dir0001/dir0002/file.0047.txt", 93}, 248 | {"/dir0001/dir0002/file.0048.txt", 95}, 249 | {"/dir0001/dir0002/file.0049.txt", 97}, 250 | {"/dir0001/dir0002/file.0050.txt", 99}, 251 | {"/dir0001/dir0002/file.0051.txt", 101}, 252 | {"/dir0001/dir0002/file.0052.txt", 103}, 253 | {"/dir0001/dir0002/file.0053.txt", 105}, 254 | {"/dir0001/dir0002/file.0054.txt", 107}, 255 | {"/dir0001/dir0002/file.0055.txt", 109}, 256 | {"/dir0001/dir0002/file.0056.txt", 111}, 257 | {"/dir0001/dir0002/file.0057.txt", 113}, 258 | {"/dir0001/dir0002/file.0058.txt", 115}, 259 | {"/dir0001/dir0002/file.0059.txt", 117}, 260 | {"/dir0001/dir0002/file.0060.txt", 119}, 261 | {"/dir0001/dir0002/file.0061.txt", 121}, 262 | {"/dir0001/dir0002/file.0062.txt", 123}, 263 | {"/dir0001/dir0002/file.0063.txt", 125}, 264 | {"/dir0001/dir0002/file.0064.txt", 127}, 265 | {"/dir0001/dir0002/file.0065.txt", 129}, 266 | {"/dir0001/dir0002/file.0066.txt", 131}, 267 | {"/dir0001/dir0002/file.0067.txt", 133}, 268 | {"/dir0001/dir0002/file.0068.txt", 135}, 269 | {"/dir0001/dir0002/file.0069.txt", 137}, 270 | {"/dir0001/dir0002/file.0070.txt", 139}, 271 | {"/dir0001/dir0002/file.0071.txt", 141}, 272 | {"/dir0001/dir0002/file.0072.txt", 143}, 273 | {"/dir0001/dir0002/file.0073.txt", 145}, 274 | {"/dir0001/dir0002/file.0074.txt", 147}, 275 | {"/dir0001/dir0002/file.0075.txt", 149}, 276 | {"/dir0001/dir0002/file.0076.txt", 151}, 277 | {"/dir0001/dir0002/file.0077.txt", 153}, 278 | {"/dir0001/dir0002/file.0078.txt", 155}, 279 | {"/dir0001/dir0002/file.0079.txt", 157}, 280 | {"/dir0001/dir0002/file.0080.txt", 159}, 281 | {"/dir0001/dir0002/file.0081.txt", 161}, 282 | {"/dir0001/dir0002/file.0082.txt", 163}, 283 | {"/dir0001/dir0002/file.0083.txt", 165}, 284 | {"/dir0001/dir0002/file.0084.txt", 167}, 285 | {"/dir0001/dir0002/file.0085.txt", 169}, 286 | {"/dir0001/dir0002/file.0086.txt", 171}, 287 | {"/dir0001/dir0002/file.0087.txt", 173}, 288 | {"/dir0001/dir0002/file.0088.txt", 175}, 289 | {"/dir0001/dir0002/file.0089.txt", 177}, 290 | {"/dir0001/dir0002/file.0090.txt", 179}, 291 | {"/dir0001/dir0002/file.0091.txt", 181}, 292 | {"/dir0001/dir0002/file.0092.txt", 183}, 293 | {"/dir0001/dir0002/file.0093.txt", 185}, 294 | {"/dir0001/dir0002/file.0094.txt", 187}, 295 | {"/dir0001/dir0002/file.0095.txt", 189}, 296 | {"/dir0001/dir0002/file.0096.txt", 191}, 297 | {"/dir0001/dir0002/file.0097.txt", 193}, 298 | {"/dir0001/dir0002/file.0098.txt", 195}, 299 | {"/dir0001/dir0002/file.0099.txt", 197}, 300 | {"/dir0001/dir0002/file.0100.txt", 199}, 301 | {"/dir0001/dir0002/file.0101.txt", 201}, 302 | } 303 | 304 | i := 0 305 | for test := range testoutput { 306 | if test.Path != tests[i].Path { 307 | t.Fatalf("expected different path %q != %q", test.Path, tests[i].Path) 308 | } 309 | if test.Node.Ino != tests[i].Ino { 310 | t.Fatalf("expected different Inode Number (Ino) %d != %d", 311 | test.Node.Ino, tests[i].Ino) 312 | } 313 | i++ 314 | } 315 | } 316 | 317 | func TestLinkUnlinkMove(t *testing.T) { 318 | ino := new(Ino) 319 | 320 | root := ino.NewDir(0o777) 321 | dirs := make([]*Inode, 2) 322 | var err error 323 | 324 | for i := range dirs { 325 | dirs[i] = ino.NewDir(0o777) 326 | err = root.Link(fmt.Sprintf("dir%02d", i), dirs[i]) 327 | if err != nil { 328 | t.Fatal(err) 329 | } 330 | } 331 | 332 | files := make([]*Inode, 2) 333 | for i := range files { 334 | files[i] = ino.New(0o666) 335 | err = root.Link(fmt.Sprintf("file_%04d.txt", i), files[i]) 336 | if err != nil { 337 | t.Fatal(err) 338 | } 339 | } 340 | list := []string{ 341 | "/", 342 | "/dir00/", 343 | "/dir01/", 344 | "/file_0000.txt", 345 | "/file_0001.txt", 346 | } 347 | i := 0 348 | err = Walk(root, "/", func(path string, n *Inode) error { 349 | if strings.HasSuffix(path, "/..") || strings.HasSuffix(path, "/.") { 350 | return nil 351 | } 352 | if n.IsDir() && path != "/" { 353 | path += "/" 354 | } 355 | if list[i] != path { 356 | t.Fatalf("expected file listing to match %s != %s", list[i], path) 357 | } 358 | i++ 359 | return nil 360 | }) 361 | if err != nil { 362 | t.Fatal(err) 363 | } 364 | 365 | err = root.Rename("/file_0001.txt", "/dir01/file_0001.txt") 366 | if err != nil { 367 | t.Fatal(err) 368 | } 369 | 370 | list = []string{ 371 | "/", 372 | "/dir00/", 373 | "/dir01/", 374 | "/dir01/file_0001.txt", 375 | "/file_0000.txt", 376 | } 377 | i = 0 378 | err = Walk(root, "/", func(path string, n *Inode) error { 379 | if strings.HasSuffix(path, "/..") || strings.HasSuffix(path, "/.") { 380 | return nil 381 | } 382 | if n.IsDir() && path != "/" { 383 | path += "/" 384 | } 385 | if list[i] != path { 386 | t.Fatalf("expected file listing to match %s != %s", list[i], path) 387 | } 388 | i++ 389 | return nil 390 | }) 391 | if err != nil { 392 | t.Fatal(err) 393 | } 394 | 395 | // move with simultaneous rename 396 | err = root.Rename("/file_0000.txt", "/dir01/file_0003.txt") 397 | if err != nil { 398 | t.Fatal(err) 399 | } 400 | 401 | err = root.Rename("/dir01", "/dir00/dir01") 402 | if err != nil { 403 | t.Fatal(err) 404 | } 405 | 406 | list = []string{ 407 | "/", 408 | "/dir00/", 409 | "/dir00/dir01/", 410 | "/dir00/dir01/file_0001.txt", 411 | "/dir00/dir01/file_0003.txt", 412 | } 413 | i = 0 414 | err = Walk(root, "/", func(path string, n *Inode) error { 415 | if strings.HasSuffix(path, "/..") || strings.HasSuffix(path, "/.") { 416 | return nil 417 | } 418 | if n.IsDir() && path != "/" { 419 | path += "/" 420 | } 421 | if list[i] != path { 422 | t.Fatalf("expected file listing to match %s != %s", list[i], path) 423 | } 424 | i++ 425 | return nil 426 | }) 427 | if err != nil { 428 | t.Fatal(err) 429 | } 430 | } 431 | 432 | func TestResolve(t *testing.T) { 433 | is := is.New(t) 434 | 435 | ino := new(Ino) 436 | 437 | var root, parent, dir *Inode 438 | root = ino.NewDir(0o777) 439 | parent = root 440 | 441 | dir = ino.NewDir(0o777) 442 | err := parent.Link("tmp", dir) 443 | if err != nil { 444 | t.Fatal(err) 445 | } 446 | err = dir.Link("..", parent) 447 | if err != nil { 448 | t.Fatal(err) 449 | } 450 | 451 | parent = dir 452 | dir = ino.NewDir(0o777) 453 | is.NoErr(parent.Link("foo", dir)) 454 | is.NoErr(dir.Link("..", parent)) 455 | 456 | dir = ino.NewDir(0o777) 457 | is.NoErr(parent.Link("bar", dir)) 458 | is.NoErr(dir.Link("..", parent)) 459 | 460 | dir = ino.NewDir(0o777) 461 | is.NoErr(parent.Link("bat", dir)) 462 | is.NoErr(dir.Link("..", parent)) 463 | 464 | tests := []struct { 465 | Path string 466 | Ino uint64 467 | }{ 468 | { 469 | Path: "/", 470 | Ino: 1, 471 | }, 472 | { 473 | Path: "/.", 474 | Ino: 1, 475 | }, 476 | { 477 | Path: "/..", 478 | Ino: 1, 479 | }, 480 | { 481 | Path: "/tmp", 482 | Ino: 2, 483 | }, 484 | { 485 | Path: "/tmp/.", 486 | Ino: 2, 487 | }, 488 | { 489 | Path: "/tmp/..", 490 | Ino: 1, 491 | }, 492 | { 493 | Path: "/tmp/bar", 494 | Ino: 4, 495 | }, 496 | { 497 | Path: "/tmp/bar/.", 498 | Ino: 4, 499 | }, 500 | { 501 | Path: "/tmp/bar/..", 502 | Ino: 2, 503 | }, 504 | { 505 | Path: "/tmp/bat", 506 | Ino: 5, 507 | }, 508 | { 509 | Path: "/tmp/bat/.", 510 | Ino: 5, 511 | }, 512 | { 513 | Path: "/tmp/bat/..", 514 | Ino: 2, 515 | }, 516 | { 517 | Path: "/tmp/foo", 518 | Ino: 3, 519 | }, 520 | { 521 | Path: "/tmp/foo/.", 522 | Ino: 3, 523 | }, 524 | { 525 | Path: "/tmp/foo/..", 526 | Ino: 2, 527 | }, 528 | } 529 | _ = tests 530 | count := 0 531 | 532 | type testcase struct { 533 | Path string 534 | Node *Inode 535 | } 536 | 537 | testoutput := make(chan *testcase) 538 | var walk func(node *Inode, path string) error 539 | walk = func(node *Inode, path string) error { 540 | count++ 541 | if count > 20 { 542 | return errors.New("counted to far") 543 | } 544 | 545 | // fmt.Printf("%d %d %s\n", node.Ino, node.Nlink, path) 546 | testoutput <- &testcase{path, node} 547 | 548 | if !node.IsDir() { 549 | if node.Dir.Len() != 0 { 550 | return errors.New("is directory") 551 | } 552 | return nil 553 | } 554 | for _, suffix := range []string{"/.", "/.."} { 555 | if strings.HasSuffix(path, suffix) { 556 | return nil 557 | } 558 | } 559 | 560 | if path == "/" { 561 | path = "" 562 | } 563 | for _, entry := range node.Dir { 564 | err := walk(entry.Inode, path+"/"+entry.Name) 565 | if err != nil { 566 | return err 567 | } 568 | } 569 | return nil 570 | } 571 | go func() { 572 | defer close(testoutput) 573 | is.NoErr(walk(root, "/")) 574 | }() 575 | 576 | i := 0 577 | for test := range testoutput { 578 | if tests[i].Path != test.Path { 579 | t.Errorf("Path: expected %q, got %q", tests[i].Path, test.Path) 580 | } 581 | 582 | if tests[i].Ino != test.Node.Ino { 583 | t.Errorf("Ino: expected %d, got %d -- %q, %q", tests[i].Ino, test.Node.Ino, tests[i].Path, test.Path) 584 | } 585 | i++ 586 | } 587 | 588 | t.Run("resolve", func(t *testing.T) { 589 | tests := make(map[string]uint64) 590 | tests["/"] = 1 591 | tests["/tmp"] = 2 592 | tests["/tmp/bar"] = 4 593 | tests["/tmp/bat"] = 5 594 | tests["/tmp/foo"] = 3 595 | var dir *Inode 596 | for Path, Ino := range tests { 597 | node, err := root.Resolve(Path) 598 | if err != nil { 599 | t.Fatal(err) 600 | } 601 | if Path == "/tmp/foo" { 602 | dir = node 603 | } 604 | if node.Ino != Ino { 605 | t.Fatalf("Ino: %d, Expected: %d\n", node.Ino, Ino) 606 | } 607 | } 608 | 609 | // test relative paths 610 | tests = make(map[string]uint64) 611 | tests["../.."] = 1 612 | tests[".."] = 2 613 | tests["../bar"] = 4 614 | tests["../bat"] = 5 615 | tests["."] = 3 616 | for Path, Ino := range tests { 617 | node, err := dir.Resolve(Path) 618 | if err != nil { 619 | t.Fatal(err) 620 | } 621 | 622 | t.Logf("%d %q \t%q", node.Ino, Path, filepath.Join("/tmp/foo", Path)) 623 | if node.Ino != Ino { 624 | t.Fatalf("Ino: %d, Expected: %d\n", node.Ino, Ino) 625 | } 626 | } 627 | }) 628 | } 629 | 630 | func Walk(node *Inode, path string, fn func(path string, n *Inode) error) error { 631 | err := fn(path, node) 632 | if err != nil { 633 | return err 634 | } 635 | 636 | if !node.IsDir() { 637 | if node.Dir.Len() != 0 { 638 | return errors.New("is directory") 639 | } 640 | return nil 641 | } 642 | 643 | for _, suffix := range []string{"/.", "/.."} { 644 | if strings.HasSuffix(path, suffix) { 645 | return nil 646 | } 647 | } 648 | 649 | if path == "/" { 650 | path = "" 651 | } 652 | for _, entry := range node.Dir { 653 | err := Walk(entry.Inode, path+"/"+entry.Name, fn) 654 | if err != nil { 655 | return err 656 | } 657 | } 658 | return nil 659 | } 660 | -------------------------------------------------------------------------------- /vfs/vfs_test.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "errors" 7 | "io" 8 | "io/fs" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | "testing" 15 | "testing/fstest" 16 | "testing/iotest" 17 | "time" 18 | 19 | "github.com/matryer/is" 20 | 21 | "github.com/capnspacehook/pandorasbox/absfs" 22 | "github.com/capnspacehook/pandorasbox/ioutil" 23 | ) 24 | 25 | const ( 26 | dots = "1....2....3....4" 27 | abc = "abcdefghijklmnop" 28 | 29 | filename = "testfile" 30 | renameFrom = "renamefrom" 31 | renameTo = "renameto" 32 | ) 33 | 34 | func TestVFS(t *testing.T) { 35 | if testing.Short() { 36 | t.SkipNow() 37 | } 38 | 39 | vfs := NewFS() 40 | 41 | if err := vfs.Mkdir("memz", 0o777); err != nil { 42 | t.Fatalf("error creating dir: %v", err) 43 | } 44 | 45 | f, err := vfs.Create("memz/chungus") 46 | if err != nil { 47 | t.Fatalf("error creating file: %v", err) 48 | } 49 | if _, err := f.Write([]byte("The quick brown fox jumped over the lazy dog.\n")); err != nil { 50 | t.Fatalf("error writing to file: %v", err) 51 | } 52 | if err := f.Close(); err != nil { 53 | t.Errorf("error closing created file: %v", err) 54 | } 55 | 56 | if err := fstest.TestFS(vfs.FS(), "memz/chungus"); err != nil { 57 | t.Errorf("error testing vfs: %v", err) 58 | } 59 | } 60 | 61 | func TestFileReader(t *testing.T) { 62 | vfs := NewFS() 63 | 64 | contents := make([]byte, 1000) 65 | if _, err := rand.Read(contents); err != nil { 66 | t.Fatalf("error getting random contents: %v", err) 67 | } 68 | 69 | f, err := vfs.Create("file") 70 | if err != nil { 71 | t.Fatalf("error creating file: %v", err) 72 | } 73 | n, err := f.Write(contents) 74 | if n != len(contents) { 75 | t.Fatalf("didn't write all of contents; got %v want %v", n, len(contents)) 76 | } 77 | if err != nil { 78 | t.Fatalf("error writing to file: %v", err) 79 | } 80 | 81 | o, err := f.Seek(0, io.SeekStart) 82 | if o != 0 { 83 | t.Fatalf("seek didn't seek to start of file; got %v want %v", o, 0) 84 | } 85 | if err != nil { 86 | t.Fatalf("error seeking in file: %v", err) 87 | } 88 | 89 | if err := iotest.TestReader(f, contents); err != nil { 90 | t.Error(err) 91 | } 92 | 93 | if err := f.Close(); err != nil { 94 | t.Fatalf("error closing file: %v", err) 95 | } 96 | } 97 | 98 | func TestMkdir(t *testing.T) { 99 | vfs := NewFS() 100 | 101 | if vfs.TempDir() != "/tmp" { 102 | t.Fatalf("wrong TempDir output: %q != %q", vfs.TempDir(), "/tmp") 103 | } 104 | 105 | testdir := path.Join(vfs.TempDir(), "mkdir_test") 106 | t.Logf("Test path: %q", testdir) 107 | 108 | err := vfs.MkdirAll(testdir, 0o777) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | var list []fs.DirEntry 114 | path := "/" 115 | outer: 116 | for _, name := range strings.Split(testdir, "/")[1:] { 117 | if name == "" { 118 | continue 119 | } 120 | f, err := vfs.Open(path) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | list, err = f.ReadDir(-1) 125 | f.Close() 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | for _, n := range list { 130 | if n.Name() == name { 131 | path = filepath.Join(path, name) 132 | continue outer 133 | } 134 | } 135 | t.Errorf("path error: %q + %q: %s", path, name, list) 136 | } 137 | } 138 | 139 | func TestOpenWrite(t *testing.T) { 140 | vfs := NewFS() 141 | 142 | f, err := vfs.Create("/test_file.txt") 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | data := []byte("The quick brown fox jumped over the lazy dog.\n") 148 | n, err := f.Write(data) 149 | f.Close() 150 | if n != len(data) { 151 | t.Errorf("write error: wrong byte count %d, expected %d", n, len(data)) 152 | } 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | 157 | f, err = vfs.Open("/test_file.txt") 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | buff := make([]byte, 512) 162 | n, err = f.Read(buff) 163 | f.Close() 164 | if n != len(data) { 165 | t.Errorf("write error: wrong byte count %d, expected %d", n, len(data)) 166 | } 167 | if !errors.Is(err, io.EOF) { 168 | t.Fatal("expected EOF, got nil error") 169 | } 170 | buff = buff[:n] 171 | if !bytes.Equal(data, buff) { 172 | t.Log(string(data)) 173 | t.Log(string(buff)) 174 | 175 | t.Fatal("bytes written do not compare to bytes read") 176 | } 177 | } 178 | 179 | func TestCreate(t *testing.T) { 180 | vfs := NewFS() 181 | // Create file with absolute path 182 | { 183 | f, err := vfs.OpenFile("/testfile", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666) 184 | if err != nil { 185 | t.Fatalf("Unexpected error creating file: %v", err) 186 | } 187 | if name := f.Name(); name != "/testfile" { 188 | t.Errorf("Wrong name: %s", name) 189 | } 190 | } 191 | 192 | // Create same file again 193 | { 194 | _, err := vfs.OpenFile("/testfile", os.O_RDWR|os.O_CREATE, 0o666) 195 | if err != nil { 196 | t.Fatalf("Unexpected error creating file: %v", err) 197 | } 198 | 199 | } 200 | 201 | // Create same file again, but truncate it 202 | { 203 | _, err := vfs.OpenFile("/testfile", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666) 204 | if err != nil { 205 | t.Fatalf("Unexpected error creating file: %v", err) 206 | } 207 | } 208 | 209 | // Create same file again with O_CREATE|O_EXCL, which is an error 210 | { 211 | _, err := vfs.OpenFile("/testfile", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666) 212 | if err == nil { 213 | t.Fatalf("Expected error creating file: %v", err) 214 | } 215 | } 216 | 217 | // Create file with unknown parent 218 | { 219 | _, err := vfs.OpenFile("/testfile/testfile", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666) 220 | if err == nil { 221 | t.Errorf("Expected error creating file") 222 | } 223 | } 224 | 225 | // Create file with relative path (workingDir == root) 226 | { 227 | f, err := vfs.OpenFile("relFile", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666) 228 | if err != nil { 229 | t.Fatalf("Unexpected error creating file: %v", err) 230 | } 231 | if name := f.Name(); name != "relFile" { 232 | t.Errorf("Wrong name: %s", name) 233 | } 234 | } 235 | } 236 | 237 | func TestMkdirAbsRel(t *testing.T) { 238 | vfs := NewFS() 239 | 240 | // Create dir with absolute path 241 | { 242 | err := vfs.Mkdir("/usr", 0) 243 | if err != nil { 244 | t.Fatalf("Unexpected error creating directory: %v", err) 245 | } 246 | } 247 | 248 | // Create dir with relative path 249 | { 250 | err := vfs.Mkdir("home", 0) 251 | if err != nil { 252 | t.Fatalf("Unexpected error creating directory: %v", err) 253 | } 254 | } 255 | 256 | // Create dir twice 257 | { 258 | err := vfs.Mkdir("/home", 0) 259 | if err == nil { 260 | t.Fatalf("Expecting error creating directory: %s", "/home") 261 | } 262 | } 263 | } 264 | 265 | func TestMkdirTree(t *testing.T) { 266 | vfs := NewFS() 267 | 268 | err := vfs.Mkdir("/home", 0) 269 | if err != nil { 270 | t.Fatalf("Unexpected error creating directory /home: %v", err) 271 | } 272 | 273 | err = vfs.Mkdir("/home/blang", 0) 274 | if err != nil { 275 | t.Fatalf("Unexpected error creating directory /home/blang: %v", err) 276 | } 277 | 278 | err = vfs.Mkdir("/home/blang/goprojects", 0) 279 | if err != nil { 280 | t.Fatalf("Unexpected error creating directory /home/blang/goprojects: %v", err) 281 | } 282 | 283 | err = vfs.Mkdir("/home/johndoe/goprojects", 0) 284 | if err == nil { 285 | t.Errorf("Expected error creating directory with non-existing parent") 286 | } 287 | 288 | // TODO: Subdir of file 289 | } 290 | 291 | func TestRemove(t *testing.T) { 292 | vfs := NewFS() 293 | err := vfs.Mkdir("/tmp", 0o777) 294 | if err != nil { 295 | t.Fatalf("Mkdir error: %v", err) 296 | } 297 | f, err := vfs.OpenFile("/tmp/README.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666) 298 | if err != nil { 299 | t.Fatalf("Create error: %v", err) 300 | } 301 | if _, err := f.Write([]byte("test")); err != nil { 302 | t.Fatalf("Write error: %v", err) 303 | } 304 | f.Close() 305 | 306 | // remove non existing file 307 | if err := vfs.Remove("/nonexisting.txt"); err == nil { 308 | t.Errorf("Expected remove to fail") 309 | } 310 | 311 | // remove non existing file from an non existing directory 312 | if err := vfs.Remove("/nonexisting/nonexisting.txt"); err == nil { 313 | t.Errorf("Expected remove to fail") 314 | } 315 | 316 | // remove created file 317 | err = vfs.Remove(f.Name()) 318 | if err != nil { 319 | t.Errorf("Remove failed: %v", err) 320 | } 321 | 322 | if _, err = vfs.OpenFile("/tmp/README.txt", os.O_RDWR, 0o666); err == nil { 323 | t.Errorf("Could open removed file!") 324 | } 325 | 326 | err = vfs.Remove("/tmp") 327 | if err != nil { 328 | t.Errorf("Remove failed: %v", err) 329 | } 330 | fis, err := vfs.ReadDir("/") 331 | if err != nil { 332 | t.Errorf("Readdir error: %v", err) 333 | } else if len(fis) != 0 { 334 | t.Errorf("Found files: %s", fis) 335 | } 336 | } 337 | 338 | // Read with length 0 should not return EOF. 339 | func TestRead0(t *testing.T) { 340 | vfs := NewFS() 341 | f, err := vfs.Create(filename) 342 | if err != nil { 343 | t.Fatal("open failed:", err) 344 | } 345 | if _, err := f.WriteString(abc); err != nil { 346 | t.Fatal("writing failed:", err) 347 | } 348 | if _, err := f.Seek(0, io.SeekStart); err != nil { 349 | t.Fatal("seeking to beginning failed:", err) 350 | } 351 | defer f.Close() 352 | 353 | b := make([]byte, 0) 354 | n, err := f.Read(b) 355 | if n != 0 || err != nil { 356 | t.Errorf("Read(0) = %d, %v, want 0, nil", n, err) 357 | } 358 | b = make([]byte, 10) 359 | n, err = f.Read(b) 360 | if n <= 0 || err != nil { 361 | t.Errorf("Read(10) = %d, %v, want >0, nil", n, err) 362 | } 363 | } 364 | 365 | // Reading a closed file should return ErrClosed error 366 | func TestReadClosed(t *testing.T) { 367 | vfs := NewFS() 368 | file, err := vfs.Create(filename) 369 | if err != nil { 370 | t.Fatal("open failed:", err) 371 | } 372 | file.Close() // close immediately 373 | 374 | b := make([]byte, 100) 375 | _, err = file.Read(b) 376 | 377 | var pErr *os.PathError 378 | if !errors.As(err, &pErr) { 379 | t.Fatalf("Read: %T(%v), want PathError", pErr, pErr) 380 | } 381 | 382 | if !errors.Is(pErr.Err, os.ErrClosed) { 383 | t.Errorf("Read: %v, want PathError(ErrClosed)", pErr) 384 | } 385 | } 386 | 387 | func TestReadWrite(t *testing.T) { 388 | vfs := NewFS() 389 | f, err := vfs.OpenFile("/readme.txt", os.O_CREATE|os.O_RDWR, 0o666) 390 | if err != nil { 391 | t.Fatalf("Could not open file: %v", err) 392 | } 393 | 394 | // Write first dots 395 | if n, err := f.Write([]byte(dots)); err != nil { 396 | t.Errorf("Unexpected error: %v", err) 397 | } else if n != len(dots) { 398 | t.Errorf("Invalid write count: %d", n) 399 | } 400 | 401 | // Write abc 402 | if n, err := f.Write([]byte(abc)); err != nil { 403 | t.Errorf("Unexpected error: %v", err) 404 | } else if n != len(abc) { 405 | t.Errorf("Invalid write count: %d", n) 406 | } 407 | 408 | // Seek to beginning of file 409 | if n, err := f.Seek(0, io.SeekStart); err != nil || n != 0 { 410 | t.Errorf("Seek error: %d %v", n, err) 411 | } 412 | 413 | // Seek to end of file 414 | if n, err := f.Seek(0, io.SeekEnd); err != nil || n != 32 { 415 | t.Errorf("Seek error: %d %v", n, err) 416 | } 417 | 418 | // Write dots at end of file 419 | if n, err := f.Write([]byte(dots)); err != nil { 420 | t.Errorf("Unexpected error: %v", err) 421 | } else if n != len(dots) { 422 | t.Errorf("Invalid write count: %d", n) 423 | } 424 | 425 | // Seek to beginning of file 426 | if n, err := f.Seek(0, io.SeekStart); err != nil || n != 0 { 427 | t.Errorf("Seek error: %d %v", n, err) 428 | } 429 | 430 | p := make([]byte, len(dots)+len(abc)+len(dots)) 431 | if n, err := f.Read(p); err != nil || n != len(dots)+len(abc)+len(dots) { 432 | t.Errorf("Read error: %d %v", n, err) 433 | } else if s := string(p); s != dots+abc+dots { 434 | t.Errorf("Invalid read: %s", s) 435 | } 436 | } 437 | 438 | func TestOpenRO(t *testing.T) { 439 | vfs := NewFS() 440 | f, err := vfs.OpenFile("/readme.txt", os.O_CREATE|os.O_RDONLY, 0o666) 441 | if err != nil { 442 | t.Fatalf("Could not open file: %v", err) 443 | } 444 | 445 | // Write first dots 446 | if _, err := f.Write([]byte(dots)); err == nil { 447 | t.Fatalf("Expected write error") 448 | } 449 | f.Close() 450 | } 451 | 452 | func TestOpenWO(t *testing.T) { 453 | vfs := NewFS() 454 | f, err := vfs.OpenFile("/readme.txt", os.O_CREATE|os.O_WRONLY, 0o666) 455 | if err != nil { 456 | t.Fatalf("Could not open file: %v", err) 457 | } 458 | 459 | // Write first dots 460 | if n, err := f.Write([]byte(dots)); err != nil { 461 | t.Errorf("Unexpected error: %v", err) 462 | } else if n != len(dots) { 463 | t.Errorf("Invalid write count: %d", n) 464 | } 465 | 466 | // Seek to beginning of file 467 | if n, err := f.Seek(0, io.SeekStart); err != nil || n != 0 { 468 | t.Errorf("Seek error: %d %v", n, err) 469 | } 470 | 471 | // Try reading 472 | p := make([]byte, len(dots)) 473 | if n, err := f.Read(p); err == nil || n > 0 { 474 | t.Errorf("Expected invalid read: %d %v", n, err) 475 | } 476 | 477 | f.Close() 478 | } 479 | 480 | func TestOpenAppend(t *testing.T) { 481 | vfs := NewFS() 482 | f, err := vfs.OpenFile("/readme.txt", os.O_CREATE|os.O_RDWR, 0o666) 483 | if err != nil { 484 | t.Fatalf("Could not open file: %v", err) 485 | } 486 | 487 | // Write first dots 488 | if n, err := f.Write([]byte(dots)); err != nil { 489 | t.Errorf("Unexpected error: %v", err) 490 | } else if n != len(dots) { 491 | t.Errorf("Invalid write count: %d", n) 492 | } 493 | f.Close() 494 | 495 | // Reopen file in append mode 496 | f, err = vfs.OpenFile("/readme.txt", os.O_APPEND|os.O_RDWR, 0o666) 497 | if err != nil { 498 | t.Fatalf("Could not open file: %v", err) 499 | } 500 | 501 | // append dots 502 | if n, err := f.Write([]byte(abc)); err != nil { 503 | t.Errorf("Unexpected error: %v", err) 504 | } else if n != len(abc) { 505 | t.Errorf("Invalid write count: %d", n) 506 | } 507 | 508 | // Seek to beginning of file 509 | if n, err := f.Seek(0, io.SeekStart); err != nil || n != 0 { 510 | t.Errorf("Seek error: %d %v", n, err) 511 | } 512 | 513 | p := make([]byte, len(dots)+len(abc)) 514 | if n, err := f.Read(p); err != nil || n != len(dots)+len(abc) { 515 | t.Errorf("Read error: %d %v", n, err) 516 | } else if s := string(p); s != dots+abc { 517 | t.Errorf("Invalid read: %s", s) 518 | } 519 | f.Close() 520 | } 521 | 522 | func TestTruncateToLength(t *testing.T) { 523 | params := []struct { 524 | size int64 525 | err bool 526 | }{ 527 | {-1, true}, 528 | {0, false}, 529 | {int64(len(dots) - 1), false}, 530 | {int64(len(dots)), false}, 531 | {int64(len(dots) + 1), false}, 532 | } 533 | 534 | for _, param := range params { 535 | vfs := NewFS() 536 | f, err := vfs.OpenFile("/readme.txt", os.O_CREATE|os.O_RDWR, 0o666) 537 | if err != nil { 538 | t.Fatalf("Could not open file: %v", err) 539 | } 540 | if n, err := f.Write([]byte(dots)); err != nil { 541 | t.Errorf("Unexpected error: %v", err) 542 | } else if n != len(dots) { 543 | t.Errorf("Invalid write count: %d", n) 544 | } 545 | f.Close() 546 | 547 | newSize := param.size 548 | err = vfs.Truncate("/readme.txt", newSize) 549 | if param.err { 550 | if err == nil { 551 | t.Errorf("Error expected truncating file to length %d", newSize) 552 | } 553 | return 554 | } else if err != nil { 555 | t.Errorf("Error truncating file: %v", err) 556 | } 557 | 558 | b, err := vfs.ReadFile("/readme.txt") 559 | if err != nil { 560 | t.Errorf("Error reading truncated file: %v", err) 561 | } 562 | if int64(len(b)) != newSize { 563 | t.Errorf("File should be empty after truncation: %d", len(b)) 564 | } 565 | if fi, err := vfs.Stat("/readme.txt"); err != nil { 566 | t.Errorf("Error stat file: %v", err) 567 | } else if fi.Size() != newSize { 568 | t.Errorf("Filesize should be %d after truncation", newSize) 569 | } 570 | } 571 | } 572 | 573 | func TestTruncateToZero(t *testing.T) { 574 | const content = "read me" 575 | vfs := NewFS() 576 | if err := vfs.WriteFile("/readme.txt", []byte(content), 0o666); err != nil { 577 | t.Errorf("Unexpected error writing file: %v", err) 578 | } 579 | 580 | f, err := vfs.OpenFile("/readme.txt", os.O_RDWR|os.O_TRUNC, 0o666) 581 | if err != nil { 582 | t.Errorf("Error opening file truncated: %v", err) 583 | } 584 | f.Close() 585 | 586 | b, err := vfs.ReadFile("/readme.txt") 587 | if err != nil { 588 | t.Errorf("Error reading truncated file: %v", err) 589 | } 590 | if len(b) != 0 { 591 | t.Errorf("File should be empty after truncation") 592 | } 593 | if fi, err := vfs.Stat("/readme.txt"); err != nil { 594 | t.Errorf("Error stat file: %v", err) 595 | } else if fi.Size() != 0 { 596 | t.Errorf("Filesize should be 0 after truncation") 597 | } 598 | } 599 | 600 | func TestStat(t *testing.T) { 601 | vfs := NewFS() 602 | f, err := vfs.OpenFile("/readme.txt", os.O_CREATE|os.O_RDWR, 0o666) 603 | if err != nil { 604 | t.Fatalf("Could not open file: %v", err) 605 | } 606 | 607 | // Write first dots 608 | if n, err := f.Write([]byte(dots)); err != nil { 609 | t.Fatalf("Unexpected error: %v", err) 610 | } else if n != len(dots) { 611 | t.Fatalf("Invalid write count: %d", n) 612 | } 613 | f.Close() 614 | 615 | if err := vfs.Mkdir("/tmp", 0o777); err != nil { 616 | t.Fatalf("Mkdir error: %v", err) 617 | } 618 | 619 | fi, err := vfs.Stat(f.Name()) 620 | if err != nil { 621 | t.Errorf("Stat error: %v", err) 622 | } 623 | 624 | // Fileinfo name is base name 625 | if name := fi.Name(); name != "readme.txt" { 626 | t.Errorf("Invalid fileinfo name: %s", name) 627 | } 628 | 629 | // File name is abs name 630 | if name := f.Name(); name != "/readme.txt" { 631 | t.Errorf("Invalid file name: %s", name) 632 | } 633 | 634 | if s := fi.Size(); s != int64(len(dots)) { 635 | t.Errorf("Invalid size: %d", s) 636 | } 637 | if fi.IsDir() { 638 | t.Errorf("Invalid IsDir") 639 | } 640 | } 641 | 642 | func TestStatError(t *testing.T) { 643 | vfs := NewFS() 644 | path := "no-such-file" 645 | 646 | fi, err := vfs.Stat(path) 647 | if err == nil { 648 | t.Fatal("got nil, want error") 649 | } 650 | if fi != nil { 651 | t.Errorf("got %v, want nil", fi) 652 | } 653 | var perr *os.PathError 654 | if !errors.As(err, &perr) { 655 | t.Errorf("got %T, want %T", err, perr) 656 | } 657 | } 658 | 659 | func TestFstat(t *testing.T) { 660 | vfs := NewFS() 661 | file, err := vfs.Create(filename) 662 | if err != nil { 663 | t.Fatal("open failed:", err) 664 | } 665 | if _, err = file.WriteString(abc); err != nil { 666 | t.Fatal("writing failed:", err) 667 | } 668 | if err = file.Sync(); err != nil { 669 | t.Fatal("syncing failed:", err) 670 | } 671 | defer file.Close() 672 | 673 | dir, err := file.Stat() 674 | if err != nil { 675 | t.Fatal("fstat failed:", err) 676 | } 677 | if filename != dir.Name() { 678 | t.Error("name should be ", filename, "; is", dir.Name()) 679 | } 680 | filesize := int64(len(abc)) 681 | if dir.Size() != filesize { 682 | t.Error("size should be", filesize, "; is", dir.Size()) 683 | } 684 | } 685 | 686 | func TestRename(t *testing.T) { 687 | const content = "read me" 688 | vfs := NewFS() 689 | if err := vfs.WriteFile("/readme.txt", []byte(content), 0o666); err != nil { 690 | t.Errorf("Unexpected error writing file: %v", err) 691 | } 692 | 693 | if err := vfs.Rename("/readme.txt", "/README.txt"); err != nil { 694 | t.Errorf("Unexpected error renaming file: %v", err) 695 | } 696 | 697 | if _, err := vfs.Stat("/readme.txt"); err == nil { 698 | t.Errorf("Old file still exists") 699 | } 700 | 701 | if _, err := vfs.Stat("/README.txt"); err != nil { 702 | t.Errorf("Error stat newfile: %v", err) 703 | } 704 | if b, err := vfs.ReadFile("/README.txt"); err != nil { 705 | t.Errorf("Error reading file: %v", err) 706 | } else if s := string(b); s != content { 707 | t.Errorf("Invalid content: %s", s) 708 | } 709 | 710 | // Rename unknown file 711 | if err := vfs.Rename("/nonexisting.txt", "/goodtarget.txt"); err == nil { 712 | t.Errorf("Expected error renaming file") 713 | } 714 | 715 | // Rename unknown file in nonexisting directory 716 | if err := vfs.Rename("/nonexisting/nonexisting.txt", "/goodtarget.txt"); err == nil { 717 | t.Errorf("Expected error renaming file") 718 | } 719 | 720 | // Rename existing file to nonexisting directory 721 | if err := vfs.Rename("/README.txt", "/nonexisting/nonexisting.txt"); err == nil { 722 | t.Errorf("Expected error renaming file") 723 | } 724 | 725 | if err := vfs.Mkdir("/newdirectory", 0o777); err != nil { 726 | t.Errorf("Error creating directory: %v", err) 727 | } 728 | 729 | if err := vfs.Rename("/README.txt", "/newdirectory/README.txt"); err != nil { 730 | t.Errorf("Error renaming file: %v", err) 731 | } 732 | 733 | // Create the same file again at root 734 | if err := vfs.WriteFile("/README.txt", []byte(content), 0o666); err != nil { 735 | t.Errorf("Unexpected error writing file: %v", err) 736 | } 737 | 738 | // Overwrite existing file 739 | if err := vfs.Rename("/newdirectory/README.txt", "/README.txt"); err != nil { 740 | t.Errorf("Unexpected error renaming file") 741 | } 742 | } 743 | 744 | func TestRenameOverwriteDest(t *testing.T) { 745 | vfs := NewFS() 746 | from, to := renameFrom, renameTo 747 | 748 | toData := []byte("to") 749 | fromData := []byte("from") 750 | 751 | err := vfs.WriteFile(to, toData, 0o777) 752 | if err != nil { 753 | t.Fatalf("write file %q failed: %v", to, err) 754 | } 755 | 756 | err = vfs.WriteFile(from, fromData, 0o777) 757 | if err != nil { 758 | t.Fatalf("write file %q failed: %v", from, err) 759 | } 760 | err = vfs.Rename(from, to) 761 | if err != nil { 762 | t.Fatalf("rename %q, %q failed: %v", to, from, err) 763 | } 764 | 765 | _, err = vfs.Stat(from) 766 | if err == nil { 767 | t.Errorf("from file %q still exists", from) 768 | } 769 | if err != nil && !errors.Is(err, fs.ErrNotExist) { 770 | t.Fatalf("stat from: %v", err) 771 | } 772 | toFi, err := vfs.Stat(to) 773 | if err != nil { 774 | t.Fatalf("stat %q failed: %v", to, err) 775 | } 776 | if toFi.Size() != int64(len(fromData)) { 777 | t.Errorf(`"to" size = %d; want %d (old "from" size)`, toFi.Size(), len(fromData)) 778 | } 779 | } 780 | 781 | func TestRenameFailed(t *testing.T) { 782 | vfs := NewFS() 783 | from, to := renameFrom, renameTo 784 | 785 | err := vfs.Rename(from, to) 786 | var linkErr *os.LinkError 787 | if errors.As(err, &linkErr) { 788 | if linkErr.Op != "rename" { 789 | t.Errorf("rename %q, %q: err.Op: want %q, got %q", from, to, "rename", linkErr.Op) 790 | } 791 | if linkErr.Old != from { 792 | t.Errorf("rename %q, %q: err.Old: want %q, got %q", from, to, from, linkErr.Old) 793 | } 794 | if linkErr.New != to { 795 | t.Errorf("rename %q, %q: err.New: want %q, got %q", from, to, to, linkErr.New) 796 | } 797 | } else if err == nil { 798 | t.Errorf("rename %q, %q: expected error, got nil", from, to) 799 | } else { 800 | t.Errorf("rename %q, %q: expected %T, got %T %v", from, to, new(os.LinkError), err, err) 801 | } 802 | } 803 | 804 | func TestRenameToDirFailed(t *testing.T) { 805 | is := is.New(t) 806 | 807 | vfs := NewFS() 808 | from, to := renameFrom, renameTo 809 | 810 | is.NoErr(vfs.Mkdir(from, 0o777)) 811 | is.NoErr(vfs.Mkdir(to, 0o777)) 812 | 813 | err := vfs.Rename(from, to) 814 | var linkErr *os.LinkError 815 | if errors.As(err, &linkErr) { 816 | if linkErr.Op != "rename" { 817 | t.Errorf("rename %q, %q: err.Op: want %q, got %q", from, to, "rename", linkErr.Op) 818 | } 819 | if linkErr.Old != from { 820 | t.Errorf("rename %q, %q: err.Old: want %q, got %q", from, to, from, linkErr.Old) 821 | } 822 | if linkErr.New != to { 823 | t.Errorf("rename %q, %q: err.New: want %q, got %q", from, to, to, linkErr.New) 824 | } 825 | } else if err == nil { 826 | t.Errorf("rename %q, %q: expected error, got nil", from, to) 827 | } else { 828 | t.Errorf("rename %q, %q: expected %T, got %T %v", from, to, new(os.LinkError), err, err) 829 | } 830 | } 831 | 832 | func checkSize(t *testing.T, f absfs.File, size int64) { 833 | t.Helper() 834 | 835 | fi, err := f.Stat() 836 | if err != nil { 837 | t.Fatalf("Stat %q (looking for size %d): %s", f.Name(), size, err) 838 | } 839 | if fi.Size() != size { 840 | t.Errorf("Stat %q: size %d want %d", f.Name(), fi.Size(), size) 841 | } 842 | } 843 | 844 | func TestFTruncate(t *testing.T) { 845 | is := is.New(t) 846 | 847 | vfs := NewFS() 848 | f, err := vfs.Create(filename) 849 | if err != nil { 850 | t.Fatal("create failed:", err) 851 | } 852 | defer f.Close() 853 | 854 | checkSize(t, f, 0) 855 | _, err = f.Write([]byte("hello, world\n")) 856 | is.NoErr(err) 857 | checkSize(t, f, 13) 858 | is.NoErr(f.Truncate(10)) 859 | checkSize(t, f, 10) 860 | is.NoErr(f.Truncate(1024)) 861 | checkSize(t, f, 1024) 862 | is.NoErr(f.Truncate(0)) 863 | checkSize(t, f, 0) 864 | _, err = f.Write([]byte("surprise!")) 865 | if err == nil { 866 | checkSize(t, f, 13+9) // wrote at offset past where hello, world was. 867 | } 868 | } 869 | 870 | func TestTruncate(t *testing.T) { 871 | is := is.New(t) 872 | 873 | vfs := NewFS() 874 | f, err := vfs.Create(filename) 875 | if err != nil { 876 | t.Fatal("create failed:", err) 877 | } 878 | defer f.Close() 879 | 880 | checkSize(t, f, 0) 881 | _, err = f.Write([]byte("hello, world\n")) 882 | is.NoErr(err) 883 | checkSize(t, f, 13) 884 | is.NoErr(vfs.Truncate(f.Name(), 10)) 885 | checkSize(t, f, 10) 886 | is.NoErr(vfs.Truncate(f.Name(), 1024)) 887 | checkSize(t, f, 1024) 888 | is.NoErr(vfs.Truncate(f.Name(), 0)) 889 | checkSize(t, f, 0) 890 | _, err = f.Write([]byte("surprise!")) 891 | if err == nil { 892 | checkSize(t, f, 13+9) // wrote at offset past where hello, world was. 893 | } 894 | } 895 | 896 | func TestChdir(t *testing.T) { 897 | vfs := NewFS() 898 | c := make(chan bool) 899 | cpwd := make(chan string) 900 | for i := range 10 { 901 | go func(i int) { 902 | // Lock half the goroutines in their own operating system 903 | // thread to exercise more scheduler possibilities. 904 | if i%2 == 1 { 905 | runtime.LockOSThread() 906 | } 907 | <-c 908 | pwd, err := vfs.Getwd() 909 | if err != nil { 910 | t.Errorf("Getwd on goroutine %d: %v", i, err) 911 | return 912 | } 913 | cpwd <- pwd 914 | }(i) 915 | } 916 | d, err := ioutil.TempDir(vfs, "", "test") 917 | if err != nil { 918 | t.Fatalf("TempDir: %v", err) 919 | } 920 | if err := vfs.Chdir(d); err != nil { 921 | t.Fatalf("Chdir: %v", err) 922 | } 923 | d, err = vfs.Getwd() 924 | if err != nil { 925 | t.Fatalf("Getwd: %v", err) 926 | } 927 | close(c) 928 | for range 10 { 929 | pwd := <-cpwd 930 | if pwd != d { 931 | t.Errorf("Getwd returned %q; want %q", pwd, d) 932 | } 933 | } 934 | } 935 | 936 | func newFile(t *testing.T, fs absfs.FileSystem, testName string) (f absfs.File) { 937 | t.Helper() 938 | 939 | f, err := ioutil.TempFile(fs, "/", "_Go_"+testName) 940 | if err != nil { 941 | t.Fatalf("TempFile %s: %s", testName, err) 942 | } 943 | return 944 | } 945 | 946 | func TestSeek(t *testing.T) { 947 | is := is.New(t) 948 | 949 | vfs := NewFS() 950 | f := newFile(t, vfs, "TestSeek") 951 | defer f.Close() 952 | 953 | const data = "hello, world\n" 954 | _, err := io.WriteString(f, data) 955 | is.NoErr(err) 956 | 957 | type test struct { 958 | in int64 959 | whence int 960 | out int64 961 | } 962 | tests := []test{ 963 | {0, io.SeekCurrent, int64(len(data))}, 964 | {0, io.SeekStart, 0}, 965 | {5, io.SeekStart, 5}, 966 | {0, io.SeekEnd, int64(len(data))}, 967 | {0, io.SeekStart, 0}, 968 | {-1, io.SeekEnd, int64(len(data)) - 1}, 969 | {1 << 33, io.SeekStart, 1 << 33}, 970 | {1 << 33, io.SeekEnd, 1<<33 + int64(len(data))}, 971 | 972 | // Issue 21681, Windows 4G-1, etc: 973 | {1<<32 - 1, io.SeekStart, 1<<32 - 1}, 974 | {0, io.SeekCurrent, 1<<32 - 1}, 975 | {2<<32 - 1, io.SeekStart, 2<<32 - 1}, 976 | {0, io.SeekCurrent, 2<<32 - 1}, 977 | } 978 | for i, tt := range tests { 979 | off, err := f.Seek(tt.in, tt.whence) 980 | if off != tt.out || err != nil { 981 | t.Errorf("#%d: Seek(%v, %v) = %v, %v want %v, nil", i, tt.in, tt.whence, off, err, tt.out) 982 | } 983 | } 984 | } 985 | 986 | func TestReadAt(t *testing.T) { 987 | is := is.New(t) 988 | 989 | vfs := NewFS() 990 | f := newFile(t, vfs, "TestReadAt") 991 | defer f.Close() 992 | 993 | const data = "hello, world\n" 994 | _, err := io.WriteString(f, data) 995 | is.NoErr(err) 996 | 997 | b := make([]byte, 5) 998 | n, err := f.ReadAt(b, 7) 999 | if err != nil || n != len(b) { 1000 | t.Fatalf("ReadAt 7: %d, %v", n, err) 1001 | } 1002 | if string(b) != "world" { 1003 | t.Fatalf("ReadAt 7: have %q want %q", string(b), "world") 1004 | } 1005 | } 1006 | 1007 | // Verify that ReadAt doesn't affect seek offset. 1008 | func TestReadAtOffset(t *testing.T) { 1009 | is := is.New(t) 1010 | 1011 | vfs := NewFS() 1012 | f := newFile(t, vfs, "TestReadAtOffset") 1013 | defer f.Close() 1014 | 1015 | const data = "hello, world\n" 1016 | _, err := io.WriteString(f, data) 1017 | is.NoErr(err) 1018 | 1019 | _, err = f.Seek(0, 0) 1020 | is.NoErr(err) 1021 | b := make([]byte, 5) 1022 | 1023 | n, err := f.ReadAt(b, 7) 1024 | if err != nil || n != len(b) { 1025 | t.Fatalf("ReadAt 7: %d, %v", n, err) 1026 | } 1027 | if string(b) != "world" { 1028 | t.Fatalf("ReadAt 7: have %q want %q", string(b), "world") 1029 | } 1030 | 1031 | n, err = f.Read(b) 1032 | if err != nil || n != len(b) { 1033 | t.Fatalf("Read: %d, %v", n, err) 1034 | } 1035 | if string(b) != "hello" { 1036 | t.Fatalf("Read: have %q want %q", string(b), "hello") 1037 | } 1038 | } 1039 | 1040 | // Verify that ReadAt doesn't allow negative offset. 1041 | func TestReadAtNegativeOffset(t *testing.T) { 1042 | is := is.New(t) 1043 | 1044 | vfs := NewFS() 1045 | f := newFile(t, vfs, "TestReadAtNegativeOffset") 1046 | defer f.Close() 1047 | 1048 | const data = "hello, world\n" 1049 | _, err := io.WriteString(f, data) 1050 | is.NoErr(err) 1051 | 1052 | _, err = f.Seek(0, 0) 1053 | is.NoErr(err) 1054 | b := make([]byte, 5) 1055 | 1056 | n, err := f.ReadAt(b, -10) 1057 | if !errors.Is(err, fs.ErrInvalid) { 1058 | t.Errorf("ReadAt(-10) = %v, %v; want 0, %v", n, err, fs.ErrInvalid) 1059 | } 1060 | } 1061 | 1062 | func TestWriteAt(t *testing.T) { 1063 | is := is.New(t) 1064 | 1065 | vfs := NewFS() 1066 | f := newFile(t, vfs, "TestWriteAt") 1067 | defer f.Close() 1068 | 1069 | const data = "hello, world\n" 1070 | _, err := io.WriteString(f, data) 1071 | is.NoErr(err) 1072 | 1073 | n, err := f.WriteAt([]byte("WORLD"), 7) 1074 | if err != nil || n != 5 { 1075 | t.Fatalf("WriteAt 7: %d, %v", n, err) 1076 | } 1077 | 1078 | b, err := vfs.ReadFile(f.Name()) 1079 | if err != nil { 1080 | t.Fatalf("ReadFile %s: %v", f.Name(), err) 1081 | } 1082 | if string(b) != "hello, WORLD\n" { 1083 | t.Fatalf("after write: have %q want %q", string(b), "hello, WORLD\n") 1084 | } 1085 | } 1086 | 1087 | // Verify that WriteAt doesn't allow negative offset. 1088 | func TestWriteAtNegativeOffset(t *testing.T) { 1089 | vfs := NewFS() 1090 | f := newFile(t, vfs, "TestWriteAtNegativeOffset") 1091 | defer f.Close() 1092 | 1093 | n, err := f.WriteAt([]byte("WORLD"), -10) 1094 | if !errors.Is(err, fs.ErrInvalid) { 1095 | t.Errorf("WriteAt(-10) = %v, %v; want 0, %v", n, err, fs.ErrInvalid) 1096 | } 1097 | } 1098 | 1099 | // Verify that WriteAt doesn't work in append mode. 1100 | func TestWriteAtInAppendMode(t *testing.T) { 1101 | vfs := NewFS() 1102 | f, err := vfs.OpenFile("write_at_in_append_mode.txt", os.O_APPEND|os.O_CREATE, 0o666) 1103 | if err != nil { 1104 | t.Fatalf("OpenFile: %v", err) 1105 | } 1106 | defer f.Close() 1107 | 1108 | _, err = f.WriteAt([]byte(""), 1) 1109 | if !errors.Is(err, os.ErrPermission) { 1110 | t.Fatalf("f.WriteAt returned %v, expected %v", err, os.ErrPermission) 1111 | } 1112 | } 1113 | 1114 | //nolint:unparam 1115 | func writeFile(t *testing.T, vfs absfs.FileSystem, fname string, flag int, text string) string { 1116 | t.Helper() 1117 | 1118 | f, err := vfs.OpenFile(fname, flag, 0o666) 1119 | if err != nil { 1120 | t.Fatalf("Open: %v", err) 1121 | } 1122 | n, err := io.WriteString(f, text) 1123 | if err != nil { 1124 | t.Fatalf("WriteString: %d, %v", n, err) 1125 | } 1126 | f.Close() 1127 | data, err := vfs.ReadFile(fname) 1128 | if err != nil { 1129 | t.Fatalf("ReadFile: %v", err) 1130 | } 1131 | 1132 | return string(data) 1133 | } 1134 | 1135 | func TestAppend(t *testing.T) { 1136 | vfs := NewFS() 1137 | const f = "append.txt" 1138 | s := writeFile(t, vfs, f, os.O_CREATE|os.O_TRUNC|os.O_RDWR, "new") 1139 | if s != "new" { 1140 | t.Fatalf("writeFile: have %q want %q", s, "new") 1141 | } 1142 | s = writeFile(t, vfs, f, os.O_APPEND|os.O_RDWR, "|append") 1143 | if s != "new|append" { 1144 | t.Fatalf("writeFile: have %q want %q", s, "new|append") 1145 | } 1146 | s = writeFile(t, vfs, f, os.O_CREATE|os.O_APPEND|os.O_RDWR, "|append") 1147 | if s != "new|append|append" { 1148 | t.Fatalf("writeFile: have %q want %q", s, "new|append|append") 1149 | } 1150 | err := vfs.Remove(f) 1151 | if err != nil { 1152 | t.Fatalf("Remove: %v", err) 1153 | } 1154 | s = writeFile(t, vfs, f, os.O_CREATE|os.O_APPEND|os.O_RDWR, "new&append") 1155 | if s != "new&append" { 1156 | t.Fatalf("writeFile: after append have %q want %q", s, "new&append") 1157 | } 1158 | s = writeFile(t, vfs, f, os.O_CREATE|os.O_RDWR, "old") 1159 | if s != "old&append" { 1160 | t.Fatalf("writeFile: after create have %q want %q", s, "old&append") 1161 | } 1162 | s = writeFile(t, vfs, f, os.O_CREATE|os.O_TRUNC|os.O_RDWR, "new") 1163 | if s != "new" { 1164 | t.Fatalf("writeFile: after truncate have %q want %q", s, "new") 1165 | } 1166 | } 1167 | 1168 | func TestModTime(t *testing.T) { 1169 | is := is.New(t) 1170 | 1171 | vfs := NewFS() 1172 | 1173 | tBeforeWrite := time.Now() 1174 | is.NoErr(vfs.WriteFile("/readme.txt", []byte{0, 0, 0}, 0o666)) 1175 | fi, _ := vfs.Stat("/readme.txt") 1176 | mtimeAfterWrite := fi.ModTime() 1177 | 1178 | if !mtimeAfterWrite.After(tBeforeWrite) { 1179 | t.Error("Open should modify mtime") 1180 | } 1181 | 1182 | f, err := vfs.OpenFile("/readme.txt", os.O_RDONLY, 0o666) 1183 | if err != nil { 1184 | t.Fatalf("Could not open file: %v", err) 1185 | } 1186 | f.Close() 1187 | tAfterRead := fi.ModTime() 1188 | 1189 | if tAfterRead != mtimeAfterWrite { 1190 | t.Error("Open with O_RDONLY should not modify mtime") 1191 | } 1192 | } 1193 | --------------------------------------------------------------------------------