functionalscript 0.35.2 → 0.36.0

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 (40) hide show
  1. package/fs/base64/module.f.js +6 -5
  2. package/fs/base64/proof.f.d.ts +1 -0
  3. package/fs/base64/proof.f.js +10 -1
  4. package/fs/cas/mcp/module.f.d.ts +8 -4
  5. package/fs/cas/mcp/module.f.js +33 -21
  6. package/fs/cas/mcp/proof.f.d.ts +0 -7
  7. package/fs/cas/mcp/proof.f.js +47 -139
  8. package/fs/cas/module.f.js +3 -10
  9. package/fs/ci/config/module.f.d.ts +7 -7
  10. package/fs/ci/config/module.f.js +8 -8
  11. package/fs/djs/parser/proof.f.js +12 -0
  12. package/fs/effects/module.f.d.ts +15 -0
  13. package/fs/effects/module.f.js +14 -0
  14. package/fs/effects/node/module.f.js +5 -15
  15. package/fs/effects/node/module.js +3 -1
  16. package/fs/effects/node/proof.f.d.ts +3 -0
  17. package/fs/effects/node/proof.f.js +19 -1
  18. package/fs/effects/node/virtual/module.f.js +1 -2
  19. package/fs/effects/proof.f.d.ts +5 -0
  20. package/fs/effects/proof.f.js +22 -1
  21. package/fs/fjs/proof.f.d.ts +1 -0
  22. package/fs/fjs/proof.f.js +6 -0
  23. package/fs/fsc/module.f.d.ts +1 -0
  24. package/fs/fsc/module.f.js +5 -1
  25. package/fs/js/tokenizer/module.f.d.ts +1 -1
  26. package/fs/js/tokenizer/module.f.js +7 -1
  27. package/fs/js/tokenizer/proof.f.js +59 -0
  28. package/fs/json/parser/proof.f.js +18 -0
  29. package/fs/path/module.f.d.ts +6 -0
  30. package/fs/path/module.f.js +6 -0
  31. package/fs/path/proof.f.d.ts +1 -0
  32. package/fs/path/proof.f.js +40 -2
  33. package/fs/text/utf8/module.f.d.ts +6 -0
  34. package/fs/text/utf8/module.f.js +15 -2
  35. package/fs/text/utf8/proof.f.js +53 -0
  36. package/fs/types/result/module.f.d.ts +4 -0
  37. package/fs/types/result/module.f.js +4 -0
  38. package/fs/types/result/proof.f.d.ts +1 -0
  39. package/fs/types/result/proof.f.js +12 -2
  40. package/package.json +1 -1
@@ -251,6 +251,18 @@ export const proof = {
251
251
  throw obj[1].message;
252
252
  }
253
253
  },
254
+ // A literal control character inside a string is not valid JSON
255
+ // syntax (RFC 8259 §7), and DJS string literals are JSON strings.
256
+ () => {
257
+ const tokenList = tokenizeString('export default "\t"');
258
+ const obj = parseFromTokens(tokenList);
259
+ if (obj[0] !== 'error') {
260
+ throw obj;
261
+ }
262
+ if (obj[1].message !== 'unexpected token') {
263
+ throw obj[1].message;
264
+ }
265
+ },
254
266
  () => {
255
267
  const tokenList = tokenizeString('export default [,]');
256
268
  const obj = parseFromTokens(tokenList);
@@ -1,9 +1,17 @@
1
1
  /**
2
2
  * Core effect type constructors and combinators.
3
3
  *
4
+ * Effect helpers are **step adapters**: functions that return a continuation
5
+ * `(t: T) => Effect<Q, R>` meant to be passed into `.step`, never wrappers
6
+ * that take the effect itself as an argument. `Effect` has no pipeline
7
+ * operator to lean on and must not be extended with new methods, so
8
+ * `.step(adapterA).step(adapterB)` is how helpers compose — flat,
9
+ * left-to-right, in evaluation order. See {@link okStep} for an example.
10
+ *
4
11
  * @module
5
12
  */
6
13
  import { type List } from '../types/list/module.f.ts';
14
+ import type { Result } from '../types/result/module.f.ts';
7
15
  export type Operation = readonly [string, (..._: readonly never[]) => unknown];
8
16
  export type Effect<O extends Operation, T> = {
9
17
  value: Value<O, T>;
@@ -43,6 +51,13 @@ export declare const foldStep: <O extends Operation, T, S>(f: (item: T) => (stat
43
51
  * results. The `void` accumulator sibling of `foldStep`.
44
52
  */
45
53
  export declare const forEachStep: <O extends Operation, T>(f: (item: T) => Effect<O, void>) => (items: List<T>) => Effect<O, void>;
54
+ /**
55
+ * A step adapter for the `error` short-circuit: `error` → pass it through
56
+ * unchanged as `pure`, `ok` → continue with `f`. Collapses the hand-written
57
+ * `r[0] === 'error' ? pure(r) : f(r[1])` check that recurs at every site
58
+ * chaining `Effect<O, Result<T, E>>` steps.
59
+ */
60
+ export declare const okStep: <T, E, O extends Operation, R>(f: (value: T) => Effect<O, Result<R, E>>) => (r: Result<T, E>) => Effect<O, Result<R, E>>;
46
61
  /**
47
62
  * The decoded form of an effect's next step: either a final `result`, or a
48
63
  * `command` to perform with its `payload` and the `continuation` to resume
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Core effect type constructors and combinators.
3
3
  *
4
+ * Effect helpers are **step adapters**: functions that return a continuation
5
+ * `(t: T) => Effect<Q, R>` meant to be passed into `.step`, never wrappers
6
+ * that take the effect itself as an argument. `Effect` has no pipeline
7
+ * operator to lean on and must not be extended with new methods, so
8
+ * `.step(adapterA).step(adapterB)` is how helpers compose — flat,
9
+ * left-to-right, in evaluation order. See {@link okStep} for an example.
10
+ *
4
11
  * @module
5
12
  */
6
13
  import { fold } from "../types/list/module.f.js";
@@ -39,6 +46,13 @@ export const foldStep = (f) => (init) => (items) => fold(item => acc => acc.step
39
46
  * results. The `void` accumulator sibling of `foldStep`.
40
47
  */
41
48
  export const forEachStep = (f) => (items) => foldStep((item) => () => f(item))(undefined)(items);
49
+ /**
50
+ * A step adapter for the `error` short-circuit: `error` → pass it through
51
+ * unchanged as `pure`, `ok` → continue with `f`. Collapses the hand-written
52
+ * `r[0] === 'error' ? pure(r) : f(r[1])` check that recurs at every site
53
+ * chaining `Effect<O, Result<T, E>>` steps.
54
+ */
55
+ export const okStep = (f) => (r) => r[0] === 'error' ? pure(r) : f(r[1]);
42
56
  /**
43
57
  * Decodes an effect's next step: a pure result, or a command to perform.
44
58
  *
@@ -13,8 +13,8 @@ import { toCodePointList } from "../../text/utf8/module.f.js";
13
13
  import { codePointListToString } from "../../text/utf16/module.f.js";
14
14
  import { reverse } from "../../types/list/module.f.js";
15
15
  import { length } from "../../types/bit_vec/module.f.js";
16
- import { ok, error as resultError } from "../../types/result/module.f.js";
17
- import { do_, pure } from "../module.f.js";
16
+ import { ok, error as resultError, mapOk } from "../../types/result/module.f.js";
17
+ import { do_, okStep, pure } from "../module.f.js";
18
18
  /**
19
19
  * True if `e` is a "file or directory does not exist" (`ENOENT`) error.
20
20
  *
@@ -43,7 +43,7 @@ export const readFile = do_('readFile');
43
43
  * on errors (e.g. convert them into domain-specific errors) or `unwrap` at the
44
44
  * call site.
45
45
  */
46
- export const readUtf8File = (path) => readFile(path).step(r => pure(r[0] === 'ok' ? ok(utf8ToString(r[1])) : r));
46
+ export const readUtf8File = (path) => readFile(path).step(r => pure(mapOk(utf8ToString)(r)));
47
47
  export const readdir = do_('readdir');
48
48
  export const writeFile = do_('writeFile');
49
49
  /** Writes a string to `path` as UTF-8 bytes. */
@@ -70,22 +70,12 @@ const writeLoop = (path) => {
70
70
  return pure(resultError('invalid buffer size'));
71
71
  }
72
72
  return writeBytes(path, offset, v)
73
- .step((r) => {
74
- if (r[0] === 'error') {
75
- return pure(r);
76
- }
77
- return f(offset + Number(lenV >> 3n), tail);
78
- });
73
+ .step(okStep(() => f(offset + Number(lenV >> 3n), tail)));
79
74
  });
80
75
  return f;
81
76
  };
82
77
  export const writeFromStream = (path, e) => createExclusive(path)
83
- .step(([r, v]) => {
84
- if (r === 'error') {
85
- return pure(resultError(v));
86
- }
87
- return writeLoop(path)(0, e);
88
- });
78
+ .step(okStep(() => writeLoop(path)(0, e)));
89
79
  export const stat = do_('stat');
90
80
  export const createServer = do_('createServer');
91
81
  export const listen = do_('listen');
@@ -146,6 +146,8 @@ const readStdinByte = async () => {
146
146
  }
147
147
  }
148
148
  };
149
+ const randomMax = Number(1n << 32n);
150
+ const { randomInt } = crypto;
149
151
  const runNodeEffect = asyncRun({
150
152
  ...memoryOperationMap(),
151
153
  all: async (...effects) => await Promise.all(effects.map(runNodeEffect)),
@@ -191,7 +193,7 @@ const runNodeEffect = asyncRun({
191
193
  await fh.close();
192
194
  }
193
195
  }),
194
- randomInt: async () => crypto.randomInt(2 ** 32),
196
+ randomInt: async () => randomInt(randomMax),
195
197
  access: path => asyncTryCatch(() => access(path)),
196
198
  createExclusive: path => asyncTryCatch(async () => {
197
199
  const fh = await open(path, 'wx');
@@ -61,4 +61,7 @@ export declare const proof: {
61
61
  randomInt: {
62
62
  increments: () => void;
63
63
  };
64
+ writeFromStream: {
65
+ createExclusiveFails: () => void;
66
+ };
64
67
  };
@@ -1,8 +1,9 @@
1
1
  import { empty, isVec, uint, vec8 } from "../../types/bit_vec/module.f.js";
2
2
  import { utf8, utf8ToString } from "../../text/module.f.js";
3
3
  import { decode, pure } from "../module.f.js";
4
- import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt } from "./module.f.js";
4
+ import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream } from "./module.f.js";
5
5
  import { create as memCreate, read as memRead, write as memWrite } from "../memory/module.f.js";
6
+ import { empty as listEmpty } from "../list/module.f.js";
6
7
  import { emptyState, virtual } from "./virtual/module.f.js";
7
8
  export const proof = {
8
9
  map: () => {
@@ -553,4 +554,21 @@ export const proof = {
553
554
  }
554
555
  },
555
556
  },
557
+ writeFromStream: {
558
+ createExclusiveFails: () => {
559
+ // The destination already exists, so `createExclusive` fails (EEXIST) and
560
+ // the error propagates without ever touching `writeBytes`.
561
+ const [state, [t, result]] = virtual({
562
+ ...emptyState,
563
+ root: { hello: [vec8(0x2an)] },
564
+ })(writeFromStream('hello', listEmpty()));
565
+ if (t !== 'error') {
566
+ throw result;
567
+ }
568
+ const file = state.root.hello;
569
+ if (!Array.isArray(file) || uint(file[0]) !== 0x2an) {
570
+ throw file;
571
+ }
572
+ },
573
+ },
556
574
  };
@@ -4,7 +4,7 @@
4
4
  * @module
5
5
  */
6
6
  import { todo } from "../../../asserts/module.f.js";
7
- import { join, parse } from "../../../path/module.f.js";
7
+ import { isProperPrefix, join, parse } from "../../../path/module.f.js";
8
8
  import { utf8ToString } from "../../../text/module.f.js";
9
9
  import { empty, length, maxLengthBytes, msb, vec } from "../../../types/bit_vec/module.f.js";
10
10
  import { error, ok } from "../../../types/result/module.f.js";
@@ -218,7 +218,6 @@ const insertEntityAt = (dir, path, entity) => {
218
218
  }
219
219
  return [{ ...dir, [first]: newSub }, result];
220
220
  };
221
- const isProperPrefix = (prefix, path) => prefix.length < path.length && prefix.every((seg, i) => seg === path[i]);
222
221
  const rename = (src, dst) => (state) => {
223
222
  const srcParsed = parse(src);
224
223
  const dstParsed = parse(dst);
@@ -2,6 +2,7 @@ export declare const proof: {
2
2
  lazy: {
3
3
  value: () => void;
4
4
  deferred: () => void;
5
+ step: () => void;
5
6
  };
6
7
  foldStep: {
7
8
  empty: () => void;
@@ -12,6 +13,10 @@ export declare const proof: {
12
13
  empty: () => void;
13
14
  runs: () => void;
14
15
  };
16
+ okStep: {
17
+ ok: () => void;
18
+ error: () => void;
19
+ };
15
20
  decode: () => void;
16
21
  match: {
17
22
  done: () => void;
@@ -1,4 +1,5 @@
1
- import { decode, do_, foldStep, forEachStep, lazy, match, pure } from "./module.f.js";
1
+ import { decode, do_, foldStep, forEachStep, lazy, match, okStep, pure } from "./module.f.js";
2
+ import { error, ok } from "../types/result/module.f.js";
2
3
  const assertPure = (e, expected) => {
3
4
  const d = decode(e);
4
5
  if (!d.done) {
@@ -26,6 +27,10 @@ export const proof = {
26
27
  throw 'decode must force the thunk';
27
28
  }
28
29
  },
30
+ step: () => {
31
+ const e = lazy(() => 5).step(v => pure(v * 2));
32
+ assertPure(e, 10);
33
+ },
29
34
  },
30
35
  foldStep: {
31
36
  empty: () => {
@@ -51,6 +56,22 @@ export const proof = {
51
56
  assertPure(e, undefined);
52
57
  },
53
58
  },
59
+ okStep: {
60
+ ok: () => {
61
+ const e = pure(ok(5)).step(okStep((v) => pure(ok(v * 2))));
62
+ const d = decode(e);
63
+ if (!d.done || d.result[0] !== 'ok' || d.result[1] !== 10) {
64
+ throw e.value;
65
+ }
66
+ },
67
+ error: () => {
68
+ const e = pure(error('oops')).step(okStep(v => pure(ok(v * 2))));
69
+ const d = decode(e);
70
+ if (!d.done || d.result[0] !== 'error' || d.result[1] !== 'oops') {
71
+ throw e.value;
72
+ }
73
+ },
74
+ },
54
75
  decode: () => {
55
76
  const d = decode(do_('add')(2, 3));
56
77
  if (d.done) {
@@ -2,6 +2,7 @@ export declare const proof: {
2
2
  help: () => void;
3
3
  compileRequiresArgs: () => void;
4
4
  runModule: () => void;
5
+ mcp: () => void;
5
6
  throw: {
6
7
  runImportError: () => void;
7
8
  };
package/fs/fjs/proof.f.js CHANGED
@@ -22,6 +22,12 @@ export const proof = {
22
22
  // `run` strips the command and file name, so `main` sees two arguments
23
23
  assertEq(code, 2);
24
24
  },
25
+ mcp: () => {
26
+ // stdin is empty in the virtual environment, so the server sees EOF
27
+ // immediately and shuts down cleanly, exercising the `mcp` handler.
28
+ const [, code] = run({})(['mcp']);
29
+ assertEq(code, 0);
30
+ },
25
31
  throw: {
26
32
  runImportError: () => {
27
33
  run({})(['run', 'missing.f.ts']);
@@ -6,5 +6,6 @@ export declare const proof: {
6
6
  throw: {
7
7
  unionConflict: () => void;
8
8
  };
9
+ defHandler: () => void;
9
10
  };
10
11
  export {};
@@ -9,6 +9,7 @@ import { reduce as listReduce, toArray, map } from "../types/list/module.f.js";
9
9
  import { range as asciiRange } from "../text/ascii/module.f.js";
10
10
  import { fn } from "../types/function/module.f.js";
11
11
  import { one } from "../types/range/module.f.js";
12
+ import { assertEq } from "../asserts/module.f.js";
12
13
  const fromCharCode = String.fromCharCode;
13
14
  const unexpectedSymbol = codePoint => [[`unexpected symbol ${codePoint}`], unexpectedSymbol];
14
15
  const def = () => unexpectedSymbol;
@@ -65,5 +66,8 @@ export const proof = {
65
66
  b(undefined);
66
67
  union(a)(b);
67
68
  }
68
- }
69
+ },
70
+ // `def` is the range-map's default handler; the public API never calls it directly
71
+ // (`init` covers every code point), so exercise it here where it's in scope.
72
+ defHandler: () => assertEq(def(undefined), unexpectedSymbol)
69
73
  };
@@ -92,7 +92,7 @@ export type JsTokenWithMetadata = {
92
92
  readonly token: JsToken;
93
93
  readonly metadata: TokenMetadata;
94
94
  };
95
- type ErrorMessage = '" are missing' | 'unescaped character' | 'invalid hex value' | 'unexpected character' | 'invalid number' | 'invalid token' | '*\/ expected' | 'unterminated string literal' | 'eof';
95
+ type ErrorMessage = '" are missing' | 'unescaped character' | 'invalid hex value' | 'unexpected character' | 'invalid number' | 'invalid token' | '*\/ expected' | 'unterminated string literal' | 'unescaped control character in string' | 'eof';
96
96
  export declare const isKeywordToken: (token: JsToken) => boolean;
97
97
  export declare const tokenize: (input: List<number>) => (path: string) => List<JsTokenWithMetadata>;
98
98
  export declare const proof: {
@@ -372,10 +372,16 @@ const invalidNumberStateOp = create(() => () => [empty, { kind: 'invalidNumber'
372
372
  return [{ first: { kind: 'error', message: 'invalid number' }, tail: next[0] }, next[1]];
373
373
  })
374
374
  ]);
375
+ const rangeSetStringControl = [
376
+ [0x00, 0x09],
377
+ [0x0b, 0x0c],
378
+ [0x0e, 0x1f],
379
+ ];
375
380
  const parseStringStateOp = create((state) => input => [empty, { kind: 'string', value: appendChar(state.value)(input) }])([
376
381
  rangeFunc(one(quotationMark))(state => () => [[{ kind: 'string', value: state.value }], { kind: 'initial' }]),
377
382
  rangeFunc(one(reverseSolidus))(state => () => [empty, { kind: 'escapeChar', value: state.value }]),
378
- rangeSetFunc(rangeSetNewLine)(() => () => [[{ kind: 'error', message: 'unterminated string literal' }], { kind: 'nl' }])
383
+ rangeSetFunc(rangeSetNewLine)(() => () => [[{ kind: 'error', message: 'unterminated string literal' }], { kind: 'nl' }]),
384
+ rangeSetFunc(rangeSetStringControl)(state => () => [[{ kind: 'error', message: 'unescaped control character in string' }], { kind: 'string', value: state.value }])
379
385
  ]);
380
386
  const parseEscapeDefault = state => input => {
381
387
  const next = tokenizeOp(input, { kind: 'string', value: state.value });
@@ -141,6 +141,65 @@ export const proof = {
141
141
  throw result;
142
142
  }
143
143
  },
144
+ () => {
145
+ // literal TAB inside a string is rejected; the escaped form is not
146
+ const result = stringify(tokenizeString('"\t"'));
147
+ if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":""},{"kind":"eof"}]') {
148
+ throw result;
149
+ }
150
+ },
151
+ () => {
152
+ const result = stringify(tokenizeString('"\\t"'));
153
+ if (result !== '[{"kind":"string","value":"\\t"},{"kind":"eof"}]') {
154
+ throw result;
155
+ }
156
+ },
157
+ () => {
158
+ // literal VT (no JSON short escape; only \u000b is valid)
159
+ const result = stringify(tokenizeString('"\v"'));
160
+ if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":""},{"kind":"eof"}]') {
161
+ throw result;
162
+ }
163
+ },
164
+ () => {
165
+ const result = stringify(tokenizeString('"\\u000b"'));
166
+ if (result !== '[{"kind":"string","value":"\\u000b"},{"kind":"eof"}]') {
167
+ throw result;
168
+ }
169
+ },
170
+ () => {
171
+ // literal FF is rejected; the escaped form is not
172
+ const result = stringify(tokenizeString('"\f"'));
173
+ if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":""},{"kind":"eof"}]') {
174
+ throw result;
175
+ }
176
+ },
177
+ () => {
178
+ const result = stringify(tokenizeString('"\\f"'));
179
+ if (result !== '[{"kind":"string","value":"\\f"},{"kind":"eof"}]') {
180
+ throw result;
181
+ }
182
+ },
183
+ () => {
184
+ // literal NUL (no JSON short escape; only \u0000 is valid)
185
+ const result = stringify(tokenizeString('"\0"'));
186
+ if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":""},{"kind":"eof"}]') {
187
+ throw result;
188
+ }
189
+ },
190
+ () => {
191
+ const result = stringify(tokenizeString('"\\u0000"'));
192
+ if (result !== '[{"kind":"string","value":"\\u0000"},{"kind":"eof"}]') {
193
+ throw result;
194
+ }
195
+ },
196
+ () => {
197
+ // control character surrounded by ordinary text still recovers the rest of the string
198
+ const result = stringify(tokenizeString('"a\tb"'));
199
+ if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":"ab"},{"kind":"eof"}]') {
200
+ throw result;
201
+ }
202
+ },
144
203
  () => {
145
204
  const result = stringify(tokenizeString('"\\u1234"'));
146
205
  if (result !== '[{"kind":"string","value":"ሴ"},{"kind":"eof"}]') {
@@ -155,6 +155,24 @@ export const proof = {
155
155
  throw result;
156
156
  }
157
157
  },
158
+ // A literal control character inside a string is not valid JSON (RFC
159
+ // 8259 §7) even though the shared tokenizer would otherwise accept it.
160
+ () => {
161
+ const tokenList = tokenizeString('"\t"');
162
+ const obj = parse(tokenList);
163
+ const result = stringify(obj);
164
+ if (result !== '["error","unexpected token"]') {
165
+ throw result;
166
+ }
167
+ },
168
+ () => {
169
+ const tokenList = tokenizeString('{"a":"\t"}');
170
+ const obj = parse(tokenList);
171
+ const result = stringify(obj);
172
+ if (result !== '["error","unexpected token"]') {
173
+ throw result;
174
+ }
175
+ },
158
176
  () => {
159
177
  const tokenList = tokenizeString('[,]');
160
178
  const obj = parse(tokenList);
@@ -38,3 +38,9 @@ export declare const join: (...list: readonly string[]) => string;
38
38
  * E.g. `relativize('/repo', '/repo/fs/a.ts')` → `'./fs/a.ts'`.
39
39
  */
40
40
  export declare const relativize: (base: string, path: string) => string;
41
+ /**
42
+ * Returns `true` when `prefix` is a strict ancestor of `path` in segment space:
43
+ * every segment of `prefix` matches the corresponding segment of `path`, and
44
+ * `path` has at least one additional segment.
45
+ */
46
+ export declare const isProperPrefix: (prefix: readonly string[], path: readonly string[]) => boolean;
@@ -63,3 +63,9 @@ export const join = (...list) => list.join('/');
63
63
  * E.g. `relativize('/repo', '/repo/fs/a.ts')` → `'./fs/a.ts'`.
64
64
  */
65
65
  export const relativize = (base, path) => base !== '' && path.startsWith(base) ? `.${path.slice(base.length)}` : path;
66
+ /**
67
+ * Returns `true` when `prefix` is a strict ancestor of `path` in segment space:
68
+ * every segment of `prefix` matches the corresponding segment of `path`, and
69
+ * `path` has at least one additional segment.
70
+ */
71
+ export const isProperPrefix = (prefix, path) => prefix.length < path.length && prefix.every((seg, i) => seg === path[i]);
@@ -4,4 +4,5 @@ export declare const proof: {
4
4
  joinTest: (() => void)[];
5
5
  relativizeTest: (() => void)[];
6
6
  toPosixTest: (() => void)[];
7
+ isProperPrefixTest: (() => void)[];
7
8
  };
@@ -1,4 +1,4 @@
1
- import { concat, join, normalize, relativize, toPosix } from "./module.f.js";
1
+ import { concat, isProperPrefix, join, normalize, relativize, toPosix } from "./module.f.js";
2
2
  const normalizeTest = [
3
3
  () => {
4
4
  const norm = normalize("dir/file.json");
@@ -129,4 +129,42 @@ const toPosixTest = [
129
129
  }
130
130
  },
131
131
  ];
132
- export const proof = { normalizeTest, concatTest, joinTest, relativizeTest, toPosixTest };
132
+ const isProperPrefixTest = [
133
+ () => {
134
+ const r = isProperPrefix(['a', 'b'], ['a', 'b', 'c']);
135
+ if (r !== true) {
136
+ throw r;
137
+ }
138
+ },
139
+ () => {
140
+ const r = isProperPrefix(['a', 'b'], ['a', 'b']);
141
+ if (r !== false) {
142
+ throw r;
143
+ }
144
+ },
145
+ () => {
146
+ const r = isProperPrefix(['a', 'x'], ['a', 'b', 'c']);
147
+ if (r !== false) {
148
+ throw r;
149
+ }
150
+ },
151
+ () => {
152
+ const r = isProperPrefix(['a', 'b', 'c'], ['a', 'b']);
153
+ if (r !== false) {
154
+ throw r;
155
+ }
156
+ },
157
+ () => {
158
+ const r = isProperPrefix([], ['a']);
159
+ if (r !== true) {
160
+ throw r;
161
+ }
162
+ },
163
+ () => {
164
+ const r = isProperPrefix([], []);
165
+ if (r !== false) {
166
+ throw r;
167
+ }
168
+ },
169
+ ];
170
+ export const proof = { normalizeTest, concatTest, joinTest, relativizeTest, toPosixTest, isProperPrefixTest };
@@ -46,6 +46,12 @@ export declare const utf8StateToError: (state: Utf8NonEmptyState) => I32;
46
46
  /**
47
47
  * Decodes a byte into a Unicode code point, using a given UTF-8 state.
48
48
  *
49
+ * Rejects overlong 3-/4-byte encodings (Unicode Table 3-7): a lead `E0` must
50
+ * be followed by a continuation `>= 0xA0`, and a lead `F0` by a continuation
51
+ * `>= 0x90`. It does not itself reject surrogates (`ED A0..BF`) or code
52
+ * points above `U+10FFFF` (`F4 90..BF`); {@link fromVec}'s
53
+ * `isValidCodePoint` pass filters those out of the raw code-point stream.
54
+ *
49
55
  * @param state - The current UTF-8 decoding state.
50
56
  * @param byte - A single byte to decode.
51
57
  * @returns A tuple containing:
@@ -135,6 +135,12 @@ export const utf8StateToError = (state) => {
135
135
  /**
136
136
  * Decodes a byte into a Unicode code point, using a given UTF-8 state.
137
137
  *
138
+ * Rejects overlong 3-/4-byte encodings (Unicode Table 3-7): a lead `E0` must
139
+ * be followed by a continuation `>= 0xA0`, and a lead `F0` by a continuation
140
+ * `>= 0x90`. It does not itself reject surrogates (`ED A0..BF`) or code
141
+ * points above `U+10FFFF` (`F4 90..BF`); {@link fromVec}'s
142
+ * `isValidCodePoint` pass filters those out of the raw code-point stream.
143
+ *
138
144
  * @param state - The current UTF-8 decoding state.
139
145
  * @param byte - A single byte to decode.
140
146
  * @returns A tuple containing:
@@ -159,8 +165,15 @@ export const utf8ByteToCodePointOp = (byte, state) => {
159
165
  if (s0 < lead3Tag) {
160
166
  return [[((s0 & lead2Mask) << 6) + contPayload(byte)], null];
161
167
  }
162
- if (s0 < 0b1111_1000)
163
- return [[], [s0, byte]];
168
+ if (s0 < 0b1111_1000) {
169
+ // Reject overlong 3-/4-byte encodings: after lead `E0` the
170
+ // first continuation must be >= 0xA0, after lead `F0` it
171
+ // must be >= 0x90 (Unicode Table 3-7).
172
+ const overlong = s0 === lead3Tag && byte < 0b1010_0000 ||
173
+ s0 === lead4Tag && byte < 0b1001_0000;
174
+ if (!overlong)
175
+ return [[], [s0, byte]];
176
+ }
164
177
  break;
165
178
  }
166
179
  case 2: {
@@ -65,6 +65,45 @@ export const proof = {
65
65
  if (result !== '[-2147448800,-2147432416]') {
66
66
  throw result;
67
67
  }
68
+ },
69
+ // Overlong 3-byte encodings (E0 80..9F ..) are rejected, not decoded.
70
+ () => {
71
+ const result = stringify(toArray(toCodePointList([224, 128, 128])));
72
+ if (result !== '[-2147483424,-2147483520,-2147483520]') {
73
+ throw result;
74
+ }
75
+ },
76
+ () => {
77
+ const result = stringify(toArray(toCodePointList([224, 159, 191])));
78
+ if (result !== '[-2147483424,-2147483489,-2147483457]') {
79
+ throw result;
80
+ }
81
+ },
82
+ // Overlong 4-byte encodings (F0 80..8F .. ..) are rejected, not decoded.
83
+ () => {
84
+ const result = stringify(toArray(toCodePointList([240, 128, 128, 128])));
85
+ if (result !== '[-2147483408,-2147483520,-2147483520,-2147483520]') {
86
+ throw result;
87
+ }
88
+ },
89
+ () => {
90
+ const result = stringify(toArray(toCodePointList([240, 143, 191, 191])));
91
+ if (result !== '[-2147483408,-2147483505,-2147483457,-2147483457]') {
92
+ throw result;
93
+ }
94
+ },
95
+ // Valid boundary cases still decode: E0 A0 80 -> U+0800, F0 90 80 80 -> U+10000.
96
+ () => {
97
+ const result = stringify(toArray(toCodePointList([224, 160, 128])));
98
+ if (result !== '[2048]') {
99
+ throw result;
100
+ }
101
+ },
102
+ () => {
103
+ const result = stringify(toArray(toCodePointList([240, 144, 128, 128])));
104
+ if (result !== '[65536]') {
105
+ throw result;
106
+ }
68
107
  }
69
108
  ],
70
109
  fromCodePointList: [
@@ -229,5 +268,19 @@ export const proof = {
229
268
  throw 'expected empty string';
230
269
  }
231
270
  },
271
+ // Overlong 3-byte encoding (E0 80 80, would decode to U+0000) → null
272
+ () => {
273
+ const v = u8ListToVec(msb)([0xe0, 0x80, 0x80]);
274
+ if (fromVec(v) !== null) {
275
+ throw 'expected null for overlong 3-byte encoding';
276
+ }
277
+ },
278
+ // Overlong 4-byte encoding (F0 80 80 80, would decode to U+0000) → null
279
+ () => {
280
+ const v = u8ListToVec(msb)([0xf0, 0x80, 0x80, 0x80]);
281
+ if (fromVec(v) !== null) {
282
+ throw 'expected null for overlong 4-byte encoding';
283
+ }
284
+ },
232
285
  ]
233
286
  };
@@ -55,3 +55,7 @@ export declare const unwrap: <T, E>([kind, v]: Result<T, E>) => T;
55
55
  * Swaps the `ok` and `error` cases of a result.
56
56
  */
57
57
  export declare const invert: <T, E>([k, v]: Result<T, E>) => Result<E, T>;
58
+ /**
59
+ * Maps the `ok` case of a result, passing an `error` through unchanged.
60
+ */
61
+ export declare const mapOk: <T, R>(f: (value: T) => R) => <E>(r: Result<T, E>) => Result<R, E>;
@@ -48,3 +48,7 @@ export const unwrap = ([kind, v]) => {
48
48
  * Swaps the `ok` and `error` cases of a result.
49
49
  */
50
50
  export const invert = ([k, v]) => k === 'ok' ? error(v) : ok(v);
51
+ /**
52
+ * Maps the `ok` case of a result, passing an `error` through unchanged.
53
+ */
54
+ export const mapOk = (f) => (r) => r[0] === 'ok' ? ok(f(r[1])) : r;
@@ -2,4 +2,5 @@ export declare const proof: {
2
2
  example: () => void;
3
3
  invertTest: () => void;
4
4
  unwrapError: () => void;
5
+ mapOkTest: () => void;
5
6
  };