goscript 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cmd/goscript/cmd_compile.go +28 -8
- package/cmd/goscript/cmd_compile_test.go +105 -6
- package/compiler/build-flags.go +9 -10
- package/compiler/gotest/runner_test.go +127 -0
- package/compiler/lowering.go +596 -136
- package/compiler/lowering_bench_test.go +350 -0
- package/compiler/package-graph.go +61 -4
- package/compiler/package-graph_test.go +30 -0
- package/compiler/semantic-model-types.go +8 -0
- package/compiler/semantic-model.go +447 -22
- package/compiler/semantic-model_test.go +138 -0
- package/compiler/skeleton_test.go +948 -14
- package/compiler/typescript-emitter.go +19 -2
- package/dist/gs/builtin/builtin.d.ts +2 -2
- package/dist/gs/builtin/builtin.js +20 -0
- package/dist/gs/builtin/builtin.js.map +1 -1
- package/dist/gs/builtin/slice.js +5 -0
- package/dist/gs/builtin/slice.js.map +1 -1
- package/dist/gs/builtin/type.d.ts +1 -1
- package/dist/gs/builtin/type.js +72 -5
- package/dist/gs/builtin/type.js.map +1 -1
- package/dist/gs/compress/zlib/index.d.ts +3 -3
- package/dist/gs/compress/zlib/index.js +88 -26
- package/dist/gs/compress/zlib/index.js.map +1 -1
- package/dist/gs/crypto/sha1/index.js +2 -5
- package/dist/gs/crypto/sha1/index.js.map +1 -1
- package/dist/gs/crypto/sha256/index.js +2 -5
- package/dist/gs/crypto/sha256/index.js.map +1 -1
- package/dist/gs/crypto/sha512/index.js +2 -5
- package/dist/gs/crypto/sha512/index.js.map +1 -1
- package/dist/gs/embed/index.d.ts +6 -0
- package/dist/gs/embed/index.js +210 -5
- package/dist/gs/embed/index.js.map +1 -1
- package/dist/gs/fmt/fmt.d.ts +3 -3
- package/dist/gs/fmt/fmt.js +29 -16
- package/dist/gs/fmt/fmt.js.map +1 -1
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +118 -6
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +1 -1
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.d.ts +45 -0
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js +229 -0
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js.map +1 -0
- package/dist/gs/io/fs/readdir.js +5 -3
- package/dist/gs/io/fs/readdir.js.map +1 -1
- package/dist/gs/io/io.d.ts +10 -6
- package/dist/gs/io/io.js +87 -42
- package/dist/gs/io/io.js.map +1 -1
- package/dist/gs/math/bits/index.d.ts +26 -5
- package/dist/gs/math/bits/index.js +13 -24
- package/dist/gs/math/bits/index.js.map +1 -1
- package/dist/gs/net/http/index.d.ts +3 -1
- package/dist/gs/net/http/index.js +18 -1
- package/dist/gs/net/http/index.js.map +1 -1
- package/dist/gs/os/types_js.gs.d.ts +6 -2
- package/dist/gs/os/types_js.gs.js +169 -8
- package/dist/gs/os/types_js.gs.js.map +1 -1
- package/dist/gs/reflect/type.d.ts +1 -0
- package/dist/gs/reflect/type.js +80 -51
- package/dist/gs/reflect/type.js.map +1 -1
- package/dist/gs/strings/reader.d.ts +1 -1
- package/dist/gs/strings/reader.js +2 -2
- package/dist/gs/strings/reader.js.map +1 -1
- package/dist/gs/sync/sync.d.ts +2 -1
- package/dist/gs/sync/sync.js +37 -16
- package/dist/gs/sync/sync.js.map +1 -1
- package/dist/gs/syscall/js/index.js +9 -0
- package/dist/gs/syscall/js/index.js.map +1 -1
- package/dist/gs/testing/testing.js +8 -6
- package/dist/gs/testing/testing.js.map +1 -1
- package/gs/builtin/builtin.ts +25 -2
- package/gs/builtin/runtime-contract.test.ts +45 -0
- package/gs/builtin/slice.ts +7 -0
- package/gs/builtin/type.ts +85 -5
- package/gs/compress/zlib/index.test.ts +97 -0
- package/gs/compress/zlib/index.ts +117 -27
- package/gs/compress/zlib/meta.json +4 -1
- package/gs/crypto/sha1/index.test.ts +19 -2
- package/gs/crypto/sha1/index.ts +3 -6
- package/gs/crypto/sha256/index.test.ts +14 -2
- package/gs/crypto/sha256/index.ts +3 -6
- package/gs/crypto/sha512/index.test.ts +17 -2
- package/gs/crypto/sha512/index.ts +3 -6
- package/gs/embed/index.test.ts +87 -0
- package/gs/embed/index.ts +229 -5
- package/gs/fmt/fmt.test.ts +41 -3
- package/gs/fmt/fmt.ts +40 -17
- package/gs/fmt/meta.json +6 -1
- package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +8 -1
- package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +139 -11
- package/gs/github.com/go-git/go-billy/v6/osfs/index.test.ts +110 -0
- package/gs/github.com/go-git/go-billy/v6/osfs/index.ts +280 -0
- package/gs/github.com/go-git/go-billy/v6/osfs/meta.json +8 -0
- package/gs/io/fs/readdir.test.ts +38 -0
- package/gs/io/fs/readdir.ts +7 -3
- package/gs/io/io.test.ts +77 -6
- package/gs/io/io.ts +114 -52
- package/gs/io/meta.json +7 -1
- package/gs/math/bits/index.ts +52 -28
- package/gs/net/http/index.test.ts +16 -0
- package/gs/net/http/index.ts +19 -2
- package/gs/os/file_unix_js.test.ts +52 -0
- package/gs/os/meta.json +4 -0
- package/gs/os/readdir.test.ts +56 -0
- package/gs/os/types_js.gs.ts +169 -8
- package/gs/reflect/deepequal.test.ts +10 -1
- package/gs/reflect/type.ts +91 -56
- package/gs/reflect/typefor.test.ts +31 -1
- package/gs/strings/meta.json +5 -2
- package/gs/strings/reader.test.ts +2 -2
- package/gs/strings/reader.ts +2 -2
- package/gs/sync/meta.json +1 -0
- package/gs/sync/sync.test.ts +41 -1
- package/gs/sync/sync.ts +41 -16
- package/gs/syscall/js/index.test.ts +18 -0
- package/gs/syscall/js/index.ts +12 -0
- package/gs/testing/testing.test.ts +32 -3
- package/gs/testing/testing.ts +13 -10
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as $ from '@goscript/builtin/index.js'
|
|
2
|
+
import * as os from '@goscript/os/index.js'
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { New } from './index.js'
|
|
9
|
+
|
|
10
|
+
const roots: string[] = []
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
for (const root of roots.splice(0)) {
|
|
14
|
+
rmSync(root, { force: true, recursive: true })
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
function tempRoot(): string {
|
|
19
|
+
const root = mkdtempSync(join(tmpdir(), 'goscript-billy-osfs-'))
|
|
20
|
+
roots.push(root)
|
|
21
|
+
return root
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('go-billy osfs override', () => {
|
|
25
|
+
it('writes through to the host directory used by os.DirFS', () => {
|
|
26
|
+
const root = tempRoot()
|
|
27
|
+
const fsys = New(root)
|
|
28
|
+
|
|
29
|
+
const [file, openErr] = fsys.OpenFile(
|
|
30
|
+
'README.md',
|
|
31
|
+
os.O_WRONLY | os.O_CREATE | os.O_TRUNC,
|
|
32
|
+
0o666,
|
|
33
|
+
)
|
|
34
|
+
expect(openErr).toBeNull()
|
|
35
|
+
const [n, writeErr] = file!.Write($.stringToBytes('hello\n'))
|
|
36
|
+
expect(writeErr).toBeNull()
|
|
37
|
+
expect(n).toBe(6)
|
|
38
|
+
expect(file!.Close()).toBeNull()
|
|
39
|
+
|
|
40
|
+
expect(readFileSync(join(root, 'README.md'), 'utf8')).toBe('hello\n')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('keeps chrooted writes under the child host directory', () => {
|
|
44
|
+
const root = tempRoot()
|
|
45
|
+
const fsys = New(root)
|
|
46
|
+
const [child, chrootErr] = fsys.Chroot('sub')
|
|
47
|
+
expect(chrootErr).toBeNull()
|
|
48
|
+
|
|
49
|
+
const [file, openErr] = child!.Create('nested/file.txt')
|
|
50
|
+
expect(openErr).toBeNull()
|
|
51
|
+
const [, writeErr] = file!.Write($.stringToBytes('child'))
|
|
52
|
+
expect(writeErr).toBeNull()
|
|
53
|
+
expect(file!.Close()).toBeNull()
|
|
54
|
+
|
|
55
|
+
expect(readFileSync(join(root, 'sub', 'nested', 'file.txt'), 'utf8')).toBe(
|
|
56
|
+
'child',
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('rejects parent traversal outside the base directory', () => {
|
|
61
|
+
const root = tempRoot()
|
|
62
|
+
const fsys = New(root)
|
|
63
|
+
|
|
64
|
+
const [file, err] = fsys.Open('../escape.txt')
|
|
65
|
+
|
|
66
|
+
expect(file).toBeNull()
|
|
67
|
+
expect(err).not.toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('accepts absolute paths inside the base directory', () => {
|
|
71
|
+
const root = tempRoot()
|
|
72
|
+
const fsys = New(root)
|
|
73
|
+
const absolute = join(root, 'inside.txt')
|
|
74
|
+
|
|
75
|
+
const [file, openErr] = fsys.Create(absolute)
|
|
76
|
+
expect(openErr).toBeNull()
|
|
77
|
+
const [, writeErr] = file!.Write($.stringToBytes('inside'))
|
|
78
|
+
expect(writeErr).toBeNull()
|
|
79
|
+
expect(file!.Close()).toBeNull()
|
|
80
|
+
|
|
81
|
+
expect(readFileSync(absolute, 'utf8')).toBe('inside')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('rejects absolute paths outside the base directory', () => {
|
|
85
|
+
const root = tempRoot()
|
|
86
|
+
const outside = tempRoot()
|
|
87
|
+
const fsys = New(root)
|
|
88
|
+
|
|
89
|
+
const [file, err] = fsys.Create(join(outside, 'outside.txt'))
|
|
90
|
+
|
|
91
|
+
expect(file).toBeNull()
|
|
92
|
+
expect(err).not.toBeNull()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('reads host directory entries', () => {
|
|
96
|
+
const root = tempRoot()
|
|
97
|
+
writeFileSync(join(root, 'one.txt'), 'one')
|
|
98
|
+
writeFileSync(join(root, 'two.txt'), 'two')
|
|
99
|
+
const fsys = New(root)
|
|
100
|
+
|
|
101
|
+
const [entries, err] = fsys.ReadDir('.')
|
|
102
|
+
|
|
103
|
+
expect(err).toBeNull()
|
|
104
|
+
expect(
|
|
105
|
+
Array.from(entries ?? [])
|
|
106
|
+
.map((entry) => entry.Name())
|
|
107
|
+
.sort(),
|
|
108
|
+
).toEqual(['one.txt', 'two.txt'])
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import * as $ from '@goscript/builtin/index.js'
|
|
2
|
+
import * as fs from '@goscript/io/fs/index.js'
|
|
3
|
+
import * as os from '@goscript/os/index.js'
|
|
4
|
+
import * as filepath from '@goscript/path/filepath/index.js'
|
|
5
|
+
|
|
6
|
+
export type Option = (o: options) => void
|
|
7
|
+
export type Type = number
|
|
8
|
+
|
|
9
|
+
export const BoundOSFS: Type = 0
|
|
10
|
+
|
|
11
|
+
const allCapabilities = 127
|
|
12
|
+
const defaultCreateMode = 0o666
|
|
13
|
+
const defaultDirectoryMode = 0o777
|
|
14
|
+
const ErrCrossedBoundary = $.newError('chroot boundary crossed')
|
|
15
|
+
const ErrBaseDirCannotBeRemoved = $.newError('base dir cannot be removed')
|
|
16
|
+
const ErrBaseDirCannotBeRenamed = $.newError('base dir cannot be renamed')
|
|
17
|
+
|
|
18
|
+
type JoinElement = string | $.Slice<string>
|
|
19
|
+
|
|
20
|
+
class options {
|
|
21
|
+
public Type: Type = BoundOSFS
|
|
22
|
+
public mmap = false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function New(
|
|
26
|
+
baseDir: string,
|
|
27
|
+
...opts: Array<Option | $.Slice<Option> | undefined>
|
|
28
|
+
): BoundOS {
|
|
29
|
+
const o = new options()
|
|
30
|
+
for (const opt of normalizeOptions(opts)) {
|
|
31
|
+
opt(o)
|
|
32
|
+
}
|
|
33
|
+
return new BoundOS({ baseDir, mmap: o.mmap })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeOptions(
|
|
37
|
+
opts: Array<Option | $.Slice<Option> | undefined>,
|
|
38
|
+
): Option[] {
|
|
39
|
+
if (opts.length === 0) {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
if (opts.length === 1 && typeof opts[0] !== 'function') {
|
|
43
|
+
const slice = opts[0] ?? null
|
|
44
|
+
const out: Option[] = []
|
|
45
|
+
for (let i = 0; i < $.len(slice); i++) {
|
|
46
|
+
out.push(slice![i])
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
return opts.filter((opt): opt is Option => typeof opt === 'function')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function WithBoundOS(): Option {
|
|
54
|
+
return (o: options): void => {
|
|
55
|
+
o.Type = BoundOSFS
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function WithMmap(): Option {
|
|
60
|
+
return (o: options): void => {
|
|
61
|
+
o.mmap = true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class BoundOS {
|
|
66
|
+
public baseDir: string
|
|
67
|
+
public mmap: boolean
|
|
68
|
+
|
|
69
|
+
constructor(init?: Partial<{ baseDir?: string; mmap?: boolean }>) {
|
|
70
|
+
const baseDir = init?.baseDir ?? '/'
|
|
71
|
+
this.baseDir = baseDir === '' ? '/' : filepath.Clean(baseDir)
|
|
72
|
+
this.mmap = init?.mmap ?? false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public clone(): BoundOS {
|
|
76
|
+
return new BoundOS({ baseDir: this.baseDir, mmap: this.mmap })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public Capabilities(): number {
|
|
80
|
+
return allCapabilities
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public Create(name: string): [os.File | null, $.GoError] {
|
|
84
|
+
return this.OpenFile(
|
|
85
|
+
name,
|
|
86
|
+
os.O_RDWR | os.O_CREATE | os.O_TRUNC,
|
|
87
|
+
defaultCreateMode,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public Open(name: string): [os.File | null, $.GoError] {
|
|
92
|
+
return this.OpenFile(name, os.O_RDONLY, 0)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public OpenFile(
|
|
96
|
+
name: string,
|
|
97
|
+
flag: number,
|
|
98
|
+
perm: fs.FileMode,
|
|
99
|
+
): [os.File | null, $.GoError] {
|
|
100
|
+
const [full, err] = this.abs(name)
|
|
101
|
+
if (err !== null) {
|
|
102
|
+
return [null, err]
|
|
103
|
+
}
|
|
104
|
+
if ((flag & os.O_CREATE) !== 0) {
|
|
105
|
+
const dir = filepath.Dir(full)
|
|
106
|
+
if (dir !== '.' && dir !== '') {
|
|
107
|
+
const mkdirErr = os.MkdirAll(dir, defaultDirectoryMode)
|
|
108
|
+
if (mkdirErr !== null) {
|
|
109
|
+
return [null, mkdirErr]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return os.OpenFile(full, flag, perm)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public Stat(name: string): [fs.FileInfo, $.GoError] {
|
|
117
|
+
const [full, err] = this.abs(name)
|
|
118
|
+
if (err !== null) {
|
|
119
|
+
return [null, err]
|
|
120
|
+
}
|
|
121
|
+
return os.Stat(full)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public Lstat(name: string): [fs.FileInfo, $.GoError] {
|
|
125
|
+
const [full, err] = this.abs(name)
|
|
126
|
+
if (err !== null) {
|
|
127
|
+
return [null, err]
|
|
128
|
+
}
|
|
129
|
+
return os.Lstat(full)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public ReadDir(name: string): [$.Slice<fs.DirEntry>, $.GoError] {
|
|
133
|
+
const [full, err] = this.abs(name)
|
|
134
|
+
if (err !== null) {
|
|
135
|
+
return [null, err]
|
|
136
|
+
}
|
|
137
|
+
const [file, openErr] = os.Open(full)
|
|
138
|
+
if (openErr !== null) {
|
|
139
|
+
return [null, openErr]
|
|
140
|
+
}
|
|
141
|
+
const [entries, readErr] = file!.ReadDir(-1)
|
|
142
|
+
const closeErr = file!.Close()
|
|
143
|
+
return [entries, readErr ?? closeErr]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public Rename(from: string, to: string): $.GoError {
|
|
147
|
+
if (this.isRoot(from)) {
|
|
148
|
+
return ErrBaseDirCannotBeRenamed
|
|
149
|
+
}
|
|
150
|
+
const [fromFull, fromErr] = this.abs(from)
|
|
151
|
+
if (fromErr !== null) {
|
|
152
|
+
return fromErr
|
|
153
|
+
}
|
|
154
|
+
const [toFull, toErr] = this.abs(to)
|
|
155
|
+
if (toErr !== null) {
|
|
156
|
+
return toErr
|
|
157
|
+
}
|
|
158
|
+
const mkdirErr = os.MkdirAll(filepath.Dir(toFull), defaultDirectoryMode)
|
|
159
|
+
if (mkdirErr !== null) {
|
|
160
|
+
return mkdirErr
|
|
161
|
+
}
|
|
162
|
+
return os.Rename(fromFull, toFull)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public Remove(name: string): $.GoError {
|
|
166
|
+
if (this.isRoot(name)) {
|
|
167
|
+
return ErrBaseDirCannotBeRemoved
|
|
168
|
+
}
|
|
169
|
+
const [full, err] = this.abs(name)
|
|
170
|
+
if (err !== null) {
|
|
171
|
+
return err
|
|
172
|
+
}
|
|
173
|
+
return os.Remove(full)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public RemoveAll(name: string): $.GoError {
|
|
177
|
+
if (this.isRoot(name)) {
|
|
178
|
+
return ErrBaseDirCannotBeRemoved
|
|
179
|
+
}
|
|
180
|
+
const [full, err] = this.abs(name)
|
|
181
|
+
if (err !== null) {
|
|
182
|
+
return err
|
|
183
|
+
}
|
|
184
|
+
return os.RemoveAll(full)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public MkdirAll(name: string, perm: fs.FileMode): $.GoError {
|
|
188
|
+
const [full, err] = this.abs(name)
|
|
189
|
+
if (err !== null) {
|
|
190
|
+
return err
|
|
191
|
+
}
|
|
192
|
+
return os.MkdirAll(full, perm)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public Symlink(target: string, link: string): $.GoError {
|
|
196
|
+
const [full, err] = this.abs(link)
|
|
197
|
+
if (err !== null) {
|
|
198
|
+
return err
|
|
199
|
+
}
|
|
200
|
+
const mkdirErr = os.MkdirAll(filepath.Dir(full), defaultDirectoryMode)
|
|
201
|
+
if (mkdirErr !== null) {
|
|
202
|
+
return mkdirErr
|
|
203
|
+
}
|
|
204
|
+
return os.Symlink(target, full)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public Readlink(link: string): [string, $.GoError] {
|
|
208
|
+
const [full, err] = this.abs(link)
|
|
209
|
+
if (err !== null) {
|
|
210
|
+
return ['', err]
|
|
211
|
+
}
|
|
212
|
+
return os.Readlink(full)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public TempFile(dir: string, prefix: string): [os.File | null, $.GoError] {
|
|
216
|
+
const targetDir = dir === '' ? '.tmp' : dir
|
|
217
|
+
const [fullDir, err] = this.abs(targetDir)
|
|
218
|
+
if (err !== null) {
|
|
219
|
+
return [null, err]
|
|
220
|
+
}
|
|
221
|
+
const mkdirErr = os.MkdirAll(fullDir, defaultDirectoryMode)
|
|
222
|
+
if (mkdirErr !== null) {
|
|
223
|
+
return [null, mkdirErr]
|
|
224
|
+
}
|
|
225
|
+
return os.CreateTemp(fullDir, prefix === '' ? 'tmp-*' : prefix + '*')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public Join(...elem: JoinElement[]): string {
|
|
229
|
+
if (elem.length === 1 && typeof elem[0] !== 'string') {
|
|
230
|
+
return filepath.Join(elem[0])
|
|
231
|
+
}
|
|
232
|
+
return filepath.Join(...(elem as string[]))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public Chroot(path: string): [BoundOS | null, $.GoError] {
|
|
236
|
+
const [full, err] = this.abs(path)
|
|
237
|
+
if (err !== null) {
|
|
238
|
+
return [null, err]
|
|
239
|
+
}
|
|
240
|
+
return [new BoundOS({ baseDir: full, mmap: this.mmap }), null]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public Root(): string {
|
|
244
|
+
return this.baseDir
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private isRoot(name: string): boolean {
|
|
248
|
+
return name === '' || name === '.'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private abs(name: string): [string, $.GoError] {
|
|
252
|
+
if (this.isRoot(name)) {
|
|
253
|
+
return [this.baseDir, null]
|
|
254
|
+
}
|
|
255
|
+
const normalized = filepath.Clean(filepath.ToSlash(name))
|
|
256
|
+
if (filepath.IsAbs(normalized)) {
|
|
257
|
+
const full = filepath.Clean(filepath.FromSlash(normalized))
|
|
258
|
+
if (!this.insideBase(full)) {
|
|
259
|
+
return ['', ErrCrossedBoundary]
|
|
260
|
+
}
|
|
261
|
+
return [full, null]
|
|
262
|
+
}
|
|
263
|
+
if (normalized === '..' || normalized.startsWith('../')) {
|
|
264
|
+
return ['', ErrCrossedBoundary]
|
|
265
|
+
}
|
|
266
|
+
return [filepath.Join(this.baseDir, filepath.FromSlash(normalized)), null]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private insideBase(path: string): boolean {
|
|
270
|
+
const full = filepath.Clean(path)
|
|
271
|
+
if (full === this.baseDir) {
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
return full.startsWith(
|
|
275
|
+
this.baseDir.endsWith('/') ? this.baseDir : this.baseDir + '/',
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const Default = New('/')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import * as $ from '@goscript/builtin/index.js'
|
|
3
|
+
|
|
4
|
+
import { File, FileInfo, ReadDir } from './index.js'
|
|
5
|
+
|
|
6
|
+
describe('io/fs ReadDir override', () => {
|
|
7
|
+
it('returns a nil entry slice without sorting when directory read fails', () => {
|
|
8
|
+
const readErr = $.newError('read failed')
|
|
9
|
+
const file: File & {
|
|
10
|
+
ReadDir(n: number): [null, $.GoError]
|
|
11
|
+
} = {
|
|
12
|
+
Close(): $.GoError {
|
|
13
|
+
return null
|
|
14
|
+
},
|
|
15
|
+
Read(_p0: Uint8Array): [number, $.GoError] {
|
|
16
|
+
return [0, readErr]
|
|
17
|
+
},
|
|
18
|
+
Stat(): [FileInfo, $.GoError] {
|
|
19
|
+
return [null, readErr]
|
|
20
|
+
},
|
|
21
|
+
ReadDir(_n: number): [null, $.GoError] {
|
|
22
|
+
return [null, readErr]
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [list, err] = ReadDir(
|
|
27
|
+
{
|
|
28
|
+
Open(_name: string): [File, $.GoError] {
|
|
29
|
+
return [file, null]
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
'dir',
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(list).toBeNull()
|
|
36
|
+
expect(err).toBe(readErr)
|
|
37
|
+
})
|
|
38
|
+
})
|
package/gs/io/fs/readdir.ts
CHANGED
|
@@ -92,9 +92,13 @@ export function ReadDir(
|
|
|
92
92
|
|
|
93
93
|
let list: $.Slice<DirEntry>
|
|
94
94
|
;[list, err] = dir!.ReadDir(-1)
|
|
95
|
-
list
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
if (list) {
|
|
96
|
+
list.sort((a: DirEntry, b: DirEntry): number => {
|
|
97
|
+
return $.pointerValue<Exclude<DirEntry, null>>(a).Name().localeCompare(
|
|
98
|
+
$.pointerValue<Exclude<DirEntry, null>>(b).Name(),
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
98
102
|
return [list, err]
|
|
99
103
|
}
|
|
100
104
|
|
package/gs/io/io.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as $ from '@goscript/builtin/index.js'
|
|
2
|
-
import { LimitedReader, MultiWriter, TeeReader } from './index.js'
|
|
2
|
+
import { LimitedReader, MultiWriter, NopCloser, Pipe, TeeReader } from './index.js'
|
|
3
3
|
import { describe, expect, test } from 'vitest'
|
|
4
4
|
|
|
5
5
|
class sliceReader {
|
|
@@ -9,7 +9,7 @@ class sliceReader {
|
|
|
9
9
|
const n = Math.min($.len(p), this.data.length)
|
|
10
10
|
p!.set(this.data.subarray(0, n), 0)
|
|
11
11
|
this.data = this.data.subarray(n)
|
|
12
|
-
return [n, n === 0 ? new Error('EOF') as $.GoError : null]
|
|
12
|
+
return [n, n === 0 ? (new Error('EOF') as $.GoError) : null]
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -37,28 +37,99 @@ describe('io override', () => {
|
|
|
37
37
|
expect(Buffer.from(buf.subarray(0, n)).toString('utf8')).toBe('abc')
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
test('TeeReader accepts nullable generated interface values', () => {
|
|
40
|
+
test('TeeReader accepts nullable generated interface values', async () => {
|
|
41
41
|
const writer = new captureWriter()
|
|
42
42
|
const reader = TeeReader(new sliceReader($.stringToBytes('abc')), writer)
|
|
43
43
|
const buf = new Uint8Array(4)
|
|
44
44
|
|
|
45
|
-
const [n, err] = reader.Read(buf)
|
|
45
|
+
const [n, err] = await reader.Read(buf)
|
|
46
46
|
|
|
47
47
|
expect(err).toBeNull()
|
|
48
48
|
expect(n).toBe(3)
|
|
49
49
|
expect(Buffer.from(writer.chunks).toString('utf8')).toBe('abc')
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
test('
|
|
52
|
+
test('NopCloser accepts nullable generated interface values', () => {
|
|
53
|
+
const reader: sliceReader | null = new sliceReader($.stringToBytes('abc'))
|
|
54
|
+
const body = NopCloser(reader)
|
|
55
|
+
const buf = new Uint8Array(4)
|
|
56
|
+
|
|
57
|
+
const [n, err] = body.Read(buf)
|
|
58
|
+
|
|
59
|
+
expect(err).toBeNull()
|
|
60
|
+
expect(n).toBe(3)
|
|
61
|
+
expect(body.Close()).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('TeeReader awaits async readers and writers', async () => {
|
|
65
|
+
const chunks: number[] = []
|
|
66
|
+
const reader = TeeReader(
|
|
67
|
+
{
|
|
68
|
+
async Read(p: $.Bytes): Promise<[number, $.GoError]> {
|
|
69
|
+
await Promise.resolve()
|
|
70
|
+
p!.set($.stringToBytes('abc'), 0)
|
|
71
|
+
return [3, null]
|
|
72
|
+
},
|
|
73
|
+
} as any,
|
|
74
|
+
{
|
|
75
|
+
async Write(p: $.Bytes): Promise<[number, $.GoError]> {
|
|
76
|
+
await Promise.resolve()
|
|
77
|
+
chunks.push(...Array.from(p ?? []))
|
|
78
|
+
return [$.len(p), null]
|
|
79
|
+
},
|
|
80
|
+
} as any,
|
|
81
|
+
)
|
|
82
|
+
const buf = new Uint8Array(4)
|
|
83
|
+
|
|
84
|
+
const [n, err] = await reader.Read(buf)
|
|
85
|
+
|
|
86
|
+
expect(err).toBeNull()
|
|
87
|
+
expect(n).toBe(3)
|
|
88
|
+
expect(Buffer.from(chunks).toString('utf8')).toBe('abc')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('MultiWriter accepts nullable generated interface values', async () => {
|
|
53
92
|
const first = new captureWriter()
|
|
54
93
|
const second = new captureWriter()
|
|
55
94
|
const writer = MultiWriter(first, second)
|
|
56
95
|
|
|
57
|
-
const [n, err] = writer.Write($.stringToBytes('abc'))
|
|
96
|
+
const [n, err] = await writer.Write($.stringToBytes('abc'))
|
|
58
97
|
|
|
59
98
|
expect(err).toBeNull()
|
|
60
99
|
expect(n).toBe(3)
|
|
61
100
|
expect(Buffer.from(first.chunks).toString('utf8')).toBe('abc')
|
|
62
101
|
expect(Buffer.from(second.chunks).toString('utf8')).toBe('abc')
|
|
63
102
|
})
|
|
103
|
+
|
|
104
|
+
test('MultiWriter awaits async generated writers', async () => {
|
|
105
|
+
const chunks: number[] = []
|
|
106
|
+
const writer = MultiWriter({
|
|
107
|
+
async Write(p: $.Bytes): Promise<[number, $.GoError]> {
|
|
108
|
+
await Promise.resolve()
|
|
109
|
+
chunks.push(...Array.from(p ?? []))
|
|
110
|
+
return [$.len(p), null]
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const [n, err] = await writer.Write($.stringToBytes('abc'))
|
|
115
|
+
|
|
116
|
+
expect(err).toBeNull()
|
|
117
|
+
expect(n).toBe(3)
|
|
118
|
+
expect(Buffer.from(chunks).toString('utf8')).toBe('abc')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('PipeReader waits for a later write', async () => {
|
|
122
|
+
const [reader, writer] = Pipe()
|
|
123
|
+
const buf = new Uint8Array(5)
|
|
124
|
+
|
|
125
|
+
const read = reader.Read(buf)
|
|
126
|
+
const [written, writeErr] = await writer.Write($.stringToBytes('later'))
|
|
127
|
+
const [readBytes, readErr] = await read
|
|
128
|
+
|
|
129
|
+
expect(writeErr).toBeNull()
|
|
130
|
+
expect(written).toBe(5)
|
|
131
|
+
expect(readErr).toBeNull()
|
|
132
|
+
expect(readBytes).toBe(5)
|
|
133
|
+
expect(Buffer.from(buf).toString('utf8')).toBe('later')
|
|
134
|
+
})
|
|
64
135
|
})
|