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.
Files changed (138) hide show
  1. package/cmd/goscript/cmd_compile.go +28 -8
  2. package/cmd/goscript/cmd_compile_test.go +105 -6
  3. package/compiler/build-flags.go +9 -10
  4. package/compiler/gotest/runner_test.go +127 -0
  5. package/compiler/lowered-program.go +1 -0
  6. package/compiler/lowering.go +1325 -194
  7. package/compiler/lowering_bench_test.go +350 -0
  8. package/compiler/override-registry_test.go +43 -0
  9. package/compiler/package-graph.go +61 -4
  10. package/compiler/package-graph_test.go +30 -0
  11. package/compiler/semantic-model-types.go +8 -0
  12. package/compiler/semantic-model.go +447 -22
  13. package/compiler/semantic-model_test.go +138 -0
  14. package/compiler/skeleton_test.go +1436 -50
  15. package/compiler/typescript-emitter.go +47 -4
  16. package/dist/gs/builtin/builtin.d.ts +2 -2
  17. package/dist/gs/builtin/builtin.js +20 -0
  18. package/dist/gs/builtin/builtin.js.map +1 -1
  19. package/dist/gs/builtin/channel.js +36 -9
  20. package/dist/gs/builtin/channel.js.map +1 -1
  21. package/dist/gs/builtin/slice.js +5 -0
  22. package/dist/gs/builtin/slice.js.map +1 -1
  23. package/dist/gs/builtin/type.d.ts +1 -1
  24. package/dist/gs/builtin/type.js +80 -8
  25. package/dist/gs/builtin/type.js.map +1 -1
  26. package/dist/gs/bytes/bytes.gs.d.ts +7 -5
  27. package/dist/gs/bytes/bytes.gs.js +10 -4
  28. package/dist/gs/bytes/bytes.gs.js.map +1 -1
  29. package/dist/gs/compress/zlib/index.d.ts +3 -3
  30. package/dist/gs/compress/zlib/index.js +88 -26
  31. package/dist/gs/compress/zlib/index.js.map +1 -1
  32. package/dist/gs/crypto/sha1/index.d.ts +5 -0
  33. package/dist/gs/crypto/sha1/index.js +103 -0
  34. package/dist/gs/crypto/sha1/index.js.map +1 -0
  35. package/dist/gs/crypto/sha256/index.js +2 -5
  36. package/dist/gs/crypto/sha256/index.js.map +1 -1
  37. package/dist/gs/crypto/sha512/index.js +2 -5
  38. package/dist/gs/crypto/sha512/index.js.map +1 -1
  39. package/dist/gs/embed/index.d.ts +6 -0
  40. package/dist/gs/embed/index.js +210 -5
  41. package/dist/gs/embed/index.js.map +1 -1
  42. package/dist/gs/fmt/fmt.d.ts +4 -4
  43. package/dist/gs/fmt/fmt.js +93 -19
  44. package/dist/gs/fmt/fmt.js.map +1 -1
  45. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +118 -6
  46. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +1 -1
  47. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.d.ts +45 -0
  48. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js +229 -0
  49. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js.map +1 -0
  50. package/dist/gs/io/fs/readdir.js +5 -3
  51. package/dist/gs/io/fs/readdir.js.map +1 -1
  52. package/dist/gs/io/io.d.ts +18 -11
  53. package/dist/gs/io/io.js +107 -44
  54. package/dist/gs/io/io.js.map +1 -1
  55. package/dist/gs/math/bits/index.d.ts +26 -5
  56. package/dist/gs/math/bits/index.js +13 -24
  57. package/dist/gs/math/bits/index.js.map +1 -1
  58. package/dist/gs/net/http/httptest/index.js +7 -5
  59. package/dist/gs/net/http/httptest/index.js.map +1 -1
  60. package/dist/gs/net/http/index.d.ts +11 -1
  61. package/dist/gs/net/http/index.js +157 -11
  62. package/dist/gs/net/http/index.js.map +1 -1
  63. package/dist/gs/os/types_js.gs.d.ts +6 -2
  64. package/dist/gs/os/types_js.gs.js +169 -8
  65. package/dist/gs/os/types_js.gs.js.map +1 -1
  66. package/dist/gs/os/zero_copy_posix.gs.js +1 -1
  67. package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
  68. package/dist/gs/reflect/type.d.ts +1 -0
  69. package/dist/gs/reflect/type.js +80 -51
  70. package/dist/gs/reflect/type.js.map +1 -1
  71. package/dist/gs/strings/reader.d.ts +1 -1
  72. package/dist/gs/strings/reader.js +2 -2
  73. package/dist/gs/strings/reader.js.map +1 -1
  74. package/dist/gs/sync/sync.d.ts +2 -1
  75. package/dist/gs/sync/sync.js +37 -16
  76. package/dist/gs/sync/sync.js.map +1 -1
  77. package/dist/gs/syscall/js/index.js +9 -0
  78. package/dist/gs/syscall/js/index.js.map +1 -1
  79. package/dist/gs/testing/testing.js +8 -6
  80. package/dist/gs/testing/testing.js.map +1 -1
  81. package/gs/builtin/builtin.ts +25 -2
  82. package/gs/builtin/channel.ts +47 -9
  83. package/gs/builtin/runtime-contract.test.ts +78 -0
  84. package/gs/builtin/slice.ts +7 -0
  85. package/gs/builtin/type.ts +97 -8
  86. package/gs/bytes/bytes.gs.ts +19 -10
  87. package/gs/bytes/bytes.test.ts +17 -0
  88. package/gs/compress/zlib/index.test.ts +97 -0
  89. package/gs/compress/zlib/index.ts +117 -27
  90. package/gs/compress/zlib/meta.json +4 -1
  91. package/gs/context/context.test.ts +5 -1
  92. package/gs/crypto/sha1/index.test.ts +45 -0
  93. package/gs/crypto/sha1/index.ts +127 -0
  94. package/gs/crypto/sha1/meta.json +8 -0
  95. package/gs/crypto/sha256/index.test.ts +14 -2
  96. package/gs/crypto/sha256/index.ts +3 -6
  97. package/gs/crypto/sha512/index.test.ts +17 -2
  98. package/gs/crypto/sha512/index.ts +3 -6
  99. package/gs/embed/index.test.ts +87 -0
  100. package/gs/embed/index.ts +229 -5
  101. package/gs/fmt/fmt.test.ts +61 -3
  102. package/gs/fmt/fmt.ts +115 -22
  103. package/gs/fmt/meta.json +6 -1
  104. package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +8 -1
  105. package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +139 -11
  106. package/gs/github.com/aperturerobotics/util/conc/index.test.ts +1 -1
  107. package/gs/github.com/go-git/go-billy/v6/osfs/index.test.ts +110 -0
  108. package/gs/github.com/go-git/go-billy/v6/osfs/index.ts +280 -0
  109. package/gs/github.com/go-git/go-billy/v6/osfs/meta.json +8 -0
  110. package/gs/io/fs/readdir.test.ts +38 -0
  111. package/gs/io/fs/readdir.ts +7 -3
  112. package/gs/io/io.test.ts +135 -0
  113. package/gs/io/io.ts +143 -63
  114. package/gs/io/meta.json +7 -1
  115. package/gs/math/bits/index.ts +52 -28
  116. package/gs/net/http/httptest/index.test.ts +34 -2
  117. package/gs/net/http/httptest/index.ts +23 -8
  118. package/gs/net/http/index.test.ts +46 -0
  119. package/gs/net/http/index.ts +178 -12
  120. package/gs/os/file_unix_js.test.ts +52 -0
  121. package/gs/os/meta.json +4 -0
  122. package/gs/os/readdir.test.ts +56 -0
  123. package/gs/os/types_js.gs.ts +169 -8
  124. package/gs/os/zero_copy_posix.gs.ts +1 -2
  125. package/gs/reflect/deepequal.test.ts +10 -1
  126. package/gs/reflect/type.ts +91 -56
  127. package/gs/reflect/typefor.test.ts +31 -1
  128. package/gs/strings/meta.json +5 -2
  129. package/gs/strings/reader.test.ts +2 -2
  130. package/gs/strings/reader.ts +2 -2
  131. package/gs/sync/meta.json +2 -0
  132. package/gs/sync/sync.test.ts +41 -1
  133. package/gs/sync/sync.ts +41 -16
  134. package/gs/syscall/js/index.test.ts +18 -0
  135. package/gs/syscall/js/index.ts +12 -0
  136. package/gs/testing/testing.test.ts +32 -3
  137. package/gs/testing/testing.ts +13 -10
  138. package/package.json +1 -1
package/gs/fmt/meta.json CHANGED
@@ -1,5 +1,10 @@
1
1
  {
2
+ "asyncFunctions": {
3
+ "Fprint": true,
4
+ "Fprintf": true,
5
+ "Fprintln": true
6
+ },
2
7
  "dependencies": [
3
8
  "errors"
4
9
  ]
5
- }
10
+ }
@@ -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: ((ctx: context.Context) => context.Context) | null = null,
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 stream = new memoryStream(
837
- this.contextFn == null ? ctx : this.contextFn(ctx),
838
- firstMsg,
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, stream),
963
+ this.invoker.InvokeMethod(service, method, serverStream),
842
964
  )
843
965
  pending.then(([handled, err]) => {
844
966
  if (!handled || err != null) {
845
- stream.Close()
967
+ clientStream.Close()
968
+ return
846
969
  }
970
+ serverStream.CloseSend()
847
971
  })
848
- return [stream, null]
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(NewCallDataPacket(null, false, true, callErr))
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] => [new closedPacketWriter(), null]) as
999
- OpenStreamFunc
1124
+ ): [PacketWriter | null, $.GoError] => [
1125
+ new closedPacketWriter(),
1126
+ null,
1127
+ ]) as OpenStreamFunc
1000
1128
  if (server != null) {
1001
1129
  openStream.__server = server
1002
1130
  }
@@ -25,6 +25,6 @@ describe('util/conc override', () => {
25
25
 
26
26
  release.close()
27
27
  expect(await q.WaitIdle(Background(), null)).toBeNull()
28
- expect(done).toEqual([0, 1, 2, 3, 4])
28
+ expect([...done].sort((a, b) => a - b)).toEqual([0, 1, 2, 3, 4])
29
29
  })
30
30
  })
@@ -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,8 @@
1
+ {
2
+ "dependencies": [
3
+ "io/fs",
4
+ "os",
5
+ "path/filepath"
6
+ ],
7
+ "asyncMethods": {}
8
+ }
@@ -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
+ })
@@ -92,9 +92,13 @@ export function ReadDir(
92
92
 
93
93
  let list: $.Slice<DirEntry>
94
94
  ;[list, err] = dir!.ReadDir(-1)
95
- list!.sort((a: DirEntry, b: DirEntry): number => {
96
- return a!.Name().localeCompare(b!.Name())
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