@takodotid/circuit 0.43.0 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +88 -38
- package/dist/types/platforms/fortios/plan.d.ts +15 -5
- package/package.json +1 -1
- package/src/platforms/fortios/plan.ts +132 -44
package/dist/cli/index.js
CHANGED
|
@@ -1611,7 +1611,9 @@ class FortiOsPlan {
|
|
|
1611
1611
|
"Fortinet_SSL_ED25519",
|
|
1612
1612
|
"Fortinet_SSL_ED448"
|
|
1613
1613
|
];
|
|
1614
|
-
static
|
|
1614
|
+
static BY_COMMENTS = { keys: [["comments"]] };
|
|
1615
|
+
static ORDERED = { ordered: true, numbered: FortiOsPlan.BY_COMMENTS };
|
|
1616
|
+
static ROUTES = { numbered: { keys: [["comment"]] }, distinct: true };
|
|
1615
1617
|
static TABLES = {
|
|
1616
1618
|
"system interface": {
|
|
1617
1619
|
factory: ["modem", "ssl.root"],
|
|
@@ -1636,8 +1638,11 @@ class FortiOsPlan {
|
|
|
1636
1638
|
"firewall local-in-policy": FortiOsPlan.ORDERED,
|
|
1637
1639
|
"firewall local-in-policy6": FortiOsPlan.ORDERED,
|
|
1638
1640
|
"firewall central-snat-map": FortiOsPlan.ORDERED,
|
|
1639
|
-
"
|
|
1640
|
-
"
|
|
1641
|
+
"firewall acl": { numbered: FortiOsPlan.BY_COMMENTS },
|
|
1642
|
+
"firewall acl6": { numbered: FortiOsPlan.BY_COMMENTS },
|
|
1643
|
+
"firewall vip": { numbered: { keys: [["comment"], ["extintf", "extip", "protocol", "extport"]], prefix: "dnat-" } },
|
|
1644
|
+
"router static": FortiOsPlan.ROUTES,
|
|
1645
|
+
"router static6": FortiOsPlan.ROUTES
|
|
1641
1646
|
};
|
|
1642
1647
|
static IGNORED = new Set(["uuid", "snmp-index"]);
|
|
1643
1648
|
static IGNORED_IN = {
|
|
@@ -1798,7 +1803,7 @@ class FortiOsPlan {
|
|
|
1798
1803
|
}
|
|
1799
1804
|
for (const [header, child] of want.blocks)
|
|
1800
1805
|
nested(header, child, have.blocks.get(header) ?? new FortiOsBlock);
|
|
1801
|
-
const entries = table?.byContent ? FortiOsPlan.byContent(path, want, have) : table?.
|
|
1806
|
+
const entries = table?.byContent ? FortiOsPlan.byContent(path, want, have) : table?.numbered ? FortiOsPlan.numbered(path, want, have, table, table.numbered, context) : FortiOsPlan.byId(path, want, have, table, context);
|
|
1802
1807
|
send.push(...entries.send);
|
|
1803
1808
|
show.push(...entries.show);
|
|
1804
1809
|
if (top) {
|
|
@@ -1840,55 +1845,100 @@ class FortiOsPlan {
|
|
|
1840
1845
|
removals.push(`delete ${entry.id}`);
|
|
1841
1846
|
removed.push(...FortiOsPlan.shown("-", [...path, FortiOsTree.bare(entry.id)], entry.block));
|
|
1842
1847
|
}
|
|
1843
|
-
if (table?.ordered) {
|
|
1844
|
-
const moved = FortiOsPlan.moves(want, have);
|
|
1845
|
-
send.push(...moved);
|
|
1846
|
-
show.push(...moved.map((line) => `+ ${FortiOsPlan.labelOf(path)}${line}`));
|
|
1847
|
-
}
|
|
1848
1848
|
return { send, show, removals, removed };
|
|
1849
1849
|
}
|
|
1850
|
-
static
|
|
1851
|
-
const
|
|
1852
|
-
const creates = [];
|
|
1850
|
+
static numbered(path, want, have, table, numbered, context) {
|
|
1851
|
+
const send = [];
|
|
1853
1852
|
const show = [];
|
|
1853
|
+
const pattern = new RegExp(`^${numbered.prefix ?? ""}(\\d+)$`);
|
|
1854
|
+
const numberOf = (entry) => Number(pattern.exec(FortiOsTree.bare(entry.id))?.[1] ?? NaN);
|
|
1855
|
+
const held = [...have.entries.values()].filter((entry) => !FortiOsPlan.isFactory(table, entry));
|
|
1856
|
+
const kept = new Map;
|
|
1857
|
+
const unmatched = new Set(held.filter((entry) => Number.isInteger(numberOf(entry))));
|
|
1858
|
+
const keep = (key, old) => {
|
|
1859
|
+
kept.set(key, old);
|
|
1860
|
+
unmatched.delete(old);
|
|
1861
|
+
};
|
|
1862
|
+
const alike = Map.groupBy(unmatched, (old) => FortiOsPlan.signature(old.block, context.section));
|
|
1863
|
+
const same = (entry, old) => !FortiOsPlan.compare([], entry.block, old.block, undefined, false, context).send.length;
|
|
1854
1864
|
for (const [key, entry] of want.entries) {
|
|
1855
|
-
const
|
|
1856
|
-
const
|
|
1857
|
-
if (
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1865
|
+
const candidates = alike.get(FortiOsPlan.signature(entry.block, context.section)) ?? [];
|
|
1866
|
+
const index = candidates.findIndex((old) => same(entry, old));
|
|
1867
|
+
if (index >= 0)
|
|
1868
|
+
keep(key, candidates.splice(index, 1)[0]);
|
|
1869
|
+
}
|
|
1870
|
+
for (const settings of numbered.keys) {
|
|
1871
|
+
const valueOf = (entry) => {
|
|
1872
|
+
const parts = settings.map((setting) => FortiOsPlan.canonical(setting, entry.block.settings.get(setting) ?? ""));
|
|
1873
|
+
return parts.some(Boolean) ? parts.join(`
|
|
1874
|
+
`) : undefined;
|
|
1875
|
+
};
|
|
1876
|
+
const changed = Map.groupBy([...want.entries].filter(([key]) => !kept.has(key)), ([, entry]) => valueOf(entry));
|
|
1877
|
+
const left = Map.groupBy(unmatched, valueOf);
|
|
1878
|
+
for (const [value, entries] of changed) {
|
|
1879
|
+
const old = left.get(value);
|
|
1880
|
+
if (value !== undefined && entries.length === 1 && old?.length === 1)
|
|
1881
|
+
keep(entries[0][0], old[0]);
|
|
1862
1882
|
}
|
|
1863
|
-
creates.push(...FortiOsPlan.creation(entry, table));
|
|
1864
|
-
show.push(...FortiOsPlan.shown("+", where, entry.block));
|
|
1865
1883
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1884
|
+
let next = Math.max(0, ...[...have.entries.values()].map(numberOf).filter(Number.isInteger)) + 1;
|
|
1885
|
+
const wanted = [];
|
|
1886
|
+
const replaced = [];
|
|
1887
|
+
for (const [key, entry] of want.entries) {
|
|
1888
|
+
const old = kept.get(key);
|
|
1889
|
+
if (!old) {
|
|
1890
|
+
const name = `${numbered.prefix ?? ""}${next++}`;
|
|
1891
|
+
const id = numbered.prefix ? FortiOsTree.quote(name) : name;
|
|
1892
|
+
wanted.push(id);
|
|
1893
|
+
send.push(...FortiOsPlan.creation(entry, table, id));
|
|
1894
|
+
show.push(...FortiOsPlan.shown("+", [...path, name], entry.block));
|
|
1868
1895
|
continue;
|
|
1869
|
-
|
|
1870
|
-
|
|
1896
|
+
}
|
|
1897
|
+
wanted.push(old.id);
|
|
1898
|
+
const where = [...path, FortiOsTree.bare(old.id)];
|
|
1899
|
+
const diff = FortiOsPlan.compare(where, entry.block, old.block, undefined, false, context);
|
|
1900
|
+
if (!diff.send.length)
|
|
1901
|
+
continue;
|
|
1902
|
+
if (table.distinct) {
|
|
1903
|
+
replaced.push(`delete ${old.id}`);
|
|
1904
|
+
send.push(...FortiOsPlan.creation(entry, table, old.id));
|
|
1905
|
+
show.push(...FortiOsPlan.shown("-", where, old.block), ...FortiOsPlan.shown("+", where, entry.block));
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
send.push(`edit ${old.id}`, ...diff.send, "next");
|
|
1909
|
+
show.push(...diff.show);
|
|
1871
1910
|
}
|
|
1872
|
-
|
|
1911
|
+
const keptEntries = new Set(kept.values());
|
|
1912
|
+
if (table.ordered) {
|
|
1913
|
+
const order = held.filter((entry) => keptEntries.has(entry)).map((entry) => entry.id);
|
|
1914
|
+
const moved = FortiOsPlan.moves(wanted, order);
|
|
1915
|
+
send.push(...moved);
|
|
1916
|
+
show.push(...moved.map((line) => `+ ${FortiOsPlan.labelOf(path)}${line}`));
|
|
1917
|
+
}
|
|
1918
|
+
const gone = held.filter((entry) => !keptEntries.has(entry));
|
|
1919
|
+
const removals = gone.map((entry) => `delete ${entry.id}`);
|
|
1920
|
+
const removed = gone.flatMap((entry) => FortiOsPlan.shown("-", [...path, FortiOsTree.bare(entry.id)], entry.block));
|
|
1921
|
+
if (table.distinct)
|
|
1922
|
+
return { send: [...replaced, ...removals, ...send], show: [...removed, ...show], removals: [], removed: [] };
|
|
1923
|
+
return { send, show, removals, removed };
|
|
1873
1924
|
}
|
|
1874
|
-
static moves(
|
|
1875
|
-
const
|
|
1876
|
-
const order = [...have.entries.keys()].filter((key) => want.entries.has(key));
|
|
1877
|
-
order.push(...wanted.filter((key) => !have.entries.has(key)));
|
|
1925
|
+
static moves(wanted, held) {
|
|
1926
|
+
const order = [...held, ...wanted.filter((id) => !held.includes(id))];
|
|
1878
1927
|
const send = [];
|
|
1879
|
-
const idOf = (key) => want.entries.get(key).id;
|
|
1880
1928
|
for (let index = 0;index < wanted.length; index++) {
|
|
1881
|
-
|
|
1929
|
+
const id = wanted[index];
|
|
1930
|
+
if (order[index] === id)
|
|
1882
1931
|
continue;
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
order.splice(
|
|
1886
|
-
order.splice(index, 0, key);
|
|
1932
|
+
send.push(index === 0 ? `move ${id} before ${order[0]}` : `move ${id} after ${wanted[index - 1]}`);
|
|
1933
|
+
order.splice(order.indexOf(id), 1);
|
|
1934
|
+
order.splice(index, 0, id);
|
|
1887
1935
|
}
|
|
1888
1936
|
return send;
|
|
1889
1937
|
}
|
|
1890
|
-
static signature(block) {
|
|
1891
|
-
|
|
1938
|
+
static signature(block, section = "") {
|
|
1939
|
+
const defaults = FortiOsPlan.DEFAULTS[section] ?? {};
|
|
1940
|
+
const isDefault = (key, value) => defaults[key] !== undefined && FortiOsPlan.canonical(key, defaults[key]) === FortiOsPlan.canonical(key, value);
|
|
1941
|
+
return [...block.settings].filter(([key, value]) => !isDefault(key, value)).map(([key, value]) => `${key} ${FortiOsPlan.canonical(key, value)}`).sort().join(`
|
|
1892
1942
|
`);
|
|
1893
1943
|
}
|
|
1894
1944
|
static byContent(path, want, have) {
|
|
@@ -5,7 +5,11 @@ export declare class FortiOsPlan {
|
|
|
5
5
|
private static readonly FACTORY_SERVICES;
|
|
6
6
|
/** The device's own certificates, which the factory makes for each device. */
|
|
7
7
|
private static readonly FACTORY_CERTIFICATES;
|
|
8
|
+
/** An entry known by what it holds, or else by its description. */
|
|
9
|
+
private static readonly BY_COMMENTS;
|
|
8
10
|
private static readonly ORDERED;
|
|
11
|
+
/** A static route known by what it holds, or else by its description. Many routes share a destination, so that is no key. */
|
|
12
|
+
private static readonly ROUTES;
|
|
9
13
|
/** Tables whose entries need more than a plain comparison, by section. */
|
|
10
14
|
private static readonly TABLES;
|
|
11
15
|
/** Settings the device keeps for itself: an object's UUID and an interface's SNMP index. */
|
|
@@ -61,13 +65,19 @@ export declare class FortiOsPlan {
|
|
|
61
65
|
* What goes is undone before what comes is set, since a setting can hide another: `send-deny-packet` exists only while a policy denies. A nested list that goes is deleted before the settings, since one can hide the list and keep its entries hidden, such as `secondary-IP`; NTP refuses to delete its last server while it synchronises, so `ntpsync` goes before its list.
|
|
62
66
|
*/
|
|
63
67
|
private static compare;
|
|
64
|
-
/** Entries matched by name: new ones created, changed ones edited, gone ones deleted
|
|
68
|
+
/** Entries matched by name: new ones created, changed ones edited, gone ones deleted. */
|
|
65
69
|
private static byId;
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Entries the render numbers by their place, while on the device each keeps the number it was created with, so they are matched by what they are.
|
|
72
|
+
*
|
|
73
|
+
* An entry that holds the same as one on the device is kept. One that changes is found by the first of the table's keys that no other entry left to match shares, such as its description, and edited under its own number. Any other is new and takes a number the device does not use; in a table the device keeps in order, it is created at the end and moved into place. An entry on the device that nothing matched is deleted last, so every entry the render keeps stays on the device through the whole change.
|
|
74
|
+
*
|
|
75
|
+
* In a `distinct` table, an entry that changes is deleted and created again under its own number, and every deletion comes before any creation.
|
|
76
|
+
*/
|
|
77
|
+
private static numbered;
|
|
78
|
+
/** The `move` commands that put a table's entries in the render's order, by the device's numbers: `wanted` in the render's order, `held` the kept ones in the device's. Kept entries hold their place on the device and new ones are added at the end. */
|
|
69
79
|
private static moves;
|
|
70
|
-
/** An entry's settings in the form two entries compare in, whatever their numbers. */
|
|
80
|
+
/** An entry's settings in the form two entries compare in, whatever their numbers. A setting at its section's default is left out, as the device does not print it. */
|
|
71
81
|
private static signature;
|
|
72
82
|
/** Entries matched by what they hold. One the device holds and the render does not is deleted; one the render adds takes a number the device does not use. */
|
|
73
83
|
private static byContent;
|
package/package.json
CHANGED
|
@@ -12,13 +12,17 @@ type Table = {
|
|
|
12
12
|
factory?: readonly string[];
|
|
13
13
|
/** The device keeps the entries in an order of their own, which `move` changes. */
|
|
14
14
|
ordered?: true;
|
|
15
|
+
/**
|
|
16
|
+
* The render numbers the entries by their place, but on the device each keeps the number it was created with. An entry is known by what it holds, or else by the settings of the first of `keys` that only it holds, such as its description. A new entry takes a number the device does not use, after `prefix` when the render names entries so.
|
|
17
|
+
*/
|
|
18
|
+
numbered?: { keys: readonly (readonly string[])[]; prefix?: string };
|
|
15
19
|
/** An entry is known by what it holds, not by its number. */
|
|
16
20
|
byContent?: true;
|
|
17
21
|
/** Settings the device gives an entry it creates. They are unset when the render does not hold them. */
|
|
18
22
|
created?: (entry: FortiOsBlock) => readonly string[];
|
|
19
23
|
/** Settings an entry keeps for life. A change deletes the entry and creates it again. */
|
|
20
24
|
fixed?: readonly string[];
|
|
21
|
-
/** The device refuses
|
|
25
|
+
/** Every entry that changes or goes is deleted before any is created. The device refuses a static route the same as another, `Duplicated entry`, even for a moment. */
|
|
22
26
|
distinct?: true;
|
|
23
27
|
};
|
|
24
28
|
|
|
@@ -154,7 +158,13 @@ export class FortiOsPlan {
|
|
|
154
158
|
"Fortinet_SSL_ED448",
|
|
155
159
|
];
|
|
156
160
|
|
|
157
|
-
|
|
161
|
+
/** An entry known by what it holds, or else by its description. */
|
|
162
|
+
private static readonly BY_COMMENTS: Table["numbered"] = { keys: [["comments"]] };
|
|
163
|
+
|
|
164
|
+
private static readonly ORDERED: Table = { ordered: true, numbered: FortiOsPlan.BY_COMMENTS };
|
|
165
|
+
|
|
166
|
+
/** A static route known by what it holds, or else by its description. Many routes share a destination, so that is no key. */
|
|
167
|
+
private static readonly ROUTES: Table = { numbered: { keys: [["comment"]] }, distinct: true };
|
|
158
168
|
|
|
159
169
|
/** Tables whose entries need more than a plain comparison, by section. */
|
|
160
170
|
private static readonly TABLES: Record<string, Table> = {
|
|
@@ -180,8 +190,12 @@ export class FortiOsPlan {
|
|
|
180
190
|
"firewall local-in-policy": FortiOsPlan.ORDERED,
|
|
181
191
|
"firewall local-in-policy6": FortiOsPlan.ORDERED,
|
|
182
192
|
"firewall central-snat-map": FortiOsPlan.ORDERED,
|
|
183
|
-
"
|
|
184
|
-
"
|
|
193
|
+
"firewall acl": { numbered: FortiOsPlan.BY_COMMENTS },
|
|
194
|
+
"firewall acl6": { numbered: FortiOsPlan.BY_COMMENTS },
|
|
195
|
+
// A virtual IP that changes is also known by the address it answers on.
|
|
196
|
+
"firewall vip": { numbered: { keys: [["comment"], ["extintf", "extip", "protocol", "extport"]], prefix: "dnat-" } },
|
|
197
|
+
"router static": FortiOsPlan.ROUTES,
|
|
198
|
+
"router static6": FortiOsPlan.ROUTES,
|
|
185
199
|
};
|
|
186
200
|
|
|
187
201
|
/** Settings the device keeps for itself: an object's UUID and an interface's SNMP index. */
|
|
@@ -399,8 +413,8 @@ export class FortiOsPlan {
|
|
|
399
413
|
|
|
400
414
|
const entries = table?.byContent
|
|
401
415
|
? FortiOsPlan.byContent(path, want, have)
|
|
402
|
-
: table?.
|
|
403
|
-
? FortiOsPlan.
|
|
416
|
+
: table?.numbered
|
|
417
|
+
? FortiOsPlan.numbered(path, want, have, table, table.numbered, context)
|
|
404
418
|
: FortiOsPlan.byId(path, want, have, table, context);
|
|
405
419
|
send.push(...entries.send);
|
|
406
420
|
show.push(...entries.show);
|
|
@@ -416,7 +430,7 @@ export class FortiOsPlan {
|
|
|
416
430
|
return { send, show, removals, removed };
|
|
417
431
|
}
|
|
418
432
|
|
|
419
|
-
/** Entries matched by name: new ones created, changed ones edited, gone ones deleted
|
|
433
|
+
/** Entries matched by name: new ones created, changed ones edited, gone ones deleted. */
|
|
420
434
|
private static byId(path: string[], want: FortiOsBlock, have: FortiOsBlock, table: Table | undefined, context: Context): Diff {
|
|
421
435
|
const send: string[] = [];
|
|
422
436
|
const show: string[] = [];
|
|
@@ -456,70 +470,144 @@ export class FortiOsPlan {
|
|
|
456
470
|
removed.push(...FortiOsPlan.shown("-", [...path, FortiOsTree.bare(entry.id)], entry.block));
|
|
457
471
|
}
|
|
458
472
|
|
|
459
|
-
if (table?.ordered) {
|
|
460
|
-
const moved = FortiOsPlan.moves(want, have);
|
|
461
|
-
send.push(...moved);
|
|
462
|
-
show.push(...moved.map((line) => `+ ${FortiOsPlan.labelOf(path)}${line}`));
|
|
463
|
-
}
|
|
464
|
-
|
|
465
473
|
return { send, show, removals, removed };
|
|
466
474
|
}
|
|
467
475
|
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
476
|
+
/**
|
|
477
|
+
* Entries the render numbers by their place, while on the device each keeps the number it was created with, so they are matched by what they are.
|
|
478
|
+
*
|
|
479
|
+
* An entry that holds the same as one on the device is kept. One that changes is found by the first of the table's keys that no other entry left to match shares, such as its description, and edited under its own number. Any other is new and takes a number the device does not use; in a table the device keeps in order, it is created at the end and moved into place. An entry on the device that nothing matched is deleted last, so every entry the render keeps stays on the device through the whole change.
|
|
480
|
+
*
|
|
481
|
+
* In a `distinct` table, an entry that changes is deleted and created again under its own number, and every deletion comes before any creation.
|
|
482
|
+
*/
|
|
483
|
+
private static numbered(
|
|
484
|
+
path: string[],
|
|
485
|
+
want: FortiOsBlock,
|
|
486
|
+
have: FortiOsBlock,
|
|
487
|
+
table: Table,
|
|
488
|
+
numbered: NonNullable<Table["numbered"]>,
|
|
489
|
+
context: Context
|
|
490
|
+
): Diff {
|
|
491
|
+
const send: string[] = [];
|
|
472
492
|
const show: string[] = [];
|
|
473
493
|
|
|
494
|
+
// The number in a name the render gives, such as 7 in `dnat-7`. An entry named otherwise is never kept.
|
|
495
|
+
const pattern = new RegExp(`^${numbered.prefix ?? ""}(\\d+)$`);
|
|
496
|
+
const numberOf = (entry: Entry) => Number(pattern.exec(FortiOsTree.bare(entry.id))?.[1] ?? NaN);
|
|
497
|
+
|
|
498
|
+
const held = [...have.entries.values()].filter((entry) => !FortiOsPlan.isFactory(table, entry));
|
|
499
|
+
const kept = new Map<string, Entry>();
|
|
500
|
+
const unmatched = new Set(held.filter((entry) => Number.isInteger(numberOf(entry))));
|
|
501
|
+
|
|
502
|
+
const keep = (key: string, old: Entry) => {
|
|
503
|
+
kept.set(key, old);
|
|
504
|
+
unmatched.delete(old);
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Only entries with the same signature can hold the same, so an entry is compared with those alone.
|
|
508
|
+
const alike = Map.groupBy(unmatched, (old) => FortiOsPlan.signature(old.block, context.section));
|
|
509
|
+
const same = (entry: Entry, old: Entry) => !FortiOsPlan.compare([], entry.block, old.block, undefined, false, context).send.length;
|
|
510
|
+
|
|
474
511
|
for (const [key, entry] of want.entries) {
|
|
475
|
-
const
|
|
476
|
-
const
|
|
512
|
+
const candidates = alike.get(FortiOsPlan.signature(entry.block, context.section)) ?? [];
|
|
513
|
+
const index = candidates.findIndex((old) => same(entry, old));
|
|
514
|
+
if (index >= 0) keep(key, candidates.splice(index, 1)[0]!);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
for (const settings of numbered.keys) {
|
|
518
|
+
const valueOf = (entry: Entry) => {
|
|
519
|
+
const parts = settings.map((setting) => FortiOsPlan.canonical(setting, entry.block.settings.get(setting) ?? ""));
|
|
520
|
+
return parts.some(Boolean) ? parts.join("\n") : undefined;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const changed = Map.groupBy(
|
|
524
|
+
[...want.entries].filter(([key]) => !kept.has(key)),
|
|
525
|
+
([, entry]) => valueOf(entry)
|
|
526
|
+
);
|
|
527
|
+
const left = Map.groupBy(unmatched, valueOf);
|
|
528
|
+
|
|
529
|
+
for (const [value, entries] of changed) {
|
|
530
|
+
const old = left.get(value);
|
|
531
|
+
if (value !== undefined && entries.length === 1 && old?.length === 1) keep(entries[0]![0], old[0]!);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// A number no entry on the device holds, not even one deleted at the end.
|
|
536
|
+
let next = Math.max(0, ...[...have.entries.values()].map(numberOf).filter(Number.isInteger)) + 1;
|
|
537
|
+
const wanted: string[] = [];
|
|
538
|
+
const replaced: string[] = [];
|
|
539
|
+
|
|
540
|
+
for (const [key, entry] of want.entries) {
|
|
541
|
+
const old = kept.get(key);
|
|
542
|
+
|
|
543
|
+
if (!old) {
|
|
544
|
+
const name = `${numbered.prefix ?? ""}${next++}`;
|
|
545
|
+
const id = numbered.prefix ? FortiOsTree.quote(name) : name;
|
|
546
|
+
wanted.push(id);
|
|
547
|
+
send.push(...FortiOsPlan.creation(entry, table, id));
|
|
548
|
+
show.push(...FortiOsPlan.shown("+", [...path, name], entry.block));
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
wanted.push(old.id);
|
|
477
553
|
|
|
478
|
-
|
|
479
|
-
|
|
554
|
+
const where = [...path, FortiOsTree.bare(old.id)];
|
|
555
|
+
const diff = FortiOsPlan.compare(where, entry.block, old.block, undefined, false, context);
|
|
556
|
+
if (!diff.send.length) continue;
|
|
480
557
|
|
|
481
|
-
|
|
482
|
-
|
|
558
|
+
if (table.distinct) {
|
|
559
|
+
replaced.push(`delete ${old.id}`);
|
|
560
|
+
send.push(...FortiOsPlan.creation(entry, table, old.id));
|
|
561
|
+
show.push(...FortiOsPlan.shown("-", where, old.block), ...FortiOsPlan.shown("+", where, entry.block));
|
|
562
|
+
continue;
|
|
483
563
|
}
|
|
484
564
|
|
|
485
|
-
|
|
486
|
-
show.push(...
|
|
565
|
+
send.push(`edit ${old.id}`, ...diff.send, "next");
|
|
566
|
+
show.push(...diff.show);
|
|
487
567
|
}
|
|
488
568
|
|
|
489
|
-
|
|
490
|
-
if (want.entries.has(key) || FortiOsPlan.isFactory(table, entry)) continue;
|
|
569
|
+
const keptEntries = new Set(kept.values());
|
|
491
570
|
|
|
492
|
-
|
|
493
|
-
|
|
571
|
+
if (table.ordered) {
|
|
572
|
+
const order = held.filter((entry) => keptEntries.has(entry)).map((entry) => entry.id);
|
|
573
|
+
const moved = FortiOsPlan.moves(wanted, order);
|
|
574
|
+
send.push(...moved);
|
|
575
|
+
show.push(...moved.map((line) => `+ ${FortiOsPlan.labelOf(path)}${line}`));
|
|
494
576
|
}
|
|
495
577
|
|
|
496
|
-
|
|
497
|
-
|
|
578
|
+
const gone = held.filter((entry) => !keptEntries.has(entry));
|
|
579
|
+
const removals = gone.map((entry) => `delete ${entry.id}`);
|
|
580
|
+
const removed = gone.flatMap((entry) => FortiOsPlan.shown("-", [...path, FortiOsTree.bare(entry.id)], entry.block));
|
|
498
581
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const order = [...have.entries.keys()].filter((key) => want.entries.has(key));
|
|
503
|
-
order.push(...wanted.filter((key) => !have.entries.has(key)));
|
|
582
|
+
if (table.distinct) return { send: [...replaced, ...removals, ...send], show: [...removed, ...show], removals: [], removed: [] };
|
|
583
|
+
return { send, show, removals, removed };
|
|
584
|
+
}
|
|
504
585
|
|
|
586
|
+
/** The `move` commands that put a table's entries in the render's order, by the device's numbers: `wanted` in the render's order, `held` the kept ones in the device's. Kept entries hold their place on the device and new ones are added at the end. */
|
|
587
|
+
private static moves(wanted: readonly string[], held: readonly string[]): string[] {
|
|
588
|
+
const order = [...held, ...wanted.filter((id) => !held.includes(id))];
|
|
505
589
|
const send: string[] = [];
|
|
506
|
-
const idOf = (key: string) => want.entries.get(key)!.id;
|
|
507
590
|
|
|
508
591
|
for (let index = 0; index < wanted.length; index++) {
|
|
509
|
-
|
|
592
|
+
const id = wanted[index]!;
|
|
593
|
+
if (order[index] === id) continue;
|
|
510
594
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
order.splice(
|
|
514
|
-
order.splice(index, 0, key);
|
|
595
|
+
send.push(index === 0 ? `move ${id} before ${order[0]}` : `move ${id} after ${wanted[index - 1]}`);
|
|
596
|
+
order.splice(order.indexOf(id), 1);
|
|
597
|
+
order.splice(index, 0, id);
|
|
515
598
|
}
|
|
516
599
|
|
|
517
600
|
return send;
|
|
518
601
|
}
|
|
519
602
|
|
|
520
|
-
/** An entry's settings in the form two entries compare in, whatever their numbers. */
|
|
521
|
-
private static signature(block: FortiOsBlock): string {
|
|
603
|
+
/** An entry's settings in the form two entries compare in, whatever their numbers. A setting at its section's default is left out, as the device does not print it. */
|
|
604
|
+
private static signature(block: FortiOsBlock, section = ""): string {
|
|
605
|
+
const defaults = FortiOsPlan.DEFAULTS[section] ?? {};
|
|
606
|
+
const isDefault = (key: string, value: string) =>
|
|
607
|
+
defaults[key] !== undefined && FortiOsPlan.canonical(key, defaults[key]) === FortiOsPlan.canonical(key, value);
|
|
608
|
+
|
|
522
609
|
return [...block.settings]
|
|
610
|
+
.filter(([key, value]) => !isDefault(key, value))
|
|
523
611
|
.map(([key, value]) => `${key} ${FortiOsPlan.canonical(key, value)}`)
|
|
524
612
|
.sort()
|
|
525
613
|
.join("\n");
|