goscript 0.1.2 → 0.1.3
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/compiler/lowered-program.go +1 -0
- package/compiler/lowering.go +715 -44
- package/compiler/override-registry_test.go +43 -0
- package/compiler/skeleton_test.go +464 -12
- package/compiler/typescript-emitter.go +28 -2
- package/dist/gs/builtin/channel.js +36 -9
- package/dist/gs/builtin/channel.js.map +1 -1
- package/dist/gs/builtin/type.js +8 -3
- 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/crypto/sha1/index.d.ts +5 -0
- package/dist/gs/crypto/sha1/index.js +106 -0
- package/dist/gs/crypto/sha1/index.js.map +1 -0
- package/dist/gs/fmt/fmt.d.ts +1 -1
- package/dist/gs/fmt/fmt.js +64 -3
- package/dist/gs/fmt/fmt.js.map +1 -1
- package/dist/gs/io/io.d.ts +8 -5
- package/dist/gs/io/io.js +20 -2
- package/dist/gs/io/io.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 +8 -0
- package/dist/gs/net/http/index.js +139 -10
- package/dist/gs/net/http/index.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/gs/builtin/channel.ts +47 -9
- package/gs/builtin/runtime-contract.test.ts +33 -0
- package/gs/builtin/type.ts +12 -3
- package/gs/bytes/bytes.gs.ts +19 -10
- package/gs/bytes/bytes.test.ts +17 -0
- package/gs/context/context.test.ts +5 -1
- package/gs/crypto/sha1/index.test.ts +28 -0
- package/gs/crypto/sha1/index.ts +130 -0
- package/gs/crypto/sha1/meta.json +8 -0
- package/gs/fmt/fmt.test.ts +20 -0
- package/gs/fmt/fmt.ts +75 -5
- package/gs/github.com/aperturerobotics/util/conc/index.test.ts +1 -1
- package/gs/io/io.test.ts +64 -0
- package/gs/io/io.ts +30 -12
- 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 +30 -0
- package/gs/net/http/index.ts +159 -10
- package/gs/os/zero_copy_posix.gs.ts +1 -2
- package/gs/sync/meta.json +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { New, Sum } from './index.js'
|
|
5
|
+
|
|
6
|
+
describe('crypto/sha1 override', () => {
|
|
7
|
+
it('matches Node digest', async () => {
|
|
8
|
+
const data = new TextEncoder().encode('goscript sha1')
|
|
9
|
+
|
|
10
|
+
expect(toHex(await Sum(data))).toBe(nodeHash(data))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('supports incremental hash.Hash use', async () => {
|
|
14
|
+
const h = New()
|
|
15
|
+
h.Write(new TextEncoder().encode('go'))
|
|
16
|
+
h.Write(new TextEncoder().encode('script'))
|
|
17
|
+
|
|
18
|
+
expect(toHex(await h.Sum(null))).toBe(nodeHash(new TextEncoder().encode('goscript')))
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function nodeHash(data: Uint8Array): string {
|
|
23
|
+
return createHash('sha1').update(data).digest('hex')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toHex(value: Uint8Array): string {
|
|
27
|
+
return Buffer.from(value).toString('hex')
|
|
28
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as $ from '@goscript/builtin/index.js'
|
|
2
|
+
import {
|
|
3
|
+
getHostRuntime,
|
|
4
|
+
type NodeCryptoHash,
|
|
5
|
+
} from '@goscript/builtin/hostio.js'
|
|
6
|
+
|
|
7
|
+
export const Size = 20
|
|
8
|
+
export const BlockSize = 64
|
|
9
|
+
|
|
10
|
+
class Sha1Error {
|
|
11
|
+
constructor(private readonly message: string) {}
|
|
12
|
+
|
|
13
|
+
Error(): string {
|
|
14
|
+
return this.message
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class Digest {
|
|
19
|
+
private chunks: Uint8Array[] = []
|
|
20
|
+
private dataLength = 0
|
|
21
|
+
private hash: NodeCryptoHash | null
|
|
22
|
+
private canCopyHash: boolean
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
this.hash = createNodeHash()
|
|
26
|
+
this.canCopyHash = typeof this.hash?.copy === 'function'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Write(p: $.Bytes): [number, $.GoError] {
|
|
30
|
+
const bytes = $.bytesToUint8Array(p)
|
|
31
|
+
this.hash?.update(bytes)
|
|
32
|
+
if (!this.canCopyHash) {
|
|
33
|
+
this.chunks.push(bytes.slice())
|
|
34
|
+
this.dataLength += bytes.length
|
|
35
|
+
}
|
|
36
|
+
return [bytes.length, null]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async Sum(b: $.Bytes): Promise<$.Bytes> {
|
|
40
|
+
const digest =
|
|
41
|
+
this.canCopyHash ?
|
|
42
|
+
new Uint8Array(this.hash!.copy!().digest())
|
|
43
|
+
: await sum(this.snapshotBytes())
|
|
44
|
+
return appendDigest($.bytesToUint8Array(b), digest)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Reset(): void {
|
|
48
|
+
this.chunks = []
|
|
49
|
+
this.dataLength = 0
|
|
50
|
+
this.hash = createNodeHash()
|
|
51
|
+
this.canCopyHash = typeof this.hash?.copy === 'function'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Size(): number {
|
|
55
|
+
return Size
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
BlockSize(): number {
|
|
59
|
+
return BlockSize
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private snapshotBytes(): Uint8Array {
|
|
63
|
+
return concatChunks(this.chunks, this.dataLength)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function New(): any {
|
|
68
|
+
return new Digest()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function Sum(data: $.Bytes): Promise<Uint8Array> {
|
|
72
|
+
return sum(data)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function sum(data: $.Bytes): Promise<Uint8Array> {
|
|
76
|
+
const hash = createNodeHash()
|
|
77
|
+
if (hash != null) {
|
|
78
|
+
return new Uint8Array(hash.update($.bytesToUint8Array(data)).digest())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const subtle = subtleCrypto()
|
|
82
|
+
if (subtle == null) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
new Sha1Error('crypto/sha1: WebCrypto digest is unavailable').Error(),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const digest = await subtle.digest(
|
|
89
|
+
'SHA-1',
|
|
90
|
+
$.bytesToUint8Array(data) as unknown as BufferSource,
|
|
91
|
+
)
|
|
92
|
+
return new Uint8Array(digest)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function appendDigest(prefix: Uint8Array, digest: Uint8Array): Uint8Array {
|
|
96
|
+
const out = new Uint8Array(prefix.length + digest.length)
|
|
97
|
+
out.set(prefix)
|
|
98
|
+
out.set(digest, prefix.length)
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createNodeHash(): NodeCryptoHash | null {
|
|
103
|
+
const nodeCrypto = getHostRuntime().nodeCrypto
|
|
104
|
+
if (!nodeCrypto?.createHash) {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return nodeCrypto.createHash('sha1')
|
|
109
|
+
} catch {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function concatChunks(chunks: Uint8Array[], length: number): Uint8Array {
|
|
115
|
+
const out = new Uint8Array(length)
|
|
116
|
+
let offset = 0
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
out.set(chunk, offset)
|
|
119
|
+
offset += chunk.length
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function subtleCrypto(): SubtleCrypto | null {
|
|
125
|
+
const crypto = globalThis.crypto
|
|
126
|
+
if (crypto?.subtle && typeof crypto.subtle.digest === 'function') {
|
|
127
|
+
return crypto.subtle
|
|
128
|
+
}
|
|
129
|
+
return null
|
|
130
|
+
}
|
package/gs/fmt/fmt.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
import { resetHostRuntimeForTests } from '@goscript/builtin/hostio.js'
|
|
3
|
+
import * as $ from '@goscript/builtin/index.js'
|
|
3
4
|
import * as fmt from './fmt.js'
|
|
4
5
|
|
|
5
6
|
const originalDeno = (globalThis as any).Deno
|
|
@@ -187,3 +188,22 @@ describe('fmt parseFormat basic cases', () => {
|
|
|
187
188
|
expect(fmt.Sprintf('%c', 65)).toBe('A')
|
|
188
189
|
})
|
|
189
190
|
})
|
|
191
|
+
|
|
192
|
+
describe('fmt scanning', () => {
|
|
193
|
+
it('scans decimal fields separated by literals', () => {
|
|
194
|
+
const start = $.varRef(0)
|
|
195
|
+
const end = $.varRef(0)
|
|
196
|
+
|
|
197
|
+
const [n, err] = fmt.Sscanf(
|
|
198
|
+
'bytes=12-34',
|
|
199
|
+
'bytes=%d-%d',
|
|
200
|
+
$.interfaceValue(start, '*int64'),
|
|
201
|
+
$.interfaceValue(end, '*int64'),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
expect(err).toBeNull()
|
|
205
|
+
expect(n).toBe(2)
|
|
206
|
+
expect(start.value).toBe(12)
|
|
207
|
+
expect(end.value).toBe(34)
|
|
208
|
+
})
|
|
209
|
+
})
|
package/gs/fmt/fmt.ts
CHANGED
|
@@ -488,12 +488,82 @@ export function Sscan(_str: string, ..._a: any[]): [number, $.GoError | null] {
|
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
export function Sscanf(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
...
|
|
491
|
+
str: string,
|
|
492
|
+
format: string,
|
|
493
|
+
...a: any[]
|
|
494
494
|
): [number, $.GoError | null] {
|
|
495
|
-
|
|
496
|
-
|
|
495
|
+
const parts = buildScanPattern(format)
|
|
496
|
+
if (parts == null) {
|
|
497
|
+
return [0, $.newError(`unsupported Sscanf format: ${format}`)]
|
|
498
|
+
}
|
|
499
|
+
const match = parts.pattern.exec(str)
|
|
500
|
+
if (match == null) {
|
|
501
|
+
return [0, $.newError('input does not match format')]
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let assigned = 0
|
|
505
|
+
for (let i = 0; i < parts.verbs.length && i < a.length; i++) {
|
|
506
|
+
const raw = match[i + 1]
|
|
507
|
+
const value = parts.verbs[i] === 'd' ? Number.parseInt(raw, 10) : raw
|
|
508
|
+
if (!assignScanValue(a[i], value)) {
|
|
509
|
+
return [assigned, $.newError('scan destination is not assignable')]
|
|
510
|
+
}
|
|
511
|
+
assigned++
|
|
512
|
+
}
|
|
513
|
+
return [assigned, null]
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function buildScanPattern(
|
|
517
|
+
format: string,
|
|
518
|
+
): { pattern: RegExp; verbs: string[] } | null {
|
|
519
|
+
let source = '^'
|
|
520
|
+
const verbs: string[] = []
|
|
521
|
+
for (let i = 0; i < format.length; i++) {
|
|
522
|
+
const ch = format[i]
|
|
523
|
+
if (ch !== '%') {
|
|
524
|
+
source += /\s/.test(ch) ? '\\s+' : escapeRegExp(ch)
|
|
525
|
+
continue
|
|
526
|
+
}
|
|
527
|
+
const verb = format[++i]
|
|
528
|
+
if (verb === '%') {
|
|
529
|
+
source += '%'
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
if (verb === 'd') {
|
|
533
|
+
source += '([+-]?\\d+)'
|
|
534
|
+
verbs.push(verb)
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
if (verb === 's') {
|
|
538
|
+
source += '(\\S+)'
|
|
539
|
+
verbs.push(verb)
|
|
540
|
+
continue
|
|
541
|
+
}
|
|
542
|
+
return null
|
|
543
|
+
}
|
|
544
|
+
source += '$'
|
|
545
|
+
return { pattern: new RegExp(source), verbs }
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function assignScanValue(target: any, value: string | number): boolean {
|
|
549
|
+
const ref =
|
|
550
|
+
$.isVarRef(target) ? target
|
|
551
|
+
: (
|
|
552
|
+
target != null &&
|
|
553
|
+
typeof target === 'object' &&
|
|
554
|
+
$.isVarRef(target.__goValue)
|
|
555
|
+
) ?
|
|
556
|
+
target.__goValue
|
|
557
|
+
: null
|
|
558
|
+
if (ref == null) {
|
|
559
|
+
return false
|
|
560
|
+
}
|
|
561
|
+
ref.value = value
|
|
562
|
+
return true
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function escapeRegExp(ch: string): string {
|
|
566
|
+
return ch.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
|
|
497
567
|
}
|
|
498
568
|
|
|
499
569
|
export function Sscanln(
|
package/gs/io/io.test.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as $ from '@goscript/builtin/index.js'
|
|
2
|
+
import { LimitedReader, MultiWriter, TeeReader } from './index.js'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
class sliceReader {
|
|
6
|
+
constructor(private data: Uint8Array) {}
|
|
7
|
+
|
|
8
|
+
Read(p: $.Bytes): [number, $.GoError] {
|
|
9
|
+
const n = Math.min($.len(p), this.data.length)
|
|
10
|
+
p!.set(this.data.subarray(0, n), 0)
|
|
11
|
+
this.data = this.data.subarray(n)
|
|
12
|
+
return [n, n === 0 ? new Error('EOF') as $.GoError : null]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class captureWriter {
|
|
17
|
+
public chunks: number[] = []
|
|
18
|
+
|
|
19
|
+
Write(p: $.Bytes): [number, $.GoError] {
|
|
20
|
+
this.chunks.push(...Array.from(p ?? []))
|
|
21
|
+
return [$.len(p), null]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('io override', () => {
|
|
26
|
+
test('LimitedReader accepts generated struct-literal construction', async () => {
|
|
27
|
+
const reader = new LimitedReader({
|
|
28
|
+
R: new sliceReader($.stringToBytes('abcdef')),
|
|
29
|
+
N: 3,
|
|
30
|
+
})
|
|
31
|
+
const buf = new Uint8Array(8)
|
|
32
|
+
|
|
33
|
+
const [n, err] = await reader.Read(buf)
|
|
34
|
+
|
|
35
|
+
expect(err).toBeNull()
|
|
36
|
+
expect(n).toBe(3)
|
|
37
|
+
expect(Buffer.from(buf.subarray(0, n)).toString('utf8')).toBe('abc')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('TeeReader accepts nullable generated interface values', () => {
|
|
41
|
+
const writer = new captureWriter()
|
|
42
|
+
const reader = TeeReader(new sliceReader($.stringToBytes('abc')), writer)
|
|
43
|
+
const buf = new Uint8Array(4)
|
|
44
|
+
|
|
45
|
+
const [n, err] = reader.Read(buf)
|
|
46
|
+
|
|
47
|
+
expect(err).toBeNull()
|
|
48
|
+
expect(n).toBe(3)
|
|
49
|
+
expect(Buffer.from(writer.chunks).toString('utf8')).toBe('abc')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('MultiWriter accepts nullable generated interface values', () => {
|
|
53
|
+
const first = new captureWriter()
|
|
54
|
+
const second = new captureWriter()
|
|
55
|
+
const writer = MultiWriter(first, second)
|
|
56
|
+
|
|
57
|
+
const [n, err] = writer.Write($.stringToBytes('abc'))
|
|
58
|
+
|
|
59
|
+
expect(err).toBeNull()
|
|
60
|
+
expect(n).toBe(3)
|
|
61
|
+
expect(Buffer.from(first.chunks).toString('utf8')).toBe('abc')
|
|
62
|
+
expect(Buffer.from(second.chunks).toString('utf8')).toBe('abc')
|
|
63
|
+
})
|
|
64
|
+
})
|
package/gs/io/io.ts
CHANGED
|
@@ -232,7 +232,7 @@ class DiscardWriter implements Writer {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
export const Discard: Writer = new DiscardWriter()
|
|
235
|
+
export const Discard: Writer | null = new DiscardWriter()
|
|
236
236
|
|
|
237
237
|
// WriteString writes the contents of the string s to w, which accepts a slice of bytes
|
|
238
238
|
export function WriteString(w: Writer, s: string): [number, $.GoError] {
|
|
@@ -248,12 +248,18 @@ export function WriteString(w: Writer, s: string): [number, $.GoError] {
|
|
|
248
248
|
|
|
249
249
|
// LimitedReader reads from R but limits the amount of data returned to just N bytes
|
|
250
250
|
export class LimitedReader implements Reader {
|
|
251
|
-
public R: Reader
|
|
251
|
+
public R: Reader | null
|
|
252
252
|
public N: number
|
|
253
253
|
|
|
254
|
-
constructor(r
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
constructor(r?: Reader | { R?: Reader | null; N?: number } | null, n?: number) {
|
|
255
|
+
if (r != null && typeof (r as { Read?: unknown }).Read !== 'function') {
|
|
256
|
+
const init = r as { R?: Reader | null; N?: number }
|
|
257
|
+
this.R = init.R ?? null
|
|
258
|
+
this.N = init.N ?? 0
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
this.R = (r as Reader | null | undefined) ?? null
|
|
262
|
+
this.N = n ?? 0
|
|
257
263
|
}
|
|
258
264
|
|
|
259
265
|
Read(p: $.Bytes): [number, $.GoError] {
|
|
@@ -261,6 +267,9 @@ export class LimitedReader implements Reader {
|
|
|
261
267
|
if (this.N <= 0) {
|
|
262
268
|
return [0, EOF]
|
|
263
269
|
}
|
|
270
|
+
if (this.R == null) {
|
|
271
|
+
throw new Error('io.LimitedReader: nil reader')
|
|
272
|
+
}
|
|
264
273
|
|
|
265
274
|
let readBuf = p
|
|
266
275
|
if ($.len(p) > this.N) {
|
|
@@ -645,19 +654,22 @@ class multiReader implements Reader {
|
|
|
645
654
|
}
|
|
646
655
|
|
|
647
656
|
// MultiWriter creates a writer that duplicates its writes to all the provided writers
|
|
648
|
-
export function MultiWriter(...writers: Writer[]): Writer {
|
|
657
|
+
export function MultiWriter(...writers: (Writer | null)[]): Writer {
|
|
649
658
|
return new multiWriter(writers.slice())
|
|
650
659
|
}
|
|
651
660
|
|
|
652
661
|
class multiWriter implements Writer {
|
|
653
|
-
private writers: Writer[]
|
|
662
|
+
private writers: (Writer | null)[]
|
|
654
663
|
|
|
655
|
-
constructor(writers: Writer[]) {
|
|
664
|
+
constructor(writers: (Writer | null)[]) {
|
|
656
665
|
this.writers = writers
|
|
657
666
|
}
|
|
658
667
|
|
|
659
668
|
Write(p: $.Bytes): [number, $.GoError] {
|
|
660
669
|
for (const w of this.writers) {
|
|
670
|
+
if (w == null) {
|
|
671
|
+
throw new Error('io.MultiWriter: nil writer')
|
|
672
|
+
}
|
|
661
673
|
const [n, err] = w.Write(p)
|
|
662
674
|
if (err !== null) {
|
|
663
675
|
return [n, err]
|
|
@@ -671,22 +683,28 @@ class multiWriter implements Writer {
|
|
|
671
683
|
}
|
|
672
684
|
|
|
673
685
|
// TeeReader returns a Reader that writes to w what it reads from r
|
|
674
|
-
export function TeeReader(r: Reader, w: Writer): Reader {
|
|
686
|
+
export function TeeReader(r: Reader | null, w: Writer | null): Reader {
|
|
675
687
|
return new teeReader(r, w)
|
|
676
688
|
}
|
|
677
689
|
|
|
678
690
|
class teeReader implements Reader {
|
|
679
|
-
private r: Reader
|
|
680
|
-
private w: Writer
|
|
691
|
+
private r: Reader | null
|
|
692
|
+
private w: Writer | null
|
|
681
693
|
|
|
682
|
-
constructor(r: Reader, w: Writer) {
|
|
694
|
+
constructor(r: Reader | null, w: Writer | null) {
|
|
683
695
|
this.r = r
|
|
684
696
|
this.w = w
|
|
685
697
|
}
|
|
686
698
|
|
|
687
699
|
Read(p: $.Bytes): [number, $.GoError] {
|
|
700
|
+
if (this.r == null) {
|
|
701
|
+
throw new Error('io.TeeReader: nil reader')
|
|
702
|
+
}
|
|
688
703
|
const [n, err] = this.r.Read(p)
|
|
689
704
|
if (n > 0) {
|
|
705
|
+
if (this.w == null) {
|
|
706
|
+
throw new Error('io.TeeReader: nil writer')
|
|
707
|
+
}
|
|
690
708
|
const [nw, ew] = this.w.Write($.goSlice(p, 0, n))
|
|
691
709
|
if (ew !== null) {
|
|
692
710
|
return [n, ew]
|
|
@@ -2,7 +2,14 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import * as $ from '@goscript/builtin/index.js'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Handler,
|
|
7
|
+
Header_Get,
|
|
8
|
+
Header_Set,
|
|
9
|
+
MethodGet,
|
|
10
|
+
NewRequest,
|
|
11
|
+
StatusPartialContent,
|
|
12
|
+
} from '../index.js'
|
|
6
13
|
import { NewServer, NewUnstartedServer, Server_Start } from './index.js'
|
|
7
14
|
|
|
8
15
|
describe('net/http/httptest override', () => {
|
|
@@ -12,12 +19,13 @@ describe('net/http/httptest override', () => {
|
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
const srv = NewServer(handler)
|
|
15
|
-
expect(srv.URL).
|
|
22
|
+
expect(srv.URL).toMatch(/^http:\/\/goscript-httptest-\d+\.invalid$/)
|
|
16
23
|
expect(srv.Client()).toBeTruthy()
|
|
17
24
|
expect(srv.Config().Handler).toBe(handler)
|
|
18
25
|
expect(Server_Start(NewUnstartedServer(handler))?.Error()).toBe(
|
|
19
26
|
'net/http/httptest: Server.Start is not implemented in GoScript',
|
|
20
27
|
)
|
|
28
|
+
srv.Close()
|
|
21
29
|
})
|
|
22
30
|
|
|
23
31
|
it('routes Client.Do through the in-memory server handler', async () => {
|
|
@@ -49,5 +57,29 @@ describe('net/http/httptest override', () => {
|
|
|
49
57
|
expect(n).toBe(4)
|
|
50
58
|
expect(Buffer.from(buf).toString('utf8')).toBe('data')
|
|
51
59
|
expect(resp!.Body!.Close()).toBeNull()
|
|
60
|
+
srv.Close()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('awaits async handlers through Server.Client transport', async () => {
|
|
64
|
+
const srv = NewServer({
|
|
65
|
+
async ServeHTTP(w) {
|
|
66
|
+
await Promise.resolve()
|
|
67
|
+
w!.WriteHeader(StatusPartialContent)
|
|
68
|
+
w!.Write($.stringToBytes('async'))
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
const [req, reqErr] = NewRequest(MethodGet, srv.URL + '/async', null)
|
|
72
|
+
expect(reqErr).toBeNull()
|
|
73
|
+
|
|
74
|
+
const [resp, err] = await srv.Client().Do(req)
|
|
75
|
+
|
|
76
|
+
expect(err).toBeNull()
|
|
77
|
+
expect(resp?.StatusCode).toBe(StatusPartialContent)
|
|
78
|
+
const buf = new Uint8Array(5)
|
|
79
|
+
const [n, readErr] = resp!.Body!.Read(buf)
|
|
80
|
+
expect(readErr).toBeNull()
|
|
81
|
+
expect(n).toBe(5)
|
|
82
|
+
expect(Buffer.from(buf).toString('utf8')).toBe('async')
|
|
83
|
+
srv.Close()
|
|
52
84
|
})
|
|
53
85
|
})
|
|
@@ -58,10 +58,16 @@ export function NewRecorder(): ResponseRecorder {
|
|
|
58
58
|
return new ResponseRecorder()
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function NewRequest(
|
|
61
|
+
export function NewRequest(
|
|
62
|
+
method: string,
|
|
63
|
+
target: string,
|
|
64
|
+
body: io.Reader | null,
|
|
65
|
+
): http.Request {
|
|
62
66
|
const [req, err] = http.NewRequest(method, target, body)
|
|
63
67
|
if (err != null || req == null) {
|
|
64
|
-
throw
|
|
68
|
+
throw (
|
|
69
|
+
err ?? errors.New('net/http/httptest: NewRequest returned nil request')
|
|
70
|
+
)
|
|
65
71
|
}
|
|
66
72
|
return req
|
|
67
73
|
}
|
|
@@ -71,21 +77,26 @@ export class Server {
|
|
|
71
77
|
private handler: http.Handler | null
|
|
72
78
|
|
|
73
79
|
constructor(init?: Partial<Server> & { Handler?: http.Handler | null }) {
|
|
74
|
-
this.URL = init?.URL ?? 'http://127.0.0.1'
|
|
75
80
|
this.handler = init?.Handler ?? null
|
|
81
|
+
this.URL = init?.URL ?? http.RegisterInProcessServer(this.handler)
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
public Client(): http.Client {
|
|
79
85
|
return new http.Client({ Transport: new serverTransport(this) })
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
public Close(): void {
|
|
88
|
+
public Close(): void {
|
|
89
|
+
http.UnregisterInProcessServer(this.URL)
|
|
90
|
+
}
|
|
83
91
|
|
|
84
92
|
public Config(): http.Server {
|
|
85
93
|
return new http.Server({ Handler: this.handler })
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
public ServeHTTP(
|
|
96
|
+
public ServeHTTP(
|
|
97
|
+
w: http.ResponseWriter | null,
|
|
98
|
+
r: http.Request | $.VarRef<http.Request> | null,
|
|
99
|
+
): void | Promise<void> {
|
|
89
100
|
return this.handler?.ServeHTTP(w, r)
|
|
90
101
|
}
|
|
91
102
|
}
|
|
@@ -93,7 +104,9 @@ export class Server {
|
|
|
93
104
|
class serverTransport implements http.RoundTripper {
|
|
94
105
|
constructor(private server: Server) {}
|
|
95
106
|
|
|
96
|
-
public RoundTrip(
|
|
107
|
+
public async RoundTrip(
|
|
108
|
+
req: http.Request | $.VarRef<http.Request> | null,
|
|
109
|
+
): Promise<[http.Response | null, $.GoError]> {
|
|
97
110
|
const request = $.pointerValue<http.Request | null>(req)
|
|
98
111
|
if (request == null) {
|
|
99
112
|
return [null, errors.New('net/http: nil Request')]
|
|
@@ -101,7 +114,7 @@ class serverTransport implements http.RoundTripper {
|
|
|
101
114
|
const recorder = NewRecorder()
|
|
102
115
|
const served = this.server.ServeHTTP(recorder, request)
|
|
103
116
|
if (served instanceof Promise) {
|
|
104
|
-
|
|
117
|
+
await served
|
|
105
118
|
}
|
|
106
119
|
return [recorder.Result(), null]
|
|
107
120
|
}
|
|
@@ -116,5 +129,7 @@ export function NewUnstartedServer(handler: http.Handler | null): Server {
|
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
export function Server_Start(_s: Server | $.VarRef<Server> | null): $.GoError {
|
|
119
|
-
return errors.New(
|
|
132
|
+
return errors.New(
|
|
133
|
+
'net/http/httptest: Server.Start is not implemented in GoScript',
|
|
134
|
+
)
|
|
120
135
|
}
|
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
File,
|
|
8
8
|
FileSystem,
|
|
9
9
|
Header,
|
|
10
|
+
Header_Add,
|
|
11
|
+
Header_Del,
|
|
12
|
+
Header_Get,
|
|
10
13
|
Header_Set,
|
|
11
14
|
HandlerFunc_ServeHTTP,
|
|
12
15
|
Get,
|
|
@@ -19,11 +22,16 @@ import {
|
|
|
19
22
|
ResponseWriter,
|
|
20
23
|
Server,
|
|
21
24
|
StatusCreated,
|
|
25
|
+
StatusForbidden,
|
|
26
|
+
StatusMethodNotAllowed,
|
|
22
27
|
StatusNotFound,
|
|
23
28
|
StatusOK,
|
|
24
29
|
StatusServiceUnavailable,
|
|
30
|
+
StatusTeapot,
|
|
25
31
|
StatusText,
|
|
26
32
|
StatusTooManyRequests,
|
|
33
|
+
StatusUnauthorized,
|
|
34
|
+
StatusUnsupportedMediaType,
|
|
27
35
|
} from './index.js'
|
|
28
36
|
|
|
29
37
|
describe('net/http override', () => {
|
|
@@ -33,6 +41,11 @@ describe('net/http override', () => {
|
|
|
33
41
|
expect(resp.StatusCode).toBe(200)
|
|
34
42
|
expect(StatusText(resp.StatusCode)).toBe('OK')
|
|
35
43
|
expect(StatusText(StatusNotFound)).toBe('Not Found')
|
|
44
|
+
expect(StatusText(StatusUnauthorized)).toBe('Unauthorized')
|
|
45
|
+
expect(StatusText(StatusForbidden)).toBe('Forbidden')
|
|
46
|
+
expect(StatusText(StatusMethodNotAllowed)).toBe('Method Not Allowed')
|
|
47
|
+
expect(StatusText(StatusUnsupportedMediaType)).toBe('Unsupported Media Type')
|
|
48
|
+
expect(StatusText(StatusTeapot)).toBe("I'm a teapot")
|
|
36
49
|
expect(StatusText(StatusTooManyRequests)).toBe('Too Many Requests')
|
|
37
50
|
expect(StatusText(StatusServiceUnavailable)).toBe('Service Unavailable')
|
|
38
51
|
expect(StatusText(599)).toBe('')
|
|
@@ -92,6 +105,23 @@ describe('net/http override', () => {
|
|
|
92
105
|
expect(resp?.StatusCode).toBe(StatusOK)
|
|
93
106
|
})
|
|
94
107
|
|
|
108
|
+
it('canonicalizes header keys for case-insensitive lookup', () => {
|
|
109
|
+
const header = new Header()
|
|
110
|
+
|
|
111
|
+
Header_Set(header, 'Content-Type', 'application/json')
|
|
112
|
+
|
|
113
|
+
expect(header.has('Content-Type')).toBe(true)
|
|
114
|
+
expect(header.has('content-type')).toBe(false)
|
|
115
|
+
expect(Header_Get(header, 'content-type')).toBe('application/json')
|
|
116
|
+
|
|
117
|
+
Header_Add(header, 'x-pack-id', 'pack-1')
|
|
118
|
+
Header_Add(header, 'X-Pack-Id', 'pack-2')
|
|
119
|
+
expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual(['pack-1', 'pack-2'])
|
|
120
|
+
|
|
121
|
+
Header_Del(header, 'x-pack-id')
|
|
122
|
+
expect(Header_Get(header, 'X-Pack-ID')).toBe('')
|
|
123
|
+
})
|
|
124
|
+
|
|
95
125
|
it('accepts server context and shutdown surfaces', () => {
|
|
96
126
|
const srv = new Server({
|
|
97
127
|
Addr: ':0',
|