@tikoci/centrs 0.1.9-next.23 → 0.1.9-next.27
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/CHANGELOG.md +163 -0
- package/GLOSSARY.txt +23 -1
- package/README.md +30 -18
- package/commands/api/AGENTS.md +21 -3
- package/commands/api/README.md +18 -7
- package/commands/api/examples.md +1 -1
- package/commands/btest/README.md +9 -0
- package/commands/btest/examples.md +7 -0
- package/commands/devices/README.md +72 -3
- package/commands/devices/examples.md +56 -1
- package/commands/execute/README.md +37 -6
- package/commands/execute/examples.md +24 -5
- package/commands/explain/README.md +675 -64
- package/commands/explain/examples.md +199 -42
- package/commands/retrieve/README.md +35 -3
- package/commands/retrieve/examples.md +114 -1
- package/commands/transfer/README.md +33 -4
- package/commands/transfer/examples.md +46 -0
- package/docs/CLI.md +30 -8
- package/docs/CONSTITUTION.md +14 -9
- package/docs/MATRIX.md +37 -22
- package/docs/errors/cdb/encrypt-failed.md +10 -0
- package/docs/errors/cdb/password-required.md +5 -4
- package/docs/errors/cdb/plaintext-backups.md +11 -0
- package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/input/invalid-routeros-path.md +25 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +9 -5
- package/src/api-fanout.ts +2 -0
- package/src/api.ts +46 -39
- package/src/btest.ts +7 -0
- package/src/cli/api.ts +18 -1
- package/src/cli/btest.ts +9 -0
- package/src/cli/devices.ts +76 -8
- package/src/cli/execute.ts +9 -0
- package/src/cli/explain.ts +206 -47
- package/src/cli/retrieve.ts +9 -0
- package/src/cli/terminal.ts +9 -0
- package/src/cli/transfer.ts +10 -1
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +28 -2
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/menu-path.ts +89 -0
- package/src/core/routeros-errors.ts +25 -0
- package/src/devices.ts +154 -9
- package/src/execute-cmr.ts +2 -8
- package/src/execute-fanout.ts +2 -0
- package/src/execute.ts +55 -20
- package/src/explain/args.ts +115 -10
- package/src/explain/blocks.ts +2 -2
- package/src/explain/catalog.ts +22 -12
- package/src/explain/group-faults.ts +162 -29
- package/src/explain/operators.ts +2 -1
- package/src/explain/pasted-prompt.ts +198 -0
- package/src/explain/pathresolve.ts +2 -2
- package/src/explain/scope-brace.ts +226 -11
- package/src/explain/segment.ts +138 -3
- package/src/explain/symbols.ts +148 -8
- package/src/explain/values.ts +55 -4
- package/src/explain/verbsplit.ts +2 -2
- package/src/explain/write.ts +2 -2
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +397 -43
- package/src/index.ts +10 -0
- package/src/protocols/adapter.ts +90 -10
- package/src/protocols/btest-session.ts +10 -30
- package/src/protocols/ec-srp5.ts +19 -10
- package/src/protocols/mac-telnet-console.ts +2 -2
- package/src/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- package/src/resolver/cdb.ts +58 -2
- package/src/resolver/index.ts +1 -0
- package/src/resolver/quickchr-provider.ts +5 -0
- package/src/resolver/selection.ts +11 -0
- package/src/retrieve-fanout.ts +2 -0
- package/src/retrieve-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +88 -27
- package/src/settings.ts +1 -0
- package/src/terminal.ts +7 -0
- package/src/transfer-fanout.ts +2 -0
- package/src/transfer.ts +117 -27
package/src/explain/args.ts
CHANGED
|
@@ -78,7 +78,12 @@
|
|
|
78
78
|
|
|
79
79
|
import { isScopeBrace, isScopeBraceIn } from "./blocks.ts";
|
|
80
80
|
import { braceSlotAcceptsBrace, braceSlotTakesArray } from "./brace-slots.ts";
|
|
81
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
braceBodyIsScriptLike,
|
|
83
|
+
braceStartsStatements,
|
|
84
|
+
isScriptBodyName,
|
|
85
|
+
scopeNameFromMasked,
|
|
86
|
+
} from "./scope-brace.ts";
|
|
82
87
|
import {
|
|
83
88
|
type MaskedRange,
|
|
84
89
|
maskComments,
|
|
@@ -353,6 +358,34 @@ function braceValueOwner(
|
|
|
353
358
|
return ARGUMENT_NAME.test(name) ? name.toLowerCase() : undefined;
|
|
354
359
|
}
|
|
355
360
|
|
|
361
|
+
/**
|
|
362
|
+
* The `name` of a `name= {…}` brace: the token before the gap ends in a glued
|
|
363
|
+
* `name=` run (#554).
|
|
364
|
+
*
|
|
365
|
+
* {@link braceValueOwner} only sees the `=` glued to the brace (`down={`). A
|
|
366
|
+
* space after the `=` starts a new token, so the brace reads as positional
|
|
367
|
+
* there — but on CHR 7.25 `down= { :beep }` lowers to `down-script=` exactly
|
|
368
|
+
* like the glued spelling, while `comment= {1;2}` is a `syntax error` at the
|
|
369
|
+
* `{`. Only spaces and tabs span the gap: a newline there is a statement
|
|
370
|
+
* boundary, not a gap, and the name run must start its token, so `=down= {`
|
|
371
|
+
* and `down = {` own nothing.
|
|
372
|
+
*/
|
|
373
|
+
function spacedBraceOwner(text: string, at: number): string | undefined {
|
|
374
|
+
let j = at - 1;
|
|
375
|
+
let gap = 0;
|
|
376
|
+
while (text[j] === " " || text[j] === "\t") {
|
|
377
|
+
j--;
|
|
378
|
+
gap++;
|
|
379
|
+
}
|
|
380
|
+
if (gap === 0 || text[j] !== "=") return undefined;
|
|
381
|
+
const nameEnd = j;
|
|
382
|
+
j--;
|
|
383
|
+
while (j >= 0 && /[A-Za-z0-9.-]/.test(text[j] as string)) j--;
|
|
384
|
+
if (j >= 0 && !/[\s([]/.test(text[j] as string)) return undefined;
|
|
385
|
+
const name = text.slice(j + 1, nameEnd);
|
|
386
|
+
return ARGUMENT_NAME.test(name) ? name.toLowerCase() : undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
356
389
|
/**
|
|
357
390
|
* Whether a `{` at `at` may open an array literal in this statement.
|
|
358
391
|
*
|
|
@@ -1715,6 +1748,22 @@ export function comparesNamedValue(
|
|
|
1715
1748
|
return /^[A-Za-z.]/.test(name) && /[\s(]/.test(text[j] as string);
|
|
1716
1749
|
}
|
|
1717
1750
|
|
|
1751
|
+
/** One top-level `{` in command arguments that is not a proven body. */
|
|
1752
|
+
export interface CommandBraceFinding {
|
|
1753
|
+
/** Offset of the `{` in the text passed to the lexer. */
|
|
1754
|
+
at: number;
|
|
1755
|
+
/**
|
|
1756
|
+
* True when `name={` with a script-like body on a non-root command outside
|
|
1757
|
+
* `where`, where `name` is not a proven script-body slot (#525). The brace
|
|
1758
|
+
* may abbreviate one (`down=` for `down-script=`), but telling it from
|
|
1759
|
+
* `comment=` needs the command's argument list, which is schema — so the
|
|
1760
|
+
* caller warns instead of rejecting, and the device decides.
|
|
1761
|
+
*/
|
|
1762
|
+
ambiguous: boolean;
|
|
1763
|
+
/** The `name` of `name=`, lower-cased, when `ambiguous` is true. */
|
|
1764
|
+
name?: string;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1718
1767
|
/**
|
|
1719
1768
|
* Locate top-level `{...}` values that RouterOS rejects in command arguments.
|
|
1720
1769
|
*
|
|
@@ -1724,15 +1773,21 @@ export function comparesNamedValue(
|
|
|
1724
1773
|
* excluded: their brace is a script body, not an array. The caller applies
|
|
1725
1774
|
* this only to non-root commands; root scripting directives use the
|
|
1726
1775
|
* `(verb, slot)` table instead.
|
|
1776
|
+
*
|
|
1777
|
+
* A `name={` with a script-like body that is not a proven slot is returned
|
|
1778
|
+
* with `ambiguous: true` rather than as an error (#525), and so is the spaced
|
|
1779
|
+
* `name= {…}` spelling (`down= { :beep }` lowers to `down-script=` on CHR
|
|
1780
|
+
* 7.25). Array-content braces (`comment={1;2}`), positional braces, `where`
|
|
1781
|
+
* comparisons and root slots keep their rejection.
|
|
1727
1782
|
*/
|
|
1728
1783
|
export function invalidCommandBraceOffsets(
|
|
1729
1784
|
text: string,
|
|
1730
1785
|
from: number,
|
|
1731
1786
|
directiveVerb?: string,
|
|
1732
1787
|
range?: MaskedRange,
|
|
1733
|
-
):
|
|
1788
|
+
): CommandBraceFinding[] {
|
|
1734
1789
|
const structural = maskOf(range) ?? maskComments(text);
|
|
1735
|
-
const offsets:
|
|
1790
|
+
const offsets: CommandBraceFinding[] = [];
|
|
1736
1791
|
const stack: string[] = [];
|
|
1737
1792
|
let i = Math.max(0, from);
|
|
1738
1793
|
let tokenStart: number | null = null;
|
|
@@ -1783,7 +1838,54 @@ export function invalidCommandBraceOffsets(
|
|
|
1783
1838
|
tokenStart === null
|
|
1784
1839
|
? undefined
|
|
1785
1840
|
: braceValueOwner(structural, tokenStart, i);
|
|
1786
|
-
|
|
1841
|
+
// A space after the `=` owns the brace the same way the glued `=`
|
|
1842
|
+
// does (#554), so the slot attributes the spaced owner before
|
|
1843
|
+
// falling back to positional: `:put x= { :beep }` reads slot `x`,
|
|
1844
|
+
// not `#0` (which `put` accepts, swallowing the error).
|
|
1845
|
+
const spacedOwner =
|
|
1846
|
+
typeof owner === "string" ? undefined : spacedBraceOwner(structural, i);
|
|
1847
|
+
const slot = owner === null ? (spacedOwner ?? `#${positionals}`) : owner;
|
|
1848
|
+
// A `where` named comparison reads the brace with the property's own
|
|
1849
|
+
// parser and rejects it (`expected <name> value`), so the #525 body
|
|
1850
|
+
// guess must not shield it below — `gateway={ :beep }` included.
|
|
1851
|
+
const inWhereComparison =
|
|
1852
|
+
directiveVerb === undefined &&
|
|
1853
|
+
whereAt !== null &&
|
|
1854
|
+
comparesNamedValue(structural, i, whereAt);
|
|
1855
|
+
// The ambiguity check comes first: `braceStartsStatements` reads an
|
|
1856
|
+
// unproven `name={` with a script-like body as a body too (#525), so
|
|
1857
|
+
// reaching that skip would swallow the warning that marks the guess.
|
|
1858
|
+
// Control scopes (`do={` on a monitor, `:retry … command={`) are not
|
|
1859
|
+
// guesses — the scope table already proves them — so they stay out.
|
|
1860
|
+
// A space after the `=` owns the brace the same way the glued `=`
|
|
1861
|
+
// does (`down= { :beep }` lowers to `down-script=` on CHR 7.25), so
|
|
1862
|
+
// the spaced owner warns under the same gates.
|
|
1863
|
+
const scopeName =
|
|
1864
|
+
directiveVerb === undefined && whereAt === null
|
|
1865
|
+
? range === undefined
|
|
1866
|
+
? scopeNameFromMasked(structural, i)
|
|
1867
|
+
: scopeNameFromMasked(range.masked, range.start + i, range.start)
|
|
1868
|
+
: null;
|
|
1869
|
+
const ambiguousOwner = typeof owner === "string" ? owner : spacedOwner;
|
|
1870
|
+
if (
|
|
1871
|
+
directiveVerb === undefined &&
|
|
1872
|
+
whereAt === null &&
|
|
1873
|
+
scopeName === null &&
|
|
1874
|
+
typeof ambiguousOwner === "string" &&
|
|
1875
|
+
!isScriptBodyName(ambiguousOwner) &&
|
|
1876
|
+
braceBodyIsScriptLike(structural, i, end ?? structural.length)
|
|
1877
|
+
) {
|
|
1878
|
+
offsets.push({ at: i, ambiguous: true, name: ambiguousOwner });
|
|
1879
|
+
i = end ?? i + 1;
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
// A root directive's slots and a `where` named comparison never take
|
|
1883
|
+
// a script body through an unproven name: `:put x={ :beep }` and
|
|
1884
|
+
// `where gateway={ :beep }` are device rejections, so the brace reads
|
|
1885
|
+
// as a VALUE for the body check and the error below stands (#554).
|
|
1886
|
+
// Proven slots still skip — the gate is only the unproven disjunct.
|
|
1887
|
+
const unprovenName =
|
|
1888
|
+
directiveVerb !== undefined || inWhereComparison ? "value" : "body";
|
|
1787
1889
|
if (
|
|
1788
1890
|
// Asked of the DOCUMENT when the caller anchored this statement, so the
|
|
1789
1891
|
// statement index behind it is the document's one index rather than a
|
|
@@ -1791,19 +1893,22 @@ export function invalidCommandBraceOffsets(
|
|
|
1791
1893
|
// subtree, and so was rebuilt once per enclosing level (#322). The floor
|
|
1792
1894
|
// makes the two readings the same answer (`scope-brace.ts`).
|
|
1793
1895
|
(range === undefined
|
|
1794
|
-
? braceStartsStatements(structural, i)
|
|
1795
|
-
: braceStartsStatements(
|
|
1896
|
+
? braceStartsStatements(structural, i, 0, unprovenName)
|
|
1897
|
+
: braceStartsStatements(
|
|
1898
|
+
range.masked,
|
|
1899
|
+
range.start + i,
|
|
1900
|
+
range.start,
|
|
1901
|
+
unprovenName,
|
|
1902
|
+
)) ||
|
|
1796
1903
|
(directiveVerb !== undefined &&
|
|
1797
1904
|
slot !== undefined &&
|
|
1798
1905
|
braceSlotAcceptsBrace(directiveVerb, slot)) ||
|
|
1799
|
-
(directiveVerb === undefined &&
|
|
1800
|
-
whereAt !== null &&
|
|
1801
|
-
!comparesNamedValue(structural, i, whereAt))
|
|
1906
|
+
(directiveVerb === undefined && whereAt !== null && !inWhereComparison)
|
|
1802
1907
|
) {
|
|
1803
1908
|
i = end ?? structural.length;
|
|
1804
1909
|
continue;
|
|
1805
1910
|
}
|
|
1806
|
-
offsets.push(i);
|
|
1911
|
+
offsets.push({ at: i, ambiguous: false });
|
|
1807
1912
|
i = end ?? i + 1;
|
|
1808
1913
|
}
|
|
1809
1914
|
return offsets;
|
package/src/explain/blocks.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Block / scope-brace classification for `explain` (centrs canonicalizer).
|
|
3
3
|
*
|
|
4
|
-
* Ratified by the phase-0 lab, question Q2 (#185)
|
|
5
|
-
*
|
|
4
|
+
* Ratified by the phase-0 lab, question Q2 (#185); the lab probe it was
|
|
5
|
+
* promoted from is history (see #185), not a runnable method. Q2 asks: can an offline,
|
|
6
6
|
* schema-free brace tracker tell a `{…}` SCOPE (`do=`/`else=`/`on-error=`, which
|
|
7
7
|
* IL lowers to `name=;(evl …)`) from a `name={…}` LITERAL value (braces
|
|
8
8
|
* stripped, body kept as RAW text, never lowered)? Descending into a literal
|
package/src/explain/catalog.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Union of two first-order sources (#228):
|
|
10
10
|
*
|
|
11
11
|
* 1. MikroTik's published CLI Reference (`https://manual.mikrotik.com/docs/cli-reference/`),
|
|
12
|
-
* 1,
|
|
12
|
+
* 1,109 pages, 1,116 entries — first-order about the definition
|
|
13
13
|
* structs and their build-time gates. Since #285 every page is a leaf whose
|
|
14
14
|
* slug is the CLI path, so no spelling has to be rewritten to be looked up.
|
|
15
15
|
* 2. 8 pinned restraml `/console/inspect` trees
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
*
|
|
29
29
|
* | Kind | Total | `both` | `inspect` | `published` |
|
|
30
30
|
* | ---- | ----- | ------ | --------- | ----------- |
|
|
31
|
-
* | `menu` |
|
|
32
|
-
* | `command` |
|
|
33
|
-
* | `settings` |
|
|
31
|
+
* | `menu` | 571 | 457 | 67 | 47 |
|
|
32
|
+
* | `command` | 483 | 436 | 0 | 47 |
|
|
33
|
+
* | `settings` | 122 | 112 | 0 | 10 |
|
|
34
34
|
*
|
|
35
35
|
* Zero navigation-vs-command contradictions between the two sources. Generation
|
|
36
36
|
* aborts if that ever stops holding, rather than picking a winner. Where a path
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
*
|
|
50
50
|
* **Gates conjoin down a path, so read them with {@link effectiveGates}, not
|
|
51
51
|
* row by row.** A row states only what the publication stated at that entry.
|
|
52
|
-
* Read row-wise,
|
|
52
|
+
* Read row-wise, 7 published-only paths look ungated; read with
|
|
53
53
|
* ancestry, the residue carrying no published explanation for its absence at
|
|
54
|
-
* all is
|
|
54
|
+
* all is 7.
|
|
55
55
|
*
|
|
56
56
|
* Tree COMMANDS are not enumerated — those are the generic CRUD leaves
|
|
57
57
|
* `verbs.ts` already owns. The command rows here are the published,
|
|
@@ -91,7 +91,7 @@ export interface CatalogEntry {
|
|
|
91
91
|
* `path|kind|provenance|package|conditions|syscap` — with trailing empty
|
|
92
92
|
* columns trimmed. Lower-cased, slash-led and sorted by path.
|
|
93
93
|
*
|
|
94
|
-
* Text rather than ~1,
|
|
94
|
+
* Text rather than ~1,176 object literals so that one path is one line in a
|
|
95
95
|
* review diff, and so the formatter has nothing to re-wrap.
|
|
96
96
|
*/
|
|
97
97
|
const ROWS = `
|
|
@@ -101,6 +101,7 @@ const ROWS = `
|
|
|
101
101
|
/app/remove|command|both|container||app
|
|
102
102
|
/app/restart|command|both|container||app
|
|
103
103
|
/app/settings|settings|both|container||app
|
|
104
|
+
/app/settings/remove-certificate|command|published|container||app
|
|
104
105
|
/app/setup|command|both|container||app
|
|
105
106
|
/app/update|command|both|container||app
|
|
106
107
|
/beep|command|both
|
|
@@ -160,9 +161,9 @@ const ROWS = `
|
|
|
160
161
|
/cmr/alert|menu|both|cmr
|
|
161
162
|
/cmr/alert/show-devices|command|both|cmr
|
|
162
163
|
/cmr/alert/test|command|both|cmr
|
|
163
|
-
/cmr/client|settings|both||!smips
|
|
164
|
-
/cmr/client/forget|command|both||!smips
|
|
165
|
-
/cmr/client/pair|command|both||!smips
|
|
164
|
+
/cmr/client|settings|both||!smips
|
|
165
|
+
/cmr/client/forget|command|both||!smips
|
|
166
|
+
/cmr/client/pair|command|both||!smips
|
|
166
167
|
/cmr/client/push-button|command|both
|
|
167
168
|
/cmr/device|menu|both|cmr
|
|
168
169
|
/cmr/device/apptraffic|command|both|cmr
|
|
@@ -485,6 +486,12 @@ const ROWS = `
|
|
|
485
486
|
/interface/wifi/dashboard/show-logs|command|both
|
|
486
487
|
/interface/wifi/datapath|menu|both
|
|
487
488
|
/interface/wifi/devel|command|both
|
|
489
|
+
/interface/wifi/easymesh|settings|published
|
|
490
|
+
/interface/wifi/easymesh/bss|menu|published
|
|
491
|
+
/interface/wifi/easymesh/dpp-trigger|command|published
|
|
492
|
+
/interface/wifi/easymesh/sta|menu|published
|
|
493
|
+
/interface/wifi/easymesh/topology|menu|published
|
|
494
|
+
/interface/wifi/easymesh/wps-push-button|command|published
|
|
488
495
|
/interface/wifi/flat-snoop|command|both
|
|
489
496
|
/interface/wifi/frequency-scan|command|both
|
|
490
497
|
/interface/wifi/interworking|menu|both
|
|
@@ -1095,6 +1102,9 @@ const ROWS = `
|
|
|
1095
1102
|
/system/resource/cpu|menu|both
|
|
1096
1103
|
/system/resource/hardware|menu|both||!powerpc, !smips
|
|
1097
1104
|
/system/resource/hardware/authorize|command|both||!powerpc, !smips
|
|
1105
|
+
/system/resource/hardware/power-cycle|command|published||!powerpc, !smips|usb-hub
|
|
1106
|
+
/system/resource/hardware/power-reset|command|published||!powerpc, !smips, !i386
|
|
1107
|
+
/system/resource/hardware/reset-overcurrent|command|published||!powerpc, !smips|usb-hub
|
|
1098
1108
|
/system/resource/hardware/usb-power-reset|command|both||!powerpc, !smips, i386
|
|
1099
1109
|
/system/resource/hardware/usb-settings|settings|both||!powerpc, !smips
|
|
1100
1110
|
/system/resource/irq|menu|both
|
|
@@ -1373,8 +1383,8 @@ export interface CatalogGate {
|
|
|
1373
1383
|
* parent's `syscap` applies even though the child entry states none: the gates
|
|
1374
1384
|
* up a path CONJOIN, they do not override.
|
|
1375
1385
|
*
|
|
1376
|
-
* Read row-wise,
|
|
1377
|
-
* ancestry the residue is
|
|
1386
|
+
* Read row-wise, 7 published-only paths look ungated; read with
|
|
1387
|
+
* ancestry the residue is 7. The gap between the two is the
|
|
1378
1388
|
* conjunction, and it is what a caller has to reproduce: a child's silence about
|
|
1379
1389
|
* a gate is not the absence of one. #228's finding — that a published-only path
|
|
1380
1390
|
* almost always explains its own absence — is about the ancestry-aware number.
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
* after `<name>=` with the property's own parser: `where (comment=+)`,
|
|
30
30
|
* `(comment=a-)`, `(comment=@)` and `(.id=*1)` parse, though each is an
|
|
31
31
|
* expression error elsewhere. So a group in a query gets only the structural
|
|
32
|
-
* checks (empty, `;`, newline)
|
|
33
|
-
* `@` rules.
|
|
32
|
+
* checks (empty, `;`, newline), #509's brace rule and the unquoted-byte
|
|
33
|
+
* rule below, never the operand or `@` rules.
|
|
34
34
|
* - **Arrays (#511).** An array that is the whole value of a statement or an
|
|
35
35
|
* argument must have an element: `:local z {}`, `:put {}`, `:return {}`,
|
|
36
36
|
* `[:len {}]` and `in={}` are errors, while `{1;{}}`, `{a={}}`, `({})`,
|
|
@@ -53,6 +53,21 @@
|
|
|
53
53
|
* declared name that is not letters and digits. `gluedAt`, `wordFaults` and
|
|
54
54
|
* `nameFault` carry the device rules. One token fault is reported per
|
|
55
55
|
* statement, and the head word is never read.
|
|
56
|
+
* - **Unquoted bytes (#358, #371).** A `?`, a backtick and a non-ASCII byte
|
|
57
|
+
* are never part of a bare word: the device rejects each at its byte in a
|
|
58
|
+
* head word, a name, a `$name`, a value, a query, an array and an
|
|
59
|
+
* expression (`/systemé`, `print?`, `$café`, `url=http://x/a?b`,
|
|
60
|
+
* `where comment=café`, `{a`b}`, `(café)`), but a menu command's argument
|
|
61
|
+
* name at the name's first byte (`addré=1`, `a?b=1`). A second `=` in a
|
|
62
|
+
* menu command's argument is rejected too (`name=a=b`, a bare base64 key
|
|
63
|
+
* ending in `=`). In a query and in an array it is an operator and a key
|
|
64
|
+
* (`where comment=a=b`, `{a=b}` parse), and a query group takes the byte
|
|
65
|
+
* rule but no other (`where (comment=a?b)` and `in {a?b}` are rejected at
|
|
66
|
+
* the `?`, `where (comment=a=b)` parses); a directive's `a=b` is a named
|
|
67
|
+
* argument on 7.24.5 and a syntax error on 7.23.8, so it is left alone. No
|
|
68
|
+
* property parser was found that takes these bytes (`regexp=a?b`,
|
|
69
|
+
* `public-key=…=`, `secret=…==`, `on-event=a=b` are all rejected). Strings
|
|
70
|
+
* and comments carry any byte (`"café"`, `$"café"`, `:local "café"`).
|
|
56
71
|
*
|
|
57
72
|
* Deliberately not here: a `$( … )` group (`:put $({})` is rejected and is
|
|
58
73
|
* read as `({})`), the `@` in a later head word (`/ip address@ print`), and
|
|
@@ -72,7 +87,9 @@ export type GroupFaultCode =
|
|
|
72
87
|
| "named-value-brace"
|
|
73
88
|
| "glued-token"
|
|
74
89
|
| "invalid-variable"
|
|
75
|
-
| "invalid-name"
|
|
90
|
+
| "invalid-name"
|
|
91
|
+
| "unquoted-non-ascii"
|
|
92
|
+
| "unquoted-reserved-byte";
|
|
76
93
|
|
|
77
94
|
export interface GroupFault {
|
|
78
95
|
code: GroupFaultCode;
|
|
@@ -84,6 +101,12 @@ export interface GroupFault {
|
|
|
84
101
|
detail?: string;
|
|
85
102
|
/** The group the fault sits in, for a message that must say which. */
|
|
86
103
|
group?: "(" | "{";
|
|
104
|
+
/**
|
|
105
|
+
* Where an unquoted-byte fault sits when it is not in a value: a menu
|
|
106
|
+
* command's argument name, or a statement's head word (path, command,
|
|
107
|
+
* `$name`). The remedy differs, since neither has a value to quote.
|
|
108
|
+
*/
|
|
109
|
+
site?: "argument-name" | "head-word";
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
interface StatementFrame {
|
|
@@ -95,6 +118,8 @@ interface StatementFrame {
|
|
|
95
118
|
whereEnd: number | null;
|
|
96
119
|
/** The head word without its colon, the key into `brace-slots.ts`. */
|
|
97
120
|
verb: string | null;
|
|
121
|
+
/** The head word is a `$` call, whose arguments no menu reads (`$f a=1`). */
|
|
122
|
+
call: boolean;
|
|
98
123
|
/** Positional operands read after the head. */
|
|
99
124
|
positionals: number;
|
|
100
125
|
/** No token of the current statement read yet. */
|
|
@@ -116,6 +141,8 @@ interface StatementToken {
|
|
|
116
141
|
closedAt: number;
|
|
117
142
|
/** Just past the token's last bare-word run, or -1. */
|
|
118
143
|
wordEnd: number;
|
|
144
|
+
/** The token's first bare `=`, or -1. */
|
|
145
|
+
eqAt: number;
|
|
119
146
|
}
|
|
120
147
|
|
|
121
148
|
interface ArrayFrame {
|
|
@@ -201,6 +228,31 @@ const STRICT_NAME_DIRECTIVES = new Set(["local", "global", "foreach"]);
|
|
|
201
228
|
const ALNUM = /[A-Za-z0-9]/;
|
|
202
229
|
const SUB = "\x1a";
|
|
203
230
|
|
|
231
|
+
/** A byte no bare word holds: `?`, a backtick, a non-ASCII stand-in (#358, #371). */
|
|
232
|
+
function isUnquotedFaultByte(c: string | undefined): boolean {
|
|
233
|
+
return c === "?" || c === "`" || c === SUB;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The first such byte in `[from, end)`, or -1. */
|
|
237
|
+
function unquotedFaultByteAt(masked: string, from: number, end: number) {
|
|
238
|
+
for (let j = from; j < end; j++) if (isUnquotedFaultByte(masked[j])) return j;
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The fault for the byte at `at`; a non-ASCII run is reported whole. */
|
|
243
|
+
function unquotedByteFault(masked: string, at: number): GroupFault {
|
|
244
|
+
if (masked[at] !== SUB)
|
|
245
|
+
return {
|
|
246
|
+
code: "unquoted-reserved-byte",
|
|
247
|
+
start: at,
|
|
248
|
+
end: at + 1,
|
|
249
|
+
detail: masked[at],
|
|
250
|
+
};
|
|
251
|
+
let end = at;
|
|
252
|
+
while (masked[end] === SUB) end++;
|
|
253
|
+
return { code: "unquoted-non-ascii", start: at, end };
|
|
254
|
+
}
|
|
255
|
+
|
|
204
256
|
function statementFrame(char: StatementFrame["char"]): StatementFrame {
|
|
205
257
|
return {
|
|
206
258
|
kind: "statement",
|
|
@@ -208,6 +260,7 @@ function statementFrame(char: StatementFrame["char"]): StatementFrame {
|
|
|
208
260
|
query: false,
|
|
209
261
|
whereEnd: null,
|
|
210
262
|
verb: null,
|
|
263
|
+
call: false,
|
|
211
264
|
positionals: 0,
|
|
212
265
|
lead: true,
|
|
213
266
|
token: null,
|
|
@@ -245,6 +298,7 @@ function openToken(frame: StatementFrame, at: number) {
|
|
|
245
298
|
equals: false,
|
|
246
299
|
closedAt: -1,
|
|
247
300
|
wordEnd: -1,
|
|
301
|
+
eqAt: -1,
|
|
248
302
|
};
|
|
249
303
|
frame.lead = false;
|
|
250
304
|
return frame.token;
|
|
@@ -287,12 +341,10 @@ function gluedAt(
|
|
|
287
341
|
(frame.verb !== null || ALNUM.test(before ?? "")));
|
|
288
342
|
if (!glued) return null;
|
|
289
343
|
const c = masked[at];
|
|
290
|
-
// A stray `\` is `symbols.ts`'s, and a non-ASCII stand-in is #371's.
|
|
291
|
-
if (c === "\\" || c === SUB) {
|
|
292
|
-
frame.settled = true;
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
344
|
frame.settled = true;
|
|
345
|
+
// A stray `\` is `symbols.ts`'s; a non-ASCII byte is its own fault (`"x"é`).
|
|
346
|
+
if (c === "\\") return null;
|
|
347
|
+
if (c === SUB) return unquotedByteFault(masked, at);
|
|
296
348
|
return { code: "glued-token", start: at, end: at + 1 };
|
|
297
349
|
}
|
|
298
350
|
|
|
@@ -343,7 +395,21 @@ function wordFaults(
|
|
|
343
395
|
if (named !== undefined) return named;
|
|
344
396
|
}
|
|
345
397
|
while (j < end) {
|
|
346
|
-
|
|
398
|
+
const c = masked[j];
|
|
399
|
+
if (isUnquotedFaultByte(c)) {
|
|
400
|
+
frame.settled = true;
|
|
401
|
+
return (
|
|
402
|
+
argumentNameFault(masked, frame, j, end) ?? unquotedByteFault(masked, j)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (c === "=") {
|
|
406
|
+
if (token.eqAt < 0) token.eqAt = j;
|
|
407
|
+
else if (frame.verb === null && !frame.call) {
|
|
408
|
+
frame.settled = true;
|
|
409
|
+
return unquotedByteFault(masked, j);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (c !== "$") {
|
|
347
413
|
j++;
|
|
348
414
|
continue;
|
|
349
415
|
}
|
|
@@ -367,14 +433,12 @@ function wordFaults(
|
|
|
367
433
|
token.wordEnd = -1;
|
|
368
434
|
return null;
|
|
369
435
|
}
|
|
436
|
+
if (next === SUB) {
|
|
437
|
+
frame.settled = true;
|
|
438
|
+
return unquotedByteFault(masked, j + 1);
|
|
439
|
+
}
|
|
370
440
|
// `$$v` is rejected after the name, not at the second `$`.
|
|
371
|
-
if (
|
|
372
|
-
next === "(" ||
|
|
373
|
-
next === "[" ||
|
|
374
|
-
next === "{" ||
|
|
375
|
-
next === "$" ||
|
|
376
|
-
next === SUB
|
|
377
|
-
) {
|
|
441
|
+
if (next === "(" || next === "[" || next === "{" || next === "$") {
|
|
378
442
|
frame.settled = true;
|
|
379
443
|
return null;
|
|
380
444
|
}
|
|
@@ -390,6 +454,26 @@ function wordFaults(
|
|
|
390
454
|
return null;
|
|
391
455
|
}
|
|
392
456
|
|
|
457
|
+
/**
|
|
458
|
+
* A bad byte at `at` inside a menu command's argument name: the device
|
|
459
|
+
* reports it at the name's first byte (`/ip/address/add addré=1` and
|
|
460
|
+
* `a?b=1` at the `a`), and the span is the name. A directive (`:put café=1`)
|
|
461
|
+
* and a `$` call (`$f café=1`) report at the byte, so this is `null` there.
|
|
462
|
+
*/
|
|
463
|
+
function argumentNameFault(
|
|
464
|
+
masked: string,
|
|
465
|
+
frame: StatementFrame & { token: StatementToken },
|
|
466
|
+
at: number,
|
|
467
|
+
end: number,
|
|
468
|
+
): GroupFault | null {
|
|
469
|
+
const { token } = frame;
|
|
470
|
+
if (frame.verb !== null || frame.call || token.eqAt >= 0) return null;
|
|
471
|
+
const eq = masked.indexOf("=", at);
|
|
472
|
+
if (eq < 0 || eq >= end) return null;
|
|
473
|
+
const fault = unquotedByteFault(masked, at);
|
|
474
|
+
return { ...fault, start: token.start, end: eq, site: "argument-name" };
|
|
475
|
+
}
|
|
476
|
+
|
|
393
477
|
/**
|
|
394
478
|
* The declared-name fault in the word at `at`, `null` when the name is
|
|
395
479
|
* well formed, or `undefined` when the word is not a bare name at all (a
|
|
@@ -473,9 +557,15 @@ const CLOSER = { "(": ")", "[": "]", "{": "}" } as const;
|
|
|
473
557
|
|
|
474
558
|
/**
|
|
475
559
|
* Find every group fault in an analyzed document. `masked` is its comment
|
|
476
|
-
* mask; `text` is read only to tell a masked comment line from spaces.
|
|
560
|
+
* mask; `text` is read only to tell a masked comment line from spaces. The
|
|
561
|
+
* walk starts at `from`, past a leading BOM, which is a positional fact and
|
|
562
|
+
* not a byte of the first word.
|
|
477
563
|
*/
|
|
478
|
-
export function findGroupFaults(
|
|
564
|
+
export function findGroupFaults(
|
|
565
|
+
text: string,
|
|
566
|
+
masked: string,
|
|
567
|
+
from = 0,
|
|
568
|
+
): GroupFault[] {
|
|
479
569
|
const faults: GroupFault[] = [];
|
|
480
570
|
const frames: Frame[] = [statementFrame("")];
|
|
481
571
|
const top = (): Frame => frames[frames.length - 1] as Frame;
|
|
@@ -502,7 +592,7 @@ export function findGroupFaults(text: string, masked: string): GroupFault[] {
|
|
|
502
592
|
// `(1 +\⏎⏎2)`, and reject `(1 +\⏎# c⏎⏎2)` at the blank line.
|
|
503
593
|
let cont: "none" | "escape" | "comment" = "none";
|
|
504
594
|
let contLineStart = false;
|
|
505
|
-
let i =
|
|
595
|
+
let i = from;
|
|
506
596
|
while (i < masked.length) {
|
|
507
597
|
const c = masked[i] as string;
|
|
508
598
|
const frame = top();
|
|
@@ -597,20 +687,27 @@ export function findGroupFaults(text: string, masked: string): GroupFault[] {
|
|
|
597
687
|
(frame.kind === "statement" && frame.char === "[")
|
|
598
688
|
? whereEnd
|
|
599
689
|
: null;
|
|
690
|
+
// A `where` named comparison never takes a script body through an
|
|
691
|
+
// unproven name — `[find where comment={ :beep }]` is `expected
|
|
692
|
+
// comment value` on CHR 7.25 — so the brace reads as a VALUE for
|
|
693
|
+
// the role below and the fault stands (#554). Proven slots still
|
|
694
|
+
// read as bodies; the gate is only the unproven disjunct.
|
|
695
|
+
const inWhereComparison =
|
|
696
|
+
c === "{" && floor !== null && comparesNamedValue(masked, i, floor);
|
|
600
697
|
const role =
|
|
601
698
|
c === "{" && frame.kind === "statement"
|
|
602
|
-
? braceStartsStatements(
|
|
699
|
+
? braceStartsStatements(
|
|
700
|
+
masked,
|
|
701
|
+
i,
|
|
702
|
+
0,
|
|
703
|
+
inWhereComparison ? "value" : "body",
|
|
704
|
+
)
|
|
603
705
|
? "program"
|
|
604
706
|
: directiveSlotRole(masked, i, frame)
|
|
605
707
|
: null;
|
|
606
708
|
// A body (`do={`) is never a comparison, though `where` is still open:
|
|
607
709
|
// corpus `eworm/hotspot-to-wpa.template.rsc` interleaves two `:if`s.
|
|
608
|
-
if (
|
|
609
|
-
c === "{" &&
|
|
610
|
-
role !== "program" &&
|
|
611
|
-
floor !== null &&
|
|
612
|
-
comparesNamedValue(masked, i, floor)
|
|
613
|
-
) {
|
|
710
|
+
if (c === "{" && role !== "program" && inWhereComparison) {
|
|
614
711
|
// The device stops at this brace, so neither its contents nor the
|
|
615
712
|
// rest of the enclosing group are read.
|
|
616
713
|
faults.push({ code: "named-value-brace", start: i, end: i + 1 });
|
|
@@ -738,10 +835,26 @@ export function findGroupFaults(text: string, masked: string): GroupFault[] {
|
|
|
738
835
|
: BARE_DECLARATIONS.has(word)
|
|
739
836
|
? word
|
|
740
837
|
: null;
|
|
838
|
+
frame.call = word.startsWith("$");
|
|
839
|
+
// `[/ip/address/find comment=a=b]` is a query, as `[find …]` is.
|
|
840
|
+
if (frame.verb === null && /(^|\/)find$/i.test(word))
|
|
841
|
+
frame.query = true;
|
|
842
|
+
const bad = unquotedFaultByteAt(masked, i, end);
|
|
843
|
+
if (bad >= 0 && (at < 0 || i + at > bad)) {
|
|
844
|
+
frame.settled = true;
|
|
845
|
+
faults.push({ ...unquotedByteFault(masked, bad), site: "head-word" });
|
|
846
|
+
}
|
|
741
847
|
}
|
|
742
848
|
if (readsTokens(frame)) {
|
|
743
849
|
const fault = wordFaults(masked, frame, i, end);
|
|
744
850
|
if (fault !== null) faults.push(fault);
|
|
851
|
+
} else if (frame.query && !frame.settled && !token.head) {
|
|
852
|
+
// A query keeps `=` as an operator, but not these bytes.
|
|
853
|
+
const bad = unquotedFaultByteAt(masked, i, end);
|
|
854
|
+
if (bad >= 0) {
|
|
855
|
+
frame.settled = true;
|
|
856
|
+
faults.push(unquotedByteFault(masked, bad));
|
|
857
|
+
}
|
|
745
858
|
}
|
|
746
859
|
if (word.includes("=")) token.equals = true;
|
|
747
860
|
const lower = word.toLowerCase();
|
|
@@ -760,6 +873,12 @@ export function findGroupFaults(text: string, masked: string): GroupFault[] {
|
|
|
760
873
|
}
|
|
761
874
|
|
|
762
875
|
if (frame.kind === "opaque") {
|
|
876
|
+
// A query's `in {…}` is read as opaque, so `where … in {}` keeps
|
|
877
|
+
// parsing; its bytes are still rejected (`in {a?b}` at the `?`).
|
|
878
|
+
if (frame.query && !frame.muted && isUnquotedFaultByte(c)) {
|
|
879
|
+
faults.push(unquotedByteFault(masked, i));
|
|
880
|
+
frame.muted = true;
|
|
881
|
+
}
|
|
763
882
|
i++;
|
|
764
883
|
continue;
|
|
765
884
|
}
|
|
@@ -824,6 +943,12 @@ export function findGroupFaults(text: string, masked: string): GroupFault[] {
|
|
|
824
943
|
i++;
|
|
825
944
|
continue;
|
|
826
945
|
}
|
|
946
|
+
// Unlike `@`, these bytes are rejected in a query too (`in {a?b}`).
|
|
947
|
+
if (isUnquotedFaultByte(c)) {
|
|
948
|
+
report(unquotedByteFault(masked, i), frame);
|
|
949
|
+
i++;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
827
952
|
frame.element = true;
|
|
828
953
|
frame.last = "element";
|
|
829
954
|
i++;
|
|
@@ -849,8 +974,16 @@ export function findGroupFaults(text: string, masked: string): GroupFault[] {
|
|
|
849
974
|
i++;
|
|
850
975
|
continue;
|
|
851
976
|
}
|
|
977
|
+
// A query group keeps its operators, but not these bytes:
|
|
978
|
+
// `where (comment=a?b)` is a `syntax error` at the `?`.
|
|
979
|
+
if (isUnquotedFaultByte(c)) {
|
|
980
|
+
report(unquotedByteFault(masked, i), frame);
|
|
981
|
+
i++;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
852
984
|
if (frame.query) {
|
|
853
|
-
// Structural checks only (module header): any other byte is
|
|
985
|
+
// Structural and byte checks only (module header): any other byte is
|
|
986
|
+
// content.
|
|
854
987
|
frame.empty = false;
|
|
855
988
|
frame.wantOperand = false;
|
|
856
989
|
i++;
|
|
@@ -918,8 +1051,8 @@ export function findGroupFaults(text: string, masked: string): GroupFault[] {
|
|
|
918
1051
|
i += spelling.length;
|
|
919
1052
|
continue;
|
|
920
1053
|
}
|
|
921
|
-
// `#`,
|
|
922
|
-
//
|
|
1054
|
+
// `#`, `'`, a stray `\`: another rule's byte, and nothing here can say
|
|
1055
|
+
// what the group means after it.
|
|
923
1056
|
frame.done = true;
|
|
924
1057
|
i++;
|
|
925
1058
|
}
|
package/src/explain/operators.ts
CHANGED
|
@@ -143,7 +143,8 @@ const OPERATORS: readonly RouterosOperator[] = [
|
|
|
143
143
|
// different operator, and arity is the only thing that separates them.
|
|
144
144
|
// Offline value-shape hints abstain on every such form: identical
|
|
145
145
|
// `(> …)` syntax can be `op` or `array` at runtime and `:parse` IL cannot
|
|
146
|
-
// distinguish them
|
|
146
|
+
// distinguish them; only `:typeof` can, and live explain never sends it,
|
|
147
|
+
// so `op` has no producer at all (#288, #225).
|
|
147
148
|
op(">", [1, 2], 5, "left", "relational", ">"),
|
|
148
149
|
op("=", [2], 5, "left", "relational", "="),
|
|
149
150
|
op("<=", [2], 5, "left", "relational", "<="),
|