@mason-data/cli 0.7.0 → 0.9.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 (155) hide show
  1. package/README.md +101 -73
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +368 -25
  5. package/dist/src/api/postgres/encode.js +3 -1
  6. package/dist/src/api/postgres/explain.js +3 -2
  7. package/dist/src/api/postgres/listener.js +11 -3
  8. package/dist/src/api/postgres/protocol.js +39 -23
  9. package/dist/src/api/postgres/proxy.js +283 -0
  10. package/dist/src/api/postgres/schema.js +6 -0
  11. package/dist/src/api/postgres/server.js +1617 -245
  12. package/dist/src/api/postgres/translate.js +127 -10
  13. package/dist/src/api/request-log.js +18 -5
  14. package/dist/src/cdc/poll.js +5 -5
  15. package/dist/src/cli/background.js +47 -25
  16. package/dist/src/cli/bridge-stdin.js +101 -0
  17. package/dist/src/cli/cancel.js +75 -0
  18. package/dist/src/cli/command.js +170 -11
  19. package/dist/src/cli/commands.js +804 -209
  20. package/dist/src/cli/control.js +59 -0
  21. package/dist/src/cli/credentials.js +182 -0
  22. package/dist/src/cli/demo.js +20 -11
  23. package/dist/src/cli/deploy.js +574 -0
  24. package/dist/src/cli/dev.js +491 -130
  25. package/dist/src/cli/doctor.js +131 -38
  26. package/dist/src/cli/generated-files.js +143 -0
  27. package/dist/src/cli/guides.js +175 -36
  28. package/dist/src/cli/hosted.js +850 -0
  29. package/dist/src/cli/init.js +162 -65
  30. package/dist/src/cli/login.js +289 -0
  31. package/dist/src/cli/machine.js +13 -12
  32. package/dist/src/cli/main.js +11 -5
  33. package/dist/src/cli/mason.cjs +17 -0
  34. package/dist/src/cli/mcp.js +31 -22
  35. package/dist/src/cli/metrics.js +79 -0
  36. package/dist/src/cli/output-gate.js +176 -0
  37. package/dist/src/cli/page-share.js +96 -0
  38. package/dist/src/cli/pg-tls.js +14 -4
  39. package/dist/src/cli/pg-url.js +4 -0
  40. package/dist/src/cli/pin.js +19 -14
  41. package/dist/src/cli/pool-size.cjs +85 -0
  42. package/dist/src/cli/ports.js +2 -0
  43. package/dist/src/cli/preflight.js +138 -46
  44. package/dist/src/cli/project.js +142 -4
  45. package/dist/src/cli/readiness.js +76 -6
  46. package/dist/src/cli/registry.js +65 -38
  47. package/dist/src/cli/replica-hosted.js +665 -0
  48. package/dist/src/cli/replica.js +76 -0
  49. package/dist/src/cli/replication-checks.js +7 -5
  50. package/dist/src/cli/replication-mode.js +105 -26
  51. package/dist/src/cli/request-filter.js +134 -0
  52. package/dist/src/cli/roles.js +585 -0
  53. package/dist/src/cli/run-server.js +637 -48
  54. package/dist/src/cli/runs.js +1 -1
  55. package/dist/src/cli/security.js +67 -18
  56. package/dist/src/cli/serve.js +204 -46
  57. package/dist/src/cli/share-run.js +329 -0
  58. package/dist/src/cli/share.js +286 -0
  59. package/dist/src/cli/source-edit.js +75 -17
  60. package/dist/src/cli/source-role.js +344 -0
  61. package/dist/src/cli/sources.js +306 -47
  62. package/dist/src/cli/style.js +2 -1
  63. package/dist/src/cli/supervise.js +27 -24
  64. package/dist/src/cli/sync.js +55 -25
  65. package/dist/src/cli/token-hosted.js +337 -0
  66. package/dist/src/cli/tunnel.js +200 -0
  67. package/dist/src/cli/ui-ssh.js +460 -0
  68. package/dist/src/cli/ui.js +124 -17
  69. package/dist/src/cli/usage.js +128 -0
  70. package/dist/src/cli/user-hosted.js +299 -0
  71. package/dist/src/cli/user.js +573 -65
  72. package/dist/src/cli/version.js +9 -7
  73. package/dist/src/demo/seed.js +29 -0
  74. package/dist/src/duck/analyze.js +36 -20
  75. package/dist/src/duck/cancel.js +10 -1
  76. package/dist/src/duck/collation.js +266 -0
  77. package/dist/src/duck/constant-key.js +94 -0
  78. package/dist/src/duck/distinct-key.js +283 -26
  79. package/dist/src/duck/events.js +165 -0
  80. package/dist/src/duck/executor-worker.js +20 -11
  81. package/dist/src/duck/executor.js +47 -33
  82. package/dist/src/duck/explain.js +60 -46
  83. package/dist/src/duck/extensions.js +160 -0
  84. package/dist/src/duck/functions.js +461 -63
  85. package/dist/src/duck/gaps.js +48 -21
  86. package/dist/src/duck/generated-plan.js +1070 -0
  87. package/dist/src/duck/generated.js +906 -0
  88. package/dist/src/duck/grants.js +176 -0
  89. package/dist/src/duck/held-types.js +64 -0
  90. package/dist/src/duck/history.js +127 -0
  91. package/dist/src/duck/jsonb-order.js +9 -12
  92. package/dist/src/duck/kept-route.js +6 -5
  93. package/dist/src/duck/kept.js +15 -13
  94. package/dist/src/duck/late.js +1469 -40
  95. package/dist/src/duck/libc-bound.js +341 -0
  96. package/dist/src/duck/libc-order.js +892 -126
  97. package/dist/src/duck/pg-eval-worker.js +7 -1
  98. package/dist/src/duck/pg-eval.js +6 -4
  99. package/dist/src/duck/pgtree.js +774 -23
  100. package/dist/src/duck/plan.js +369 -47
  101. package/dist/src/duck/publication.js +20 -6
  102. package/dist/src/duck/requests.js +269 -93
  103. package/dist/src/duck/resolved.js +117 -2
  104. package/dist/src/duck/rewrite.js +4584 -580
  105. package/dist/src/duck/row-filter.js +655 -0
  106. package/dist/src/duck/statement.js +19 -0
  107. package/dist/src/duck/store.js +439 -177
  108. package/dist/src/duck/stream.js +19 -10
  109. package/dist/src/duck/sync.js +1411 -368
  110. package/dist/src/duck/token.js +4 -1
  111. package/dist/src/duck/types.js +69 -12
  112. package/dist/src/duck/usage.js +326 -0
  113. package/dist/src/duck/users.js +69 -15
  114. package/dist/src/duck/verified.js +161 -1
  115. package/dist/src/duck/views.js +5 -5
  116. package/dist/src/lib/database-name.js +62 -0
  117. package/dist/src/lib/database-url.js +55 -10
  118. package/dist/src/lib/dotenv.js +17 -4
  119. package/dist/src/lib/instant.js +50 -0
  120. package/dist/src/lib/log-buffer.js +77 -0
  121. package/dist/src/lib/log-read.js +196 -0
  122. package/dist/src/lib/pg-numeric.js +390 -49
  123. package/dist/src/lib/project-version.js +23 -23
  124. package/dist/src/lib/secret-mask.js +142 -0
  125. package/dist/src/lib/table-sync.js +282 -0
  126. package/dist/src/web/app.css +706 -66
  127. package/dist/src/web/app.js +331 -409
  128. package/dist/src/web/charts.js +229 -0
  129. package/dist/src/web/index.html +82 -18
  130. package/dist/src/web/pages/queries.js +192 -0
  131. package/dist/src/web/pages/status.js +611 -0
  132. package/dist/src/web/pages/syncs.js +155 -0
  133. package/dist/src/web/pages/tables.js +439 -0
  134. package/dist/src/web/pages/usage.js +397 -0
  135. package/dist/src/web/pages/users.js +219 -0
  136. package/dist/src/web/shared.js +584 -0
  137. package/dist/src/web/vendor/README.md +25 -0
  138. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  139. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  140. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  141. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  142. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  143. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  144. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  145. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  146. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  147. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  148. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  149. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  150. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  151. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  152. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  153. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  154. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  155. package/package.json +12 -4
@@ -1,37 +1,65 @@
1
1
  /**
2
- * TEXT IN A GLIBC LOCALE'S ORDER, MEASURED FROM THE SOURCE (the BI-tools test of #517: Metabase's field lists
3
- * under en_US.utf8 listed Hangul, `$` / `€` and U+FFFD elsewhere than Postgres, and `s < 'b'` counted other rows). The
4
- * order was emulated from assumptions until then — ICU's order of the letters and digits, the rest ignored, then the
5
- * punctuation by its bytes — and glibc differs from each: Hangul has no letter weight in it, `$` and `€` have one where
6
- * `@` and `~` have none, U+FFFD sorts after the letters. So the order is no longer assumed: it is READ from the source
7
- * database, once (read-only SELECTs, a few seconds), kept in the copy (`mason.libc_order`, src/duck/store.ts), read again
8
- * when the database's collation version moves (`pg_database.datcollversion`: glibc's), and every sort key is built from it.
2
+ * TEXT IN A GLIBC LOCALE'S ORDER, MEASURED FROM THE SOURCE (the BI-tools test of #517: Metabase's field lists under
3
+ * en_US.utf8 listed Hangul, `$` / `€` and U+FFFD elsewhere than Postgres; issue 128: one emoji's U+FE0F refused a whole
4
+ * column's sort). The order is never assumed: it is READ from the source database, once (read-only SELECTs over
5
+ * constants, no table, a few seconds), kept in the copy (`_mason.libc_order`, src/duck/store.ts), read again when the
6
+ * database's collation version moves (`pg_database.datcollversion`: glibc's), and every sort key is built from it, in
7
+ * one JavaScript call a row (no real `strcoll` is reachable: not the source's per row, and this machine's glibc, if it
8
+ * has one, is another version of another table).
9
9
  *
10
- * What glibc compares, as measured (Postgres 18 / glibc 2.41 en_US.utf8, the disposable server's):
11
- * 1. the FIRST level: a weight per character that has one (letters, digits, `$`, U+FFFD; not punctuation, Hangul or
12
- * spaces), compared in order. Read as CLASSES: the characters in the database's order, two neighbours x < y in one
13
- * class when x||'b' > y||'a' (only the second character told them apart). A character equal there to a string of
14
- * others (ß = ss, æ = ae, fi = fi) is that string: proposed by its compatibility decomposition, kept only when the
15
- * source says it is equal;
16
- * 2. accents, then 3. case: ICU's second and third levels over the characters of level 1 (the source's own agree
17
- * with them over everything the sample below holds; nothing here reads them from the source);
18
- * 4. the LAST level: for each character that has a weight there (punctuation, spaces, Hangul's one shared weight,
19
- * Latin letters; not CJK), the pair (its position in the string, its weight), position first — `三 < :三` (`三` has
20
- * none) and `가三 < 三가` both follow. The punctuation's weights are their order (they differ in nothing else); a
21
- * character of level 1 is placed among them by binary search (p||x < x exactly when p's weight is below x's), and
22
- * one placed below all of them has none when x||chr(1) is not below chr(1)||x;
23
- * 5. a tie at every level is Postgres's byte order (`varstr_cmp`'s strcmp), which the caller adds.
24
- * Held against the source's ORDER BY over 30,000 random strings of 17 scripts and symbol sets: 2 out of order, both
25
- * full-width digits beside Hangul. Not covered, so REFUSED by name when a value holding one is ordered
26
- * (`libc-uncovered`): a combining mark (glibc weighs `e` + U+0301 otherwise than ICU), a Thai or Lao vowel written
27
- * before its consonant (glibc reorders them), a code point this Node does not know as assigned, a private use one. A
28
- * known difference (`libc-symbol-punctuation`): punctuation right after a digit or symbol outranks a case difference
29
- * after it in glibc (`1-X < 1x`), and not here.
10
+ * What glibc's strcoll compares (read from its `iso14651_t1_common` and held to Postgres 18 / glibc 2.41 en_US.utf8 over
11
+ * random strings of 15 scripts): each ELEMENT of the string, a character or a contraction (Thai's vowel written before
12
+ * its consonant, `เก`; an NFD spelling of a letter of its own, `И` + U+0306), has weights at four levels, each a
13
+ * sequence, any of them empty, and the levels are compared in turn, each over the whole string:
14
+ * 1. letters, digits, `$`, U+FFFD; not punctuation, spaces, emoji or marks; a ligature is several (`ß` = ss);
15
+ * 2. accents: a letter's BASE, a combining mark's own, `é` = BASE AIGUT, and `e` + U+0301 is the same sequence.
16
+ * AN ELEMENT OF glibc's <SPECIAL> SECTION (digits, symbols, punctuation, marks) IS `backward` HERE: a run of them is
17
+ * read in reverse, and a run closed by a forward element SKIPS the element before the run's last (get_next_seq's
18
+ * `backw` is decremented twice; that is what `1-X < 1x` is: the `1` is not read, so that side is shorter);
19
+ * 3. case and form (MIN, CAP, COMPAT, FONT …);
20
+ * 4. the last: the pairs (its position, its weight) of each element with a weight there, position first — `三 < :三`
21
+ * (`三` has none) and `가三 < 三가` both follow. What the table does not list (Hangul, an unassigned or a private use
22
+ * code point) has no other level, and is ONE ELEMENT PER BYTE of its UTF-8, each of one shared weight (findidx
23
+ * takes its lead byte alone): U+0378 is two of them, U+E000 three, U+F0000 four.
24
+ * A tie at every level is Postgres's byte order (`varstr_cmp`'s strcmp), which the caller adds.
25
+ *
26
+ * HOW IT IS READ, by comparisons alone (`readLibcOrder`), so any table answers, a tailored locale's or another glibc's:
27
+ * the characters sorted once, each neighbour asked which level told it apart; a weight's place by binary search among
28
+ * the marks or the punctuation; every spelling a character could stand for (its NFD, its NFKD, a Thai or Lao reordering)
29
+ * proposed by Unicode's data and KEPT ONLY WHEN THE SOURCE SAYS THE TWO ARE EQUAL.
30
+ *
31
+ * WHAT IS READ IS HELD, AND THE VERDICT IS A BRANCH (`holdLibcOrder`): pairs of strings, ASCII first, compared by the source and
32
+ * by the key, counted, every pair. A locale of the plain table (`isPlainTable`: en_*, glibc's iso14651_t1 as it comes)
33
+ * IS NEVER REFUSED, of any character, on any C library: what differs stays as it was read, and is recorded
34
+ * (`LibcOrder.held`: the log and mason status), and what no level could be read of is left to the bytes. A difference
35
+ * there is a decided one (`libc-rare-letter`, `libc-mark-run-form`). Any other locale has what differs uncovered where it
36
+ * is rare; a pair that differs in ASCII or Latin alone is a table this reading does not follow (tr_TR, sv_SE, cs_CZ;
37
+ * another C library), and the whole order is `unread`.
38
+ * REFUSED BY NAME under such a locale alone, never an exit at the start and never another order: an `unread` order refuses
39
+ * every text it is asked to order (`libc-locale-unread`), a character not covered the values holding it
40
+ * (`libc-uncovered`), and so does every code point not read where a sample of them does not share one weight. A strcoll
41
+ * that is byte order (musl's, macOS's) reads as that (`bytes`), with nothing else to read.
30
42
  */
43
+ import { Buffer } from "node:buffer";
31
44
  import { gapMark } from "./gaps.js";
32
45
  import { PgEvalError } from "./pg-eval.js";
33
46
  const MAX = 0x110000;
34
- /** The code points read from the source: assigned, not private use, not a surrogate (the rest are uncovered). */
47
+ const BATCH = 20000;
48
+ /** what a code point not read is: the first of the private use area, which no collation table lists */
49
+ const UNLISTED = 0xe000;
50
+ /** one in this many of the code points not read is held to `UNLISTED` */
51
+ const UNREAD_SAMPLE = 211;
52
+ /** the pairs of random strings of each pool (all, ASCII, Latin) a read order is held to (`holdLibcOrder`) */
53
+ const HELD_PAIRS = 6000;
54
+ /** a ligature's units whose case is searched for: longer ones (Arabic's) are left to `hold` */
55
+ const MAX_UNITS = 4;
56
+ /** a mark class's place at the second level, and the place just below it of a weight no mark has (the `nth` of its letter) */
57
+ const markPlace = (k) => (2 * k + 2) * 4096;
58
+ const gapPlace = (k, nth) => (2 * k + 1) * 4096 + nth;
59
+ /** the third level's weight of a case's rank in its letter (rank 0 is 1, the common one), and of one between two ranks */
60
+ const caseOf = (rank) => 2 * rank * 1024 + 1;
61
+ const caseBelow = (rank, nth) => (2 * rank - 1) * 1024 + nth + 1;
62
+ /** The code points read from the source: assigned, not private use, not a surrogate (`UNREAD_SAMPLE` holds the rest). */
35
63
  export function assignedCodePoints() {
36
64
  const out = [];
37
65
  for (let g = 1; g < MAX; g++) {
@@ -43,14 +71,165 @@ export function assignedCodePoints() {
43
71
  }
44
72
  return out;
45
73
  }
46
- /** what glibc orders otherwise than this ordering: combining marks, Thai and Lao vowels written before their consonant */
47
- const UNCOVERED = /[\p{M}เ-ไເ-ໄ]/u;
48
74
  const ints = (xs) => `'{${xs.join(",")}}'::int[]`;
49
75
  const texts = (xs) => `ARRAY[${xs.map((x) => `'${x.replaceAll("'", "''")}'`).join(",")}]::text[]`;
50
- /** Reads the source database's order of text (its default collation), as the module's header says. */
51
- export async function readLibcOrder(query, collate, version) {
76
+ const cps = (s) => [...s].map((c) => c.codePointAt(0));
77
+ const str = (xs) => String.fromCodePoint(...xs);
78
+ /** An empty order: nothing covered. */
79
+ export function emptyLibcOrder(collate, version) {
80
+ return { collate, version, bytes: false, unread: false, held: { pairs: 0, differ: 0, common: 0 }, l1: new Uint32Array(MAX), l2: new Uint32Array(MAX), l3: new Uint32Array(MAX), l4: new Uint32Array(MAX), back: new Uint8Array(MAX),
81
+ uncovered: new Uint8Array(MAX).fill(1), unlisted: new Uint8Array(MAX), special: new Uint8Array(MAX), sequences: new Map(), contractions: new Map() };
82
+ }
83
+ /** the order with `special` said from its sequences and contractions */
84
+ export function withLookups(o) {
85
+ o.special.set(o.unlisted);
86
+ for (const g of o.sequences.keys())
87
+ o.special[g] = 1;
88
+ for (const g of o.contractions.keys())
89
+ o.special[g] = 1;
90
+ return o;
91
+ }
92
+ /** the element of one character, its sequence or its one weight at each level */
93
+ function elementOf(o, g) {
94
+ const s = o.sequences.get(g);
95
+ if (s !== undefined)
96
+ return s;
97
+ const one = (x) => x > 0 ? [x] : [];
98
+ return { l1: one(o.l1[g]), l2: one(o.l2[g]), l3: one(o.l3[g]), l4: one(o.l4[g]), back: o.back[g] === 1 };
99
+ }
100
+ const joined = (es) => ({ l1: es.flatMap((e) => e.l1), l2: es.flatMap((e) => e.l2), l3: es.flatMap((e) => e.l3), l4: es.flatMap((e) => e.l4), back: es[0].back });
101
+ /** Asks the source about pairs of code points (`a`, `b` in `expr`), in batches: its answer to each. */
102
+ async function asked(query, pairs, expr) {
103
+ const out = [];
104
+ for (let at = 0; at < pairs.length; at += BATCH) {
105
+ const part = pairs.slice(at, at + BATCH);
106
+ const rows = await query(`SELECT (${expr})::text FROM unnest(${ints(part.map((p) => p[0]))}, ${ints(part.map((p) => p[1]))}) WITH ORDINALITY AS u(a, b, n) ORDER BY n`);
107
+ if (rows.length !== part.length)
108
+ throw new Error(`reading how the database orders text: ${part.length} comparisons asked, ${rows.length} answered`);
109
+ for (const [v] of rows)
110
+ out.push(v === "true");
111
+ }
112
+ return out;
113
+ }
114
+ /** Asks the source about pairs of strings (`s`, `t` in `expr`), in batches: its answer to each. */
115
+ async function askedText(query, pairs, expr) {
116
+ const out = [];
117
+ for (let at = 0; at < pairs.length; at += BATCH / 4) {
118
+ const part = pairs.slice(at, at + BATCH / 4);
119
+ const rows = await query(`SELECT (${expr})::text FROM unnest(${texts(part.map((p) => p[0]))}, ${texts(part.map((p) => p[1]))}) WITH ORDINALITY AS u(s, t, n) ORDER BY n`);
120
+ if (rows.length !== part.length)
121
+ throw new Error(`reading how the database orders text: ${part.length} comparisons asked, ${rows.length} answered`);
122
+ for (const [v] of rows)
123
+ out.push(v === "true");
124
+ }
125
+ return out;
126
+ }
127
+ /** a binary search of many at once: `below(i, mid)` asked of each still open, until each has its lowest mid answering yes */
128
+ async function searched(n, top, ask) {
129
+ const lo = new Int32Array(n), hi = new Int32Array(n);
130
+ for (let i = 0; i < n; i++)
131
+ hi[i] = top(i);
132
+ for (;;) {
133
+ const open = [];
134
+ for (let i = 0; i < n; i++)
135
+ if (lo[i] < hi[i])
136
+ open.push({ i, mid: (lo[i] + hi[i]) >> 1 });
137
+ if (open.length === 0)
138
+ return lo;
139
+ const yes = await ask(open);
140
+ yes.forEach((y, j) => { const { i, mid } = open[j]; if (y)
141
+ hi[i] = mid;
142
+ else
143
+ lo[i] = mid + 1; });
144
+ }
145
+ }
146
+ /**
147
+ * A locale that is glibc's table as it comes, so one this reading is held to by design and never refuses: English's,
148
+ * each `en_XX` being `copy "iso14651_t1"` (en_CA moves capitals before small letters, which the read asks of the source;
149
+ * en_AG, en_BW, en_IL and en_ZW copy another of these). Nothing else is, de_DE and fr_FR included, though most hold: a
150
+ * tailoring is not told from a name, so those are held to the measurement alone.
151
+ */
152
+ export const isPlainTable = (collate) => /^en_[A-Za-z]{2}([.@]|$)/.test(collate);
153
+ /** code points in byte order, of each kind a collation moves: what a byte-ordering strcoll keeps in this order */
154
+ const BYTE_ORDER_SAMPLE = [0x20, 0x21, 0x2d, 0x30, 0x41, 0x5a, 0x5f, 0x61, 0x7a, 0xc0, 0xe9, 0x100, 0x300, 0x4e00, 0xac00, 0xe000, 0xfffd, 0x1f600];
155
+ /**
156
+ * A symbol's first level, its letters `head` and the class its last weight lies above: `lo` is the first of `classes`
157
+ * it sorts below, so none (0) leaves the letters alone and all of them (`classes.length`) means the last.
158
+ */
159
+ export const closedWeights = (head, classes, lo) => lo === 0 ? [...head] : [...head, classes[lo - 1]];
160
+ /**
161
+ * A READ UNDER A NAMED COLLATION (`collation`, its SQL name: `pg_catalog."fr_FR"`): every text the read compares or
162
+ * sorts is made of `chr(…)` or of an array of strings, so each of those carries `COLLATE <collation>` (explicit, it
163
+ * decides every comparison and ORDER BY it is in), outside the read's string literals. A read of the database's own
164
+ * collation sends its SQL as it is.
165
+ */
166
+ export function collatedSql(sql, collation) {
167
+ let out = "";
168
+ for (let i = 0; i < sql.length;) {
169
+ if (sql[i] === "'") {
170
+ let j = i + 1;
171
+ for (; j < sql.length; j++) {
172
+ if (sql[j] === "'") {
173
+ if (sql[j + 1] === "'") {
174
+ j++;
175
+ continue;
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ out += sql.slice(i, j + 1);
181
+ i = j + 1;
182
+ continue;
183
+ }
184
+ if (sql.startsWith("chr(", i) && !/[\w.]/.test(sql[i - 1] ?? "")) {
185
+ let depth = 0, j = i + 3;
186
+ for (; j < sql.length; j++) {
187
+ if (sql[j] === "(")
188
+ depth++;
189
+ else if (sql[j] === ")" && --depth === 0)
190
+ break;
191
+ }
192
+ // (its argument is a code point's number: no text of its own)
193
+ out += `(${sql.slice(i, j + 1)} COLLATE ${collation})`;
194
+ i = j + 1;
195
+ continue;
196
+ }
197
+ if (sql.startsWith("]::text[]", i)) {
198
+ out += `]::text[] COLLATE ${collation}`;
199
+ i += 9;
200
+ continue;
201
+ }
202
+ out += sql[i];
203
+ i++;
204
+ }
205
+ return out;
206
+ }
207
+ /** The source's order of text under its default collation, or under the named `collation` (`collatedSql`), as the
208
+ * module's header says: `collate` is the locale (collcollate) it is read and kept under. */
209
+ export async function readLibcOrder(source, collate, version, collation) {
210
+ const query = collation === undefined ? source : (sql) => source(collatedSql(sql, collation));
211
+ const o = emptyLibcOrder(collate, version);
212
+ const held = () => holdLibcOrder(o, async (pairs) => {
213
+ const less = await askedText(query, pairs, "s < t");
214
+ const more = await askedText(query, pairs, "s > t");
215
+ return pairs.map((_, i) => less[i] ? -1 : more[i] ? 1 : 0);
216
+ });
217
+ // a strcoll with no level glibc's has, another C library's: not followed, but for the plain table's name, which is
218
+ // never refused: the first level where that is all there is, the bytes for the rest, and what that differs on recorded
219
+ const noLevels = async () => {
220
+ if (!isPlainTable(collate)) {
221
+ o.unread = true;
222
+ return o;
223
+ }
224
+ o.uncovered.fill(0);
225
+ await held();
226
+ return o;
227
+ };
228
+ const inOrder = await asked(query, BYTE_ORDER_SAMPLE.slice(1).map((g, i) => [BYTE_ORDER_SAMPLE[i], g]), "chr(a) < chr(b)");
229
+ if (inOrder.every(Boolean))
230
+ return { ...o, bytes: true };
52
231
  const points = assignedCodePoints();
53
- const l1 = new Uint32Array(MAX), l4 = new Uint32Array(MAX), uncovered = new Uint8Array(MAX).fill(1);
232
+ points.push(UNLISTED);
54
233
  // each character: whether it has a first-level weight ('b'||c < 'ba' and c||'b' > 'aa' only for one that has none);
55
234
  // those that have one in the database's order, each marked when its neighbour below is of its class; the rest in
56
235
  // the database's order, each marked when its neighbour below has the same last-level weight (pp > c only then)
@@ -63,27 +242,135 @@ export async function readLibcOrder(query, collate, version) {
63
242
  // each kind in the database's order
64
243
  rows.sort((a, b) => a[0] === b[0] ? Number(a[3]) - Number(b[3]) : a[0] < b[0] ? -1 : 1);
65
244
  const weighted = [];
66
- const ignoredClasses = []; // one character per last-level weight of the ignored ones, in order
67
- let k = 0;
245
+ const classes = []; // the characters of each first-level class, in the database's order
246
+ const noFirst = [];
68
247
  for (const [kind, g0, same] of rows) {
69
248
  const g = Number(g0);
70
- uncovered[g] = UNCOVERED.test(String.fromCodePoint(g)) ? 1 : 0;
71
249
  if (kind === "n") {
72
- if (same !== "true")
73
- k++;
74
- l1[g] = k;
250
+ if (same !== "true" || classes.length === 0)
251
+ classes.push([]);
252
+ classes.at(-1).push(g);
75
253
  weighted.push(g);
254
+ o.l1[g] = classes.length;
76
255
  }
77
- else {
78
- if (same !== "true" || ignoredClasses.length === 0)
79
- ignoredClasses.push(g);
80
- l4[g] = 2 * ignoredClasses.length;
256
+ else
257
+ noFirst.push({ g, same: same === "true" });
258
+ }
259
+ if (weighted.length === 0 || noFirst.length === 0)
260
+ return noLevels();
261
+ for (const g of points)
262
+ o.uncovered[g] = 0;
263
+ /** a character's place among those the last level compares it with: its first-level class's, or all the marks */
264
+ const ties = new Map();
265
+ const nfd = (g) => cps(String.fromCodePoint(g).normalize("NFD"));
266
+ // of the characters with no first-level weight, those with a second-level one: a mark's, above BASE, outranks the case
267
+ // of a later letter. The others sort below them: each last-level class one character, in order
268
+ const isMark = await asked(query, noFirst.map((c) => [c.g, c.g]), "'x' || chr(a) || 'x' > 'xX'");
269
+ const ignoredClasses = [];
270
+ const marksRead = [];
271
+ noFirst.forEach(({ g, same }, i) => {
272
+ if (isMark[i]) {
273
+ marksRead.push(g);
274
+ return;
275
+ }
276
+ if (!same || ignoredClasses.length === 0)
277
+ ignoredClasses.push(g);
278
+ o.l4[g] = 2 * ignoredClasses.length;
279
+ });
280
+ if (ignoredClasses.length === 0)
281
+ return noLevels();
282
+ // the marks at the second level: in order behind a letter, each class of equal ones, and their case within
283
+ const marks = marksRead.length === 0 ? [] : (await query(`SELECT g::text FROM unnest(${ints(marksRead)}) AS u(g) ORDER BY 'x' || chr(g)`)).map(([g]) => Number(g));
284
+ const pairsOf = (xs) => xs.slice(1).map((x, i) => [xs[i], x]);
285
+ const mark = marks[0];
286
+ /** the two are equal at the first level: and at the second? (only a later mark told them apart) */
287
+ const sameSecond = (x, y) => `${x} || 'x' || chr(${mark}) > ${y} || 'x'`;
288
+ const sameCase = (x, y) => `${x} || 'X' > ${y} || 'x'`;
289
+ const markClasses = []; // the marks of each second-level class, in order
290
+ if (mark !== undefined) {
291
+ const adjacent = pairsOf(marks);
292
+ const [second, third] = [await asked(query, adjacent, sameSecond("'x' || chr(a)", "'x' || chr(b)")), await asked(query, adjacent, sameCase("'x' || chr(a)", "'x' || chr(b)"))];
293
+ let rank = 0;
294
+ marks.forEach((g, i) => {
295
+ if (i === 0 || !second[i - 1]) {
296
+ markClasses.push([]);
297
+ rank = 0;
298
+ }
299
+ else if (!third[i - 1])
300
+ rank++;
301
+ markClasses.at(-1).push(g);
302
+ o.l2[g] = markPlace(markClasses.length - 1);
303
+ o.l3[g] = caseOf(rank);
304
+ // (a skipped mark, the module's header, is compared at the last level with a mark of another class: all in one order)
305
+ ties.set(g, i);
306
+ });
307
+ }
308
+ // each class of the first level: which level tells its neighbours apart
309
+ const groupOf = new Map();
310
+ {
311
+ const adjacent = classes.flatMap(pairsOf);
312
+ const sameSeconds = mark === undefined ? adjacent.map(() => true) : await asked(query, adjacent, sameSecond("chr(a)", "chr(b)"));
313
+ const sameThirds = await asked(query, adjacent, sameCase("chr(a)", "chr(b)"));
314
+ let at = 0;
315
+ for (const cls of classes) {
316
+ let second = 0, third = 0;
317
+ cls.forEach((g, i) => {
318
+ if (i > 0) {
319
+ if (!sameSeconds[at]) {
320
+ second++;
321
+ third = 0;
322
+ }
323
+ else if (!sameThirds[at])
324
+ third++;
325
+ at++;
326
+ }
327
+ groupOf.set(g, { second, third });
328
+ // (`é` and `e` + U+0301 are equal down to here: the last level compares a letter with its class's others)
329
+ ties.set(g, i);
330
+ });
81
331
  }
82
332
  }
83
- if (weighted.length === 0 || ignoredClasses.length === 0)
84
- throw new Error(`reading how the database orders text (${collate}) found no ${weighted.length === 0 ? "letters" : "punctuation"}: Mason cannot sort text as it does`);
85
- // expansions: proposed by each character's compatibility decomposition (its marks dropped) and the letters NFKD keeps
86
- // whole, kept where the source says the two are equal at the first level (each sorts after the other given a later b)
333
+ // which elements are read backward at the second level, and which have no second level at all (CJK's, here): a
334
+ // character x then a mark m is [BASE, m], m then x [m, BASE], and they turn round when x is backward; closed by a letter
335
+ // the run of the backward one skips the element before its last (the module's header), which tells it from one with none
336
+ const noSecond = new Set();
337
+ if (mark !== undefined) {
338
+ const each = weighted.map((g) => [g, mark]);
339
+ const forward = await asked(query, each, "chr(a) || chr(b) < chr(b) || chr(a)");
340
+ const backward = await asked(query, each, "chr(b) || chr(a) || 'X' < chr(a) || chr(b) || 'x'");
341
+ weighted.forEach((g, i) => { if (!forward[i]) {
342
+ if (backward[i])
343
+ o.back[g] = 1;
344
+ else
345
+ noSecond.add(g);
346
+ } });
347
+ const [low, high] = [markClasses[0]?.[0], markClasses[1]?.[0]];
348
+ if (low !== undefined && high !== undefined) {
349
+ const rest = noFirst.map((c) => c.g);
350
+ const turned = await asked(query, rest.map((g) => [g, g]), `'x' || chr(${low}) || chr(a) || chr(${high}) || 'x' > 'x' || chr(${high}) || chr(a) || chr(${low}) || 'x'`);
351
+ rest.forEach((g, i) => { if (turned[i])
352
+ o.back[g] = 1; });
353
+ }
354
+ }
355
+ // a mark of two (U+0344): those two
356
+ for (const g of marks) {
357
+ const parts = nfd(g);
358
+ if (parts.length > 1 && parts.every((p) => o.l2[p] > 0))
359
+ o.sequences.set(g, { ...joined(parts.map((p) => elementOf(o, p))), back: o.back[g] === 1 });
360
+ }
361
+ // the plain ones' second and third levels: the first of a class's accents is BASE, each case its rank
362
+ for (const g of weighted) {
363
+ if (noSecond.has(g))
364
+ continue;
365
+ const { second, third } = groupOf.get(g);
366
+ o.l2[g] = 1;
367
+ o.l3[g] = second === 0 ? caseOf(third) : 1;
368
+ }
369
+ /** whether each pair of strings is equal at the second level, and at the first three */
370
+ const equalSecond = (ps) => mark === undefined ? Promise.resolve(ps.map(() => true)) : askedText(query, ps, `(${sameSecond("s", "t")}) AND (${sameSecond("t", "s")})`);
371
+ const equalAt = (ps) => askedText(query, ps, `(s || 'y' > t || 'x') AND (t || 'y' > s || 'x') ${mark === undefined ? "" : `AND (${sameSecond("s", "t")}) AND (${sameSecond("t", "s")})`} AND (${sameCase("s", "t")}) AND (${sameCase("t", "s")})`);
372
+ // a ligature's first level: proposed by its compatibility decomposition (its marks dropped) and the letters NFKD keeps
373
+ // whole, kept where the source says the two are equal at the first level
87
374
  const proposed = [];
88
375
  for (const g of weighted) {
89
376
  const d = String.fromCodePoint(g).normalize("NFKD").replace(/\p{M}/gu, "");
@@ -93,98 +380,577 @@ export async function readLibcOrder(query, collate, version) {
93
380
  for (const [ch, e] of [["ß", "ss"], ["ẞ", "SS"], ["æ", "ae"], ["Æ", "AE"], ["œ", "oe"], ["Œ", "OE"], ["þ", "th"], ["Þ", "TH"]])
94
381
  proposed.push([ch.codePointAt(0), e]);
95
382
  const expansions = new Map();
96
- for (let at = 0; at < proposed.length; at += 5000) {
97
- const part = proposed.slice(at, at + 5000);
98
- const eq = await query(`SELECT g::text, ((chr(g) || 'b' > e || 'a') AND (e || 'b' > chr(g) || 'a'))::text FROM unnest(${ints(part.map((p) => p[0]))}, ${texts(part.map((p) => p[1]))}) AS u(g, e)`);
99
- eq.forEach(([g, yes], j) => { if (yes === "true")
100
- expansions.set(Number(g), [...part[j][1]].map((c) => c.codePointAt(0))); });
101
- }
102
- // each first-level character's last-level weight, among the ignored ones' by binary search
103
- const lo = new Int32Array(weighted.length), hi = new Int32Array(weighted.length).fill(ignoredClasses.length);
104
- for (;;) {
105
- const idx = [], mid = [];
106
- for (let i = 0; i < weighted.length; i++)
107
- if (lo[i] < hi[i]) {
108
- idx.push(i);
109
- mid.push((lo[i] + hi[i]) >> 1);
383
+ {
384
+ const eq = await askedText(query, proposed.map(([g, e]) => [String.fromCodePoint(g), e]), "(s || 'b' > t || 'a') AND (t || 'b' > s || 'a')");
385
+ // (of the units with a first-level weight: `⅖` is 2, 5, the slash between them weighs nothing there)
386
+ proposed.forEach(([g, e], i) => { const units = cps(e).filter((u) => o.l1[u] > 0); if (eq[i] && units.length > 1)
387
+ expansions.set(g, units); });
388
+ }
389
+ // a ligature Unicode does not decompose (ȸ, Ꜷ): sorted among its first letter's words, so below that letter and the
390
+ // highest character; each unit found by binary search among the single letters, kept when the source says equal
391
+ {
392
+ const whole = classes.map((c) => c[0]);
393
+ const highest = whole.at(-1);
394
+ const single = new Set(whole.filter((g) => !expansions.has(g)));
395
+ for (let round = 0; round < 4; round++) {
396
+ const asking = [];
397
+ let below = whole[0];
398
+ for (const g of whole) {
399
+ if (single.has(g) && g !== below)
400
+ asking.push([g, below]);
401
+ if (single.has(g))
402
+ below = g;
403
+ }
404
+ const yes = await asked(query, asking, `chr(a) || 'x' < chr(b) || chr(${highest})`);
405
+ const found = asking.filter((_, i) => yes[i]);
406
+ if (found.length === 0)
407
+ break;
408
+ for (const [g] of found)
409
+ single.delete(g);
410
+ const letters = whole.filter((g) => single.has(g));
411
+ const lo = await searched(found.length, () => letters.length - 1, (open) => askedText(query, open.map(({ i, mid }) => [String.fromCodePoint(found[i][0]), str([found[i][1], letters[mid], highest])]), "s < t"));
412
+ const spelled = found.map(([g, first], i) => [g, str([first, letters[lo[i]]])]);
413
+ const eq = await askedText(query, spelled.map(([g, e]) => [String.fromCodePoint(g), e]), "(s || 'b' > t || 'a') AND (t || 'b' > s || 'a')");
414
+ // (every character of its class is that spelling)
415
+ spelled.forEach(([g, e], i) => { if (eq[i])
416
+ for (const c of classes[o.l1[g] - 1])
417
+ if (!expansions.has(c))
418
+ expansions.set(c, cps(e)); });
419
+ }
420
+ }
421
+ // a ligature of several letters whose spelling the source did not confirm has first-level weights no letter has
422
+ // (`㏩`: 10 and a CJK weight of its own): nothing here can say them. Under the plain table it stays, at the one weight
423
+ // it sorted by, a decided difference (`libc-rare-letter`)
424
+ for (const [g, e] of proposed)
425
+ if (!expansions.has(g) && e === String.fromCodePoint(g).normalize("NFKD").replace(/\p{M}/gu, "") && cps(e).filter((u) => o.l1[u] > 0).length > 1 && !isPlainTable(collate))
426
+ o.uncovered[g] = 1;
427
+ // an accented letter: its NFD's parts, once they are read
428
+ const decomposed = new Set(weighted.filter((g) => !expansions.has(g) && nfd(g).length > 1));
429
+ /** a weight of its own, its place among the marks': after `before`, before `after` (`nth` its letter's accent) */
430
+ const place = async (items) => {
431
+ const lo = await searched(items.length, () => markClasses.length, (open) => askedText(query, open.map(({ i, mid }) => [String.fromCodePoint(items[i].g), `${items[i].before}${String.fromCodePoint(markClasses[mid][0])}${items[i].after}`]), "s || 'x' < t || 'x'"));
432
+ const same = await equalSecond(items.map((it, i) => [String.fromCodePoint(it.g), `${it.before}${String.fromCodePoint(markClasses[Math.max(lo[i] - 1, 0)][0])}${it.after}`]));
433
+ return items.map((it, i) => lo[i] > 0 && same[i] ? markPlace(lo[i] - 1) : gapPlace(lo[i], it.nth));
434
+ };
435
+ // a letter of accents its NFD does not spell (ø: BASE and a weight of its own)
436
+ if (markClasses.length > 0) {
437
+ const variants = weighted.filter((g) => !expansions.has(g) && !decomposed.has(g) && !noSecond.has(g) && groupOf.get(g).second > 0);
438
+ const placed = await place(variants.map((g) => ({ g, before: String.fromCodePoint(classes[o.l1[g] - 1][0]), after: "", nth: groupOf.get(g).second })));
439
+ variants.forEach((g, i) => o.sequences.set(g, { l1: [o.l1[g]], l2: [1, placed[i]], l3: [caseOf(groupOf.get(g).third), 1], l4: [], back: o.back[g] === 1 }));
440
+ }
441
+ // a ligature's second level, its letters', or with a weight of its own after the first (ß, æ); its case unit by unit,
442
+ // each searched for among the cases of its letter, the units after it at the highest of theirs
443
+ {
444
+ const list = [...expansions.keys()];
445
+ const units = (g) => expansions.get(g);
446
+ const plain = await equalSecond(list.map((g) => [String.fromCodePoint(g), str(units(g))]));
447
+ const ladder = (u) => {
448
+ const out = [];
449
+ for (const g of classes[o.l1[u] - 1] ?? []) {
450
+ const p = groupOf.get(g);
451
+ if (p.second === 0 && p.third === out.length)
452
+ out.push(g);
453
+ }
454
+ return out;
455
+ };
456
+ const flat = list.filter((g, i) => plain[i] && units(g).length <= MAX_UNITS);
457
+ const chosen = new Map(flat.map((g) => [g, []]));
458
+ for (let unit = 0; unit < MAX_UNITS; unit++) {
459
+ const here = flat.filter((g) => units(g).length > unit);
460
+ const lo = await searched(here.length, (i) => ladder(units(here[i])[unit]).length, (open) => askedText(query, open.map(({ i, mid }) => {
461
+ const g = here[i], us = units(g);
462
+ const done = chosen.get(g).map((r, k) => ladder(us[k])[r] ?? ladder(us[k]).at(-1) ?? us[k]);
463
+ const top = us.slice(unit + 1).map((u) => ladder(u).at(-1) ?? u);
464
+ return [String.fromCodePoint(g), str([...done, ladder(us[unit])[mid] ?? us[unit], ...top])];
465
+ }), "s || 'x' < t || 'X'"));
466
+ here.forEach((g, i) => chosen.get(g).push(lo[i]));
467
+ }
468
+ // a case no letter has (a fraction's, a circled digit's) lies between two ranks: those of one spelling in their own order
469
+ const exact = await equalAt(flat.map((g) => [String.fromCodePoint(g), str(chosen.get(g).map((r, k) => ladder(units(g)[k])[r] ?? ladder(units(g)[k]).at(-1) ?? units(g)[k]))]));
470
+ const between = flat.filter((_, i) => !exact[i]);
471
+ const nthOf = new Map();
472
+ if (between.length > 0) {
473
+ const spelledAs = (g) => `${units(g).join(",")}/${chosen.get(g).join(",")}`;
474
+ const sorted = (await query(`SELECT g::text FROM unnest(${ints(between)}) AS u(g) ORDER BY chr(g)`)).map(([g]) => Number(g));
475
+ const same = await asked(query, pairsOf(sorted), sameCase("chr(a)", "chr(b)"));
476
+ let nth = 0;
477
+ sorted.forEach((g, i) => { if (i > 0 && spelledAs(sorted[i - 1]) === spelledAs(g) && !same[i - 1])
478
+ nth++;
479
+ else if (i > 0 && spelledAs(sorted[i - 1]) !== spelledAs(g))
480
+ nth = 0; nthOf.set(g, nth); });
481
+ }
482
+ const own = list.filter((_, i) => !plain[i]);
483
+ const placed = new Map(markClasses.length === 0 ? [] : (await place(own.map((g) => ({ g, before: String.fromCodePoint(units(g)[0]), after: str(units(g).slice(1)), nth: groupOf.get(g).second + 1 })))).map((p, i) => [own[i], p]));
484
+ list.forEach((g, i) => {
485
+ const es = units(g).map((u) => elementOf(o, u));
486
+ const whole = joined(es);
487
+ if (plain[i]) {
488
+ const cases = chosen.get(g), nth = nthOf.get(g);
489
+ if (cases !== undefined)
490
+ whole.l3 = es.flatMap((e, k) => e.l3.length === 0 ? [] : [nth === undefined || cases[k] === 0 ? caseOf(cases[k]) : caseBelow(cases[k], nth), ...e.l3.slice(1)]);
491
+ }
492
+ else {
493
+ const at = es[0].l2.length;
494
+ whole.l2 = [...whole.l2.slice(0, at), placed.get(g) ?? 1, ...whole.l2.slice(at)];
495
+ whole.l3 = [caseOf(groupOf.get(g).third), ...whole.l3.map(() => 1)];
110
496
  }
111
- if (idx.length === 0)
112
- break;
113
- const below = await query(`SELECT (chr(p) || chr(x) < chr(x))::text FROM unnest(${ints(mid.map((m) => ignoredClasses[m]))}, ${ints(idx.map((i) => weighted[i]))}) WITH ORDINALITY AS u(p, x, n) ORDER BY n`);
114
- below.forEach(([b], j) => { const i = idx[j]; if (b === "true")
115
- lo[i] = mid[j] + 1;
497
+ o.sequences.set(g, { ...whole, back: o.back[g] === 1 });
498
+ });
499
+ }
500
+ // an accented letter equal to its spelling is that spelling: the letter of one mark fewer and its last mark, never
501
+ // its NFD whole, whose run of marks is read backward and skipped (the module's header). So the fewest marks first
502
+ {
503
+ const spelling = (g) => {
504
+ const d = nfd(g), head = cps(str(d.slice(0, -1)).normalize("NFC"));
505
+ return head.length === 1 ? [head[0], d.at(-1)] : d;
506
+ };
507
+ const ordered = [...decomposed].sort((a, b) => nfd(a).length - nfd(b).length);
508
+ for (let size = 2; ordered.some((g) => nfd(g).length === size); size++) {
509
+ const list = ordered.filter((g) => nfd(g).length === size);
510
+ const spelled = await equalAt(list.map((g) => [String.fromCodePoint(g), str(spelling(g))]));
511
+ list.forEach((g, i) => {
512
+ const parts = joined(spelling(g).map((p) => elementOf(o, p)));
513
+ // (equal to a letter of its own, it is that letter: its spelling is a contraction, below)
514
+ if (spelled[i] && (parts.l1.length > 1 || parts.l1[0] === o.l1[g]))
515
+ o.sequences.set(g, { ...parts, back: o.back[g] === 1 });
516
+ else if (groupOf.get(g).second > 0 && !noSecond.has(g))
517
+ o.sequences.set(g, { l1: [o.l1[g]], l2: [1, gapPlace(markClasses.length, groupOf.get(g).second)], l3: [caseOf(groupOf.get(g).third), 1], l4: [], back: o.back[g] === 1 });
518
+ });
519
+ }
520
+ }
521
+ // a symbol the first spelling did not confirm, under the plain table, which refuses nothing: `㏩` is 1, 0 and a weight no
522
+ // character has, so its letters and the class that weight lies above, found by binary search after them. Close: it
523
+ // sorts in its place among what a column holds (`libc-rare-letter`, the rest)
524
+ if (isPlainTable(collate)) {
525
+ const whole = classes.map((c) => c[0]);
526
+ const stuck = proposed.flatMap(([g, e]) => {
527
+ if (expansions.has(g) || o.sequences.has(g) || e !== String.fromCodePoint(g).normalize("NFKD").replace(/\p{M}/gu, ""))
528
+ return [];
529
+ const units = cps(e).filter((u) => o.l1[u] > 0);
530
+ // (a symbol with no Han or Kana unit: findIndex's -1 is `slice(0, -1)`, and its last unit is the searched class)
531
+ const head = units.slice(0, units.findIndex((u) => /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}/u.test(String.fromCodePoint(u))));
532
+ return head.length > 0 && head.length < units.length ? [{ g, head }] : [];
533
+ });
534
+ const lo = await searched(stuck.length, () => whole.length, (open) => askedText(query, open.map(({ i, mid }) => [String.fromCodePoint(stuck[i].g), str([...stuck[i].head, whole[mid]])]), "s < t"));
535
+ stuck.forEach(({ g, head }, i) => {
536
+ const l1 = closedWeights(head.map((u) => o.l1[u]), whole.map((c) => o.l1[c]), lo[i]);
537
+ o.sequences.set(g, { l1, l2: l1.map(() => 1), l3: l1.map(() => 1), l4: [], back: o.back[g] === 1 });
538
+ });
539
+ }
540
+ // the last level: the marks' and the letters' weights, each by binary search among the punctuation's (p||x < x exactly
541
+ // when p's weight is below x's), two of one class and case told apart by their order; one placed below all of them has
542
+ // none when x||chr(1) is not below chr(1)||x
543
+ {
544
+ const placing = [...marks, ...weighted];
545
+ const lo = await searched(placing.length, () => ignoredClasses.length, (open) => asked(query, open.map(({ i, mid }) => [ignoredClasses[mid], placing[i]]), "NOT (chr(a) || chr(b) < chr(b))"));
546
+ const bottom = placing.filter((_, i) => lo[i] === 0);
547
+ const yes = await asked(query, bottom.map((x) => [x, x]), "chr(a) || chr(1) < chr(1) || chr(a)");
548
+ const has = new Set(bottom.filter((_, i) => yes[i]));
549
+ // the weights in one order: the punctuation's even, a letter's the odd one above its place, then its tie
550
+ const SCALE = 1 << 13;
551
+ const keyed = [];
552
+ for (const { g } of noFirst)
553
+ if (o.l4[g] > 0)
554
+ keyed.push([g, o.l4[g] * SCALE]);
555
+ placing.forEach((g, i) => { if (lo[i] > 0 || has.has(g))
556
+ keyed.push([g, (2 * lo[i] + 1) * SCALE + Math.min(ties.get(g) ?? 0, SCALE - 1)]); });
557
+ const dense = new Map([...new Set(keyed.map((k) => k[1]))].sort((a, b) => a - b).map((v, i) => [v, i + 1]));
558
+ for (const g of placing)
559
+ o.l4[g] = 0;
560
+ for (const [g, v] of keyed)
561
+ o.l4[g] = dense.get(v);
562
+ for (const [g, e] of o.sequences)
563
+ e.l4 = o.l4[g] > 0 ? [o.l4[g]] : [];
564
+ }
565
+ // the second level's places made its weights, BASE 1
566
+ {
567
+ const values = new Set([1]);
568
+ for (let g = 0; g < MAX; g++)
569
+ if (o.l2[g] > 0)
570
+ values.add(o.l2[g]);
571
+ for (const e of o.sequences.values())
572
+ for (const v of e.l2)
573
+ values.add(v);
574
+ const dense = new Map([...values].sort((a, b) => a - b).map((v, i) => [v, i + 1]));
575
+ for (let g = 0; g < MAX; g++)
576
+ if (o.l2[g] > 0)
577
+ o.l2[g] = dense.get(o.l2[g]);
578
+ for (const e of o.sequences.values())
579
+ e.l2 = e.l2.map((v) => dense.get(v));
580
+ }
581
+ // contractions: a spelling the source says equals what it spells, where its characters read one by one do not
582
+ {
583
+ const proposals = [];
584
+ const reordered = (vowels, consonants) => {
585
+ for (let v = vowels[0]; v <= vowels[1]; v++)
586
+ for (let c = consonants[0]; c <= consonants[1]; c++)
587
+ proposals.push([[v, c], [c, v]]);
588
+ };
589
+ reordered([0x0e40, 0x0e44], [0x0e01, 0x0e2e]);
590
+ reordered([0x0ec0, 0x0ec4], [0x0e81, 0x0eae]);
591
+ reordered([0x0ec0, 0x0ec4], [0x0edc, 0x0edf]);
592
+ for (const g of points) {
593
+ if (g === UNLISTED)
594
+ continue;
595
+ const ch = String.fromCodePoint(g);
596
+ for (const form of ["NFD", "NFKD"]) {
597
+ const d = cps(ch.normalize(form));
598
+ if (d.length < 2 || d.length > 3 || d.includes(0))
599
+ continue;
600
+ proposals.push([d, [g]]);
601
+ if (d.length === 3) {
602
+ const head = cps(str(d.slice(0, 2)).normalize("NFC"));
603
+ if (head.length === 1)
604
+ proposals.push([[head[0], d[2]], [g]]);
605
+ }
606
+ }
607
+ }
608
+ const levels = (e) => JSON.stringify([e.l1, e.l2, e.l3]);
609
+ const known = (xs) => xs.every((x) => o.uncovered[x] === 0);
610
+ const seen = new Set();
611
+ const differ = proposals.filter(([p, t]) => {
612
+ const k = p.join(",");
613
+ if (seen.has(k) || !known(p) || !known(t) || levels(joined(p.map((x) => elementOf(o, x)))) === levels(joined(t.map((x) => elementOf(o, x)))))
614
+ return false;
615
+ seen.add(k);
616
+ return true;
617
+ });
618
+ const equal = await equalAt(differ.map(([p, t]) => [str(p), str(t)]));
619
+ differ.forEach(([p, t], i) => {
620
+ if (!equal[i])
621
+ return;
622
+ const list = o.contractions.get(p[0]) ?? [];
623
+ list.push({ rest: p.slice(1), element: joined(t.map((x) => elementOf(o, x))) });
624
+ list.sort((a, b) => b.rest.length - a.rest.length);
625
+ o.contractions.set(p[0], list);
626
+ });
627
+ }
628
+ // what was not read: what the table does not list, one weight at the last level, when a sample says so; else not
629
+ // covered, but under the plain table, where it is that all the same and the sample's differences are recorded
630
+ let unreadDiffer = 0, unreadPairs = 0;
631
+ {
632
+ const read = new Set(points);
633
+ const unread = [];
634
+ for (let g = 1; g < MAX; g++)
635
+ if (!(g >= 0xd800 && g <= 0xdfff) && !read.has(g))
636
+ unread.push(g);
637
+ // of one length of UTF-8, held to one of them: two lengths differ at the last level by design
638
+ const firstOf = new Map([[3, UNLISTED]]);
639
+ for (const g of unread)
640
+ if (!firstOf.has(utf8Length(g)))
641
+ firstOf.set(utf8Length(g), g);
642
+ const [p, q] = [ignoredClasses[0], ignoredClasses[1] ?? ignoredClasses[0]];
643
+ const sample = unread.filter((_, i) => i % UNREAD_SAMPLE === 0).map((g) => [g, firstOf.get(utf8Length(g))]);
644
+ const alike = await asked(query, sample, `(chr(a) || 'y' > chr(b) || 'x') AND (chr(b) || 'y' > chr(a) || 'x') AND (chr(a) || 'X' > chr(b) || 'x') AND (chr(b) || 'X' > chr(a) || 'x')
645
+ AND (chr(a) || chr(${q}) > chr(b) || chr(${p})) AND (chr(b) || chr(${q}) > chr(a) || chr(${p}))`);
646
+ const plain = isPlainTable(collate);
647
+ if (plain) {
648
+ unreadPairs = alike.length;
649
+ unreadDiffer = alike.filter((x) => !x).length;
650
+ }
651
+ if (plain || (alike.every(Boolean) && o.l1[UNLISTED] === 0)) {
652
+ const weight = o.l4[UNLISTED];
653
+ if (weight > 0)
654
+ for (const { g } of noFirst)
655
+ if (o.l4[g] === weight)
656
+ o.unlisted[g] = 1;
657
+ for (const g of unread) {
658
+ o.l4[g] = weight;
659
+ o.back[g] = o.back[UNLISTED];
660
+ o.unlisted[g] = 1;
661
+ o.uncovered[g] = 0;
662
+ }
663
+ }
664
+ else
665
+ o.uncovered[UNLISTED] = 1;
666
+ }
667
+ withLookups(o);
668
+ await held();
669
+ o.held = { ...o.held, pairs: o.held.pairs + unreadPairs, differ: o.held.differ + unreadDiffer };
670
+ return o;
671
+ }
672
+ /** Pairs of strings of the covered characters, near each other as a column's values are, by `seed`. */
673
+ function heldPairs(o, seed) {
674
+ // a generator of its own, never Math.random: one source's order reads to one result
675
+ const next = (n) => {
676
+ seed = (seed + 0x6d2b79f5) | 0;
677
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
678
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
679
+ return ((t ^ (t >>> 14)) >>> 0) % n;
680
+ };
681
+ const pool = [];
682
+ for (let g = 0x21; g < MAX; g++)
683
+ if (o.uncovered[g] === 0 && (o.unlisted[g] === 0 || g % 4099 === 0))
684
+ pool.push(g);
685
+ // (ASCII's own pool: a tailoring of punctuation or of `i` shows there first, in the values every column holds)
686
+ const ascii = [];
687
+ for (let g = 0x20; g < 0x7f; g++)
688
+ if (o.uncovered[g] === 0)
689
+ ascii.push(g);
690
+ const latin = pool.filter((g) => g < 0x250 && /\p{L}/u.test(String.fromCodePoint(g)));
691
+ const pairs = [];
692
+ const some = (pick) => {
693
+ for (let i = 0; i < HELD_PAIRS; i++) {
694
+ const a = Array.from({ length: 1 + next(4) }, pick);
695
+ const b = [...a];
696
+ b[next(b.length)] = pick();
697
+ if (next(2) === 0)
698
+ b.splice(next(b.length + 1), 0, pick());
699
+ pairs.push([str(a), str(b)]);
700
+ }
701
+ };
702
+ const of = (xs) => () => xs[next(xs.length)];
703
+ const asciiOf = of(ascii), poolOf = of(pool);
704
+ some(() => next(3) === 0 ? asciiOf() : poolOf());
705
+ some(asciiOf);
706
+ some(of(latin));
707
+ // every two neighbours of the Latin letters alone, and of one first-level class, alone and between letters: what no
708
+ // random pair is likely to meet
709
+ const sortedLatin = latin.map((g) => String.fromCodePoint(g)).sort(keyOrder(o));
710
+ for (let i = 1; i < sortedLatin.length; i++)
711
+ pairs.push([sortedLatin[i - 1], sortedLatin[i]]);
712
+ const byFirst = new Map();
713
+ for (let g = 0x21; g < MAX; g++) {
714
+ if (o.uncovered[g] === 1 || o.unlisted[g] === 1)
715
+ continue;
716
+ const first = libcKey(o, String.fromCodePoint(g)).split(" ")[0];
717
+ if (first === "")
718
+ continue;
719
+ const list = byFirst.get(first);
720
+ if (list === undefined)
721
+ byFirst.set(first, [g]);
116
722
  else
117
- hi[i] = mid[j]; });
118
- }
119
- const bottom = [];
120
- weighted.forEach((g, i) => { l4[g] = 2 * lo[i] + 1; if (lo[i] === 0)
121
- bottom.push(g); });
122
- if (bottom.length) {
123
- const has = await query(`SELECT x::text, (chr(x) || chr(1) < chr(1) || chr(x))::text FROM unnest(${ints(bottom)}) AS u(x)`);
124
- for (const [x, yes] of has)
125
- if (yes !== "true")
126
- l4[Number(x)] = 0;
127
- }
128
- return { collate, version, l1, l4, uncovered, expansions };
129
- }
130
- /** The characters ICU's accent and case levels read: those with a first-level weight, in order. */
131
- export function libcKept(o, s) {
132
- let kept = "";
133
- for (const ch of s)
134
- if (o.l1[ch.codePointAt(0)] > 0)
135
- kept += ch;
136
- return kept;
137
- }
138
- const hex6 = (n) => n.toString(16).padStart(6, "0");
139
- /** ICU's sort key from its accent level on: what follows the first level's terminator (byte 01, at an even offset) */
140
- function afterPrimary(icuKey) {
141
- for (let i = 0; i < icuKey.length; i += 2)
142
- if (icuKey.charCodeAt(i) === 48 && icuKey.charCodeAt(i + 1) === 49)
143
- return icuKey.slice(i + 2);
144
- return "";
723
+ list.push(g);
724
+ }
725
+ for (const list of byFirst.values()) {
726
+ const sorted = list.map((g) => String.fromCodePoint(g)).sort(keyOrder(o));
727
+ for (let i = 1; i < sorted.length; i++)
728
+ for (const [before, after] of [["", ""], ["", "x"], ["x", ""]])
729
+ pairs.push([`${before}${sorted[i - 1]}${after}`, `${before}${sorted[i]}${after}`]);
730
+ }
731
+ return pairs;
732
+ }
733
+ /** below it, the characters most columns hold: ASCII and Latin. A tailoring that moves one is a table not followed */
734
+ const COMMON = 0x250;
735
+ /**
736
+ * WHAT WAS READ IS HELD, the verdict a branch (the module's header), in two rounds on two sets of pairs, every pair
737
+ * counted. The plain table only records. Under any other locale a pair that differs has its rarest character uncovered
738
+ * when one is not common (with it not covered, the other still is and the pair is gone), and a pair of common characters
739
+ * alone leaves nothing to uncover, so the table is not followed: a difference in ASCII or Latin is in every column.
740
+ */
741
+ export async function holdLibcOrder(o, compare) {
742
+ const plain = isPlainTable(o.collate);
743
+ const held = { pairs: 0, differ: 0, common: 0 };
744
+ for (const seed of [0x9e3779b9, 0x51ed270b]) {
745
+ const pairs = heldPairs(o, seed);
746
+ const said = await compare(pairs);
747
+ const differ = pairs.filter(([s, t], i) => keyOrder(o)(s, t) !== said[i]);
748
+ held.pairs += pairs.length;
749
+ held.differ += differ.length;
750
+ for (const [s, t] of differ) {
751
+ const told = [...cps(s).filter((x) => !cps(t).includes(x)), ...cps(t).filter((x) => !cps(s).includes(x))];
752
+ const rare = told.filter((x) => x >= COMMON);
753
+ if (rare.length === 0)
754
+ held.common++;
755
+ if (plain)
756
+ continue;
757
+ if (rare.length > 0)
758
+ o.uncovered[Math.max(...rare)] = 1;
759
+ else
760
+ o.unread = true;
761
+ }
762
+ o.held = held;
763
+ if (o.unread)
764
+ return;
765
+ }
766
+ }
767
+ /** how two texts compare by their keys, a tie by their bytes (as Postgres breaks one) */
768
+ const keyOrder = (o) => (s, t) => {
769
+ const [x, y] = [libcKey(o, s), libcKey(o, t)];
770
+ return x < y ? -1 : x > y ? 1 : Math.sign(Buffer.compare(Buffer.from(s), Buffer.from(t)));
771
+ };
772
+ /**
773
+ * What an order's read leaves to say, for mason status and the log: an order not followed, or the pairs that differ,
774
+ * told into rare characters and common ones (the detector of a glibc that moved them). Null: nothing.
775
+ */
776
+ export function libcOrderNote(o) {
777
+ if (o.bytes)
778
+ return null;
779
+ if (o.unread)
780
+ return `${o.collate} orders text in a way Mason could not read, so ORDER BY, <, min and max of text are refused (Mason gap libc-locale-unread); add COLLATE "C" or an ICU collation to them`;
781
+ if (o.held.differ === 0)
782
+ return null;
783
+ const rare = o.held.differ - o.held.common;
784
+ return `${o.collate} read from the database; ${o.held.differ.toLocaleString("en-US")} of ${o.held.pairs.toLocaleString("en-US")} pairs of strings held to it order otherwise: ` +
785
+ `${rare.toLocaleString("en-US")} of rare characters, ${o.held.common.toLocaleString("en-US")} of ASCII or Latin alone (Mason gaps libc-rare-letter, libc-mark-run-form)`;
786
+ }
787
+ /** The refusal of every text under an order `holdLibcOrder` did not follow: by the gap and the collation, as `libc-uncovered`'s. */
788
+ const unreadRefusal = (o) => new PgEvalError("0A000", `the database's collation ${o.collate} orders text in a way Mason could not read, so text under it is not ordered (${gapMark("libc-locale-unread")})`);
789
+ const utf8Length = (g) => g < 0x80 ? 1 : g < 0x800 ? 2 : g < 0x10000 ? 3 : 4;
790
+ const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
791
+ const hex6 = (n) => HEX[(n >>> 16) & 0xff] + HEX[(n >>> 8) & 0xff] + HEX[n & 0xff];
792
+ /**
793
+ * A level of weights as text that orders as the level does, its common weight (1, the lowest) counted, so a value of
794
+ * plain letters costs one token: a run of n of it before another weight w is `1`, the complement of n (a longer run has
795
+ * the lower weight where a shorter one ended) and w; the run the level ends with is `0` and n (below any `1`, the shorter
796
+ * first).
797
+ */
798
+ function level(xs) {
799
+ let out = "", run = 0;
800
+ for (let i = 0; i < xs.length; i++) {
801
+ const x = xs[i];
802
+ if (x === 1) {
803
+ run++;
804
+ continue;
805
+ }
806
+ out += `1${hex6(0xffffff - run)}${hex6(x)}`;
807
+ run = 0;
808
+ }
809
+ return `${out}0${hex6(run)}`;
145
810
  }
146
811
  /**
147
812
  * The sort key of `s` as one text that orders as glibc orders (bytewise; a space parts the levels, below every hex digit,
148
- * so a shorter level sorts first): the first-level classes, ICU's accent and case levels of `libcKept(s)` (`icuKey`),
149
- * the last level's (position, weight) pairs. A value holding a character not covered is refused, naming it.
813
+ * so a shorter level sorts first): the module's header's four levels. A value holding a character not covered is
814
+ * refused, and every value of an order not followed, by the gap alone.
150
815
  */
151
- export function libcKey(o, s, icuKey) {
816
+ export function libcKey(o, s) {
817
+ if (o.unread)
818
+ throw unreadRefusal(o);
152
819
  let first = "", last = "", at = 0;
153
- for (const ch of s) {
820
+ const second = [], third = [];
821
+ const run = [];
822
+ const push = (e) => {
823
+ if (typeof e !== "number") {
824
+ for (const x of e.l2)
825
+ second.push(x);
826
+ return;
827
+ }
828
+ const x = o.l2[e];
829
+ if (x > 0)
830
+ second.push(x);
831
+ };
832
+ // glibc's run read in reverse; closed by a forward element, it skips the element before its last
833
+ const flush = (closed) => {
834
+ const n = run.length;
835
+ for (let k = n - 1; k >= 0; k--)
836
+ if (!(closed && n >= 2 && k === n - 2))
837
+ push(run[k]);
838
+ run.length = 0;
839
+ };
840
+ for (let i = 0; i < s.length;) {
841
+ const g = s.codePointAt(i);
842
+ i += g > 0xffff ? 2 : 1;
154
843
  at++;
155
- const g = ch.codePointAt(0);
156
- if (o.uncovered[g]) {
157
- throw new PgEvalError("0A000", `the text ${JSON.stringify(s.length > 40 ? `${s.slice(0, 40)}…` : s)} holds U+${g.toString(16).toUpperCase().padStart(4, "0")}, which Mason cannot order as the database's collation ${o.collate} does (${gapMark("libc-uncovered")})`);
844
+ if (o.uncovered[g] === 1) {
845
+ // never the value, nor the character: a refusal reaches any user, of a row a role may hide (issue 128)
846
+ throw new PgEvalError("0A000", `a text being ordered holds a character Mason cannot order as the database's collation ${o.collate} does (${gapMark("libc-uncovered")})`);
847
+ }
848
+ if (o.special[g] === 0) {
849
+ const w = o.l1[g];
850
+ if (w > 0)
851
+ first += hex6(w);
852
+ const c = o.l3[g];
853
+ if (c > 0)
854
+ third.push(c);
855
+ const p = o.l4[g];
856
+ if (p > 0)
857
+ last += hex6(at) + hex6(p);
858
+ if (o.back[g] === 1)
859
+ run.push(g);
860
+ else {
861
+ if (run.length > 0)
862
+ flush(true);
863
+ push(g);
864
+ }
865
+ continue;
866
+ }
867
+ if (o.unlisted[g] === 1) {
868
+ if (run.length > 0 && o.back[g] === 0)
869
+ flush(true);
870
+ for (let n = utf8Length(g); n > 0; n--) {
871
+ last += hex6(at) + hex6(o.l4[g]);
872
+ if (n > 1)
873
+ at++;
874
+ if (o.back[g] === 1)
875
+ run.push(g);
876
+ }
877
+ continue;
878
+ }
879
+ let e = o.sequences.get(g);
880
+ for (const c of o.contractions.get(g) ?? []) {
881
+ let j = i, whole = true;
882
+ for (const r of c.rest) {
883
+ if (s.codePointAt(j) !== r) {
884
+ whole = false;
885
+ break;
886
+ }
887
+ j += r > 0xffff ? 2 : 1;
888
+ }
889
+ if (whole) {
890
+ e = c.element;
891
+ i = j;
892
+ break;
893
+ }
894
+ }
895
+ if (e === undefined)
896
+ e = elementOf(o, g);
897
+ for (const w of e.l1)
898
+ first += hex6(w);
899
+ for (const c of e.l3)
900
+ third.push(c);
901
+ for (let k = 0; k < e.l4.length; k++) {
902
+ if (k > 0)
903
+ at++;
904
+ last += hex6(at) + hex6(e.l4[k]);
905
+ }
906
+ if (e.back)
907
+ run.push(e);
908
+ else {
909
+ if (run.length > 0)
910
+ flush(true);
911
+ push(e);
158
912
  }
159
- if (o.l1[g] > 0)
160
- for (const x of o.expansions.get(g) ?? [g])
161
- if (o.l1[x] > 0)
162
- first += hex6(o.l1[x]);
163
- if (o.l4[g] > 0)
164
- last += hex6(at) + hex6(o.l4[g]);
165
913
  }
166
- return `${first} ${afterPrimary(icuKey)} ${last}`;
914
+ if (run.length > 0)
915
+ flush(false);
916
+ return `${first} ${level(second)} ${level(third)} ${last}`;
167
917
  }
168
- /** The order as bytes, to keep in the copy: the three arrays and the expansions, as JSON. */
918
+ /** an array as its runs of one value, 0 left out: the order is mostly empty, and one value past the assigned code points */
919
+ function runsOf(a) {
920
+ const out = [];
921
+ for (let g = 0; g < MAX;) {
922
+ const v = a[g];
923
+ let end = g + 1;
924
+ while (end < MAX && a[end] === v)
925
+ end++;
926
+ if (v !== 0)
927
+ out.push([g, end - g, v]);
928
+ g = end;
929
+ }
930
+ return out;
931
+ }
932
+ const fromRuns = (a, runs) => { for (const [g, n, v] of runs)
933
+ a.fill(v, g, g + n); };
934
+ /** what `encodeLibcOrder` says first: an order kept by a release that read less is read again (`isCurrentLibcOrder`) */
935
+ const FORM = '{"form":4,';
936
+ export const isCurrentLibcOrder = (text) => text.startsWith(FORM);
937
+ /** The order as bytes, to keep in the copy. */
169
938
  export function encodeLibcOrder(o) {
170
- // only the code points that are not 0 / covered: the arrays are mostly empty
171
- const sparse = (a, skip) => {
172
- const out = [];
173
- for (let g = 0; g < MAX; g++)
174
- if (a[g] !== skip)
175
- out.push([g, a[g]]);
176
- return out;
177
- };
178
- return JSON.stringify({ collate: o.collate, version: o.version, l1: sparse(o.l1, 0), l4: sparse(o.l4, 0), covered: sparse(o.uncovered, 1).map(([g]) => g), expansions: [...o.expansions] });
939
+ return FORM + JSON.stringify({ collate: o.collate, version: o.version, bytes: o.bytes, unread: o.unread, held: o.held, l1: runsOf(o.l1), l2: runsOf(o.l2), l3: runsOf(o.l3), l4: runsOf(o.l4), back: runsOf(o.back), uncovered: runsOf(o.uncovered), unlisted: runsOf(o.unlisted),
940
+ sequences: [...o.sequences], contractions: [...o.contractions] }).slice(1);
179
941
  }
180
942
  export function decodeLibcOrder(text) {
181
943
  const v = JSON.parse(text);
182
- const l1 = new Uint32Array(MAX), l4 = new Uint32Array(MAX), uncovered = new Uint8Array(MAX).fill(1);
183
- for (const [g, x] of v.l1)
184
- l1[g] = x;
185
- for (const [g, x] of v.l4)
186
- l4[g] = x;
187
- for (const g of v.covered)
188
- uncovered[g] = 0;
189
- return { collate: v.collate, version: v.version, l1, l4, uncovered, expansions: new Map(v.expansions) };
944
+ const o = { ...emptyLibcOrder(v.collate, v.version), bytes: v.bytes, unread: v.unread, held: v.held };
945
+ o.uncovered.fill(0);
946
+ fromRuns(o.l1, v.l1);
947
+ fromRuns(o.l2, v.l2);
948
+ fromRuns(o.l3, v.l3);
949
+ fromRuns(o.l4, v.l4);
950
+ fromRuns(o.back, v.back);
951
+ fromRuns(o.uncovered, v.uncovered);
952
+ fromRuns(o.unlisted, v.unlisted);
953
+ o.sequences = new Map(v.sequences);
954
+ o.contractions = new Map(v.contractions);
955
+ return withLookups(o);
190
956
  }