functionalscript 0.45.0 → 0.46.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.
- package/README.md +5 -3
- package/fjs/asn.1/module.f.mjs +8 -1
- package/fjs/asn.1/proof.f.d.mts +10 -0
- package/fjs/asn.1/proof.f.mjs +16 -0
- package/fjs/basen/base128/module.f.mjs +22 -5
- package/fjs/bnf/data/module.f.d.mts +32 -6
- package/fjs/bnf/data/module.f.mjs +112 -7
- package/fjs/bnf/data/proof.f.d.mts +2 -0
- package/fjs/bnf/data/proof.f.mjs +74 -3
- package/fjs/bnf/data/types.d.ts +20 -2
- package/fjs/bnf/descent/module.f.d.mts +28 -14
- package/fjs/bnf/descent/module.f.mjs +122 -77
- package/fjs/bnf/descent/proof.f.d.mts +2 -0
- package/fjs/bnf/descent/proof.f.mjs +117 -31
- package/fjs/bnf/descent/types.d.ts +12 -14
- package/fjs/bnf/ll1/module.f.d.mts +64 -22
- package/fjs/bnf/ll1/module.f.mjs +214 -154
- package/fjs/bnf/ll1/proof.f.d.mts +15 -2
- package/fjs/bnf/ll1/proof.f.mjs +323 -149
- package/fjs/bnf/ll1/types.d.ts +16 -24
- package/fjs/bnf/matcher/module.f.d.mts +66 -0
- package/fjs/bnf/matcher/module.f.mjs +81 -0
- package/fjs/bnf/matcher/proof.f.d.mts +10 -0
- package/fjs/bnf/matcher/proof.f.mjs +79 -0
- package/fjs/bnf/matcher/types.d.ts +54 -0
- package/fjs/bnf/testlib.f.d.mts +31 -0
- package/fjs/bnf/testlib.f.mjs +80 -0
- package/fjs/cas/cli/module.f.d.mts +1 -1
- package/fjs/cas/cli/module.f.mjs +14 -20
- package/fjs/cas/cli/proof.f.d.mts +1 -3
- package/fjs/cas/cli/proof.f.mjs +44 -33
- package/fjs/cas/evo/module.f.d.mts +64 -18
- package/fjs/cas/evo/module.f.mjs +148 -70
- package/fjs/cas/evo/proof.f.d.mts +10 -1
- package/fjs/cas/evo/proof.f.mjs +305 -223
- package/fjs/cas/evo/types.d.ts +45 -9
- package/fjs/cas/module.f.d.mts +18 -32
- package/fjs/cas/module.f.mjs +129 -128
- package/fjs/cas/proof.f.d.mts +11 -8
- package/fjs/cas/proof.f.mjs +259 -147
- package/fjs/cas/types.d.ts +24 -10
- package/fjs/ci/config/module.f.d.mts +2 -2
- package/fjs/ci/config/module.f.mjs +2 -2
- package/fjs/ci/module.f.d.mts +5 -5
- package/fjs/ci/module.f.mjs +8 -7
- package/fjs/ci/nix/module.f.d.mts +7 -5
- package/fjs/ci/nix/module.f.mjs +13 -12
- package/fjs/ci/nix/proof.f.mjs +2 -2
- package/fjs/ci/proof.f.mjs +9 -5
- package/fjs/cli/module.f.d.mts +4 -6
- package/fjs/cli/module.f.mjs +4 -8
- package/fjs/cli/proof.f.mjs +17 -16
- package/fjs/cli/types.d.ts +2 -3
- package/fjs/common/monoid/types.d.ts +1 -1
- package/fjs/crypto/hmac/module.f.mjs +2 -2
- package/fjs/crypto/sha2/module.f.mjs +3 -1
- package/fjs/crypto/sha2/proof.f.d.mts +1 -0
- package/fjs/crypto/sha2/proof.f.mjs +24 -0
- package/fjs/crypto/sha2/types.d.ts +11 -0
- package/fjs/crypto/sign/module.f.mjs +2 -2
- package/fjs/dev/module.f.d.mts +13 -4
- package/fjs/dev/module.f.mjs +56 -27
- package/fjs/dev/update/module.f.d.mts +9 -4
- package/fjs/dev/update/module.f.mjs +14 -10
- package/fjs/dev/update/proof.f.d.mts +1 -3
- package/fjs/dev/update/proof.f.mjs +10 -5
- package/fjs/djs/module.f.d.mts +13 -5
- package/fjs/djs/module.f.mjs +31 -16
- package/fjs/djs/parser/module.f.d.mts +13 -3
- package/fjs/djs/parser/module.f.mjs +117 -16
- package/fjs/djs/parser/proof.f.d.mts +5 -1
- package/fjs/djs/parser/proof.f.mjs +274 -12
- package/fjs/djs/parser/types.d.ts +7 -1
- package/fjs/djs/proof.f.d.mts +18 -2
- package/fjs/djs/proof.f.mjs +187 -12
- package/fjs/djs/serializer/module.f.d.mts +25 -8
- package/fjs/djs/serializer/module.f.mjs +61 -15
- package/fjs/djs/serializer/proof.f.d.mts +5 -0
- package/fjs/djs/serializer/proof.f.mjs +23 -1
- package/fjs/djs/tokenizer/module.f.d.mts +17 -5
- package/fjs/djs/tokenizer/module.f.mjs +99 -60
- package/fjs/djs/tokenizer/proof.f.d.mts +1 -1
- package/fjs/djs/tokenizer/proof.f.mjs +74 -67
- package/fjs/djs/transpiler/module.f.d.mts +12 -7
- package/fjs/djs/transpiler/module.f.mjs +82 -56
- package/fjs/djs/transpiler/types.d.ts +7 -3
- package/fjs/djs/types.d.ts +7 -1
- package/fjs/effects/list/module.f.d.mts +14 -11
- package/fjs/effects/list/module.f.mjs +13 -11
- package/fjs/effects/list/types.d.ts +27 -7
- package/fjs/effects/memory/module.f.d.mts +2 -1
- package/fjs/effects/memory/module.f.mjs +3 -4
- package/fjs/effects/memory/proof.f.mjs +13 -7
- package/fjs/effects/memory/types.d.ts +4 -3
- package/fjs/effects/mock/module.f.d.mts +20 -4
- package/fjs/effects/mock/module.f.mjs +37 -5
- package/fjs/effects/mock/types.d.ts +10 -1
- package/fjs/effects/module.d.mts +4 -2
- package/fjs/effects/module.f.d.mts +466 -280
- package/fjs/effects/module.f.mjs +537 -299
- package/fjs/effects/module.mjs +2 -1
- package/fjs/effects/node/memory/module.d.mts +4 -2
- package/fjs/effects/node/memory/module.mjs +6 -3
- package/fjs/effects/node/memory/proof.mjs +10 -4
- package/fjs/effects/node/module.d.mts +4 -3
- package/fjs/effects/node/module.f.d.mts +168 -33
- package/fjs/effects/node/module.f.mjs +256 -52
- package/fjs/effects/node/module.mjs +57 -34
- package/fjs/effects/node/proof.f.d.mts +28 -2
- package/fjs/effects/node/proof.f.mjs +161 -42
- package/fjs/effects/node/types.d.ts +106 -18
- package/fjs/effects/node/virtual/module.f.d.mts +18 -4
- package/fjs/effects/node/virtual/module.f.mjs +110 -68
- package/fjs/effects/node/virtual/proof.f.d.mts +28 -2
- package/fjs/effects/node/virtual/proof.f.mjs +190 -9
- package/fjs/effects/proof.f.d.mts +69 -37
- package/fjs/effects/proof.f.mjs +410 -130
- package/fjs/effects/types.d.ts +161 -33
- package/fjs/emergent_testing/module.f.d.mts +19 -12
- package/fjs/emergent_testing/module.f.mjs +93 -33
- package/fjs/emergent_testing/proof.f.d.mts +21 -7
- package/fjs/emergent_testing/proof.f.mjs +166 -32
- package/fjs/emergent_testing/types.d.ts +22 -4
- package/fjs/fsm/module.f.d.mts +14 -4
- package/fjs/fsm/module.f.mjs +54 -37
- package/fjs/fsm/proof.f.d.mts +2 -0
- package/fjs/fsm/proof.f.mjs +83 -114
- package/fjs/js/keywords/module.f.d.mts +52 -0
- package/fjs/js/keywords/module.f.mjs +72 -0
- package/fjs/js/keywords/proof.f.d.mts +3 -0
- package/fjs/js/keywords/proof.f.mjs +13 -0
- package/fjs/js/tokenizer/module.f.d.mts +26 -6
- package/fjs/js/tokenizer/module.f.mjs +145 -159
- package/fjs/js/tokenizer/proof.f.d.mts +1 -0
- package/fjs/js/tokenizer/proof.f.mjs +54 -24
- package/fjs/js/tokenizer/types.d.ts +33 -24
- package/fjs/mcp/cas/module.f.d.mts +1 -6
- package/fjs/mcp/cas/module.f.mjs +55 -51
- package/fjs/mcp/cas/proof.f.d.mts +15 -0
- package/fjs/mcp/cas/proof.f.mjs +174 -0
- package/fjs/mcp/evo/module.f.d.mts +19 -10
- package/fjs/mcp/evo/module.f.mjs +48 -27
- package/fjs/mcp/evo/proof.f.d.mts +6 -1
- package/fjs/mcp/evo/proof.f.mjs +115 -31
- package/fjs/mcp/module.f.d.mts +4 -4
- package/fjs/mcp/module.f.mjs +6 -6
- package/fjs/mcp/proof.f.d.mts +5 -3
- package/fjs/mcp/proof.f.mjs +112 -46
- package/fjs/media/html/module.f.mjs +1 -1
- package/fjs/media/json/extended/module.f.d.mts +82 -0
- package/fjs/media/json/extended/module.f.mjs +153 -0
- package/fjs/media/json/extended/proof.f.d.mts +42 -0
- package/fjs/media/json/extended/proof.f.mjs +127 -0
- package/fjs/media/json/extended/types.d.ts +23 -0
- package/fjs/media/json/module.f.d.mts +8 -2
- package/fjs/media/json/module.f.mjs +43 -41
- package/fjs/media/json/number/module.f.d.mts +59 -0
- package/fjs/media/json/number/module.f.mjs +136 -0
- package/fjs/media/json/number/proof.f.d.mts +24 -0
- package/fjs/media/json/number/proof.f.mjs +86 -0
- package/fjs/media/json/number/types.d.ts +28 -0
- package/fjs/media/json/parser/module.f.d.mts +25 -13
- package/fjs/media/json/parser/module.f.mjs +114 -70
- package/fjs/media/json/parser/proof.f.d.mts +5 -0
- package/fjs/media/json/parser/proof.f.mjs +31 -1
- package/fjs/media/json/parser/types.d.ts +33 -14
- package/fjs/media/json/rtti/module.f.d.mts +1 -1
- package/fjs/media/json/rtti/module.f.mjs +1 -1
- package/fjs/media/json/rtti/proof.f.mjs +9 -9
- package/fjs/media/json/schema/module.f.mjs +3 -13
- package/fjs/media/json/schema/proof.f.d.mts +0 -1
- package/fjs/media/json/schema/proof.f.mjs +1 -2
- package/fjs/media/json/serializer/module.f.d.mts +32 -1
- package/fjs/media/json/serializer/module.f.mjs +64 -2
- package/fjs/media/json/tokenizer/module.f.mjs +7 -3
- package/fjs/media/json/tokenizer/proof.f.d.mts +6 -0
- package/fjs/media/json/tokenizer/proof.f.mjs +62 -21
- package/fjs/media/json/types.d.ts +36 -10
- package/fjs/media/lock/module.f.d.mts +100 -0
- package/fjs/media/lock/module.f.mjs +125 -0
- package/fjs/media/lock/proof.f.d.mts +33 -0
- package/fjs/media/lock/proof.f.mjs +196 -0
- package/fjs/media/lock/types.d.ts +15 -0
- package/fjs/media/module.f.d.mts +6 -5
- package/fjs/media/module.f.mjs +8 -7
- package/fjs/media/note/module.f.d.mts +121 -0
- package/fjs/media/note/module.f.mjs +131 -0
- package/fjs/media/note/proof.f.d.mts +29 -0
- package/fjs/media/note/proof.f.mjs +150 -0
- package/fjs/media/note/types.d.ts +10 -0
- package/fjs/media/proof.f.d.mts +4 -1
- package/fjs/media/proof.f.mjs +40 -21
- package/fjs/media/revision/module.f.d.mts +78 -7
- package/fjs/media/revision/module.f.mjs +119 -12
- package/fjs/media/revision/proof.f.d.mts +10 -0
- package/fjs/media/revision/proof.f.mjs +88 -0
- package/fjs/media/revision/types.d.ts +34 -5
- package/fjs/media/type/module.f.d.mts +33 -15
- package/fjs/media/type/module.f.mjs +35 -29
- package/fjs/media/type/proof.f.d.mts +2 -1
- package/fjs/media/type/proof.f.mjs +30 -10
- package/fjs/module.f.mjs +29 -8
- package/fjs/nanvm/proof.f.mjs +3 -3
- package/fjs/nanvm/rust/module.f.mjs +1 -1
- package/fjs/nanvm/update/module.f.d.mts +4 -4
- package/fjs/nanvm/update/module.f.mjs +8 -9
- package/fjs/nanvm/update/proof.f.mjs +4 -3
- package/fjs/proof.f.d.mts +3 -3
- package/fjs/proof.f.mjs +36 -10
- package/fjs/protocol/json_rpc/module.f.d.mts +2 -2
- package/fjs/protocol/json_rpc/module.f.mjs +3 -3
- package/fjs/protocol/json_rpc/proof.f.mjs +4 -4
- package/fjs/protocol/mcp/module.f.d.mts +10 -13
- package/fjs/protocol/mcp/module.f.mjs +74 -58
- package/fjs/protocol/mcp/proof.f.d.mts +13 -4
- package/fjs/protocol/mcp/proof.f.mjs +193 -61
- package/fjs/protocol/mcp/stdio/module.f.d.mts +15 -7
- package/fjs/protocol/mcp/stdio/module.f.mjs +38 -25
- package/fjs/protocol/mcp/stdio/proof.f.d.mts +4 -2
- package/fjs/protocol/mcp/stdio/proof.f.mjs +45 -12
- package/fjs/protocol/mcp/stdio/types.d.ts +1 -1
- package/fjs/protocol/mcp/types.d.ts +17 -6
- package/fjs/sul/id/module.f.d.mts +0 -1
- package/fjs/sul/id/module.f.mjs +2 -3
- package/fjs/sul/level/hash/module.f.mjs +2 -1
- package/fjs/sul/level/hash/proof.f.mjs +2 -2
- package/fjs/sul/module.f.mjs +18 -13
- package/fjs/text/code_point/module.f.d.mts +8 -0
- package/fjs/text/code_point/module.f.mjs +8 -1
- package/fjs/text/code_point/proof.f.d.mts +1 -0
- package/fjs/text/code_point/proof.f.mjs +11 -0
- package/fjs/text/sgr/module.f.d.mts +4 -6
- package/fjs/text/sgr/module.f.mjs +6 -7
- package/fjs/text/utf16/module.f.mjs +7 -2
- package/fjs/text/utf16/proof.f.mjs +3 -3
- package/fjs/text/utf8/module.f.mjs +4 -2
- package/fjs/types/array/module.f.mjs +14 -2
- package/fjs/types/bit_vec/module.f.d.mts +0 -2
- package/fjs/types/bit_vec/module.f.mjs +46 -32
- package/fjs/types/bit_vec/proof.f.mjs +2 -2
- package/fjs/types/btree/remove/module.f.mjs +1 -1
- package/fjs/types/btree/set/module.f.mjs +9 -11
- package/fjs/types/btree/set/proof.f.mjs +12 -0
- package/fjs/types/byte_set/module.f.d.mts +11 -4
- package/fjs/types/byte_set/module.f.mjs +14 -6
- package/fjs/types/byte_set/proof.f.mjs +7 -7
- package/fjs/types/function/compare/module.f.mjs +10 -3
- package/fjs/types/list/module.f.d.mts +1 -1
- package/fjs/types/list/module.f.mjs +1 -1
- package/fjs/types/nullable/module.f.d.mts +18 -4
- package/fjs/types/nullable/module.f.mjs +21 -4
- package/fjs/types/nullable/proof.f.d.mts +4 -0
- package/fjs/types/nullable/proof.f.mjs +15 -0
- package/fjs/types/object/module.f.d.mts +12 -2
- package/fjs/types/object/module.f.mjs +11 -1
- package/fjs/types/patricia_trie/module.f.mjs +26 -11
- package/fjs/types/result/module.f.d.mts +3 -3
- package/fjs/types/result/module.f.mjs +3 -3
- package/fjs/types/rtti/common/module.f.d.mts +25 -28
- package/fjs/types/rtti/common/module.f.mjs +36 -32
- package/fjs/types/rtti/common/proof.f.mjs +5 -5
- package/fjs/types/rtti/data/module.f.d.mts +14 -0
- package/fjs/types/rtti/data/module.f.mjs +25 -2
- package/fjs/types/rtti/data/proof.f.d.mts +1 -0
- package/fjs/types/rtti/data/proof.f.mjs +25 -2
- package/fjs/types/rtti/parse/module.f.d.mts +24 -14
- package/fjs/types/rtti/parse/module.f.mjs +37 -28
- package/fjs/types/rtti/parse/proof.f.d.mts +3 -2
- package/fjs/types/rtti/parse/proof.f.mjs +33 -14
- package/fjs/types/rtti/proof.f.mjs +3 -1
- package/fjs/types/rtti/ts/module.f.mjs +13 -17
- package/fjs/types/rtti/ts/types.d.ts +14 -1
- package/fjs/types/rtti/validate/module.f.d.mts +81 -19
- package/fjs/types/rtti/validate/module.f.mjs +107 -61
- package/fjs/types/rtti/validate/proof.f.d.mts +13 -10
- package/fjs/types/rtti/validate/proof.f.mjs +186 -197
- package/fjs/types/sorted_set/module.f.d.mts +18 -0
- package/fjs/types/sorted_set/module.f.mjs +22 -0
- package/fjs/types/sorted_set/proof.f.d.mts +1 -0
- package/fjs/types/sorted_set/proof.f.mjs +16 -1
- package/fjs/types/uint8array/module.f.d.mts +1 -1
- package/fjs/types/uint8array/module.f.mjs +1 -1
- package/fjs/website/module.f.d.mts +3 -3
- package/fjs/website/module.f.mjs +4 -7
- package/fjs/website/proof.f.mjs +2 -1
- package/package.json +2 -2
- package/fjs/dev/package_json/module.f.d.mts +0 -39
- package/fjs/dev/package_json/module.f.mjs +0 -40
- package/fjs/dev/package_json/proof.f.d.mts +0 -6
- package/fjs/dev/package_json/proof.f.mjs +0 -32
- package/fjs/effects/eff/module.f.d.mts +0 -20
- package/fjs/effects/eff/module.f.mjs +0 -70
- package/fjs/effects/eff/proof.f.d.mts +0 -15
- package/fjs/effects/eff/proof.f.mjs +0 -69
- package/fjs/effects/eff/types.d.ts +0 -71
- package/fjs/types/rtti/validate/types.d.ts +0 -6
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
* @import { Unknown } from '../json/types.ts'
|
|
18
18
|
* @import { Result } from '../../types/result/types.ts'
|
|
19
19
|
* @import { DialectEntry } from '../types.ts'
|
|
20
|
-
* @import {
|
|
20
|
+
* @import { String as RttiString } from '../../types/rtti/types.ts'
|
|
21
|
+
* @import { LockField, LockFieldSchema, LockMap, LockSchema, Revision, RevisionError } from './types.ts'
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
|
-
import { array, number, option,
|
|
24
|
-
import {
|
|
24
|
+
import { array, number, option, string } from '../../types/rtti/module.f.mjs'
|
|
25
|
+
import { parse as rttiParse } from '../../types/rtti/parse/module.f.mjs'
|
|
25
26
|
import { parse as parseJson } from '../json/module.f.mjs'
|
|
26
27
|
import { cBase32ToVec } from '../../basen/cbase32/module.f.mjs'
|
|
27
28
|
import { error, ok, okThen } from '../../types/result/module.f.mjs'
|
|
@@ -49,8 +50,64 @@ export const mediaType = /** @type {const} */ (`application/${dialect}+json`)
|
|
|
49
50
|
*/
|
|
50
51
|
export const hash = string
|
|
51
52
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* rtti schema for a lock map: an open map whose every value is either a
|
|
55
|
+
* direct hash string or a nested lock map, to any depth.
|
|
56
|
+
*
|
|
57
|
+
* Self-referential through {@link lockValue}, which is a module-level
|
|
58
|
+
* constant rather than a union rebuilt inside the thunk: the rtti data form
|
|
59
|
+
* (`fjs/types/rtti/data`, which `toJsonSchema` routes through) closes
|
|
60
|
+
* reference cycles by *identity*, so a schema handing out a fresh union thunk
|
|
61
|
+
* on every call would present an infinite graph and never terminate.
|
|
62
|
+
*
|
|
63
|
+
* The named `@type` — rather than `@type {const}` — is what a self-referential
|
|
64
|
+
* schema needs twice over: a `const` cannot reference itself in its own
|
|
65
|
+
* initializer at all, and naming the recursive position is also what keeps
|
|
66
|
+
* declaration emit from inlining the structure and giving up at depth (see
|
|
67
|
+
* `fjs/AGENTS.md` §3.2 and `../json/rtti/module.f.mjs`).
|
|
68
|
+
*
|
|
69
|
+
* Like `hash`, this is `string` at the structural level; cbase32 decodability
|
|
70
|
+
* of every direct value, at every depth, is enforced by
|
|
71
|
+
* {@link checkReferences}.
|
|
72
|
+
*
|
|
73
|
+
* @type {LockSchema}
|
|
74
|
+
*/
|
|
75
|
+
export const lock = () => ['record', lockValue]
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One lock-map value: a direct hash, or a nested lock map. Written as the
|
|
79
|
+
* union tuple rather than as `or(string, lock)` so the thunk carries the name
|
|
80
|
+
* `lockValue`: this is where the reference cycle closes, and the data form
|
|
81
|
+
* names a rule after its defining function — an anonymous thunk would publish
|
|
82
|
+
* the recursion as `$defs: { '': … }` in every derived JSON Schema.
|
|
83
|
+
*
|
|
84
|
+
* @type {() => readonly['or', RttiString, LockSchema]}
|
|
85
|
+
*/
|
|
86
|
+
const lockValue = () => ['or', string, lock]
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* rtti schema for a revision's `lock` **field**: the bindings inline as a lock
|
|
90
|
+
* map, or a hash naming a `vnd.fjs.lock` blob (`fjs/media/lock`) that holds
|
|
91
|
+
* one to share — see [Shared lock references](./README.md#shared-lock-references).
|
|
92
|
+
*
|
|
93
|
+
* Structurally identical to {@link lockValue}, and deliberately a separate
|
|
94
|
+
* name: the two positions mean different things. A string *inside* a map is a
|
|
95
|
+
* dependency's content hash; a string in this position is a lock blob's hash,
|
|
96
|
+
* i.e. where the whole map lives. Only the top level is widened, so a nested
|
|
97
|
+
* string keeps meaning exactly what it always did and no position is
|
|
98
|
+
* ambiguous.
|
|
99
|
+
*
|
|
100
|
+
* Widening the field rather than adding a `lockRef` sibling is what keeps this
|
|
101
|
+
* dialect: an older reader validates `lock` as a map and rejects a string
|
|
102
|
+
* outright, whereas an unknown sibling field would validate and be read as
|
|
103
|
+
* "no bindings were recorded" — the fail-open misread the versioning rule
|
|
104
|
+
* exists to prevent. It also makes "inline map *and* reference" unstatable, so
|
|
105
|
+
* the format defines no precedence between them, consistent with its refusal
|
|
106
|
+
* to define overlay or inheritance for nested maps.
|
|
107
|
+
*
|
|
108
|
+
* @type {LockFieldSchema}
|
|
109
|
+
*/
|
|
110
|
+
export const lockField = () => ['or', hash, lock]
|
|
54
111
|
|
|
55
112
|
/**
|
|
56
113
|
* rtti schema for a `revision` BLOB. See the README for the full semantics of
|
|
@@ -64,7 +121,7 @@ export const revisionSchema = /** @type {const} */ ({
|
|
|
64
121
|
snapshot: hash,
|
|
65
122
|
generation: number,
|
|
66
123
|
archived: option(true),
|
|
67
|
-
lock: option(
|
|
124
|
+
lock: option(lockField),
|
|
68
125
|
})
|
|
69
126
|
|
|
70
127
|
/** Serializes a revision canonically, recursively sorting every object's property names.
|
|
@@ -73,19 +130,70 @@ export const revisionSchema = /** @type {const} */ ({
|
|
|
73
130
|
export const encodeText = stringify(sort)
|
|
74
131
|
|
|
75
132
|
/** Structural-only validator: checks the shape, not the hash / generation semantics. */
|
|
76
|
-
const validateShape =
|
|
133
|
+
const validateShape = rttiParse(revisionSchema)
|
|
77
134
|
|
|
78
135
|
/** True when `s` decodes as a cbase32 CAS hash (rejects `https://` and any other non-cbase32 string).
|
|
79
136
|
* @type {(s: string) => boolean}
|
|
80
137
|
*/
|
|
81
138
|
export const isHash = s => cBase32ToVec(s) !== null
|
|
82
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The first reason a structurally valid lock map is not a valid one, or
|
|
142
|
+
* `null` when every direct value at every depth is a cbase32 hash
|
|
143
|
+
* ({@link isHash}). Nested maps are scopes, not references, so only the
|
|
144
|
+
* strings are checked; `scope` names the path walked to reach the offending
|
|
145
|
+
* value, so a failure deep in a nested map still says where it is.
|
|
146
|
+
*
|
|
147
|
+
* @type {(scope: readonly string[]) => (lock: LockMap) => string | null}
|
|
148
|
+
*/
|
|
149
|
+
const lockError = scope => lock => {
|
|
150
|
+
for (const [subject, value] of definedEntries(lock)) {
|
|
151
|
+
const path = [...scope, subject]
|
|
152
|
+
const message = typeof value === 'string'
|
|
153
|
+
? (isHash(value) ? null : `lock value for ${path.join('/')} is not a valid hash: ${value}`)
|
|
154
|
+
: lockError(path)(value)
|
|
155
|
+
if (message !== null) { return message }
|
|
156
|
+
}
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The first reason a structurally valid lock map is not a valid one, or `null`
|
|
162
|
+
* — {@link lockError} rooted at the empty scope, so a reported path is
|
|
163
|
+
* relative to the map itself.
|
|
164
|
+
*
|
|
165
|
+
* Exported because `fjs/media/lock` validates the very same map as a
|
|
166
|
+
* standalone blob: one recursive schema and one semantic check, so a map means
|
|
167
|
+
* the same thing inline and shared, and the two forms cannot drift.
|
|
168
|
+
*
|
|
169
|
+
* @type {(lock: LockMap) => string | null}
|
|
170
|
+
*/
|
|
171
|
+
export const lockMapError = lockError([])
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The first reason a structurally valid `lock` field is not a valid one, or
|
|
175
|
+
* `null`. A map is checked entry by entry ({@link lockMapError}); a string is
|
|
176
|
+
* a reference to a `vnd.fjs.lock` blob and is checked as a cbase32 hash and
|
|
177
|
+
* nothing more — this module is pure format with no store access, so whether
|
|
178
|
+
* the blob exists, and what its bindings mean once fetched, stay a resolver's
|
|
179
|
+
* business exactly as they do for `snapshot`.
|
|
180
|
+
*
|
|
181
|
+
* @type {(value: LockField) => string | null}
|
|
182
|
+
*/
|
|
183
|
+
export const lockFieldError = value =>
|
|
184
|
+
typeof value === 'string'
|
|
185
|
+
? (isHash(value) ? null : `lock reference is not a valid hash: ${value}`)
|
|
186
|
+
: lockMapError(value)
|
|
187
|
+
|
|
83
188
|
/**
|
|
84
189
|
* Checks the semantic refinements the structural schema can't express on an
|
|
85
190
|
* already shape-valid revision: every `parents` entry and the `snapshot` must
|
|
86
|
-
* decode as a cbase32 hash ({@link isHash}),
|
|
191
|
+
* decode as a cbase32 hash ({@link isHash}), the `lock` field must too —
|
|
192
|
+
* every direct value at every depth of an inline map, or the shared-lock
|
|
193
|
+
* reference itself ({@link lockFieldError}) — and `generation` must be a
|
|
87
194
|
* non-negative *safe* integer. `subject` is not checked — it is an identity
|
|
88
|
-
* string, never a snapshot reference, so any string is valid
|
|
195
|
+
* string, never a snapshot reference, so any string is valid, and the same
|
|
196
|
+
* goes for a lock map's keys, which are subjects.
|
|
89
197
|
*
|
|
90
198
|
* `generation` uses `Number.isSafeInteger`, not `Number.isInteger`: a value at
|
|
91
199
|
* or above `2 ** 53` passes `isInteger` but is no longer uniquely
|
|
@@ -113,9 +221,8 @@ export const checkReferences = r => {
|
|
|
113
221
|
if (!isHash(p)) { return error(`parent is not a valid hash: ${p}`) }
|
|
114
222
|
}
|
|
115
223
|
if (!isHash(r.snapshot)) { return error(`snapshot is not a valid hash: ${r.snapshot}`) }
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
224
|
+
const lockMessage = r.lock === undefined ? null : lockFieldError(r.lock)
|
|
225
|
+
if (lockMessage !== null) { return error(lockMessage) }
|
|
119
226
|
if (!Number.isSafeInteger(r.generation) || r.generation < 0) {
|
|
120
227
|
return error(`generation must be a non-negative safe integer: ${r.generation}`)
|
|
121
228
|
}
|
|
@@ -28,6 +28,13 @@ export declare const proof: {
|
|
|
28
28
|
malformedLockRejected: () => void;
|
|
29
29
|
invalidHashLockRejected: () => void;
|
|
30
30
|
aliasHashLockAccepted: () => void;
|
|
31
|
+
nestedLockAccepted: () => void;
|
|
32
|
+
deeplyNestedLockAccepted: () => void;
|
|
33
|
+
malformedNestedLockRejected: () => void;
|
|
34
|
+
invalidNestedHashLockRejected: () => void;
|
|
35
|
+
sharedLockReferenceAccepted: () => void;
|
|
36
|
+
invalidSharedLockReferenceRejected: () => void;
|
|
37
|
+
nestedValueIsStillNotAReference: () => void;
|
|
31
38
|
wrongDialectRejected: () => void;
|
|
32
39
|
missingSubjectRejected: () => void;
|
|
33
40
|
extraFieldsAccepted: () => void;
|
|
@@ -40,6 +47,9 @@ export declare const proof: {
|
|
|
40
47
|
};
|
|
41
48
|
encodeText: {
|
|
42
49
|
recursivelySortsObjectsLexicographically: () => void;
|
|
50
|
+
sortsNestedLockKeys: () => void;
|
|
51
|
+
equivalentNestedLockOrdersConverge: () => void;
|
|
52
|
+
serializesSharedLockReferenceAsAString: () => void;
|
|
43
53
|
preservesArrayOrder: () => void;
|
|
44
54
|
parsedEquivalentSourcesConverge: () => void;
|
|
45
55
|
};
|
|
@@ -16,6 +16,11 @@ const alias = /** @type {const} */ ('I')
|
|
|
16
16
|
/** @type {LockMap} */
|
|
17
17
|
const _lockMapAllowsMissingSubjects = {}
|
|
18
18
|
|
|
19
|
+
// A lock map's values are `string | LockMap | undefined` at every depth, so
|
|
20
|
+
// the same value is a legal binding for a direct hash and for a nested scope.
|
|
21
|
+
/** @type {LockMap} */
|
|
22
|
+
const _lockMapNests = { direct: h1, scope: { direct: h2, deeper: { direct: h1 } } }
|
|
23
|
+
|
|
19
24
|
// A shape-valid revision: every required field present (`snapshot` and
|
|
20
25
|
// `generation` included), with `extra` overriding or adding fields per test.
|
|
21
26
|
/** @type {(extra: JsonObject) => JsonObject} */
|
|
@@ -155,6 +160,62 @@ export const proof = {
|
|
|
155
160
|
assertEq(t, 'ok')
|
|
156
161
|
},
|
|
157
162
|
|
|
163
|
+
// A nested map scopes further bindings under a subject — the
|
|
164
|
+
// incompatible-diamond case from the README, where `B` and `C` each
|
|
165
|
+
// pick their own `D`.
|
|
166
|
+
nestedLockAccepted: () => {
|
|
167
|
+
const r = validate(revisionOf({ lock: { B: { B: h1, D: h1 }, C: { C: h2, D: h2 } } }))
|
|
168
|
+
assert(r[0] === 'ok', ['expected ok', r])
|
|
169
|
+
assert(encodeText(r[1]).includes(`"lock":{"B":{"B":"${h1}","D":"${h1}"},"C":{"C":"${h2}","D":"${h2}"}}`))
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Nesting has no depth limit, and a nested map may be sparse — it need
|
|
173
|
+
// not bind the subject it appears under, or anything at all.
|
|
174
|
+
deeplyNestedLockAccepted: () => {
|
|
175
|
+
const [t] = validate(revisionOf({ lock: { B: { C: { D: { E: h1 } } }, empty: {} } }))
|
|
176
|
+
assertEq(t, 'ok')
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// A direct value must be a string or a map at every depth: a number
|
|
180
|
+
// nested two levels down fails structural (rtti) validation, exactly
|
|
181
|
+
// as it does at the root.
|
|
182
|
+
malformedNestedLockRejected: () => {
|
|
183
|
+
const [t] = validate(revisionOf({ lock: { B: { D: 1 } } }))
|
|
184
|
+
assertEq(t, 'error')
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Semantic reference checking recurses too: a non-hash string deep in
|
|
188
|
+
// a nested scope is rejected, and the message names the path to it.
|
|
189
|
+
invalidNestedHashLockRejected: () => {
|
|
190
|
+
const r = validate(revisionOf({ lock: { B: { D: 'https://example.com/x' } } }))
|
|
191
|
+
assert(r[0] === 'error', ['expected error', r])
|
|
192
|
+
assertEq(r[1], 'lock value for B/D is not a valid hash: https://example.com/x')
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// In place of the map, `lock` may be the hash of a `vnd.fjs.lock`
|
|
196
|
+
// blob holding one — the only position where a string means "where
|
|
197
|
+
// the bindings are" rather than "this dependency's content".
|
|
198
|
+
sharedLockReferenceAccepted: () => {
|
|
199
|
+
const r = validate(revisionOf({ lock: h1 }))
|
|
200
|
+
assert(r[0] === 'ok', ['expected ok', r])
|
|
201
|
+
assertEq(r[1].lock, h1)
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// A reference is validated as a cbase32 hash and nothing more — the
|
|
205
|
+
// same contract `snapshot` has — so a non-hash one is rejected.
|
|
206
|
+
invalidSharedLockReferenceRejected: () => {
|
|
207
|
+
const r = validate(revisionOf({ lock: 'https://example.com/lock' }))
|
|
208
|
+
assert(r[0] === 'error', ['expected error', r])
|
|
209
|
+
assertEq(r[1], 'lock reference is not a valid hash: https://example.com/lock')
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// Only the top level is widened: a nested value is still a hash or a
|
|
213
|
+
// map, so nothing about an inline map's meaning changed.
|
|
214
|
+
nestedValueIsStillNotAReference: () => {
|
|
215
|
+
const [t] = validate(revisionOf({ lock: { B: { D: h1 } } }))
|
|
216
|
+
assertEq(t, 'ok')
|
|
217
|
+
},
|
|
218
|
+
|
|
158
219
|
// Wrong dialect tag: structural validation rejects it outright.
|
|
159
220
|
wrongDialectRejected: () => {
|
|
160
221
|
const [t] = validate({ dialect: 'vnd.fjs.other', subject: h1, parents: [], snapshot: h2, generation: 0 })
|
|
@@ -213,6 +274,32 @@ export const proof = {
|
|
|
213
274
|
`{"dialect":"${dialect}","generation":0,"lock":{"10":"${h1}","2":"${h2}"},"parents":[],"snapshot":"${h2}","subject":"${h1}"}`,
|
|
214
275
|
)
|
|
215
276
|
},
|
|
277
|
+
// The one `stringify(sort)` rule already reaches every depth, so a
|
|
278
|
+
// nested lock map needs no serialization special case: its keys sort
|
|
279
|
+
// as strings like every other object's ("10" before "2").
|
|
280
|
+
sortsNestedLockKeys: () => {
|
|
281
|
+
const revision = revisionOf({ lock: { scope: { '2': h2, '10': h1 }, '2': h2, '10': h1 } })
|
|
282
|
+
const decoded = validate(revision)
|
|
283
|
+
assert(decoded[0] === 'ok', ['expected ok', decoded])
|
|
284
|
+
assert(encodeText(decoded[1]).includes(
|
|
285
|
+
`"lock":{"10":"${h1}","2":"${h2}","scope":{"10":"${h1}","2":"${h2}"}}`))
|
|
286
|
+
},
|
|
287
|
+
// Two nested maps differing only in property order converge on one
|
|
288
|
+
// byte sequence, so they address the same CAS blob.
|
|
289
|
+
equivalentNestedLockOrdersConverge: () => {
|
|
290
|
+
const a = validate(revisionOf({ lock: { B: { B: h1, D: h2 }, C: { D: h1 } } }))
|
|
291
|
+
const b = validate(revisionOf({ lock: { C: { D: h1 }, B: { D: h2, B: h1 } } }))
|
|
292
|
+
assert(a[0] === 'ok', ['expected ok', a])
|
|
293
|
+
assert(b[0] === 'ok', ['expected ok', b])
|
|
294
|
+
assertEq(encodeText(a[1]), encodeText(b[1]))
|
|
295
|
+
},
|
|
296
|
+
// A shared-lock reference serializes as the plain string it is — no
|
|
297
|
+
// wrapper, no marker: the two forms are told apart by JSON type.
|
|
298
|
+
serializesSharedLockReferenceAsAString: () => {
|
|
299
|
+
const decoded = validate(revisionOf({ lock: h1 }))
|
|
300
|
+
assert(decoded[0] === 'ok', ['expected ok', decoded])
|
|
301
|
+
assert(encodeText(decoded[1]).includes(`"lock":"${h1}"`))
|
|
302
|
+
},
|
|
216
303
|
preservesArrayOrder: () => {
|
|
217
304
|
const decoded = validate(revisionOf({ parents: [h2, h1] }))
|
|
218
305
|
assert(decoded[0] === 'ok', ['expected ok', decoded])
|
|
@@ -229,3 +316,4 @@ export const proof = {
|
|
|
229
316
|
}
|
|
230
317
|
|
|
231
318
|
void _lockMapAllowsMissingSubjects
|
|
319
|
+
void _lockMapNests
|
|
@@ -1,15 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type-level API for `fjs/media/revision/module.f.mjs`: `LockMap`,
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
3
|
+
* `LockSchema`, `LockField`, `LockFieldSchema`, `Revision`, and
|
|
4
|
+
* `RevisionError`.
|
|
5
|
+
*
|
|
6
|
+
* `LockMap` is written by hand rather than derived, so that the recursion
|
|
7
|
+
* reads directly, and is then pinned against the module's rtti schema with
|
|
8
|
+
* `Assert<Equal<LockMap, Ts<typeof lock>>>` — the same arrangement the JSON
|
|
9
|
+
* data model uses in [`../json/types.ts`](../json/types.ts). `LockSchema` is
|
|
10
|
+
* the schema side of the same recursion: `lock` cannot infer its own type
|
|
11
|
+
* (a `const` may not reference itself in its own initializer), so it carries
|
|
12
|
+
* this named annotation instead.
|
|
5
13
|
*
|
|
6
14
|
* @module
|
|
7
15
|
*/
|
|
8
16
|
import type { Ts } from '../../types/rtti/ts/types.ts';
|
|
17
|
+
import type { String as RttiString } from '../../types/rtti/types.ts';
|
|
9
18
|
import type { ValidationError } from '../../types/rtti/common/types.ts';
|
|
10
|
-
import type {
|
|
11
|
-
/**
|
|
12
|
-
|
|
19
|
+
import type { revisionSchema } from './module.f.mjs';
|
|
20
|
+
/**
|
|
21
|
+
* A set of subject-to-snapshot bindings supplied to dependency resolvers.
|
|
22
|
+
* A direct string value selects immutable content; a nested map scopes
|
|
23
|
+
* further bindings under that subject (see the README).
|
|
24
|
+
*
|
|
25
|
+
* Spelled inline rather than as `StringMap<string | LockMap>`: a type alias
|
|
26
|
+
* may not reference itself through another alias's instantiation (TS2456).
|
|
27
|
+
*/
|
|
28
|
+
export type LockMap = {
|
|
29
|
+
readonly [subject in string]?: string | LockMap;
|
|
30
|
+
};
|
|
31
|
+
/** The rtti schema type of `lock` — a record of `string | LockSchema`. */
|
|
32
|
+
export type LockSchema = () => readonly ['record', () => readonly ['or', RttiString, LockSchema]];
|
|
33
|
+
/**
|
|
34
|
+
* A revision's `lock` field: the bindings inline as a {@link LockMap}, or the
|
|
35
|
+
* cbase32 hash of a `vnd.fjs.lock` blob (`fjs/media/lock`) holding one to
|
|
36
|
+
* share. Only the top level admits a hash — a string *inside* a map is a
|
|
37
|
+
* dependency's content, unchanged.
|
|
38
|
+
*/
|
|
39
|
+
export type LockField = string | LockMap;
|
|
40
|
+
/** The rtti schema type of `lockField` — a shared-lock reference or a lock map. */
|
|
41
|
+
export type LockFieldSchema = () => readonly ['or', RttiString, LockSchema];
|
|
13
42
|
/** The TypeScript type derived from `revisionSchema` — the single source of truth. */
|
|
14
43
|
export type Revision = Ts<typeof revisionSchema>;
|
|
15
44
|
/** Either a structural validation error or a semantic (hash / generation) error message. */
|
|
@@ -37,24 +37,41 @@
|
|
|
37
37
|
*
|
|
38
38
|
* @import { Vec } from '../../types/bit_vec/types.ts'
|
|
39
39
|
* @import { Nullable } from '../../types/nullable/types.ts'
|
|
40
|
-
* @import {
|
|
40
|
+
* @import { Operation } from '../../effects/types.ts'
|
|
41
41
|
* @import { List } from '../../effects/list/types.ts'
|
|
42
|
-
* @import { IoResult } from '../../effects/node/types.ts'
|
|
42
|
+
* @import { IoChannel, IoResult } from '../../effects/node/types.ts'
|
|
43
|
+
* @import { Effect } from '../../effects/types.ts'
|
|
43
44
|
* @import { DetectMeta, DetectState, _MagicState, _Signature, _Utf8Detect } from './types.ts'
|
|
44
45
|
*/
|
|
45
46
|
import type { Vec } from '../../types/bit_vec/types.ts';
|
|
46
47
|
import type { Nullable } from '../../types/nullable/types.ts';
|
|
47
|
-
import type {
|
|
48
|
+
import type { Operation } from '../../effects/types.ts';
|
|
48
49
|
import type { List } from '../../effects/list/types.ts';
|
|
49
|
-
import type {
|
|
50
|
+
import type { IoChannel } from '../../effects/node/types.ts';
|
|
51
|
+
import type { Effect } from '../../effects/types.ts';
|
|
50
52
|
import type { DetectMeta, DetectState } from './types.ts';
|
|
51
53
|
/**
|
|
52
|
-
* Detects the MIME type of `bytes` from its leading magic-byte signature
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* Detects the MIME type of `bytes` from its leading magic-byte signature, as
|
|
55
|
+
* the magic projection of the streaming detector below — one fold, read two
|
|
56
|
+
* ways, rather than a second copy of it.
|
|
57
|
+
*
|
|
58
|
+
* The two agree because `magicStep` returns a non-`scan` state unchanged, so
|
|
59
|
+
* both absorbing states are its fixed points. `detect`'s old loop stopped at
|
|
60
|
+
* the first one; `push` stops only once the *whole* verdict is settled and may
|
|
61
|
+
* keep folding for the UTF-8 factor after the magic state is `dead` — but a
|
|
62
|
+
* `dead` or `matched` magic cannot move again, so the final magic is the same
|
|
63
|
+
* either way. A signature still in `scan` at the end of the `Vec` — a prefix
|
|
64
|
+
* too short to complete any signature — reads as `null`.
|
|
65
|
+
*
|
|
66
|
+
* Cost follows `push`, which stops only once the whole verdict is settled, not
|
|
67
|
+
* once the magic state is. On a signature match or on non-text bytes that is
|
|
68
|
+
* still an early exit; on valid text it now reads the whole `Vec`, because the
|
|
69
|
+
* shared machine is still tracking the UTF-8 factor this function ignores
|
|
70
|
+
* (measured: 100 KB of ASCII, 1.2 ms before, 82 ms after). That is the same
|
|
71
|
+
* work {@link detectVec} already does on the same bytes, and `Vec` is capped at
|
|
72
|
+
* `maxLength`, so the cost is bounded rather than unbounded — but a caller
|
|
73
|
+
* scanning many large text blobs for signatures alone should know it is paying
|
|
74
|
+
* for the UTF-8 fold too.
|
|
58
75
|
*
|
|
59
76
|
* The bytes come from `u8List`, so a `Vec` whose length is not a whole number of
|
|
60
77
|
* bytes has its trailing partial byte zero-padded — the same reading of a ragged
|
|
@@ -104,11 +121,12 @@ export declare const finish: (s: DetectState) => DetectMeta;
|
|
|
104
121
|
export declare const detectVec: (bytes: Vec) => DetectMeta;
|
|
105
122
|
/**
|
|
106
123
|
* Folds a CAS read stream through {@link push} and reads {@link finish} at EOF,
|
|
107
|
-
* deriving `cas_get` metadata without ever materializing the blob. A
|
|
108
|
-
*
|
|
124
|
+
* deriving `cas_get` metadata without ever materializing the blob. A stream that
|
|
125
|
+
* fails carries its failure out as this one's, through `step` rather than
|
|
126
|
+
* through a case in the loop.
|
|
109
127
|
*
|
|
110
128
|
* @template {Operation} O
|
|
111
|
-
* @param {List<O,
|
|
112
|
-
* @returns {Effect<O,
|
|
129
|
+
* @param {List<O, Vec, IoChannel>} stream
|
|
130
|
+
* @returns {Effect<O, DetectMeta, IoChannel>}
|
|
113
131
|
*/
|
|
114
|
-
export declare const detectStream: <O extends Operation>(stream: List<O,
|
|
132
|
+
export declare const detectStream: <O extends Operation>(stream: List<O, Vec, IoChannel>) => Effect<O, DetectMeta, IoChannel>;
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
*
|
|
38
38
|
* @import { Vec } from '../../types/bit_vec/types.ts'
|
|
39
39
|
* @import { Nullable } from '../../types/nullable/types.ts'
|
|
40
|
-
* @import {
|
|
40
|
+
* @import { Operation } from '../../effects/types.ts'
|
|
41
41
|
* @import { List } from '../../effects/list/types.ts'
|
|
42
|
-
* @import { IoResult } from '../../effects/node/types.ts'
|
|
42
|
+
* @import { IoChannel, IoResult } from '../../effects/node/types.ts'
|
|
43
|
+
* @import { Effect } from '../../effects/types.ts'
|
|
43
44
|
* @import { DetectMeta, DetectState, _MagicState, _Signature, _Utf8Detect } from './types.ts'
|
|
44
45
|
*/
|
|
45
46
|
|
|
46
47
|
import { msb, length, u8List } from '../../types/bit_vec/module.f.mjs'
|
|
47
48
|
import { iterable } from '../../types/list/module.f.mjs'
|
|
48
|
-
import {
|
|
49
|
-
import { ok, error } from '../../types/result/module.f.mjs'
|
|
49
|
+
import { pureOk, step as ioStep } from '../../effects/module.f.mjs'
|
|
50
50
|
import { isValidCodePoint, isTextCodePoint } from '../../text/code_point/module.f.mjs'
|
|
51
51
|
import { utf8ByteToCodePointOp } from '../../text/utf8/module.f.mjs'
|
|
52
52
|
|
|
@@ -100,12 +100,27 @@ const magicStep = (m, byte) => {
|
|
|
100
100
|
const magicMime = m => m.tag === 'matched' ? m.mime : null
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* Detects the MIME type of `bytes` from its leading magic-byte signature
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
103
|
+
* Detects the MIME type of `bytes` from its leading magic-byte signature, as
|
|
104
|
+
* the magic projection of the streaming detector below — one fold, read two
|
|
105
|
+
* ways, rather than a second copy of it.
|
|
106
|
+
*
|
|
107
|
+
* The two agree because `magicStep` returns a non-`scan` state unchanged, so
|
|
108
|
+
* both absorbing states are its fixed points. `detect`'s old loop stopped at
|
|
109
|
+
* the first one; `push` stops only once the *whole* verdict is settled and may
|
|
110
|
+
* keep folding for the UTF-8 factor after the magic state is `dead` — but a
|
|
111
|
+
* `dead` or `matched` magic cannot move again, so the final magic is the same
|
|
112
|
+
* either way. A signature still in `scan` at the end of the `Vec` — a prefix
|
|
113
|
+
* too short to complete any signature — reads as `null`.
|
|
114
|
+
*
|
|
115
|
+
* Cost follows `push`, which stops only once the whole verdict is settled, not
|
|
116
|
+
* once the magic state is. On a signature match or on non-text bytes that is
|
|
117
|
+
* still an early exit; on valid text it now reads the whole `Vec`, because the
|
|
118
|
+
* shared machine is still tracking the UTF-8 factor this function ignores
|
|
119
|
+
* (measured: 100 KB of ASCII, 1.2 ms before, 82 ms after). That is the same
|
|
120
|
+
* work {@link detectVec} already does on the same bytes, and `Vec` is capped at
|
|
121
|
+
* `maxLength`, so the cost is bounded rather than unbounded — but a caller
|
|
122
|
+
* scanning many large text blobs for signatures alone should know it is paying
|
|
123
|
+
* for the UTF-8 fold too.
|
|
109
124
|
*
|
|
110
125
|
* The bytes come from `u8List`, so a `Vec` whose length is not a whole number of
|
|
111
126
|
* bytes has its trailing partial byte zero-padded — the same reading of a ragged
|
|
@@ -117,15 +132,7 @@ const magicMime = m => m.tag === 'matched' ? m.mime : null
|
|
|
117
132
|
*
|
|
118
133
|
* @type {(bytes: Vec) => Nullable<string>}
|
|
119
134
|
*/
|
|
120
|
-
export const detect = bytes =>
|
|
121
|
-
/** @type {_MagicState} */
|
|
122
|
-
let magic = magicInit
|
|
123
|
-
for (const byte of iterable(u8List(msb)(bytes))) {
|
|
124
|
-
magic = magicStep(magic, byte)
|
|
125
|
-
if (magic.tag !== 'scan') { break }
|
|
126
|
-
}
|
|
127
|
-
return magicMime(magic)
|
|
128
|
-
}
|
|
135
|
+
export const detect = bytes => magicMime(push(detectInit)(bytes).magic)
|
|
129
136
|
|
|
130
137
|
// ── Streaming detector ────────────────────────────────────────────────────────────
|
|
131
138
|
//
|
|
@@ -242,24 +249,23 @@ export const detectVec = bytes => finish(push(detectInit)(bytes))
|
|
|
242
249
|
|
|
243
250
|
/**
|
|
244
251
|
* Folds a CAS read stream through {@link push} and reads {@link finish} at EOF,
|
|
245
|
-
* deriving `cas_get` metadata without ever materializing the blob. A
|
|
246
|
-
*
|
|
252
|
+
* deriving `cas_get` metadata without ever materializing the blob. A stream that
|
|
253
|
+
* fails carries its failure out as this one's, through `step` rather than
|
|
254
|
+
* through a case in the loop.
|
|
247
255
|
*
|
|
248
256
|
* @template {Operation} O
|
|
249
|
-
* @param {List<O,
|
|
250
|
-
* @returns {Effect<O,
|
|
257
|
+
* @param {List<O, Vec, IoChannel>} stream
|
|
258
|
+
* @returns {Effect<O, DetectMeta, IoChannel>}
|
|
251
259
|
*/
|
|
252
260
|
export const detectStream = stream => {
|
|
253
|
-
/** @type {(s: DetectState) => (l: List<O,
|
|
261
|
+
/** @type {(s: DetectState) => (l: List<O, Vec, IoChannel>) => Effect<O, DetectMeta, IoChannel>} */
|
|
254
262
|
const loop = s => l =>
|
|
255
|
-
|
|
263
|
+
ioStep(
|
|
256
264
|
l,
|
|
257
265
|
node => {
|
|
258
|
-
if (node === undefined) { return
|
|
266
|
+
if (node === undefined) { return pureOk(finish(s)) }
|
|
259
267
|
const { first, tail } = node
|
|
260
|
-
|
|
261
|
-
if (t === 'error') { return pure(error(v)) }
|
|
262
|
-
return loop(push(s)(v))(tail)
|
|
268
|
+
return loop(push(s)(first))(tail)
|
|
263
269
|
})
|
|
264
270
|
return loop(detectInit)(stream)
|
|
265
271
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { Vec } from '../../types/bit_vec/types.ts'
|
|
3
3
|
* @import { List } from '../../effects/list/types.ts'
|
|
4
|
-
* @import {
|
|
4
|
+
* @import { IoChannel } from '../../effects/node/types.ts'
|
|
5
5
|
* @import { DetectMeta } from './types.ts'
|
|
6
6
|
*/
|
|
7
7
|
export declare const proof: {
|
|
@@ -16,6 +16,7 @@ export declare const proof: {
|
|
|
16
16
|
spannedZip: () => void;
|
|
17
17
|
webp: () => void;
|
|
18
18
|
riffNotWebp: () => void;
|
|
19
|
+
settledMagicIsAFixedPoint: (() => void)[];
|
|
19
20
|
textIsNull: () => void;
|
|
20
21
|
shortIsNull: () => void;
|
|
21
22
|
emptyIsNull: () => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { Vec } from '../../types/bit_vec/types.ts'
|
|
3
3
|
* @import { List } from '../../effects/list/types.ts'
|
|
4
|
-
* @import {
|
|
4
|
+
* @import { IoChannel } from '../../effects/node/types.ts'
|
|
5
5
|
* @import { DetectMeta } from './types.ts'
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -9,7 +9,9 @@ import { assert, assertEq } from '../../asserts/module.f.mjs'
|
|
|
9
9
|
import { msb, u8ListToVec, vec8, repeat, empty } from '../../types/bit_vec/module.f.mjs'
|
|
10
10
|
import { runPure } from '../../effects/module.f.mjs'
|
|
11
11
|
import { nonEmpty, empty as emptyList } from '../../effects/list/module.f.mjs'
|
|
12
|
-
import {
|
|
12
|
+
import { pureError } from '../../effects/module.f.mjs'
|
|
13
|
+
import { error, ok } from '../../types/result/module.f.mjs'
|
|
14
|
+
import { ioError } from '../../effects/node/module.f.mjs'
|
|
13
15
|
import { detect, detectStream, detectVec } from './module.f.mjs'
|
|
14
16
|
|
|
15
17
|
// Builds a big-endian `Vec` from a list of byte values — mirrors how the CAS
|
|
@@ -19,12 +21,12 @@ const bytes = (...b) => u8ListToVec(msb)(b)
|
|
|
19
21
|
|
|
20
22
|
// ── Streaming detector helpers ──────────────────────────────────────────────────
|
|
21
23
|
|
|
22
|
-
// Builds a CAS-style read stream from a sequence of
|
|
23
|
-
/** @type {(...chunks: readonly Vec[]) => List<never,
|
|
24
|
+
// Builds a CAS-style read stream from a sequence of chunks.
|
|
25
|
+
/** @type {(...chunks: readonly Vec[]) => List<never, Vec, IoChannel>} */
|
|
24
26
|
const stream = (...chunks) =>
|
|
25
27
|
chunks.reduceRight(
|
|
26
|
-
(tail, c) => nonEmpty(
|
|
27
|
-
/** @
|
|
28
|
+
(tail, c) => nonEmpty(c, tail),
|
|
29
|
+
/** @satisfies {List<never, Vec, IoChannel>} */ (emptyList()))
|
|
28
30
|
|
|
29
31
|
// Runs the streaming detector over the given chunks and unwraps the metadata.
|
|
30
32
|
/** @type {(...chunks: readonly Vec[]) => DetectMeta} */
|
|
@@ -84,6 +86,25 @@ export const proof = {
|
|
|
84
86
|
null),
|
|
85
87
|
|
|
86
88
|
// Plain ASCII text matches no signature.
|
|
89
|
+
// `detect` is the magic projection of `push`, and that only holds because
|
|
90
|
+
// `magicStep` leaves a non-`scan` state alone. These pin both absorbing
|
|
91
|
+
// states as fixed points from the outside, where `magicStep` is private.
|
|
92
|
+
settledMagicIsAFixedPoint: [
|
|
93
|
+
// `matched` survives whatever follows the signature — including bytes
|
|
94
|
+
// that would not extend it, and a long tail that would otherwise keep
|
|
95
|
+
// the fold running.
|
|
96
|
+
() => assertEq(detect(bytes(0xff, 0xd8, 0xff, 0x00, 0x01, 0x02)), 'image/jpeg'),
|
|
97
|
+
() => assertEq(detect(bytes(0xff, 0xd8, 0xff, ...Array(64).fill(0x41))), 'image/jpeg'),
|
|
98
|
+
// `dead` survives too. This is the case where `push` keeps folding
|
|
99
|
+
// after the magic state is settled — `isSettled` stays false while the
|
|
100
|
+
// UTF-8 factor is still valid text — so a `dead` magic is fed many more
|
|
101
|
+
// bytes here than `detect`'s old loop ever gave it.
|
|
102
|
+
() => assertEq(detect(bytes(0x41)), null),
|
|
103
|
+
() => assertEq(detect(bytes(0x41, ...Array(64).fill(0x42))), null),
|
|
104
|
+
// ...and the same input still reads as text through the full detector,
|
|
105
|
+
// confirming the fold really did continue past the dead magic.
|
|
106
|
+
() => assertEq(detectVec(bytes(0x41, ...Array(64).fill(0x42))).type, 'text'),
|
|
107
|
+
],
|
|
87
108
|
textIsNull: () =>
|
|
88
109
|
assertEq(detect(bytes(0x68, 0x65, 0x6c, 0x6c, 0x6f)), null),
|
|
89
110
|
|
|
@@ -270,11 +291,10 @@ export const proof = {
|
|
|
270
291
|
assertEq(m.length, 8n)
|
|
271
292
|
},
|
|
272
293
|
|
|
273
|
-
// A
|
|
294
|
+
// A stream that fails carries its failure out as the detector's own.
|
|
274
295
|
readErrorSurfaces: () => {
|
|
275
|
-
/** @type {List<never,
|
|
276
|
-
const errStream =
|
|
277
|
-
nonEmpty(/** @type {const} */ (['error', 'boom']), emptyList())
|
|
296
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
297
|
+
const errStream = pureError(ioError({ message: 'boom' }))
|
|
278
298
|
const o = runPure(detectStream(errStream))
|
|
279
299
|
assert(o.length === 1, 'effect is not pure')
|
|
280
300
|
assert(o[0][0] === 'error')
|