goscript 0.2.6 → 0.2.7

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 (264) hide show
  1. package/cmd/goscript/cmd-compile.go +7 -0
  2. package/cmd/goscript/cmd_compile_test.go +83 -0
  3. package/compiler/compile-request.go +3 -0
  4. package/compiler/compiler-cache.go +828 -0
  5. package/compiler/compiler-cache_test.go +705 -0
  6. package/compiler/config.go +2 -0
  7. package/compiler/index.test.ts +26 -1
  8. package/compiler/index.ts +5 -0
  9. package/compiler/lowered-program.go +31 -20
  10. package/compiler/lowering.go +349 -93
  11. package/compiler/lowering_bench_test.go +1 -0
  12. package/compiler/override-facts.go +309 -8
  13. package/compiler/override-parity-verifier.go +45 -1
  14. package/compiler/override-parity-verifier_test.go +100 -0
  15. package/compiler/override-registry_test.go +1 -0
  16. package/compiler/package-graph.go +40 -12
  17. package/compiler/package-graph_test.go +29 -0
  18. package/compiler/runtime-contract.go +8 -0
  19. package/compiler/service.go +98 -11
  20. package/compiler/skeleton_test.go +110 -14
  21. package/compiler/typescript-emitter.go +120 -23
  22. package/dist/compiler/index.d.ts +2 -0
  23. package/dist/compiler/index.js +3 -0
  24. package/dist/compiler/index.js.map +1 -1
  25. package/dist/gs/builtin/builtin.d.ts +24 -33
  26. package/dist/gs/builtin/builtin.js +54 -61
  27. package/dist/gs/builtin/builtin.js.map +1 -1
  28. package/dist/gs/builtin/hostio.d.ts +1 -0
  29. package/dist/gs/builtin/hostio.js +1 -1
  30. package/dist/gs/builtin/hostio.js.map +1 -1
  31. package/dist/gs/builtin/index.d.ts +1 -0
  32. package/dist/gs/builtin/index.js +1 -0
  33. package/dist/gs/builtin/index.js.map +1 -1
  34. package/dist/gs/builtin/panic.d.ts +18 -0
  35. package/dist/gs/builtin/panic.js +98 -0
  36. package/dist/gs/builtin/panic.js.map +1 -0
  37. package/dist/gs/builtin/slice.d.ts +10 -0
  38. package/dist/gs/builtin/slice.js +110 -53
  39. package/dist/gs/builtin/slice.js.map +1 -1
  40. package/dist/gs/builtin/type.js +15 -3
  41. package/dist/gs/builtin/type.js.map +1 -1
  42. package/dist/gs/builtin/varRef.d.ts +1 -1
  43. package/dist/gs/builtin/varRef.js +3 -2
  44. package/dist/gs/builtin/varRef.js.map +1 -1
  45. package/dist/gs/bytes/bytes.gs.js +51 -38
  46. package/dist/gs/bytes/bytes.gs.js.map +1 -1
  47. package/dist/gs/bytes/reader.gs.d.ts +1 -1
  48. package/dist/gs/bytes/reader.gs.js +6 -7
  49. package/dist/gs/bytes/reader.gs.js.map +1 -1
  50. package/dist/gs/cmp/index.d.ts +1 -1
  51. package/dist/gs/cmp/index.js +43 -10
  52. package/dist/gs/cmp/index.js.map +1 -1
  53. package/dist/gs/context/context.d.ts +2 -2
  54. package/dist/gs/context/context.js +1 -1
  55. package/dist/gs/context/context.js.map +1 -1
  56. package/dist/gs/embed/index.js +1 -1
  57. package/dist/gs/embed/index.js.map +1 -1
  58. package/dist/gs/encoding/binary/index.js +201 -8
  59. package/dist/gs/encoding/binary/index.js.map +1 -1
  60. package/dist/gs/encoding/json/index.d.ts +5 -0
  61. package/dist/gs/encoding/json/index.js +388 -25
  62. package/dist/gs/encoding/json/index.js.map +1 -1
  63. package/dist/gs/errors/errors.js +17 -24
  64. package/dist/gs/errors/errors.js.map +1 -1
  65. package/dist/gs/fmt/fmt.js +129 -35
  66. package/dist/gs/fmt/fmt.js.map +1 -1
  67. package/dist/gs/golang.org/x/crypto/cryptobyte/index.js +1 -1
  68. package/dist/gs/golang.org/x/crypto/cryptobyte/index.js.map +1 -1
  69. package/dist/gs/internal/bytealg/index.js +43 -8
  70. package/dist/gs/internal/bytealg/index.js.map +1 -1
  71. package/dist/gs/internal/byteorder/index.d.ts +2 -2
  72. package/dist/gs/internal/byteorder/index.js +2 -2
  73. package/dist/gs/internal/byteorder/index.js.map +1 -1
  74. package/dist/gs/io/fs/format.js +2 -2
  75. package/dist/gs/io/fs/format.js.map +1 -1
  76. package/dist/gs/io/fs/fs.d.ts +1 -1
  77. package/dist/gs/io/fs/fs.js +1 -1
  78. package/dist/gs/io/fs/fs.js.map +1 -1
  79. package/dist/gs/io/io.d.ts +21 -21
  80. package/dist/gs/io/io.js +49 -50
  81. package/dist/gs/io/io.js.map +1 -1
  82. package/dist/gs/math/bits/index.js +26 -8
  83. package/dist/gs/math/bits/index.js.map +1 -1
  84. package/dist/gs/math/copysign.gs.js +10 -17
  85. package/dist/gs/math/copysign.gs.js.map +1 -1
  86. package/dist/gs/math/pow.gs.js +5 -0
  87. package/dist/gs/math/pow.gs.js.map +1 -1
  88. package/dist/gs/math/signbit.gs.js +6 -2
  89. package/dist/gs/math/signbit.gs.js.map +1 -1
  90. package/dist/gs/mime/index.js +1 -0
  91. package/dist/gs/mime/index.js.map +1 -1
  92. package/dist/gs/net/http/index.d.ts +6 -6
  93. package/dist/gs/net/http/index.js +507 -43
  94. package/dist/gs/net/http/index.js.map +1 -1
  95. package/dist/gs/os/stat.gs.d.ts +2 -2
  96. package/dist/gs/os/types.gs.d.ts +1 -1
  97. package/dist/gs/os/types.gs.js +1 -1
  98. package/dist/gs/os/types.gs.js.map +1 -1
  99. package/dist/gs/os/types_js.gs.d.ts +1 -1
  100. package/dist/gs/os/types_js.gs.js +7 -7
  101. package/dist/gs/os/types_js.gs.js.map +1 -1
  102. package/dist/gs/os/types_unix.gs.d.ts +1 -1
  103. package/dist/gs/os/types_unix.gs.js +1 -1
  104. package/dist/gs/os/types_unix.gs.js.map +1 -1
  105. package/dist/gs/os/zero_copy_posix.gs.d.ts +1 -1
  106. package/dist/gs/os/zero_copy_posix.gs.js +1 -1
  107. package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
  108. package/dist/gs/path/filepath/match.js +8 -4
  109. package/dist/gs/path/filepath/match.js.map +1 -1
  110. package/dist/gs/path/filepath/path.js +216 -42
  111. package/dist/gs/path/filepath/path.js.map +1 -1
  112. package/dist/gs/path/match.js +6 -3
  113. package/dist/gs/path/match.js.map +1 -1
  114. package/dist/gs/reflect/type.d.ts +5 -4
  115. package/dist/gs/reflect/type.js +29 -11
  116. package/dist/gs/reflect/type.js.map +1 -1
  117. package/dist/gs/slices/slices.js +11 -11
  118. package/dist/gs/slices/slices.js.map +1 -1
  119. package/dist/gs/strconv/atof.gs.js +156 -43
  120. package/dist/gs/strconv/atof.gs.js.map +1 -1
  121. package/dist/gs/strconv/atoi.gs.d.ts +3 -2
  122. package/dist/gs/strconv/atoi.gs.js +86 -67
  123. package/dist/gs/strconv/atoi.gs.js.map +1 -1
  124. package/dist/gs/strconv/ftoa.gs.js +73 -3
  125. package/dist/gs/strconv/ftoa.gs.js.map +1 -1
  126. package/dist/gs/strconv/itoa.gs.d.ts +4 -4
  127. package/dist/gs/strconv/itoa.gs.js +5 -4
  128. package/dist/gs/strconv/itoa.gs.js.map +1 -1
  129. package/dist/gs/strconv/quote.gs.d.ts +1 -1
  130. package/dist/gs/strconv/quote.gs.js +311 -103
  131. package/dist/gs/strconv/quote.gs.js.map +1 -1
  132. package/dist/gs/strings/reader.d.ts +1 -1
  133. package/dist/gs/strings/reader.js +8 -8
  134. package/dist/gs/strings/reader.js.map +1 -1
  135. package/dist/gs/strings/strings.js +87 -61
  136. package/dist/gs/strings/strings.js.map +1 -1
  137. package/dist/gs/sync/atomic/doc_64.gs.d.ts +14 -14
  138. package/dist/gs/sync/atomic/doc_64.gs.js +10 -10
  139. package/dist/gs/sync/atomic/doc_64.gs.js.map +1 -1
  140. package/dist/gs/sync/atomic/type.gs.d.ts +22 -22
  141. package/dist/gs/sync/atomic/type.gs.js +4 -4
  142. package/dist/gs/sync/atomic/type.gs.js.map +1 -1
  143. package/dist/gs/sync/sync.js +50 -12
  144. package/dist/gs/sync/sync.js.map +1 -1
  145. package/dist/gs/syscall/fs.d.ts +6 -6
  146. package/dist/gs/syscall/fs.js +1 -1
  147. package/dist/gs/syscall/fs.js.map +1 -1
  148. package/dist/gs/time/time.d.ts +18 -18
  149. package/dist/gs/time/time.js +58 -55
  150. package/dist/gs/time/time.js.map +1 -1
  151. package/dist/gs/unicode/tables.d.ts +11 -0
  152. package/dist/gs/unicode/tables.js +635 -0
  153. package/dist/gs/unicode/tables.js.map +1 -0
  154. package/dist/gs/unicode/unicode.d.ts +58 -38
  155. package/dist/gs/unicode/unicode.js +362 -278
  156. package/dist/gs/unicode/unicode.js.map +1 -1
  157. package/go.sum +13 -0
  158. package/gs/builtin/builtin.ts +83 -93
  159. package/gs/builtin/hostio.ts +1 -1
  160. package/gs/builtin/index.ts +1 -0
  161. package/gs/builtin/panic.test.ts +189 -0
  162. package/gs/builtin/panic.ts +107 -0
  163. package/gs/builtin/runtime-contract.test.ts +5 -5
  164. package/gs/builtin/slice.test.ts +23 -0
  165. package/gs/builtin/slice.ts +133 -95
  166. package/gs/builtin/type.ts +16 -3
  167. package/gs/builtin/varRef.ts +4 -2
  168. package/gs/builtin/wide-int.test.ts +41 -0
  169. package/gs/bytes/bytes.gs.ts +54 -41
  170. package/gs/bytes/bytes.test.ts +18 -1
  171. package/gs/bytes/reader.gs.ts +7 -8
  172. package/gs/cmp/index.test.ts +55 -0
  173. package/gs/cmp/index.ts +45 -9
  174. package/gs/context/context.ts +3 -3
  175. package/gs/embed/index.ts +2 -2
  176. package/gs/encoding/binary/index.test.ts +104 -0
  177. package/gs/encoding/binary/index.ts +259 -11
  178. package/gs/encoding/json/index.test.ts +107 -0
  179. package/gs/encoding/json/index.ts +400 -29
  180. package/gs/errors/errors.test.ts +44 -1
  181. package/gs/errors/errors.ts +15 -31
  182. package/gs/fmt/fmt.test.ts +70 -2
  183. package/gs/fmt/fmt.ts +128 -34
  184. package/gs/golang.org/x/crypto/cryptobyte/index.ts +1 -1
  185. package/gs/internal/bytealg/index.test.ts +26 -1
  186. package/gs/internal/bytealg/index.ts +44 -8
  187. package/gs/internal/byteorder/index.ts +6 -4
  188. package/gs/io/fs/format.ts +2 -2
  189. package/gs/io/fs/fs.ts +2 -2
  190. package/gs/io/fs/stat.test.ts +2 -2
  191. package/gs/io/fs/sub.test.ts +2 -2
  192. package/gs/io/fs/walk.test.ts +2 -2
  193. package/gs/io/io.test.ts +47 -5
  194. package/gs/io/io.ts +73 -73
  195. package/gs/io/limit.test.ts +103 -0
  196. package/gs/math/bits/index.test.ts +128 -0
  197. package/gs/math/bits/index.ts +26 -8
  198. package/gs/math/copysign.gs.test.ts +3 -1
  199. package/gs/math/copysign.gs.ts +10 -22
  200. package/gs/math/pow.gs.test.ts +4 -5
  201. package/gs/math/pow.gs.ts +5 -0
  202. package/gs/math/signbit.gs.test.ts +2 -1
  203. package/gs/math/signbit.gs.ts +6 -3
  204. package/gs/mime/index.ts +1 -0
  205. package/gs/net/http/index.test.ts +683 -2
  206. package/gs/net/http/index.ts +598 -57
  207. package/gs/net/http/meta.json +3 -0
  208. package/gs/os/stat.gs.ts +2 -2
  209. package/gs/os/types.gs.ts +2 -2
  210. package/gs/os/types_js.gs.ts +9 -9
  211. package/gs/os/types_unix.gs.ts +2 -2
  212. package/gs/os/zero_copy_posix.gs.ts +2 -2
  213. package/gs/path/filepath/match.test.ts +16 -0
  214. package/gs/path/filepath/match.ts +8 -4
  215. package/gs/path/filepath/path.test.ts +91 -9
  216. package/gs/path/filepath/path.ts +223 -49
  217. package/gs/path/match.test.ts +32 -0
  218. package/gs/path/match.ts +6 -3
  219. package/gs/reflect/deepequal.test.ts +1 -1
  220. package/gs/reflect/field.test.ts +1 -1
  221. package/gs/reflect/function-types.test.ts +6 -6
  222. package/gs/reflect/sliceat.test.ts +13 -13
  223. package/gs/reflect/structof.test.ts +4 -4
  224. package/gs/reflect/type.ts +34 -14
  225. package/gs/reflect/typefor.test.ts +5 -5
  226. package/gs/runtime/pprof/index.test.ts +20 -0
  227. package/gs/runtime/trace/index.test.ts +3 -0
  228. package/gs/slices/slices.test.ts +31 -0
  229. package/gs/slices/slices.ts +11 -11
  230. package/gs/strconv/append.test.ts +99 -0
  231. package/gs/strconv/atof.gs.ts +156 -42
  232. package/gs/strconv/atof.test.ts +45 -0
  233. package/gs/strconv/atoi.gs.ts +87 -69
  234. package/gs/strconv/atoi.test.ts +49 -0
  235. package/gs/strconv/ftoa.gs.ts +85 -10
  236. package/gs/strconv/ftoa.test.ts +43 -0
  237. package/gs/strconv/itoa.gs.ts +10 -9
  238. package/gs/strconv/quote.gs.ts +335 -108
  239. package/gs/strconv/quote.test.ts +111 -0
  240. package/gs/strings/reader.test.ts +10 -10
  241. package/gs/strings/reader.ts +9 -9
  242. package/gs/strings/strings.test.ts +18 -5
  243. package/gs/strings/strings.ts +81 -68
  244. package/gs/sync/atomic/doc_64.gs.ts +24 -24
  245. package/gs/sync/atomic/doc_64.test.ts +5 -5
  246. package/gs/sync/atomic/type.gs.ts +28 -28
  247. package/gs/sync/sync.test.ts +109 -1
  248. package/gs/sync/sync.ts +46 -12
  249. package/gs/syscall/fs.ts +8 -8
  250. package/gs/syscall/net.test.ts +1 -1
  251. package/gs/time/parse.test.ts +45 -0
  252. package/gs/time/time.test.ts +46 -23
  253. package/gs/time/time.ts +69 -66
  254. package/gs/unicode/gen.go +198 -0
  255. package/gs/unicode/tables.ts +646 -0
  256. package/gs/unicode/unicode.test.ts +69 -0
  257. package/gs/unicode/unicode.ts +396 -312
  258. package/package.json +1 -1
  259. package/dist/gs/github.com/aperturerobotics/util/conc/index.d.ts +0 -20
  260. package/dist/gs/github.com/aperturerobotics/util/conc/index.js +0 -134
  261. package/dist/gs/github.com/aperturerobotics/util/conc/index.js.map +0 -1
  262. package/gs/github.com/aperturerobotics/util/conc/index.test.ts +0 -30
  263. package/gs/github.com/aperturerobotics/util/conc/index.ts +0 -172
  264. package/gs/github.com/aperturerobotics/util/conc/meta.json +0 -9
@@ -4,8 +4,11 @@
4
4
  "Head": true,
5
5
  "Post": true,
6
6
  "PostForm": true,
7
+ "ReadRequest": true,
8
+ "ReadResponse": true,
7
9
  "Redirect": true,
8
10
  "ServeContent": true,
11
+ "ServeFileFS": true,
9
12
  "SetCookie": true
10
13
  },
11
14
  "asyncMethods": {
package/gs/os/stat.gs.ts CHANGED
@@ -9,7 +9,7 @@ export function Stat(name: string): [null | {
9
9
  ModTime(): any
10
10
  Mode(): any
11
11
  Name(): string
12
- Size(): number
12
+ Size(): bigint
13
13
  Sys(): null | any
14
14
  }, $.GoError] {
15
15
  // testlog.Stat(name) // Testlog not available in JavaScript
@@ -29,7 +29,7 @@ export function Lstat(name: string): [null | {
29
29
  ModTime(): Time
30
30
  Mode(): FileMode
31
31
  Name(): string
32
- Size(): number
32
+ Size(): bigint
33
33
  Sys(): null | any
34
34
  }, $.GoError] {
35
35
  // testlog.Stat(name) // Testlog not available in JavaScript
package/gs/os/types.gs.ts CHANGED
@@ -73,8 +73,8 @@ export class File {
73
73
  return [0, ErrUnimplemented]
74
74
  }
75
75
 
76
- public Seek(offset: number, whence: number): [number, $.GoError] {
77
- return [0, ErrUnimplemented]
76
+ public Seek(offset: bigint, whence: number): [bigint, $.GoError] {
77
+ return [0n, ErrUnimplemented]
78
78
  }
79
79
 
80
80
  public WriteString(s: string): [number, $.GoError] {
@@ -163,7 +163,7 @@ class hostFileInfo {
163
163
 
164
164
  constructor(init?: Partial<{modTime?: time.Time, mode?: fs.FileMode, name?: string, size?: number, sys?: HostStatLike | null}>) {
165
165
  this.name = init?.name ?? ""
166
- this.modTime = init?.modTime ?? time.Unix(0, 0)
166
+ this.modTime = init?.modTime ?? time.Unix(0n, 0n)
167
167
  this.mode = init?.mode ?? 0
168
168
  this.size = init?.size ?? 0
169
169
  this.sys = init?.sys ?? null
@@ -185,8 +185,8 @@ class hostFileInfo {
185
185
  return this.name
186
186
  }
187
187
 
188
- public Size(): number {
189
- return this.size
188
+ public Size(): bigint {
189
+ return BigInt(this.size)
190
190
  }
191
191
 
192
192
  public Sys(): HostStatLike | null {
@@ -212,10 +212,10 @@ export function createFileInfo(name: string, stat: HostStatLike): fs.FileInfo {
212
212
  }
213
213
 
214
214
  return new hostFileInfo({
215
- modTime: time.UnixMilli(mtimeMs),
215
+ modTime: time.UnixMilli(BigInt(Math.trunc(mtimeMs))),
216
216
  mode,
217
217
  name: normalizedName,
218
- size: stat.size ?? 0,
218
+ size: Math.trunc(stat.size ?? 0),
219
219
  sys: stat,
220
220
  })
221
221
  }
@@ -589,15 +589,15 @@ export class File {
589
589
  }
590
590
  }
591
591
 
592
- public Seek(offset: number, whence: number): [number, $.GoError] {
592
+ public Seek(offset: bigint, whence: number): [bigint, $.GoError] {
593
593
  const handle = this.file?.handle
594
594
  if (!handle || typeof handle.seekSync !== "function") {
595
- return [0, ErrUnimplemented]
595
+ return [0n, ErrUnimplemented]
596
596
  }
597
597
  try {
598
- return [handle.seekSync(offset, whence), null]
598
+ return [BigInt(handle.seekSync(Number(offset), whence)), null]
599
599
  } catch (err) {
600
- return [0, newHostError(err)]
600
+ return [0n, newHostError(err)]
601
601
  }
602
602
  }
603
603
 
@@ -82,9 +82,9 @@ class fileStat {
82
82
  return (fileStat!.Mode() & fs.ModeDir) !== 0
83
83
  }
84
84
 
85
- public Size(): number {
85
+ public Size(): bigint {
86
86
  const fs = this
87
- return fs!.size
87
+ return BigInt(fs!.size)
88
88
  }
89
89
 
90
90
  public Mode(): fs.FileMode {
@@ -20,9 +20,9 @@ export function wrapSyscallError(name: string, err: $.GoError): $.GoError {
20
20
  // tryLimitedReader tries to assert the io.Reader to io.LimitedReader, it returns the io.LimitedReader,
21
21
  // the underlying io.Reader and the remaining amount of bytes if the assertion succeeds,
22
22
  // otherwise it just returns the original io.Reader and the theoretical unlimited remaining amount of bytes.
23
- export function tryLimitedReader(r: io.Reader): [io.LimitedReader | null, io.Reader, number] {
23
+ export function tryLimitedReader(r: io.Reader): [io.LimitedReader | null, io.Reader, bigint] {
24
24
  // by default, copy until EOF
25
- let remain: number = Number.MAX_SAFE_INTEGER - 1
25
+ let remain: bigint = (1n << 63n) - 1n
26
26
 
27
27
  let { value: lr, ok: ok } = $.typeAssert<io.LimitedReader | null>(r, {kind: $.TypeKind.Pointer, elemType: 'io.LimitedReader'})
28
28
  if (!ok) {
@@ -173,6 +173,22 @@ describe('path/filepath - Pattern matching functions', () => {
173
173
  expect(err1).toBeNull()
174
174
  expect(match1).toBe(false)
175
175
  })
176
+
177
+ it('should not let a star cross a path separator', () => {
178
+ // Go: '*' matches a non-Separator run only.
179
+ const [m1, e1] = Match('*c', 'a/c')
180
+ expect(e1).toBeNull()
181
+ expect(m1).toBe(false)
182
+
183
+ const [m2, e2] = Match('*', 'a/b')
184
+ expect(e2).toBeNull()
185
+ expect(m2).toBe(false)
186
+
187
+ // A star can still consume the run right up to a separator.
188
+ const [m3, e3] = Match('*/c', 'a/c')
189
+ expect(e3).toBeNull()
190
+ expect(m3).toBe(true)
191
+ })
176
192
  })
177
193
 
178
194
  describe('Error cases', () => {
@@ -102,20 +102,24 @@ function matchPattern(pattern: string, name: string): boolean {
102
102
 
103
103
  switch (p) {
104
104
  case '*':
105
- // Handle star - match any sequence of characters
105
+ // Handle star - match any sequence of non-Separator characters.
106
106
  patternIndex++
107
107
  if (patternIndex >= pattern.length) {
108
- // Pattern ends with *, matches rest of name
109
- return true
108
+ // Trailing * matches the rest of name unless it crosses a separator.
109
+ return name.substring(nameIndex).indexOf('/') < 0
110
110
  }
111
111
 
112
- // Try to match the rest of the pattern with remaining name
112
+ // Try to match the rest of the pattern with remaining name, but the
113
+ // star cannot consume a separator (Go matches non-Separator runs only).
113
114
  for (let i = nameIndex; i <= name.length; i++) {
114
115
  if (
115
116
  matchPattern(pattern.substring(patternIndex), name.substring(i))
116
117
  ) {
117
118
  return true
118
119
  }
120
+ if (i < name.length && name[i] === '/') {
121
+ break
122
+ }
119
123
  }
120
124
  return false
121
125
 
@@ -1,5 +1,12 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import {
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ realpathSync,
6
+ rmSync,
7
+ symlinkSync,
8
+ writeFileSync,
9
+ } from 'node:fs'
3
10
  import { tmpdir } from 'node:os'
4
11
  import { join } from 'node:path'
5
12
  import {
@@ -18,6 +25,7 @@ import {
18
25
  HasPrefix,
19
26
  Abs,
20
27
  Rel,
28
+ Localize,
21
29
  EvalSymlinks,
22
30
  Walk,
23
31
  WalkDir,
@@ -177,9 +185,11 @@ describe('path/filepath - Path manipulation functions', () => {
177
185
  expect(err1).toBeNull()
178
186
  expect(result1).toBe('/absolute/path')
179
187
 
188
+ // A relative path is joined against the real working directory, matching
189
+ // Go's filepath.Abs (os.Getwd), not a fabricated root.
180
190
  const [result2, err2] = Abs('relative/path')
181
191
  expect(err2).toBeNull()
182
- expect(result2).toBe('/relative/path')
192
+ expect(result2).toBe(Join(process.cwd(), 'relative/path'))
183
193
  })
184
194
  })
185
195
 
@@ -193,17 +203,66 @@ describe('path/filepath - Path manipulation functions', () => {
193
203
  expect(err2).toBeNull()
194
204
  expect(result2).toBe('bin')
195
205
 
206
+ // Go emits ".." components to climb out of base before descending into
207
+ // the divergent target tail.
196
208
  const [result3, err3] = Rel('/usr/local', '/other/path')
197
209
  expect(err3).toBeNull()
198
- expect(result3).toBe('/other/path')
210
+ expect(result3).toBe('../../other/path')
211
+
212
+ const [result4, err4] = Rel('/a/b', '/a/c')
213
+ expect(err4).toBeNull()
214
+ expect(result4).toBe('../c')
215
+
216
+ const [result5, err5] = Rel('a/b', 'a/c')
217
+ expect(err5).toBeNull()
218
+ expect(result5).toBe('../c')
219
+
220
+ // A relative base cannot be made relative to an absolute target.
221
+ const [, err6] = Rel('a/b', '/a/c')
222
+ expect(err6).not.toBeNull()
223
+ })
224
+ })
225
+
226
+ describe('Localize', () => {
227
+ it('should accept io/fs.ValidPath inputs and reject the rest', () => {
228
+ const [ok, okErr] = Localize('a/b')
229
+ expect(okErr).toBeNull()
230
+ expect(ok).toBe('a/b')
231
+
232
+ const [dot, dotErr] = Localize('.')
233
+ expect(dotErr).toBeNull()
234
+ expect(dot).toBe('.')
235
+
236
+ for (const bad of ['', '/a', '../a', 'a/../b', 'a/']) {
237
+ const [, err] = Localize(bad)
238
+ expect(err).not.toBeNull()
239
+ }
199
240
  })
200
241
  })
201
242
 
202
243
  describe('EvalSymlinks', () => {
203
- it('should return cleaned path (stubbed)', () => {
204
- const [result, err] = EvalSymlinks('/path/with/../dots')
205
- expect(err).toBeNull()
206
- expect(result).toBe('/path/dots')
244
+ it('should resolve symlinks against the host filesystem', () => {
245
+ // realpathSync canonicalizes the temp root (macOS /var -> /private/var) so
246
+ // the expected resolved path uses the same canonical base.
247
+ const root = realpathSync(
248
+ mkdtempSync(join(tmpdir(), 'goscript-filepath-eval-')),
249
+ )
250
+ try {
251
+ mkdirSync(join(root, 'real'))
252
+ writeFileSync(join(root, 'real', 'file.txt'), 'ok')
253
+ symlinkSync(join(root, 'real'), join(root, 'link'))
254
+
255
+ const [result, err] = EvalSymlinks(join(root, 'link', 'file.txt'))
256
+ expect(err).toBeNull()
257
+ expect(result).toBe(join(root, 'real', 'file.txt'))
258
+ } finally {
259
+ rmSync(root, { force: true, recursive: true })
260
+ }
261
+ })
262
+
263
+ it('should error when a path component does not exist', () => {
264
+ const [, err] = EvalSymlinks('/goscript-nonexistent-xyz/file')
265
+ expect(err).not.toBeNull()
207
266
  })
208
267
  })
209
268
 
@@ -226,6 +285,28 @@ describe('path/filepath - Path manipulation functions', () => {
226
285
  rmSync(root, { force: true, recursive: true })
227
286
  }
228
287
  })
288
+
289
+ it('should not descend into a symlinked directory (Lstat walk)', async () => {
290
+ const root = mkdtempSync(join(tmpdir(), 'goscript-filepath-walklink-'))
291
+ try {
292
+ mkdirSync(join(root, 'real'))
293
+ writeFileSync(join(root, 'real', 'inside.txt'), 'ok')
294
+ symlinkSync(join(root, 'real'), join(root, 'link'))
295
+
296
+ const visited: string[] = []
297
+ const err = await Walk(root, (path) => {
298
+ visited.push(path.slice(root.length).replace(/^\/?/, '') || '.')
299
+ return null
300
+ })
301
+
302
+ expect(err).toBeNull()
303
+ // The symlink is visited but not traversed, so link/inside.txt never
304
+ // appears.
305
+ expect(visited).toEqual(['.', 'link', 'real', 'real/inside.txt'])
306
+ } finally {
307
+ rmSync(root, { force: true, recursive: true })
308
+ }
309
+ })
229
310
  })
230
311
 
231
312
  describe('WalkDir', () => {
@@ -296,9 +377,10 @@ describe('Complex path operations', () => {
296
377
  expect(Clean('a/b/../../c')).toBe('c')
297
378
  expect(Clean('../../a/b')).toBe('../../a/b')
298
379
 
299
- // Test Join with various inputs
380
+ // Test Join with various inputs. A later absolute element does not discard
381
+ // earlier elements; Go joins the suffix and Clean collapses double slashes.
300
382
  expect(Join('/a', '../b', 'c')).toBe('/b/c')
301
- expect(Join('a', '/b', 'c')).toBe('/b/c')
383
+ expect(Join('a', '/b', 'c')).toBe('a/b/c')
302
384
 
303
385
  // Test Split edge cases
304
386
  expect(Split('/a/')).toEqual(['/a/', ''])
@@ -1,8 +1,12 @@
1
1
  // Package filepath implements utility routines for manipulating filename paths
2
2
  // in a way compatible with the target operating system-defined file paths.
3
3
  import * as $ from '@goscript/builtin/index.js'
4
- import { getHostRuntime } from '@goscript/builtin/hostio.js'
4
+ import {
5
+ getCurrentWorkingDirectory,
6
+ getHostRuntime,
7
+ } from '@goscript/builtin/hostio.js'
5
8
  import type { DirEntry } from '@goscript/io/fs/fs.js'
9
+ import { ValidPath } from '@goscript/io/fs/fs.js'
6
10
  import { FileInfoToDirEntry } from '@goscript/io/fs/readdir.js'
7
11
 
8
12
  type JoinElement = string | $.Slice<string>
@@ -142,32 +146,15 @@ export function Clean(path: string): string {
142
146
  // an empty string.
143
147
  export function Join(...elem: JoinElement[]): string {
144
148
  const partsArg = normalizeJoinElements(elem)
145
- if (partsArg.length === 0) {
146
- return ''
147
- }
148
-
149
- // Filter out empty elements but handle absolute paths
150
- const parts: string[] = []
151
-
152
- for (const e of partsArg) {
153
- if (e === '') {
154
- continue
149
+ // Join all elements from the first non-empty one onward, then Clean. A later
150
+ // absolute element does not discard earlier elements (Go joins the suffix and
151
+ // lets Clean collapse the resulting double separators).
152
+ for (let i = 0; i < partsArg.length; i++) {
153
+ if (partsArg[i] !== '') {
154
+ return Clean(partsArg.slice(i).join('/'))
155
155
  }
156
-
157
- // If this element is absolute, start over from here
158
- if (IsAbs(e)) {
159
- parts.length = 0 // Clear previous parts
160
- parts.push(e)
161
- } else {
162
- parts.push(e)
163
- }
164
- }
165
-
166
- if (parts.length === 0) {
167
- return ''
168
156
  }
169
-
170
- return Clean(parts.join('/'))
157
+ return ''
171
158
  }
172
159
 
173
160
  // Split splits path immediately following the final Separator,
@@ -278,38 +265,183 @@ export function HasPrefix(p: string, prefix: string): boolean {
278
265
  return false
279
266
  }
280
267
 
281
- // Stubs for functions that require filesystem operations
282
- // These are simplified implementations for compatibility
283
-
268
+ // Abs returns an absolute representation of path. If the path is not absolute
269
+ // it is joined with the current working directory to turn it into an absolute
270
+ // path, then Cleaned. This mirrors Go's filepath.Abs, which calls os.Getwd for
271
+ // relative inputs rather than fabricating a root.
284
272
  export function Abs(path: string): [string, $.GoError] {
285
273
  if (IsAbs(path)) {
286
274
  return [Clean(path), null]
287
275
  }
288
- // In a real implementation, this would resolve relative to current working directory
289
- // For our purposes, we'll just prepend a fake absolute path
290
- return ['/' + Clean(path), null]
276
+ const wd = getCurrentWorkingDirectory()
277
+ if (wd === null) {
278
+ return ['', $.newError('filepath: cannot determine working directory')]
279
+ }
280
+ return [Join(wd, path), null]
291
281
  }
292
282
 
293
283
  export function Rel(basepath: string, targpath: string): [string, $.GoError] {
294
- // Simplified implementation - in reality this is much more complex
295
284
  const base = Clean(basepath)
296
285
  const targ = Clean(targpath)
297
-
298
- if (base === targ) {
286
+ if (targ === base) {
299
287
  return ['.', null]
300
288
  }
301
289
 
302
- // Very basic relative path calculation
303
- if (targ.startsWith(base + '/')) {
304
- return [targ.substring(base.length + 1), null]
290
+ const b = base === '.' ? '' : base
291
+ const t = targ === '.' ? '' : targ
292
+ const baseSlashed = b.length > 0 && b[0] === '/'
293
+ const targSlashed = t.length > 0 && t[0] === '/'
294
+ if (baseSlashed !== targSlashed) {
295
+ return [
296
+ '',
297
+ $.newError(`Rel: can't make ${targpath} relative to ${basepath}`),
298
+ ]
299
+ }
300
+
301
+ // Walk both paths element by element until they first differ.
302
+ const bl = b.length
303
+ const tl = t.length
304
+ let b0 = 0
305
+ let bi = 0
306
+ let t0 = 0
307
+ let ti = 0
308
+ for (;;) {
309
+ while (bi < bl && b[bi] !== '/') {
310
+ bi++
311
+ }
312
+ while (ti < tl && t[ti] !== '/') {
313
+ ti++
314
+ }
315
+ if (t.slice(t0, ti) !== b.slice(b0, bi)) {
316
+ break
317
+ }
318
+ if (bi < bl) {
319
+ bi++
320
+ }
321
+ if (ti < tl) {
322
+ ti++
323
+ }
324
+ b0 = bi
325
+ t0 = ti
326
+ }
327
+ if (b.slice(b0, bi) === '..') {
328
+ return [
329
+ '',
330
+ $.newError(`Rel: can't make ${targpath} relative to ${basepath}`),
331
+ ]
332
+ }
333
+
334
+ if (b0 !== bl) {
335
+ // Base elements remain; emit one ".." per leftover base element, then the
336
+ // remaining target tail.
337
+ let seps = 0
338
+ for (let i = b0; i < bl; i++) {
339
+ if (b[i] === '/') {
340
+ seps++
341
+ }
342
+ }
343
+ let out = '..'
344
+ for (let i = 0; i < seps; i++) {
345
+ out += '/..'
346
+ }
347
+ if (t0 !== tl) {
348
+ out += '/' + t.slice(t0)
349
+ }
350
+ return [out, null]
305
351
  }
306
-
307
- return [targ, null]
352
+ return [t.slice(t0), null]
308
353
  }
309
354
 
355
+ // EvalSymlinks returns the path name after the evaluation of any symbolic
356
+ // links. This is a lexical port of Go's path/filepath walkSymlinks: it resolves
357
+ // each component with Lstat and follows symlinks with Readlink, preserving the
358
+ // relative-vs-absolute shape of the input. It deliberately does not use a host
359
+ // realpath, which would always force an absolute result and would not surface
360
+ // Go's per-component ENOTDIR and link-cycle errors. volLen is 0 for a relative
361
+ // input and 1 for an absolute root.
310
362
  export function EvalSymlinks(path: string): [string, $.GoError] {
311
- // No filesystem support, just return the cleaned path
312
- return [Clean(path), null]
363
+ let p = path
364
+ const volLen = p.length > 0 && p[0] === '/' ? 1 : 0
365
+ const vol = p.slice(0, volLen)
366
+ let dest = vol
367
+ let linksWalked = 0
368
+ for (let start = volLen; start < p.length; ) {
369
+ while (start < p.length && p[start] === '/') {
370
+ start++
371
+ }
372
+ let end = start
373
+ while (end < p.length && p[end] !== '/') {
374
+ end++
375
+ }
376
+
377
+ const component = p.slice(start, end)
378
+ if (end === start) {
379
+ break
380
+ } else if (component === '.') {
381
+ start = end
382
+ continue
383
+ } else if (component === '..') {
384
+ // Back up to the previous component unless it is itself a kept "..".
385
+ let r = dest.length - 1
386
+ for (; r >= volLen; r--) {
387
+ if (dest[r] === '/') {
388
+ break
389
+ }
390
+ }
391
+ if (r < volLen || dest.slice(r + 1) === '..') {
392
+ if (dest.length > volLen) {
393
+ dest += '/'
394
+ }
395
+ dest += '..'
396
+ } else {
397
+ dest = dest.slice(0, r)
398
+ }
399
+ start = end
400
+ continue
401
+ }
402
+
403
+ if (dest.length > volLen && dest[dest.length - 1] !== '/') {
404
+ dest += '/'
405
+ }
406
+ dest += component
407
+
408
+ const [stat, statErr] = lstatPath(dest)
409
+ if (statErr !== null) {
410
+ return ['', statErr]
411
+ }
412
+ if (!statIsSymlink(stat!)) {
413
+ if (!statIsDir(stat!) && end < p.length) {
414
+ return ['', $.newError('not a directory')]
415
+ }
416
+ start = end
417
+ continue
418
+ }
419
+
420
+ linksWalked++
421
+ if (linksWalked > 255) {
422
+ return ['', $.newError('EvalSymlinks: too many links')]
423
+ }
424
+ const [link, linkErr] = readlinkPath(dest)
425
+ if (linkErr !== null) {
426
+ return ['', linkErr]
427
+ }
428
+ p = link! + p.slice(end)
429
+ if (link!.length > 0 && link![0] === '/') {
430
+ // Absolute symlink target: restart from the root.
431
+ dest = '/'
432
+ } else {
433
+ // Relative symlink target: replace the last component of dest.
434
+ let r = dest.length - 1
435
+ for (; r >= volLen; r--) {
436
+ if (dest[r] === '/') {
437
+ break
438
+ }
439
+ }
440
+ dest = r < volLen ? vol : dest.slice(0, r)
441
+ }
442
+ start = 0
443
+ }
444
+ return [Clean(dest), null]
313
445
  }
314
446
 
315
447
  export function Glob(_pattern: string): [string[], $.GoError] {
@@ -345,8 +477,13 @@ export async function WalkDir(
345
477
  return await walkDirHost(root, walkFn)
346
478
  }
347
479
 
348
- // Localize is a stub - in Go it's used for Windows path localization
480
+ // Localize converts a slash-separated path into an operating system path.
481
+ // The input must be a valid path as reported by io/fs.ValidPath. On a
482
+ // Unix-like target the only further restriction is the absence of a NUL byte.
349
483
  export function Localize(path: string): [string, $.GoError] {
484
+ if (!ValidPath(path) || path.indexOf('\x00') >= 0) {
485
+ return ['', $.newError('invalid path')]
486
+ }
350
487
  return [path, null]
351
488
  }
352
489
 
@@ -377,6 +514,13 @@ function statIsDir(stat: HostStat): boolean {
377
514
  return !!stat.isDirectory
378
515
  }
379
516
 
517
+ function statIsSymlink(stat: any): boolean {
518
+ if (typeof stat?.isSymbolicLink === 'function') {
519
+ return stat.isSymbolicLink()
520
+ }
521
+ return !!stat?.isSymlink
522
+ }
523
+
380
524
  function fileInfo(path: string, stat: HostStat): any {
381
525
  return {
382
526
  IsDir: () => statIsDir(stat),
@@ -392,20 +536,50 @@ function dirEntry(path: string, stat: HostStat): DirEntry {
392
536
  return FileInfoToDirEntry(fileInfo(path, stat))
393
537
  }
394
538
 
395
- function statPath(path: string): [HostStat | null, $.GoError] {
539
+ // lstatPath stats a path without following a terminal symlink, matching Go's
540
+ // Walk/WalkDir, which Lstat every entry so a symlinked directory is reported as
541
+ // a symlink and is never descended into. Falls back to stat only when the host
542
+ // exposes no lstat.
543
+ function lstatPath(path: string): [HostStat | null, $.GoError] {
544
+ const runtime = getHostRuntime()
545
+ const deno = runtime.deno
546
+ const denoStat = deno?.lstatSync ?? deno?.statSync
547
+ if (denoStat) {
548
+ try {
549
+ return [denoStat.call(deno, path), null]
550
+ } catch (err) {
551
+ return [null, hostError(err)]
552
+ }
553
+ }
554
+
555
+ const nodeFS = runtime.nodeFS
556
+ const nodeStat = nodeFS?.lstatSync ?? nodeFS?.statSync
557
+ if (nodeStat) {
558
+ try {
559
+ return [nodeStat.call(nodeFS, path), null]
560
+ } catch (err) {
561
+ return [null, hostError(err)]
562
+ }
563
+ }
564
+
565
+ return [null, $.newError('filesystem not supported')]
566
+ }
567
+
568
+ function readlinkPath(path: string): [string | null, $.GoError] {
396
569
  const runtime = getHostRuntime()
397
- if (runtime.deno?.statSync) {
570
+ const deno = runtime.deno
571
+ if (deno?.readLinkSync) {
398
572
  try {
399
- return [runtime.deno.statSync(path), null]
573
+ return [deno.readLinkSync(path), null]
400
574
  } catch (err) {
401
575
  return [null, hostError(err)]
402
576
  }
403
577
  }
404
578
 
405
579
  const nodeFS = runtime.nodeFS
406
- if (nodeFS?.statSync) {
580
+ if (nodeFS?.readlinkSync) {
407
581
  try {
408
- return [nodeFS.statSync(path), null]
582
+ return [String(nodeFS.readlinkSync(path)), null]
409
583
  } catch (err) {
410
584
  return [null, hostError(err)]
411
585
  }
@@ -452,7 +626,7 @@ function readDir(path: string): [HostEntry[] | null, $.GoError] {
452
626
  }
453
627
 
454
628
  async function walkHost(path: string, walkFn: WalkFunc): Promise<$.GoError> {
455
- const [stat, statErr] = statPath(path)
629
+ const [stat, statErr] = lstatPath(path)
456
630
  if (statErr !== null) {
457
631
  return await walkFn(path, null, statErr)
458
632
  }
@@ -514,7 +688,7 @@ async function walkDirHost(
514
688
  path: string,
515
689
  walkFn: WalkDirFunc,
516
690
  ): Promise<$.GoError> {
517
- const [stat, statErr] = statPath(path)
691
+ const [stat, statErr] = lstatPath(path)
518
692
  if (statErr !== null) {
519
693
  return normalizeRootWalkDirErr(await walkFn(path, null, statErr))
520
694
  }