goscript 0.1.2 → 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/lowered-program.go +1 -0
- package/compiler/lowering.go +1325 -194
- package/compiler/lowering_bench_test.go +350 -0
- package/compiler/override-registry_test.go +43 -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 +1436 -50
- package/compiler/typescript-emitter.go +47 -4
- 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/channel.js +36 -9
- package/dist/gs/builtin/channel.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 +80 -8
- package/dist/gs/builtin/type.js.map +1 -1
- package/dist/gs/bytes/bytes.gs.d.ts +7 -5
- package/dist/gs/bytes/bytes.gs.js +10 -4
- package/dist/gs/bytes/bytes.gs.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.d.ts +5 -0
- package/dist/gs/crypto/sha1/index.js +103 -0
- package/dist/gs/crypto/sha1/index.js.map +1 -0
- 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 +4 -4
- package/dist/gs/fmt/fmt.js +93 -19
- 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 +18 -11
- package/dist/gs/io/io.js +107 -44
- 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/httptest/index.js +7 -5
- package/dist/gs/net/http/httptest/index.js.map +1 -1
- package/dist/gs/net/http/index.d.ts +11 -1
- package/dist/gs/net/http/index.js +157 -11
- 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/os/zero_copy_posix.gs.js +1 -1
- package/dist/gs/os/zero_copy_posix.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/channel.ts +47 -9
- package/gs/builtin/runtime-contract.test.ts +78 -0
- package/gs/builtin/slice.ts +7 -0
- package/gs/builtin/type.ts +97 -8
- package/gs/bytes/bytes.gs.ts +19 -10
- package/gs/bytes/bytes.test.ts +17 -0
- 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/context/context.test.ts +5 -1
- package/gs/crypto/sha1/index.test.ts +45 -0
- package/gs/crypto/sha1/index.ts +127 -0
- package/gs/crypto/sha1/meta.json +8 -0
- 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 +61 -3
- package/gs/fmt/fmt.ts +115 -22
- 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/aperturerobotics/util/conc/index.test.ts +1 -1
- 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 +135 -0
- package/gs/io/io.ts +143 -63
- package/gs/io/meta.json +7 -1
- package/gs/math/bits/index.ts +52 -28
- package/gs/net/http/httptest/index.test.ts +34 -2
- package/gs/net/http/httptest/index.ts +23 -8
- package/gs/net/http/index.test.ts +46 -0
- package/gs/net/http/index.ts +178 -12
- 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/os/zero_copy_posix.gs.ts +1 -2
- 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 +2 -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
package/gs/fmt/meta.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import * as context from '@goscript/context/index.js'
|
|
4
|
+
import * as $ from '@goscript/builtin/index.js'
|
|
4
5
|
|
|
5
|
-
import { MuxedConn, NewWebSocketConn } from './index.js'
|
|
6
|
+
import { MuxedConn, NewClientSet, NewPrefixClient, NewWebSocketConn } from './index.js'
|
|
6
7
|
|
|
7
8
|
describe('starpc/srpc override', () => {
|
|
8
9
|
it('wraps websocket connections with the same outbound direction as Go', () => {
|
|
@@ -28,4 +29,10 @@ describe('starpc/srpc override', () => {
|
|
|
28
29
|
expect(serverConn?.outbound).toBe(false)
|
|
29
30
|
expect(clientConn?.outbound).toBe(true)
|
|
30
31
|
})
|
|
32
|
+
|
|
33
|
+
it('registers prefixed clients as srpc.Client values', () => {
|
|
34
|
+
const client = NewPrefixClient(NewClientSet(null), ['plugin-host/'])
|
|
35
|
+
|
|
36
|
+
expect($.typeAssert(client, 'srpc.Client').ok).toBe(true)
|
|
37
|
+
})
|
|
31
38
|
})
|
|
@@ -115,6 +115,33 @@ export interface Client {
|
|
|
115
115
|
): Promise<[Stream | null, $.GoError]>
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
$.registerInterfaceType('srpc.Client', null, [
|
|
119
|
+
{
|
|
120
|
+
name: 'ExecCall',
|
|
121
|
+
args: [
|
|
122
|
+
{ name: 'ctx', type: 'context.Context' },
|
|
123
|
+
{ name: 'service', type: { kind: $.TypeKind.Basic, name: 'string' } },
|
|
124
|
+
{ name: 'method', type: { kind: $.TypeKind.Basic, name: 'string' } },
|
|
125
|
+
{ name: 'in', type: 'srpc.Message' },
|
|
126
|
+
{ name: 'out', type: 'srpc.Message' },
|
|
127
|
+
],
|
|
128
|
+
returns: [{ name: '_r0', type: 'error' }],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'NewStream',
|
|
132
|
+
args: [
|
|
133
|
+
{ name: 'ctx', type: 'context.Context' },
|
|
134
|
+
{ name: 'service', type: { kind: $.TypeKind.Basic, name: 'string' } },
|
|
135
|
+
{ name: 'method', type: { kind: $.TypeKind.Basic, name: 'string' } },
|
|
136
|
+
{ name: 'firstMsg', type: 'srpc.Message' },
|
|
137
|
+
],
|
|
138
|
+
returns: [
|
|
139
|
+
{ name: '_r0', type: 'srpc.Stream' },
|
|
140
|
+
{ name: '_r1', type: 'error' },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
])
|
|
144
|
+
|
|
118
145
|
export class ClientSet implements Client {
|
|
119
146
|
private clients: $.Slice<Client | null>
|
|
120
147
|
|
|
@@ -395,6 +422,84 @@ class memoryStream implements Stream {
|
|
|
395
422
|
}
|
|
396
423
|
}
|
|
397
424
|
|
|
425
|
+
class streamQueue {
|
|
426
|
+
private queue: (Message | null)[] = []
|
|
427
|
+
private waiters: ((msg: Message | null, err: $.GoError) => void)[] = []
|
|
428
|
+
private closed = false
|
|
429
|
+
|
|
430
|
+
public send(msg: Message | null): $.GoError {
|
|
431
|
+
if (this.closed) {
|
|
432
|
+
return ErrCompleted
|
|
433
|
+
}
|
|
434
|
+
const waiter = this.waiters.shift()
|
|
435
|
+
if (waiter != null) {
|
|
436
|
+
waiter(msg, null)
|
|
437
|
+
return null
|
|
438
|
+
}
|
|
439
|
+
this.queue.push(msg)
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
public recv(msg: Message | null): MaybePromise<$.GoError> {
|
|
444
|
+
const next = this.queue.shift()
|
|
445
|
+
if (next !== undefined) {
|
|
446
|
+
if (msg != null && next != null) {
|
|
447
|
+
Object.assign(msg, next)
|
|
448
|
+
}
|
|
449
|
+
return null
|
|
450
|
+
}
|
|
451
|
+
if (this.closed) {
|
|
452
|
+
return io.EOF
|
|
453
|
+
}
|
|
454
|
+
return new Promise<$.GoError>((resolve) => {
|
|
455
|
+
this.waiters.push((sent, err) => {
|
|
456
|
+
if (msg != null && sent != null) {
|
|
457
|
+
Object.assign(msg, sent)
|
|
458
|
+
}
|
|
459
|
+
resolve(err)
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
public close(): $.GoError {
|
|
465
|
+
this.closed = true
|
|
466
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
467
|
+
waiter(null, io.EOF)
|
|
468
|
+
}
|
|
469
|
+
return null
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
class pairedMemoryStream implements Stream {
|
|
474
|
+
constructor(
|
|
475
|
+
private ctx: context.Context,
|
|
476
|
+
private incoming: streamQueue,
|
|
477
|
+
private outgoing: streamQueue,
|
|
478
|
+
) {}
|
|
479
|
+
|
|
480
|
+
public Context(): context.Context {
|
|
481
|
+
return this.ctx
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
public MsgSend(msg: Message | null): $.GoError {
|
|
485
|
+
return this.outgoing.send(msg)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
public MsgRecv(msg: Message | null): MaybePromise<$.GoError> {
|
|
489
|
+
return this.incoming.recv(msg)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
public CloseSend(): $.GoError {
|
|
493
|
+
return this.outgoing.close()
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
public Close(): $.GoError {
|
|
497
|
+
const incomingErr = this.incoming.close()
|
|
498
|
+
const outgoingErr = this.outgoing.close()
|
|
499
|
+
return incomingErr ?? outgoingErr
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
398
503
|
class streamWithClose implements Stream {
|
|
399
504
|
constructor(
|
|
400
505
|
private stream: Stream,
|
|
@@ -738,7 +843,7 @@ class transportClient implements Client {
|
|
|
738
843
|
if (output != null) {
|
|
739
844
|
output.Reset()
|
|
740
845
|
}
|
|
741
|
-
return await writer?.Close() ?? null
|
|
846
|
+
return (await writer?.Close()) ?? null
|
|
742
847
|
}
|
|
743
848
|
|
|
744
849
|
public async NewStream(
|
|
@@ -792,7 +897,9 @@ export function NewClient(openStream: OpenStreamFunc | null): Client {
|
|
|
792
897
|
class invokerClient implements Client {
|
|
793
898
|
constructor(
|
|
794
899
|
private invoker: Invoker | null,
|
|
795
|
-
private contextFn:
|
|
900
|
+
private contextFn:
|
|
901
|
+
| ((ctx: context.Context) => context.Context)
|
|
902
|
+
| null = null,
|
|
796
903
|
) {}
|
|
797
904
|
|
|
798
905
|
public async ExecCall(
|
|
@@ -833,19 +940,36 @@ class invokerClient implements Client {
|
|
|
833
940
|
if (this.invoker == null) {
|
|
834
941
|
return [null, ErrNoAvailableClients]
|
|
835
942
|
}
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
943
|
+
const streamCtx = this.contextFn == null ? ctx : this.contextFn(ctx)
|
|
944
|
+
const clientInput = new streamQueue()
|
|
945
|
+
const serverInput = new streamQueue()
|
|
946
|
+
const clientStream = new pairedMemoryStream(
|
|
947
|
+
streamCtx,
|
|
948
|
+
clientInput,
|
|
949
|
+
serverInput,
|
|
950
|
+
)
|
|
951
|
+
const serverStream = new pairedMemoryStream(
|
|
952
|
+
streamCtx,
|
|
953
|
+
serverInput,
|
|
954
|
+
clientInput,
|
|
839
955
|
)
|
|
956
|
+
if (firstMsg != null) {
|
|
957
|
+
const err = serverInput.send(firstMsg)
|
|
958
|
+
if (err != null) {
|
|
959
|
+
return [null, err]
|
|
960
|
+
}
|
|
961
|
+
}
|
|
840
962
|
const pending = Promise.resolve(
|
|
841
|
-
this.invoker.InvokeMethod(service, method,
|
|
963
|
+
this.invoker.InvokeMethod(service, method, serverStream),
|
|
842
964
|
)
|
|
843
965
|
pending.then(([handled, err]) => {
|
|
844
966
|
if (!handled || err != null) {
|
|
845
|
-
|
|
967
|
+
clientStream.Close()
|
|
968
|
+
return
|
|
846
969
|
}
|
|
970
|
+
serverStream.CloseSend()
|
|
847
971
|
})
|
|
848
|
-
return [
|
|
972
|
+
return [clientStream, null]
|
|
849
973
|
}
|
|
850
974
|
}
|
|
851
975
|
|
|
@@ -936,7 +1060,9 @@ export class ServerRPC {
|
|
|
936
1060
|
stream,
|
|
937
1061
|
)
|
|
938
1062
|
const callErr = err ?? (handled ? null : ErrUnimplemented)
|
|
939
|
-
await this.writer?.WritePacket(
|
|
1063
|
+
await this.writer?.WritePacket(
|
|
1064
|
+
NewCallDataPacket(null, false, true, callErr),
|
|
1065
|
+
)
|
|
940
1066
|
return callErr
|
|
941
1067
|
}
|
|
942
1068
|
|
|
@@ -995,8 +1121,10 @@ export function NewServerPipe(server: Server | null): OpenStreamFunc {
|
|
|
995
1121
|
_ctx: context.Context,
|
|
996
1122
|
_msgHandler: PacketDataHandler,
|
|
997
1123
|
_closeHandler: CloseHandler,
|
|
998
|
-
): [PacketWriter | null, $.GoError] => [
|
|
999
|
-
|
|
1124
|
+
): [PacketWriter | null, $.GoError] => [
|
|
1125
|
+
new closedPacketWriter(),
|
|
1126
|
+
null,
|
|
1127
|
+
]) as OpenStreamFunc
|
|
1000
1128
|
if (server != null) {
|
|
1001
1129
|
openStream.__server = server
|
|
1002
1130
|
}
|
|
@@ -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
|
|