goscript 0.1.4 → 0.2.1

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 (295) hide show
  1. package/README.md +5 -2
  2. package/cmd/go_js_wasm_exec/main.go +201 -0
  3. package/cmd/go_js_wasm_exec/main_test.go +83 -0
  4. package/cmd/goscript/{cmd_compile.go → cmd-compile.go} +7 -0
  5. package/cmd/goscript/cmd-test.go +14 -0
  6. package/cmd/goscript/cmd-test_test.go +1 -1
  7. package/cmd/goscript-wasm/main.go +38 -6
  8. package/compiler/compile-request.go +12 -9
  9. package/compiler/compliance_test.go +0 -1
  10. package/compiler/config.go +2 -0
  11. package/compiler/diagnostic.go +104 -12
  12. package/compiler/diagnostic_test.go +106 -0
  13. package/compiler/gotest/request.go +28 -0
  14. package/compiler/gotest/runner.go +354 -44
  15. package/compiler/gotest/runner_test.go +293 -1
  16. package/compiler/gotest/testdata/browserapi/browserapi_test.go +20 -0
  17. package/compiler/gotest/testdata/browserapi/go.mod +3 -0
  18. package/compiler/index.test.ts +23 -0
  19. package/compiler/lowered-program.go +33 -24
  20. package/compiler/lowering.go +746 -194
  21. package/compiler/lowering_bench_test.go +42 -27
  22. package/compiler/lowering_internal_test.go +18 -0
  23. package/compiler/override-facts.go +15 -0
  24. package/compiler/override-parity-verifier.go +450 -0
  25. package/compiler/override-parity.go +122 -0
  26. package/compiler/override-registry_test.go +559 -0
  27. package/compiler/protobuf-ts-binding.go +567 -0
  28. package/compiler/protobuf-ts-binding_test.go +402 -0
  29. package/compiler/runtime-contract.go +4 -0
  30. package/compiler/runtime-contract_test.go +2 -0
  31. package/compiler/semantic-model-types.go +9 -4
  32. package/compiler/semantic-model.go +282 -70
  33. package/compiler/semantic-model_test.go +82 -1
  34. package/compiler/service.go +21 -1
  35. package/compiler/skeleton_test.go +118 -10
  36. package/compiler/typescript-emitter.go +128 -13
  37. package/compiler/wasm/compile_test.go +37 -4
  38. package/compiler/{wasm_api.go → wasm-api.go} +57 -7
  39. package/dist/gs/builtin/hostio.js +5 -0
  40. package/dist/gs/builtin/hostio.js.map +1 -1
  41. package/dist/gs/builtin/slice.d.ts +13 -2
  42. package/dist/gs/builtin/slice.js +187 -6
  43. package/dist/gs/builtin/slice.js.map +1 -1
  44. package/dist/gs/builtin/type.d.ts +13 -5
  45. package/dist/gs/builtin/type.js +153 -60
  46. package/dist/gs/builtin/type.js.map +1 -1
  47. package/dist/gs/builtin/varRef.d.ts +11 -0
  48. package/dist/gs/builtin/varRef.js +57 -2
  49. package/dist/gs/builtin/varRef.js.map +1 -1
  50. package/dist/gs/bytes/buffer.gs.js +1 -1
  51. package/dist/gs/bytes/buffer.gs.js.map +1 -1
  52. package/dist/gs/bytes/reader.gs.js +1 -1
  53. package/dist/gs/bytes/reader.gs.js.map +1 -1
  54. package/dist/gs/compress/zlib/index.d.ts +10 -3
  55. package/dist/gs/compress/zlib/index.js +50 -16
  56. package/dist/gs/compress/zlib/index.js.map +1 -1
  57. package/dist/gs/encoding/json/index.d.ts +114 -0
  58. package/dist/gs/encoding/json/index.js +544 -36
  59. package/dist/gs/encoding/json/index.js.map +1 -1
  60. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.d.ts +101 -0
  61. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js +589 -0
  62. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js.map +1 -1
  63. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.d.ts +1 -0
  64. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.js +17 -11
  65. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.js.map +1 -1
  66. package/dist/gs/github.com/pkg/errors/errors.js +54 -30
  67. package/dist/gs/github.com/pkg/errors/errors.js.map +1 -1
  68. package/dist/gs/go/scanner/index.d.ts +2 -0
  69. package/dist/gs/go/scanner/index.js +29 -5
  70. package/dist/gs/go/scanner/index.js.map +1 -1
  71. package/dist/gs/go/token/index.js +22 -6
  72. package/dist/gs/go/token/index.js.map +1 -1
  73. package/dist/gs/hash/index.d.ts +6 -0
  74. package/dist/gs/hash/index.js +20 -0
  75. package/dist/gs/hash/index.js.map +1 -1
  76. package/dist/gs/internal/byteorder/index.js +2 -2
  77. package/dist/gs/internal/byteorder/index.js.map +1 -1
  78. package/dist/gs/internal/goarch/index.d.ts +43 -3
  79. package/dist/gs/internal/goarch/index.js +42 -10
  80. package/dist/gs/internal/goarch/index.js.map +1 -1
  81. package/dist/gs/io/fs/fs.js +26 -14
  82. package/dist/gs/io/fs/fs.js.map +1 -1
  83. package/dist/gs/io/fs/readdir.js +4 -2
  84. package/dist/gs/io/fs/readdir.js.map +1 -1
  85. package/dist/gs/io/fs/sub.js +8 -1
  86. package/dist/gs/io/fs/sub.js.map +1 -1
  87. package/dist/gs/io/io.d.ts +2 -0
  88. package/dist/gs/io/io.js.map +1 -1
  89. package/dist/gs/math/bits/index.d.ts +5 -0
  90. package/dist/gs/math/bits/index.js +16 -4
  91. package/dist/gs/math/bits/index.js.map +1 -1
  92. package/dist/gs/mime/index.d.ts +16 -0
  93. package/dist/gs/mime/index.js +315 -6
  94. package/dist/gs/mime/index.js.map +1 -1
  95. package/dist/gs/net/http/httptest/index.d.ts +12 -0
  96. package/dist/gs/net/http/httptest/index.js +85 -6
  97. package/dist/gs/net/http/httptest/index.js.map +1 -1
  98. package/dist/gs/net/http/index.d.ts +300 -5
  99. package/dist/gs/net/http/index.js +1598 -58
  100. package/dist/gs/net/http/index.js.map +1 -1
  101. package/dist/gs/os/dir_unix.gs.js +1 -1
  102. package/dist/gs/os/dir_unix.gs.js.map +1 -1
  103. package/dist/gs/os/error.gs.js +1 -1
  104. package/dist/gs/os/error.gs.js.map +1 -1
  105. package/dist/gs/os/exec.gs.d.ts +1 -0
  106. package/dist/gs/os/exec.gs.js +4 -8
  107. package/dist/gs/os/exec.gs.js.map +1 -1
  108. package/dist/gs/os/exec_posix.gs.js +1 -1
  109. package/dist/gs/os/exec_posix.gs.js.map +1 -1
  110. package/dist/gs/os/index.d.ts +1 -1
  111. package/dist/gs/os/index.js +1 -1
  112. package/dist/gs/os/index.js.map +1 -1
  113. package/dist/gs/os/proc.gs.d.ts +4 -0
  114. package/dist/gs/os/proc.gs.js +12 -6
  115. package/dist/gs/os/proc.gs.js.map +1 -1
  116. package/dist/gs/os/root_js.gs.js +1 -1
  117. package/dist/gs/os/root_js.gs.js.map +1 -1
  118. package/dist/gs/os/types.gs.js +1 -1
  119. package/dist/gs/os/types.gs.js.map +1 -1
  120. package/dist/gs/os/types_js.gs.js +1 -1
  121. package/dist/gs/os/types_js.gs.js.map +1 -1
  122. package/dist/gs/os/types_unix.gs.js +1 -1
  123. package/dist/gs/os/types_unix.gs.js.map +1 -1
  124. package/dist/gs/path/path.js +11 -7
  125. package/dist/gs/path/path.js.map +1 -1
  126. package/dist/gs/reflect/index.d.ts +5 -4
  127. package/dist/gs/reflect/index.js +4 -3
  128. package/dist/gs/reflect/index.js.map +1 -1
  129. package/dist/gs/reflect/map.js +15 -0
  130. package/dist/gs/reflect/map.js.map +1 -1
  131. package/dist/gs/reflect/type.d.ts +25 -6
  132. package/dist/gs/reflect/type.js +1475 -228
  133. package/dist/gs/reflect/type.js.map +1 -1
  134. package/dist/gs/reflect/types.d.ts +14 -6
  135. package/dist/gs/reflect/types.js +35 -1
  136. package/dist/gs/reflect/types.js.map +1 -1
  137. package/dist/gs/reflect/value.d.ts +1 -0
  138. package/dist/gs/reflect/value.js +83 -41
  139. package/dist/gs/reflect/value.js.map +1 -1
  140. package/dist/gs/reflect/visiblefields.js +4 -140
  141. package/dist/gs/reflect/visiblefields.js.map +1 -1
  142. package/dist/gs/runtime/pprof/index.d.ts +8 -2
  143. package/dist/gs/runtime/pprof/index.js +50 -30
  144. package/dist/gs/runtime/pprof/index.js.map +1 -1
  145. package/dist/gs/runtime/runtime.js +5 -4
  146. package/dist/gs/runtime/runtime.js.map +1 -1
  147. package/dist/gs/runtime/trace/index.js +5 -19
  148. package/dist/gs/runtime/trace/index.js.map +1 -1
  149. package/dist/gs/strconv/atoi.gs.js +1 -1
  150. package/dist/gs/strconv/atoi.gs.js.map +1 -1
  151. package/dist/gs/strconv/complex.gs.d.ts +3 -0
  152. package/dist/gs/strconv/complex.gs.js +148 -0
  153. package/dist/gs/strconv/complex.gs.js.map +1 -0
  154. package/dist/gs/strconv/index.d.ts +1 -0
  155. package/dist/gs/strconv/index.js +1 -0
  156. package/dist/gs/strconv/index.js.map +1 -1
  157. package/dist/gs/strings/builder.js +1 -1
  158. package/dist/gs/strings/reader.js +9 -5
  159. package/dist/gs/strings/reader.js.map +1 -1
  160. package/dist/gs/strings/replace.js +15 -7
  161. package/dist/gs/strings/replace.js.map +1 -1
  162. package/dist/gs/strings/strings.d.ts +5 -0
  163. package/dist/gs/strings/strings.js +57 -5
  164. package/dist/gs/strings/strings.js.map +1 -1
  165. package/dist/gs/sync/atomic/doc_64.gs.js +7 -6
  166. package/dist/gs/sync/atomic/doc_64.gs.js.map +1 -1
  167. package/dist/gs/sync/atomic/type.gs.js +9 -9
  168. package/dist/gs/sync/atomic/type.gs.js.map +1 -1
  169. package/dist/gs/sync/atomic/value.gs.js +2 -2
  170. package/dist/gs/sync/atomic/value.gs.js.map +1 -1
  171. package/dist/gs/syscall/env.js +22 -14
  172. package/dist/gs/syscall/env.js.map +1 -1
  173. package/dist/gs/testing/testing.js +55 -13
  174. package/dist/gs/testing/testing.js.map +1 -1
  175. package/dist/gs/time/time.d.ts +24 -1
  176. package/dist/gs/time/time.js +43 -3
  177. package/dist/gs/time/time.js.map +1 -1
  178. package/dist/gs/unique/index.js +7 -1
  179. package/dist/gs/unique/index.js.map +1 -1
  180. package/go.mod +3 -3
  181. package/go.sum +16 -0
  182. package/gs/builtin/hostio.test.ts +16 -0
  183. package/gs/builtin/hostio.ts +7 -0
  184. package/gs/builtin/runtime-contract.test.ts +246 -21
  185. package/gs/builtin/slice.ts +269 -24
  186. package/gs/builtin/type.ts +226 -59
  187. package/gs/builtin/varRef.ts +85 -2
  188. package/gs/bytes/buffer.gs.ts +1 -1
  189. package/gs/bytes/reader.gs.ts +1 -1
  190. package/gs/compress/zlib/index.test.ts +62 -1
  191. package/gs/compress/zlib/index.ts +53 -16
  192. package/gs/compress/zlib/parity.json +51 -0
  193. package/gs/encoding/json/index.test.ts +360 -6
  194. package/gs/encoding/json/index.ts +679 -38
  195. package/gs/encoding/json/parity.json +81 -0
  196. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.test.ts +373 -3
  197. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.ts +893 -1
  198. package/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.test.ts +18 -0
  199. package/gs/github.com/aperturerobotics/protobuf-go-lite/json/index.ts +17 -11
  200. package/gs/github.com/pkg/errors/errors.ts +54 -30
  201. package/gs/go/scanner/index.test.ts +39 -56
  202. package/gs/go/scanner/index.ts +33 -5
  203. package/gs/go/scanner/parity.json +27 -0
  204. package/gs/go/token/index.ts +22 -6
  205. package/gs/hash/index.test.ts +20 -33
  206. package/gs/hash/index.ts +28 -0
  207. package/gs/hash/parity.json +21 -0
  208. package/gs/internal/byteorder/index.test.ts +2 -2
  209. package/gs/internal/byteorder/index.ts +2 -2
  210. package/gs/internal/goarch/index.test.ts +32 -0
  211. package/gs/internal/goarch/index.ts +45 -13
  212. package/gs/internal/goarch/parity.json +144 -0
  213. package/gs/io/fs/fs.ts +26 -14
  214. package/gs/io/fs/readdir.ts +4 -4
  215. package/gs/io/fs/sub.ts +8 -1
  216. package/gs/io/io.ts +1 -0
  217. package/gs/io/parity.json +162 -0
  218. package/gs/math/bits/index.test.ts +14 -1
  219. package/gs/math/bits/index.ts +23 -4
  220. package/gs/math/bits/parity.json +156 -0
  221. package/gs/mime/index.test.ts +90 -0
  222. package/gs/mime/index.ts +369 -6
  223. package/gs/mime/parity.json +36 -0
  224. package/gs/net/http/httptest/index.test.ts +98 -2
  225. package/gs/net/http/httptest/index.ts +101 -6
  226. package/gs/net/http/httptest/parity.json +15 -0
  227. package/gs/net/http/index.test.ts +781 -12
  228. package/gs/net/http/index.ts +1860 -139
  229. package/gs/net/http/meta.json +16 -1
  230. package/gs/net/http/parity.json +193 -0
  231. package/gs/os/dir_unix.gs.ts +1 -1
  232. package/gs/os/error.gs.ts +1 -1
  233. package/gs/os/exec.gs.ts +4 -8
  234. package/gs/os/exec_posix.gs.ts +1 -1
  235. package/gs/os/index.test.ts +9 -0
  236. package/gs/os/index.ts +1 -0
  237. package/gs/os/parity.json +9 -0
  238. package/gs/os/proc.gs.ts +18 -5
  239. package/gs/os/proc.test.ts +26 -0
  240. package/gs/os/root_js.gs.ts +1 -1
  241. package/gs/os/types.gs.ts +1 -1
  242. package/gs/os/types_js.gs.ts +1 -1
  243. package/gs/os/types_unix.gs.ts +1 -1
  244. package/gs/path/path.ts +11 -7
  245. package/gs/reflect/field.test.ts +37 -15
  246. package/gs/reflect/function-types.test.ts +518 -22
  247. package/gs/reflect/index.ts +8 -6
  248. package/gs/reflect/map.ts +20 -0
  249. package/gs/reflect/meta.json +6 -4
  250. package/gs/reflect/parity.json +234 -0
  251. package/gs/reflect/sliceat.test.ts +156 -0
  252. package/gs/reflect/structof.test.ts +401 -0
  253. package/gs/reflect/type.ts +1961 -317
  254. package/gs/reflect/typefor.test.ts +530 -10
  255. package/gs/reflect/types.ts +43 -18
  256. package/gs/reflect/value.ts +105 -45
  257. package/gs/reflect/visiblefields.ts +5 -168
  258. package/gs/runtime/parity.json +24 -0
  259. package/gs/runtime/pprof/index.test.ts +29 -7
  260. package/gs/runtime/pprof/index.ts +56 -30
  261. package/gs/runtime/pprof/parity.json +27 -0
  262. package/gs/runtime/runtime.test.ts +3 -1
  263. package/gs/runtime/runtime.ts +4 -3
  264. package/gs/runtime/trace/index.test.ts +5 -3
  265. package/gs/runtime/trace/index.ts +8 -20
  266. package/gs/runtime/trace/parity.json +36 -0
  267. package/gs/strconv/atoi.gs.ts +1 -1
  268. package/gs/strconv/complex.gs.ts +174 -0
  269. package/gs/strconv/complex.test.ts +65 -0
  270. package/gs/strconv/index.ts +1 -0
  271. package/gs/strconv/parity.json +120 -0
  272. package/gs/strings/builder.ts +1 -1
  273. package/gs/strings/parity.json +186 -0
  274. package/gs/strings/reader.ts +9 -5
  275. package/gs/strings/replace.ts +15 -7
  276. package/gs/strings/strings.test.ts +22 -2
  277. package/gs/strings/strings.ts +64 -6
  278. package/gs/sync/atomic/doc_64.gs.ts +6 -7
  279. package/gs/sync/atomic/doc_64.test.ts +43 -0
  280. package/gs/sync/atomic/type.gs.ts +9 -9
  281. package/gs/sync/atomic/value.gs.ts +2 -2
  282. package/gs/syscall/env.ts +29 -14
  283. package/gs/testing/testing.test.ts +67 -0
  284. package/gs/testing/testing.ts +87 -19
  285. package/gs/time/parity.json +225 -0
  286. package/gs/time/time.test.ts +20 -2
  287. package/gs/time/time.ts +49 -7
  288. package/gs/unique/index.ts +7 -1
  289. package/package.json +4 -2
  290. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.d.ts +0 -217
  291. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +0 -926
  292. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +0 -1
  293. package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +0 -38
  294. package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +0 -1361
  295. package/gs/github.com/aperturerobotics/starpc/srpc/meta.json +0 -46
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ AddExtensionType,
5
+ BEncoding,
6
+ ErrInvalidMediaParameter,
7
+ ExtensionsByType,
8
+ FormatMediaType,
9
+ ParseMediaType,
10
+ QEncoding,
11
+ TypeByExtension,
12
+ WordDecoder,
13
+ WordEncoder_Encode,
14
+ } from './index.js'
15
+
16
+ describe('mime override', () => {
17
+ it('parses, formats, and looks up media types', () => {
18
+ const [mediaType, params, err] = ParseMediaType(
19
+ 'Text/Plain; Charset="utf-8"; format=flowed',
20
+ )
21
+ expect(err).toBeNull()
22
+ expect(mediaType).toBe('text/plain')
23
+ expect(params.get('charset')).toBe('utf-8')
24
+ expect(params.get('format')).toBe('flowed')
25
+ const [trailingMediaType, trailingParams, trailingErr] = ParseMediaType('text/plain;')
26
+ expect(trailingErr).toBeNull()
27
+ expect(trailingMediaType).toBe('text/plain')
28
+ expect(trailingParams.size).toBe(0)
29
+
30
+ expect(
31
+ FormatMediaType('text/plain', new Map([['charset', 'utf-8']])),
32
+ ).toBe('text/plain; charset=utf-8')
33
+ expect(
34
+ FormatMediaType('Attachment', new Map([['FileName', 'hello.txt']])),
35
+ ).toBe('attachment; filename=hello.txt')
36
+ expect(
37
+ FormatMediaType('attachment', new Map([['filename', 'foo bar.txt']])),
38
+ ).toBe('attachment; filename="foo bar.txt"')
39
+ expect(
40
+ FormatMediaType('text/plain', new Map([
41
+ ['z', 'last'],
42
+ ['a', 'first'],
43
+ ])),
44
+ ).toBe('text/plain; a=first; z=last')
45
+
46
+ const encoded = FormatMediaType(
47
+ 'attachment',
48
+ new Map([['filename', 'résumé.txt']]),
49
+ )
50
+ expect(encoded).toBe("attachment; filename*=utf-8''r%C3%A9sum%C3%A9.txt")
51
+ const [, encodedParams, encodedErr] = ParseMediaType(encoded)
52
+ expect(encodedErr).toBeNull()
53
+ expect(encodedParams.get('filename')).toBe('résumé.txt')
54
+ const [, pathParams, pathErr] = ParseMediaType('form-data; filename="C:\\dev\\go.txt"; quoted="a\\"b"')
55
+ expect(pathErr).toBeNull()
56
+ expect(pathParams.get('filename')).toBe('C:\\dev\\go.txt')
57
+ expect(pathParams.get('quoted')).toBe('a"b')
58
+
59
+ expect(TypeByExtension('.json')).toBe('application/json')
60
+ expect(AddExtensionType('.goscript', 'text/plain; charset=utf-8')).toBeNull()
61
+ const [extensions, extensionErr] = ExtensionsByType('text/plain')
62
+ expect(extensionErr).toBeNull()
63
+ expect(extensions).toContain('.goscript')
64
+ })
65
+
66
+ it('reports invalid media parameters', () => {
67
+ const [, , err] = ParseMediaType('text/plain; broken')
68
+ expect(err).toBe(ErrInvalidMediaParameter)
69
+ expect(ParseMediaType('text/plain; filename=foo bar')[2]).toBe(ErrInvalidMediaParameter)
70
+ expect(ParseMediaType('text/plain; filename="unterminated')[2]).toBe(ErrInvalidMediaParameter)
71
+ expect(ParseMediaType('text/plain; x=1; x=2')[2]).toBe(ErrInvalidMediaParameter)
72
+ expect(ParseMediaType('text/plain; bad[]=x')[2]).toBe(ErrInvalidMediaParameter)
73
+ expect(ParseMediaType('text/plain; bad{}=x')[1].get('bad{}')).toBe('x')
74
+ })
75
+
76
+ it('encodes and decodes RFC 2047 words', () => {
77
+ expect(WordEncoder_Encode(BEncoding, 'utf-8', 'hello')).toBe('hello')
78
+
79
+ const bWord = WordEncoder_Encode(BEncoding, 'utf-8', 'héllo')
80
+ expect(new WordDecoder().Decode(bWord)).toEqual(['héllo', null])
81
+
82
+ const qWord = WordEncoder_Encode(QEncoding, 'utf-8', 'hello world')
83
+ expect(qWord).toBe('hello world')
84
+
85
+ const encodedQWord = WordEncoder_Encode(QEncoding, 'utf-8', 'héllo world')
86
+ const [header, err] = new WordDecoder().DecodeHeader(`subject ${encodedQWord}`)
87
+ expect(err).toBeNull()
88
+ expect(header).toBe('subject héllo world')
89
+ })
90
+ })
package/gs/mime/index.ts CHANGED
@@ -1,34 +1,253 @@
1
+ import * as $ from '@goscript/builtin/index.js'
2
+ import * as errors from '@goscript/errors/index.js'
3
+ import type * as io from '@goscript/io/index.js'
4
+
5
+ export type WordEncoder = number
6
+
7
+ export const BEncoding: WordEncoder = 'b'.charCodeAt(0)
8
+ export const QEncoding: WordEncoder = 'q'.charCodeAt(0)
9
+
10
+ export const ErrInvalidMediaParameter = errors.New(
11
+ 'mime: invalid media parameter',
12
+ )
13
+
14
+ const extensionTypes = new Map<string, string>([
15
+ ['.css', 'text/css; charset=utf-8'],
16
+ ['.gif', 'image/gif'],
17
+ ['.html', 'text/html; charset=utf-8'],
18
+ ['.htm', 'text/html; charset=utf-8'],
19
+ ['.jpeg', 'image/jpeg'],
20
+ ['.jpg', 'image/jpeg'],
21
+ ['.js', 'text/javascript; charset=utf-8'],
22
+ ['.json', 'application/json'],
23
+ ['.mjs', 'text/javascript; charset=utf-8'],
24
+ ['.png', 'image/png'],
25
+ ['.svg', 'image/svg+xml'],
26
+ ['.txt', 'text/plain; charset=utf-8'],
27
+ ['.wasm', 'application/wasm'],
28
+ ['.xml', 'text/xml; charset=utf-8'],
29
+ ])
30
+
31
+ type nodeBufferConstructor = {
32
+ from(data: Uint8Array): { toString(encoding: string): string }
33
+ from(data: string, encoding: string): Uint8Array
34
+ }
35
+
36
+ type nodeProcess = {
37
+ getBuiltinModule?: (
38
+ name: string,
39
+ ) => { Buffer?: nodeBufferConstructor } | undefined
40
+ }
41
+
1
42
  export function FormatMediaType(
2
43
  t: string,
3
44
  param: Map<string, string> | Record<string, string> | null,
4
45
  ): string {
5
- if (!isToken(t)) {
46
+ const mediaType = formatMediaTypeDisposition(t)
47
+ if (mediaType === '') {
6
48
  return ''
7
49
  }
8
- let out = t
50
+ let out = mediaType
9
51
  const entries =
10
52
  param instanceof Map ?
11
53
  Array.from(param.entries())
12
54
  : Object.entries(param ?? {})
13
- entries.sort(([a], [b]) => a.localeCompare(b))
55
+ entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
14
56
  for (const [key, value] of entries) {
15
57
  if (!isToken(key)) {
16
58
  return ''
17
59
  }
18
- out += formatParam(key, String(value))
60
+ out += formatParam(key.toLowerCase(), String(value))
19
61
  }
20
62
  return out
21
63
  }
22
64
 
65
+ export function ParseMediaType(
66
+ v: string,
67
+ ): [string, Map<string, string>, $.GoError] {
68
+ const parts = splitMediaType(v)
69
+ const mediatype = formatMediaTypeDisposition(parts.shift()?.trim() ?? '')
70
+ if (mediatype === '') {
71
+ return ['', new Map(), ErrInvalidMediaParameter]
72
+ }
73
+ const params = new Map<string, string>()
74
+ for (let i = 0; i < parts.length; i++) {
75
+ const part = parts[i]
76
+ if (part.trim() === '' && i === parts.length - 1) {
77
+ break
78
+ }
79
+ const eq = part.indexOf('=')
80
+ if (eq < 0) {
81
+ return [mediatype, params, ErrInvalidMediaParameter]
82
+ }
83
+ const key = part.slice(0, eq).trim().toLowerCase()
84
+ const rawValue = part.slice(eq + 1).trim()
85
+ const value = parseParamValue(rawValue)
86
+ if (!isToken(key)) {
87
+ return [mediatype, params, ErrInvalidMediaParameter]
88
+ }
89
+ if (value === null) {
90
+ return [mediatype, params, ErrInvalidMediaParameter]
91
+ }
92
+ if (key.endsWith('*')) {
93
+ const decoded = decode2231Value(value)
94
+ if (decoded !== null) {
95
+ if (params.has(key.slice(0, -1)) && params.get(key.slice(0, -1)) !== decoded) {
96
+ return ['', new Map(), ErrInvalidMediaParameter]
97
+ }
98
+ params.set(key.slice(0, -1), decoded)
99
+ }
100
+ continue
101
+ }
102
+ if (params.has(key) && params.get(key) !== value) {
103
+ return ['', new Map(), ErrInvalidMediaParameter]
104
+ }
105
+ params.set(key, value)
106
+ }
107
+ return [mediatype, params, null]
108
+ }
109
+
110
+ export function AddExtensionType(ext: string, typ: string): $.GoError {
111
+ if (!ext.startsWith('.')) {
112
+ return ErrInvalidMediaParameter
113
+ }
114
+ const [, , err] = ParseMediaType(typ)
115
+ if (err !== null) {
116
+ return err
117
+ }
118
+ extensionTypes.set(ext.toLowerCase(), typ)
119
+ return null
120
+ }
121
+
122
+ export function ExtensionsByType(typ: string): [$.Slice<string>, $.GoError] {
123
+ const [mediaType, , err] = ParseMediaType(typ)
124
+ if (err !== null) {
125
+ return [null, err]
126
+ }
127
+ const out: string[] = []
128
+ for (const [ext, extType] of extensionTypes) {
129
+ const [known] = ParseMediaType(extType)
130
+ if (known === mediaType) {
131
+ out.push(ext)
132
+ }
133
+ }
134
+ out.sort()
135
+ return [$.arrayToSlice(out), null]
136
+ }
137
+
138
+ export function TypeByExtension(ext: string): string {
139
+ return extensionTypes.get(ext.toLowerCase()) ?? ''
140
+ }
141
+
142
+ export class WordDecoder {
143
+ public CharsetReader:
144
+ | ((charset: string, input: io.Reader) => [io.Reader | null, $.GoError])
145
+ | null = null
146
+
147
+ public Decode(word: string): [string, $.GoError] {
148
+ const decoded = decodeWord(word)
149
+ if (decoded === null) {
150
+ return ['', ErrInvalidMediaParameter]
151
+ }
152
+ return [decoded, null]
153
+ }
154
+
155
+ public DecodeHeader(header: string): [string, $.GoError] {
156
+ return [
157
+ header.replace(/=\?[^?\s]+\?[bBqQ]\?[^?\s]*\?=/g, (word) => {
158
+ return decodeWord(word) ?? word
159
+ }),
160
+ null,
161
+ ]
162
+ }
163
+ }
164
+
165
+ export function WordEncoder_Encode(
166
+ e: WordEncoder,
167
+ charset: string,
168
+ s: string,
169
+ ): string {
170
+ if (s === '') {
171
+ return ''
172
+ }
173
+ if (!needsWordEncoding(s)) {
174
+ return s
175
+ }
176
+ const encoding = String.fromCharCode(e).toLowerCase() === 'q' ? 'Q' : 'B'
177
+ const body =
178
+ encoding === 'Q' ? qEncode(s) : base64Encode(new TextEncoder().encode(s))
179
+ return `=?${charset}?${encoding}?${body}?=`
180
+ }
181
+
182
+ function needsWordEncoding(value: string): boolean {
183
+ for (const char of value) {
184
+ const code = char.codePointAt(0) ?? 0
185
+ if ((code < 0x20 || code > 0x7e) && char !== '\t') {
186
+ return true
187
+ }
188
+ }
189
+ return false
190
+ }
191
+
23
192
  function formatParam(key: string, value: string): string {
193
+ if (needsWordEncoding(value)) {
194
+ return `; ${key}*=utf-8''${percentEncode(value)}`
195
+ }
24
196
  if (isToken(value)) {
25
197
  return `; ${key}=${value}`
26
198
  }
27
- return `; ${key}*=utf-8''${percentEncode(value)}`
199
+ return `; ${key}="${value.replace(/(["\\])/g, '\\$1')}"`
28
200
  }
29
201
 
30
202
  function isToken(value: string): boolean {
31
- return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value)
203
+ return /^[!#$%&'*+\-.^_`{}|~0-9A-Za-z]+$/.test(value)
204
+ }
205
+
206
+ function parseParamValue(value: string): string | null {
207
+ if (value.startsWith('"')) {
208
+ return parseQuotedParamValue(value)
209
+ }
210
+ return isToken(value) ? value : null
211
+ }
212
+
213
+ function parseQuotedParamValue(value: string): string | null {
214
+ let out = ''
215
+ for (let i = 1; i < value.length; i++) {
216
+ const char = value[i]
217
+ if (char === '"') {
218
+ return i === value.length - 1 ? out : null
219
+ }
220
+ if (char === '\\' && i + 1 < value.length && isTSpecial(value[i + 1])) {
221
+ out += value[i + 1]
222
+ i++
223
+ continue
224
+ }
225
+ if (char === '\r' || char === '\n') {
226
+ return null
227
+ }
228
+ out += char
229
+ }
230
+ return null
231
+ }
232
+
233
+ function isTSpecial(value: string): boolean {
234
+ return '()<>@,;:\\"/[]?='.includes(value)
235
+ }
236
+
237
+ function formatMediaTypeDisposition(value: string): string {
238
+ const slash = value.indexOf('/')
239
+ if (slash < 0) {
240
+ return isToken(value) ? value.toLowerCase() : ''
241
+ }
242
+ if (slash === 0 || slash === value.length - 1) {
243
+ return ''
244
+ }
245
+ const major = value.slice(0, slash)
246
+ const sub = value.slice(slash + 1)
247
+ if (!isToken(major) || !isToken(sub)) {
248
+ return ''
249
+ }
250
+ return `${major.toLowerCase()}/${sub.toLowerCase()}`
32
251
  }
33
252
 
34
253
  function percentEncode(value: string): string {
@@ -58,3 +277,147 @@ function percentEncode(value: string): string {
58
277
  }
59
278
  return out
60
279
  }
280
+
281
+ function decode2231Value(value: string): string | null {
282
+ const first = value.indexOf("'")
283
+ if (first < 0) {
284
+ return null
285
+ }
286
+ const second = value.indexOf("'", first + 1)
287
+ if (second < 0) {
288
+ return null
289
+ }
290
+ const charset = value.slice(0, first).toLowerCase()
291
+ if (charset !== 'utf-8' && charset !== 'us-ascii') {
292
+ return null
293
+ }
294
+ const bytes = percentDecode(value.slice(second + 1))
295
+ if (bytes === null) {
296
+ return null
297
+ }
298
+ if (charset === 'us-ascii' && bytes.some((byte) => byte > 0x7f)) {
299
+ return null
300
+ }
301
+ return new TextDecoder('utf-8').decode(bytes)
302
+ }
303
+
304
+ function percentDecode(value: string): Uint8Array | null {
305
+ const bytes: number[] = []
306
+ for (let i = 0; i < value.length; i++) {
307
+ if (value[i] !== '%') {
308
+ bytes.push(value.charCodeAt(i))
309
+ continue
310
+ }
311
+ if (i + 2 >= value.length || !isHex(value[i + 1]) || !isHex(value[i + 2])) {
312
+ return null
313
+ }
314
+ bytes.push(Number.parseInt(value.slice(i + 1, i + 3), 16))
315
+ i += 2
316
+ }
317
+ return new Uint8Array(bytes)
318
+ }
319
+
320
+ function isHex(char: string): boolean {
321
+ return /^[0-9A-Fa-f]$/.test(char)
322
+ }
323
+
324
+ function splitMediaType(value: string): string[] {
325
+ const parts: string[] = []
326
+ let current = ''
327
+ let quoted = false
328
+ for (let i = 0; i < value.length; i++) {
329
+ const char = value[i]
330
+ if (char === '"' && value[i - 1] !== '\\') {
331
+ quoted = !quoted
332
+ }
333
+ if (char === ';' && !quoted) {
334
+ parts.push(current)
335
+ current = ''
336
+ continue
337
+ }
338
+ current += char
339
+ }
340
+ parts.push(current)
341
+ return parts
342
+ }
343
+
344
+ function base64Encode(bytes: Uint8Array): string {
345
+ const chars = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')
346
+ if (typeof btoa === 'function') {
347
+ return btoa(chars)
348
+ }
349
+ const buffer = nodeBuffer()
350
+ if (buffer != null) {
351
+ return buffer.from(bytes).toString('base64')
352
+ }
353
+ throw new Error('mime: base64 encoder unavailable')
354
+ }
355
+
356
+ function base64Decode(text: string): Uint8Array {
357
+ if (typeof atob === 'function') {
358
+ return new Uint8Array(Array.from(atob(text), (char) => char.charCodeAt(0)))
359
+ }
360
+ const buffer = nodeBuffer()
361
+ if (buffer != null) {
362
+ return new Uint8Array(buffer.from(text, 'base64'))
363
+ }
364
+ throw new Error('mime: base64 decoder unavailable')
365
+ }
366
+
367
+ function nodeBuffer(): nodeBufferConstructor | null {
368
+ const processObj = (globalThis as { process?: nodeProcess }).process
369
+ return processObj?.getBuiltinModule?.('buffer')?.Buffer ?? null
370
+ }
371
+
372
+ function qEncode(value: string): string {
373
+ let out = ''
374
+ for (const byte of new TextEncoder().encode(value)) {
375
+ if (byte === 0x20) {
376
+ out += '_'
377
+ continue
378
+ }
379
+ if (
380
+ (byte >= 0x30 && byte <= 0x39) ||
381
+ (byte >= 0x41 && byte <= 0x5a) ||
382
+ (byte >= 0x61 && byte <= 0x7a)
383
+ ) {
384
+ out += String.fromCharCode(byte)
385
+ continue
386
+ }
387
+ out += `=${byte.toString(16).toUpperCase().padStart(2, '0')}`
388
+ }
389
+ return out
390
+ }
391
+
392
+ function qDecode(value: string): string {
393
+ const bytes: number[] = []
394
+ for (let i = 0; i < value.length; i++) {
395
+ const char = value[i]
396
+ if (char === '_') {
397
+ bytes.push(0x20)
398
+ continue
399
+ }
400
+ if (char === '=' && i + 2 < value.length) {
401
+ bytes.push(Number.parseInt(value.slice(i + 1, i + 3), 16))
402
+ i += 2
403
+ continue
404
+ }
405
+ bytes.push(char.charCodeAt(0))
406
+ }
407
+ return new TextDecoder().decode(new Uint8Array(bytes))
408
+ }
409
+
410
+ function decodeWord(word: string): string | null {
411
+ const match = /^=\?([^?]+)\?([bBqQ])\?([^?]*)\?=$/.exec(word)
412
+ if (match === null) {
413
+ return null
414
+ }
415
+ const [, charset, encoding, body] = match
416
+ if (!/^utf-?8$|^us-ascii$/i.test(charset)) {
417
+ return null
418
+ }
419
+ if (encoding.toLowerCase() === 'b') {
420
+ return new TextDecoder().decode(base64Decode(body))
421
+ }
422
+ return qDecode(body)
423
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "strict": true,
4
+ "symbols": {
5
+ "AddExtensionType": {
6
+ "status": "real"
7
+ },
8
+ "BEncoding": {
9
+ "status": "real"
10
+ },
11
+ "ErrInvalidMediaParameter": {
12
+ "status": "real"
13
+ },
14
+ "ExtensionsByType": {
15
+ "status": "real"
16
+ },
17
+ "FormatMediaType": {
18
+ "status": "real"
19
+ },
20
+ "ParseMediaType": {
21
+ "status": "real"
22
+ },
23
+ "QEncoding": {
24
+ "status": "real"
25
+ },
26
+ "TypeByExtension": {
27
+ "status": "real"
28
+ },
29
+ "WordDecoder": {
30
+ "status": "real"
31
+ },
32
+ "WordEncoder": {
33
+ "status": "real"
34
+ }
35
+ }
36
+ }
@@ -1,16 +1,30 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import * as $ from '@goscript/builtin/index.js'
4
+ import * as bytes from '@goscript/bytes/index.js'
5
+ import * as context from '@goscript/context/index.js'
6
+ import * as io from '@goscript/io/index.js'
4
7
 
5
8
  import {
6
9
  Handler,
7
10
  Header_Get,
8
11
  Header_Set,
9
12
  MethodGet,
13
+ MethodPost,
14
+ NoBody,
10
15
  NewRequest,
11
16
  StatusPartialContent,
12
17
  } from '../index.js'
13
- import { NewServer, NewUnstartedServer, Server_Start } from './index.js'
18
+ import {
19
+ DefaultRemoteAddr,
20
+ NewRequest as NewTestRequest,
21
+ NewRecorder,
22
+ NewRequestWithContext,
23
+ NewServer,
24
+ NewTLSServer,
25
+ NewUnstartedServer,
26
+ Server_Start,
27
+ } from './index.js'
14
28
 
15
29
  describe('net/http/httptest override', () => {
16
30
  it('exports server helpers for typechecked server tests', () => {
@@ -19,20 +33,59 @@ describe('net/http/httptest override', () => {
19
33
  }
20
34
 
21
35
  const srv = NewServer(handler)
36
+ const tlsSrv = NewTLSServer(handler)
22
37
  expect(srv.URL).toMatch(/^http:\/\/goscript-httptest-\d+\.invalid$/)
38
+ expect(tlsSrv.URL).toMatch(/^https:\/\/goscript-httptest-\d+\.invalid$/)
23
39
  expect(srv.Client()).toBeTruthy()
24
40
  expect(srv.Config().Handler).toBe(handler)
41
+ expect(tlsSrv.Client()).toBeTruthy()
25
42
  expect(Server_Start(NewUnstartedServer(handler))?.Error()).toBe(
26
43
  'net/http/httptest: Server.Start is not implemented in GoScript',
27
44
  )
28
45
  srv.Close()
46
+ tlsSrv.Close()
47
+ })
48
+
49
+ it('exports request defaults and recorder compatibility helpers', () => {
50
+ const req = NewRequestWithContext(context.Background(), '', 'https://example.invalid/path', null)
51
+ expect(DefaultRemoteAddr).toBe('1.2.3.4')
52
+ expect(req.Method).toBe(MethodGet)
53
+ expect(req.Host).toBe('example.invalid')
54
+ expect(req.RemoteAddr).toBe('192.0.2.1:1234')
55
+ expect(req.TLS).not.toBeNull()
56
+
57
+ const pathReq = NewRequestWithContext(context.Background(), MethodGet, '/local?x=1', null)
58
+ expect(pathReq.Host).toBe('example.com')
59
+ expect(pathReq.RequestURI).toBe('/local?x=1')
60
+ expect(pathReq.URL.Host).toBe('')
61
+ expect(pathReq.URL.Scheme).toBe('')
62
+
63
+ const bodyReq = NewTestRequest(MethodPost, '/body', bytes.NewReader($.stringToBytes('abc')))
64
+ expect(bodyReq.ContentLength).toBe(3)
65
+ const noBodyReq = NewTestRequest(MethodPost, '/empty', NoBody)
66
+ expect(noBodyReq.ContentLength).toBe(0)
67
+ const unknownReq = NewTestRequest(MethodPost, '/unknown', {
68
+ Read: () => [0, io.EOF],
69
+ })
70
+ expect(unknownReq.ContentLength).toBe(-1)
71
+
72
+ const recorder = NewRecorder()
73
+ recorder.WriteString('ok')
74
+ recorder.Flush()
75
+ expect(recorder.Code).toBe(200)
76
+ expect(recorder.Flushed).toBe(true)
77
+ expect(Buffer.from(recorder.Body.Bytes()).toString('utf8')).toBe('ok')
29
78
  })
30
79
 
31
80
  it('routes Client.Do through the in-memory server handler', async () => {
32
81
  const srv = NewServer({
33
82
  ServeHTTP(w, r) {
83
+ const req = $.pointerValue(r)!
84
+ expect(req.RequestURI).toBe('/pack.kvf')
85
+ expect(req.URL.Host).toBe('')
86
+ expect(req.Host).toMatch(/^goscript-httptest-\d+\.invalid$/)
34
87
  Header_Set(w!.Header(), 'Content-Range', 'bytes 0-3/4')
35
- const range = Header_Get($.pointerValue(r)!.Header, 'Range')
88
+ const range = Header_Get(req.Header, 'Range')
36
89
  if (range !== 'bytes=0-3') {
37
90
  w!.WriteHeader(400)
38
91
  w!.Write($.stringToBytes(range))
@@ -82,4 +135,47 @@ describe('net/http/httptest override', () => {
82
135
  expect(Buffer.from(buf).toString('utf8')).toBe('async')
83
136
  srv.Close()
84
137
  })
138
+
139
+ it('closes request bodies through Server.Client transport', async () => {
140
+ const srv = NewServer({
141
+ ServeHTTP(w) {
142
+ w!.WriteHeader(StatusPartialContent)
143
+ },
144
+ })
145
+ let closed = false
146
+ const body = {
147
+ Read: (_p: Uint8Array): [number, $.GoError] => [0, io.EOF],
148
+ Close: (): $.GoError => {
149
+ closed = true
150
+ return null
151
+ },
152
+ }
153
+ const [req, reqErr] = NewRequest(MethodPost, srv.URL + '/close', body)
154
+ expect(reqErr).toBeNull()
155
+
156
+ const [resp, err] = await srv.Client().Do(req)
157
+
158
+ expect(err).toBeNull()
159
+ expect(resp?.StatusCode).toBe(StatusPartialContent)
160
+ expect(closed).toBe(true)
161
+ srv.Close()
162
+ })
163
+
164
+ it('suppresses bodies for Server.Client HEAD requests', async () => {
165
+ const srv = NewServer({
166
+ ServeHTTP(w) {
167
+ w!.WriteHeader(StatusPartialContent)
168
+ w!.Write($.stringToBytes('hidden'))
169
+ },
170
+ })
171
+
172
+ const [resp, err] = await srv.Client().Head(srv.URL + '/head')
173
+
174
+ expect(err).toBeNull()
175
+ expect(resp?.StatusCode).toBe(StatusPartialContent)
176
+ const [n, readErr] = resp!.Body!.Read(new Uint8Array(8))
177
+ expect(n).toBe(0)
178
+ expect(readErr).toBe(io.EOF)
179
+ srv.Close()
180
+ })
85
181
  })