feat(fls): write the topology of a .fls, loaded by Java as the original
topologyWriter.ts writes konfiguration.xml from Java's view of a project: the version line, the node items (label position, device with name, host label flags, interfaces or ports), the cable items and the documentation items. What a device's constructor makes is changed through getters, as XMLEncoder does, and more is added; fewer than it makes set the list anew. Cable ends name the port objects the getters return (the writer now names a getter's result, <void id="Port0" property="port"/>), and each cable carries the nodes the editor draws it between. Checked by Java against Java: every corpus file, read and written again, loads in Java FILIUS (the oracle's loading method, --sha256 ties its result to the written bytes) to the same devices, names, positions, interfaces, cables and documentation as the original; so does a synthetic topology with what the corpus never sets. The oracle and the reader now also report a cable's drawing ends (JCablePanel ziel1/ziel2): in the corpus the nodes it joins, for all 183 cables. Mutation probes rewrite the files and let Java load each mutant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -42,13 +42,15 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
|
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
|
||||||
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
|
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
|
||||||
| `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ A/MX/NS, Name nicht leer); Javas Fehler nicht übernommen: Label mit Ziffer vorn erlaubt, kein Backslash, letztes Label nicht rein numerisch (RFC 1123/3696), Typ in jeder Schreibung (RFC 4343), negative TTL ungültig (RFC 2181); an 76 Fällen mit Java und Swift gemessen |
|
| `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ A/MX/NS, Name nicht leer); Javas Fehler nicht übernommen: Label mit Ziffer vorn erlaubt, kein Backslash, letztes Label nicht rein numerisch (RFC 1123/3696), Typ in jeder Schreibung (RFC 4343), negative TTL ungültig (RFC 2181); an 76 Fällen mit Java und Swift gemessen |
|
||||||
| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter |
|
| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter, die Zeichen-Enden eines Kabels |
|
||||||
| `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java |
|
| `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java |
|
||||||
| `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt |
|
| `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt |
|
||||||
| `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java |
|
| `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java |
|
||||||
| `src/javaEmail.ts` | Java `EmailServer` (Konten-Bean, `kontenLaden`/`stringZuListeBenutzerkonten`), `EmailAnwendung` (Ordner, Adressbuch, `laden`), `EmailKonto`, `Email`, `AddressEntry`, `EmailUtils` | neu nach Java: E-Mail-Beans über ihre Setter, `/mailserver/konten.txt` und `/konten.txt` im Format von Javas Lesern, ohne ihre Fehler (leere Felder zählen, eine kurze Zeile fällt allein weg, Nachrichten mit leerem Text oder Betreff bleiben, `;` im Betreff bleibt, CRLF, beim Client die letzte vollständige Zeile); Empfänger älterer Dateien als Zeichenketten; gegen Java auf dem Korpus und an 43 Fällen geprüft (13 benannte Java-Fehler) |
|
| `src/javaEmail.ts` | Java `EmailServer` (Konten-Bean, `kontenLaden`/`stringZuListeBenutzerkonten`), `EmailAnwendung` (Ordner, Adressbuch, `laden`), `EmailKonto`, `Email`, `AddressEntry`, `EmailUtils` | neu nach Java: E-Mail-Beans über ihre Setter, `/mailserver/konten.txt` und `/konten.txt` im Format von Javas Lesern, ohne ihre Fehler (leere Felder zählen, eine kurze Zeile fällt allein weg, Nachrichten mit leerem Text oder Betreff bleiben, `;` im Betreff bleibt, CRLF, beim Client die letzte vollständige Zeile); Empfänger älterer Dateien als Zeichenketten; gegen Java auf dem Korpus und an 43 Fällen geprüft (13 benannte Java-Fehler) |
|
||||||
| `src/javaBeansWriter.ts` | — | neu: Schreiber für das `XMLDecoder`-Format, Gegenstück zu `javabeans.ts`; ausdrücklich typisierte Eingabe, geteilte Objekte über `id`/`idref`, was XML nicht fassen kann (und CR) als `<char code>`; von Java selbst geprüft (`BeansDump`) |
|
| `src/javaBeansWriter.ts` | — | neu: Schreiber für das `XMLDecoder`-Format, Gegenstück zu `javabeans.ts`; ausdrücklich typisierte Eingabe, geteilte Objekte über `id`/`idref`, was XML nicht fassen kann (und CR) als `<char code>`; von Java selbst geprüft (`BeansDump`) |
|
||||||
| `test/javaBeansWriter.test.ts`, `test/beans/` | — | neu: 9 Dateien des Schreibers, von Javas `XMLDecoder` gelesen und beschrieben; Erwartungen von Hand |
|
| `test/javaBeansWriter.test.ts`, `test/beans/` | — | neu: 9 Dateien des Schreibers, von Javas `XMLDecoder` gelesen und beschrieben; Erwartungen von Hand |
|
||||||
|
| `src/topologyWriter.ts` | Java `SzenarioVerwaltung.netzwerkSpeichern`, Konstruktoren von `NetzwerkInterface`, `Port`, `GUIKabelItem` | neu nach Java: `konfiguration.xml` aus Javas Sicht — Geräte, Schnittstellen (vom Konstruktor angelegte über Getter, weitere per `add`), Ports über Getter-Verweise, Kabel samt Zeichen-Enden (`kabelpanel`), Doku-Elemente; Kopf „Filius version: 2.14.0 (mdg-filius)“ |
|
||||||
|
| `test/topologyWriter.test.ts`, `test/written.java.json` | — | neu: alle 23 Korpus-Dateien gelesen und neu geschrieben, von Java geladen — Topologie wie beim Original (Java gegen Java); dazu eine künstliche Topologie mit dem, was der Korpus nicht hat |
|
||||||
| `src/importEmail.ts` | `Persistence/TopologyProjectStore.swift` (`registerJavaEmailConfigurations`, `assignedJavaEmailMessages`) | übertragen, Eingabe ist Javas Sicht: Server-Konten aus `/mailserver/konten.txt`, ohne Datei aus der Bean-Liste (Java verlöre sie); Client-Konto aus `/konten.txt`, ohne Datei aus der älteren `kontoListe` (Filius 2.14 liest sie nicht mehr); Konten und Nachrichten bleiben, wo Swift verwirft, was seine Prüfung nicht besteht; Vor- und Nachname getrennt; Empfangszeit nur aus der Datei (Swift nummeriert); gegen Swifts Modell geprüft |
|
| `src/importEmail.ts` | `Persistence/TopologyProjectStore.swift` (`registerJavaEmailConfigurations`, `assignedJavaEmailMessages`) | übertragen, Eingabe ist Javas Sicht: Server-Konten aus `/mailserver/konten.txt`, ohne Datei aus der Bean-Liste (Java verlöre sie); Client-Konto aus `/konten.txt`, ohne Datei aus der älteren `kontoListe` (Filius 2.14 liest sie nicht mehr); Konten und Nachrichten bleiben, wo Swift verwirft, was seine Prüfung nicht besteht; Vor- und Nachname getrennt; Empfangszeit nur aus der Datei (Swift nummeriert); gegen Swifts Modell geprüft |
|
||||||
| `test/importEmail.test.ts`, `test/email-*.swift.json` | `State/TopologyEmail.swift` (`decodeJavaServer`, `decodeJavaClient`) als Vergleich | neu: dieselben 43 Kontodateien mit Swifts Leser; wie Swift bis auf benannte Fälle, in denen Swift prüft und die ganze Datei verwirft, wo Java liest |
|
| `test/importEmail.test.ts`, `test/email-*.swift.json` | `State/TopologyEmail.swift` (`decodeJavaServer`, `decodeJavaClient`) als Vergleich | neu: dieselben 43 Kontodateien mit Swifts Leser; wie Swift bis auf benannte Fälle, in denen Swift prüft und die ganze Datei verwirft, wo Java liest |
|
||||||
| `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen |
|
| `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen |
|
||||||
|
|||||||
+915
-183
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
test/written/
|
||||||
@@ -9,4 +9,5 @@ export * from './javaString.js';
|
|||||||
export * from './javabeans.js';
|
export * from './javabeans.js';
|
||||||
export * from './systemSoftware.js';
|
export * from './systemSoftware.js';
|
||||||
export * from './topology.js';
|
export * from './topology.js';
|
||||||
|
export * from './topologyWriter.js';
|
||||||
export * from './zip.js';
|
export * from './zip.js';
|
||||||
|
|||||||
@@ -21,7 +21,22 @@ export type BeanValue =
|
|||||||
| BeanPrimitive
|
| BeanPrimitive
|
||||||
| BeanObject
|
| BeanObject
|
||||||
| BeanArray
|
| BeanArray
|
||||||
| { readonly kind: 'class'; readonly name: string };
|
| { readonly kind: 'class'; readonly name: string }
|
||||||
|
| BeanReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object only Java makes — a getter's result, such as the port a network
|
||||||
|
* interface's constructor creates — named where the getter is called
|
||||||
|
* (`<void id="Port0" property="port"/>`) and referenced after.
|
||||||
|
*/
|
||||||
|
export interface BeanReference {
|
||||||
|
readonly kind: 'reference';
|
||||||
|
readonly name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reference(name: string): BeanReference {
|
||||||
|
return { kind: 'reference', name };
|
||||||
|
}
|
||||||
|
|
||||||
export interface BeanObject {
|
export interface BeanObject {
|
||||||
readonly kind: 'object';
|
readonly kind: 'object';
|
||||||
@@ -42,14 +57,14 @@ export interface BeanArray {
|
|||||||
export type BeanStatement =
|
export type BeanStatement =
|
||||||
/** target.setP(value) */
|
/** target.setP(value) */
|
||||||
| { readonly set: string; readonly value: BeanValue }
|
| { readonly set: string; readonly value: BeanValue }
|
||||||
/** target.getP(), then the statements on its result */
|
/** target.getP(), then the statements on its result; `as` names the result */
|
||||||
| { readonly get: string; readonly statements: readonly BeanStatement[] }
|
| { readonly get: string; readonly statements: readonly BeanStatement[]; readonly as?: BeanReference }
|
||||||
/** target.m(args) */
|
/** target.m(args) */
|
||||||
| { readonly call: string; readonly args: readonly BeanValue[] }
|
| { readonly call: string; readonly args: readonly BeanValue[] }
|
||||||
/** target.set(index, value) on a list */
|
/** target.set(index, value) on a list */
|
||||||
| { readonly setIndex: number; readonly value: BeanValue }
|
| { readonly setIndex: number; readonly value: BeanValue }
|
||||||
/** target.get(index), then the statements on its result */
|
/** target.get(index), then the statements on its result; `as` names the result */
|
||||||
| { readonly getIndex: number; readonly statements: readonly BeanStatement[] };
|
| { readonly getIndex: number; readonly statements: readonly BeanStatement[]; readonly as?: BeanReference };
|
||||||
|
|
||||||
export const int = (value: number): BeanPrimitive => ({ kind: 'int', value });
|
export const int = (value: number): BeanPrimitive => ({ kind: 'int', value });
|
||||||
export const long = (value: number | bigint): BeanPrimitive => ({ kind: 'long', value });
|
export const long = (value: number | bigint): BeanPrimitive => ({ kind: 'long', value });
|
||||||
@@ -70,7 +85,7 @@ export function encodeJavaBeans(values: readonly BeanValue[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Writer {
|
class Writer {
|
||||||
private readonly ids = new Map<BeanObject | BeanArray, string>();
|
private readonly ids = new Map<BeanObject | BeanArray | BeanReference, string>();
|
||||||
private readonly counters = new Map<string, number>();
|
private readonly counters = new Map<string, number>();
|
||||||
/** Objects written more than once need an id; found before writing. */
|
/** Objects written more than once need an id; found before writing. */
|
||||||
constructor(private readonly shared: ReadonlySet<BeanObject | BeanArray>) {}
|
constructor(private readonly shared: ReadonlySet<BeanObject | BeanArray>) {}
|
||||||
@@ -109,6 +124,12 @@ class Writer {
|
|||||||
case 'object':
|
case 'object':
|
||||||
this.object(value, depth, lines);
|
this.object(value, depth, lines);
|
||||||
break;
|
break;
|
||||||
|
case 'reference': {
|
||||||
|
const id = this.ids.get(value);
|
||||||
|
if (id === undefined) throw new Error(`reference '${value.name}' used before the getter that names it`);
|
||||||
|
lines.push(`${indent}<object idref="${id}"/>`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,10 +192,7 @@ class Writer {
|
|||||||
if ('set' in statement) {
|
if ('set' in statement) {
|
||||||
nested(`<void property="${escapeAttribute(statement.set)}"`, () => this.value(statement.value, depth + 1, lines));
|
nested(`<void property="${escapeAttribute(statement.set)}"`, () => this.value(statement.value, depth + 1, lines));
|
||||||
} else if ('get' in statement) {
|
} else if ('get' in statement) {
|
||||||
if (statement.statements.length === 0) return;
|
this.getter(`property="${escapeAttribute(statement.get)}"`, statement.statements, statement.as, depth, lines);
|
||||||
nested(`<void property="${escapeAttribute(statement.get)}"`, () => {
|
|
||||||
for (const inner of statement.statements) this.statement(inner, depth + 1, lines);
|
|
||||||
});
|
|
||||||
} else if ('call' in statement) {
|
} else if ('call' in statement) {
|
||||||
if (statement.args.length === 0) {
|
if (statement.args.length === 0) {
|
||||||
lines.push(`${indent}<void method="${escapeAttribute(statement.call)}"/>`);
|
lines.push(`${indent}<void method="${escapeAttribute(statement.call)}"/>`);
|
||||||
@@ -186,13 +204,38 @@ class Writer {
|
|||||||
} else if ('setIndex' in statement) {
|
} else if ('setIndex' in statement) {
|
||||||
nested(`<void index="${statement.setIndex}"`, () => this.value(statement.value, depth + 1, lines));
|
nested(`<void index="${statement.setIndex}"`, () => this.value(statement.value, depth + 1, lines));
|
||||||
} else {
|
} else {
|
||||||
if (statement.statements.length === 0) return;
|
this.getter(`index="${statement.getIndex}"`, statement.statements, statement.as, depth, lines);
|
||||||
nested(`<void index="${statement.getIndex}"`, () => {
|
|
||||||
for (const inner of statement.statements) this.statement(inner, depth + 1, lines);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A getter's call: left out when nothing happens to its result and nothing names it. */
|
||||||
|
private getter(
|
||||||
|
attribute: string,
|
||||||
|
statements: readonly BeanStatement[],
|
||||||
|
name: BeanReference | undefined,
|
||||||
|
depth: number,
|
||||||
|
lines: string[],
|
||||||
|
): void {
|
||||||
|
const indent = ' '.repeat(depth);
|
||||||
|
if (statements.length === 0 && name === undefined) return;
|
||||||
|
let idAttribute = '';
|
||||||
|
if (name !== undefined) {
|
||||||
|
if (this.ids.has(name)) throw new Error(`reference '${name.name}' named twice`);
|
||||||
|
const count = this.counters.get(name.name) ?? 0;
|
||||||
|
this.counters.set(name.name, count + 1);
|
||||||
|
const id = `${name.name}${count}`;
|
||||||
|
this.ids.set(name, id);
|
||||||
|
idAttribute = ` id="${id}"`;
|
||||||
|
}
|
||||||
|
if (statements.length === 0) {
|
||||||
|
lines.push(`${indent}<void${idAttribute} ${attribute}/>`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lines.push(`${indent}<void${idAttribute} ${attribute}>`);
|
||||||
|
for (const inner of statements) this.statement(inner, depth + 1, lines);
|
||||||
|
lines.push(`${indent}</void>`);
|
||||||
|
}
|
||||||
|
|
||||||
private idFor(value: BeanObject | BeanArray, name: string): string | undefined {
|
private idFor(value: BeanObject | BeanArray, name: string): string | undefined {
|
||||||
if (!this.shared.has(value)) return undefined;
|
if (!this.shared.has(value)) return undefined;
|
||||||
const count = this.counters.get(name) ?? 0;
|
const count = this.counters.get(name) ?? 0;
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ export interface FlsPortReference {
|
|||||||
export interface FlsLink {
|
export interface FlsLink {
|
||||||
ends: [FlsPortReference, FlsPortReference];
|
ends: [FlsPortReference, FlsPortReference];
|
||||||
wireless: boolean;
|
wireless: boolean;
|
||||||
|
/**
|
||||||
|
* The nodes the editor draws the cable between (JCablePanel ziel1, ziel2),
|
||||||
|
* by index; null where the file names none.
|
||||||
|
*/
|
||||||
|
panel: [number | null, number | null];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FlsColor = [red: number, green: number, blue: number, alpha: number];
|
export type FlsColor = [red: number, green: number, blue: number, alpha: number];
|
||||||
@@ -146,7 +151,10 @@ export function readTopology(values: readonly JavaValue[]): FlsTopology {
|
|||||||
const nodes: FlsNode[] = [];
|
const nodes: FlsNode[] = [];
|
||||||
for (const [index, item] of nodeItems.entries()) {
|
for (const [index, item] of nodeItems.entries()) {
|
||||||
const node = context.node(item, index, nodes.length);
|
const node = context.node(item, index, nodes.length);
|
||||||
if (node !== undefined) nodes.push(node);
|
if (node !== undefined) {
|
||||||
|
if (isJavaObject(item)) context.nodeItems.set(item, nodes.length);
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const links: FlsLink[] = [];
|
const links: FlsLink[] = [];
|
||||||
for (const [index, item] of cableItems.entries()) {
|
for (const [index, item] of cableItems.entries()) {
|
||||||
@@ -168,6 +176,8 @@ function listOf(value: JavaValue | undefined, className: string): JavaValue[] {
|
|||||||
class Reader {
|
class Reader {
|
||||||
/** Every port object a node owns, to resolve cable ends by identity as Java does. */
|
/** Every port object a node owns, to resolve cable ends by identity as Java does. */
|
||||||
private readonly portOwners = new Map<JavaObject, FlsPortReference>();
|
private readonly portOwners = new Map<JavaObject, FlsPortReference>();
|
||||||
|
/** Every node item read, to resolve a cable panel's ends. */
|
||||||
|
readonly nodeItems = new Map<JavaObject, number>();
|
||||||
|
|
||||||
constructor(private readonly warnings: string[]) {}
|
constructor(private readonly warnings: string[]) {}
|
||||||
|
|
||||||
@@ -310,9 +320,15 @@ class Reader {
|
|||||||
this.warnings.push(`cable ${itemIndex + 1}: an end is not a port of a device, left out`);
|
this.warnings.push(`cable ${itemIndex + 1}: an end is not a port of a device, left out`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const panel = (item as JavaObject).properties.get('kabelpanel');
|
||||||
|
const panelEnd = (property: string) => {
|
||||||
|
const target = isJavaObject(panel) ? panel.properties.get(property) : undefined;
|
||||||
|
return isJavaObject(target) ? (this.nodeItems.get(target) ?? null) : null;
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
ends: [first, second],
|
ends: [first, second],
|
||||||
wireless: this.booleanProperty(cable, 'wireless', `cable ${itemIndex + 1}`),
|
wireless: this.booleanProperty(cable, 'wireless', `cable ${itemIndex + 1}`),
|
||||||
|
panel: [panelEnd('ziel1'), panelEnd('ziel2')],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
// Writes `projekt/konfiguration.xml` from Java's view of a project — the
|
||||||
|
// counterpart of topology.ts. The four values Java's netzwerkLaden reads: the
|
||||||
|
// version line, the list of GUIKnotenItem, of GUIKabelItem and of
|
||||||
|
// GUIDocuItem. What a node's constructor makes (its interfaces, a switch's
|
||||||
|
// ports) is reached through getters and changed there, as XMLEncoder does;
|
||||||
|
// what the constructor does not make is added. Cable ends name the port
|
||||||
|
// objects of the nodes, so the same cable joins the same ports when Java
|
||||||
|
// loads the file.
|
||||||
|
//
|
||||||
|
// Checked by Java: every corpus file, read and written again, loads in Java
|
||||||
|
// FILIUS to the same topology (topologyWriter.test.ts, tools/java-oracle).
|
||||||
|
|
||||||
|
import {
|
||||||
|
encodeJavaBeans,
|
||||||
|
int,
|
||||||
|
object,
|
||||||
|
reference,
|
||||||
|
type BeanObject,
|
||||||
|
type BeanReference,
|
||||||
|
type BeanStatement,
|
||||||
|
type BeanValue,
|
||||||
|
} from './javaBeansWriter.js';
|
||||||
|
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNode, FlsNodeClass } from './topology.js';
|
||||||
|
|
||||||
|
/** The version line of the files this writer makes: the format of FILIUS 2.14. */
|
||||||
|
export const WRITTEN_VERSION_LINE = 'Filius version: 2.14.0 (mdg-filius)';
|
||||||
|
|
||||||
|
export type NodeToWrite = Pick<
|
||||||
|
FlsNode,
|
||||||
|
'javaClass' | 'name' | 'x' | 'y' | 'useIpAsName' | 'useMacAsName' | 'interfaces' | 'portCount'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface TopologyToWrite {
|
||||||
|
readonly nodes: readonly NodeToWrite[];
|
||||||
|
readonly links: readonly FlsLink[];
|
||||||
|
readonly documentation: readonly FlsDocumentationItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What each device class's constructor makes, and whether it is a host. */
|
||||||
|
const CONSTRUCTED: Record<FlsNodeClass, { count: number; local: boolean; host: boolean }> = {
|
||||||
|
Rechner: { count: 1, local: false, host: true },
|
||||||
|
Notebook: { count: 1, local: false, host: true },
|
||||||
|
Vermittlungsrechner: { count: 1, local: false, host: false },
|
||||||
|
Gateway: { count: 2, local: false, host: false },
|
||||||
|
Switch: { count: 24, local: true, host: false },
|
||||||
|
Modem: { count: 1, local: true, host: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function writeTopologyXml(topology: TopologyToWrite): string {
|
||||||
|
return encodeJavaBeans(topologyBeans(topology));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function topologyBeans(topology: TopologyToWrite): BeanValue[] {
|
||||||
|
const ports: BeanValue[][] = [];
|
||||||
|
const items = topology.nodes.map((node, index) => {
|
||||||
|
const nodePorts: BeanValue[] = [];
|
||||||
|
ports[index] = nodePorts;
|
||||||
|
return nodeItem(node, nodePorts);
|
||||||
|
});
|
||||||
|
const cables = topology.links.map((link) => cableItem(link, items, ports));
|
||||||
|
return [
|
||||||
|
WRITTEN_VERSION_LINE,
|
||||||
|
listOf(items),
|
||||||
|
listOf(cables),
|
||||||
|
listOf(topology.documentation.map(documentationItem)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function listOf(values: readonly BeanValue[]): BeanObject {
|
||||||
|
return object(
|
||||||
|
'java.util.LinkedList',
|
||||||
|
values.map((value) => ({ call: 'add', args: [value] })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A GUIKnotenItem: its label (the position) and its device; `ports`
|
||||||
|
* receives the device's port objects. The label's size is Java's to work
|
||||||
|
* out when it loads the file; the written 0 × 0 does not move it (measured
|
||||||
|
* on the corpus).
|
||||||
|
*/
|
||||||
|
function nodeItem(node: NodeToWrite, ports: BeanValue[]): BeanObject {
|
||||||
|
const label = object('filius.gui.netzwerksicht.JSidebarButton', [
|
||||||
|
{ set: 'bounds', value: object('java.awt.Rectangle', [], [int(node.x), int(node.y), int(0), int(0)]) },
|
||||||
|
]);
|
||||||
|
return object('filius.gui.netzwerksicht.GUIKnotenItem', [
|
||||||
|
{ set: 'imageLabel', value: label },
|
||||||
|
{ set: 'knoten', value: device(node, ports) },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function device(node: NodeToWrite, ports: BeanValue[]): BeanObject {
|
||||||
|
const constructed = CONSTRUCTED[node.javaClass];
|
||||||
|
const statements: BeanStatement[] = [{ set: 'name', value: node.name }];
|
||||||
|
if (constructed.host) {
|
||||||
|
statements.push({ set: 'useIPAsName', value: node.useIpAsName }, { set: 'useMACAsName', value: node.useMacAsName });
|
||||||
|
}
|
||||||
|
if (constructed.local) {
|
||||||
|
statements.push(
|
||||||
|
constructedList('anschluesse', node.portCount, constructed.count, ports, () => ({ statements: [] }), () => {
|
||||||
|
const port = object('filius.hardware.Port');
|
||||||
|
return { value: port, port };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
statements.push(
|
||||||
|
constructedList(
|
||||||
|
'netzwerkInterfaces',
|
||||||
|
node.interfaces.length,
|
||||||
|
constructed.count,
|
||||||
|
ports,
|
||||||
|
(index) => {
|
||||||
|
const port = reference('Port');
|
||||||
|
return { statements: [...interfaceStatements(node.interfaces[index]!), { get: 'port', statements: [], as: port }], port };
|
||||||
|
},
|
||||||
|
(index) => {
|
||||||
|
const port = reference('Port');
|
||||||
|
const nic = object('filius.hardware.NetzwerkInterface', [
|
||||||
|
...interfaceStatements(node.interfaces[index]!),
|
||||||
|
{ get: 'port', statements: [], as: port },
|
||||||
|
]);
|
||||||
|
return { value: nic, port };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return object(`filius.hardware.knoten.${node.javaClass}`, statements);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list the constructor filled with `constructed` elements: those are
|
||||||
|
* changed through `get(index)`, more are added. With fewer, the list is set
|
||||||
|
* anew. `ports` receives each element's port, in order.
|
||||||
|
*/
|
||||||
|
function constructedList(
|
||||||
|
property: string,
|
||||||
|
count: number,
|
||||||
|
constructed: number,
|
||||||
|
ports: BeanValue[],
|
||||||
|
existing: (index: number) => { statements: BeanStatement[]; port?: BeanReference },
|
||||||
|
added: (index: number) => { value: BeanValue; port: BeanValue },
|
||||||
|
): BeanStatement {
|
||||||
|
if (count < constructed) {
|
||||||
|
const values = Array.from({ length: count }, (_, index) => {
|
||||||
|
const { value, port } = added(index);
|
||||||
|
ports.push(port);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
return { set: property, value: listOf(values) };
|
||||||
|
}
|
||||||
|
const statements: BeanStatement[] = [];
|
||||||
|
for (let index = 0; index < count; index++) {
|
||||||
|
if (index < constructed) {
|
||||||
|
// The element's own port, or — a switch's — the element itself.
|
||||||
|
const { statements: inner, port } = existing(index);
|
||||||
|
if (port === undefined) {
|
||||||
|
const element = reference('Port');
|
||||||
|
ports.push(element);
|
||||||
|
statements.push({ getIndex: index, statements: inner, as: element });
|
||||||
|
} else {
|
||||||
|
ports.push(port);
|
||||||
|
statements.push({ getIndex: index, statements: inner });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { value, port } = added(index);
|
||||||
|
ports.push(port);
|
||||||
|
statements.push({ call: 'add', args: [value] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { get: property, statements };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A network interface's setters. An address Java's setter rejected (null)
|
||||||
|
* is not written: setIp(null) throws, and the constructor's address stays.
|
||||||
|
* A MAC the file lacked stays unwritten, so Java rolls one as it did.
|
||||||
|
*/
|
||||||
|
function interfaceStatements(nic: FlsInterface): BeanStatement[] {
|
||||||
|
const statements: BeanStatement[] = [];
|
||||||
|
if (nic.ip !== null) statements.push({ set: 'ip', value: nic.ip });
|
||||||
|
if (nic.mask !== null) statements.push({ set: 'subnetzMaske', value: nic.mask });
|
||||||
|
if (nic.mac !== undefined) statements.push({ set: 'mac', value: nic.mac });
|
||||||
|
statements.push({ set: 'gateway', value: nic.gateway }, { set: 'dns', value: nic.dns });
|
||||||
|
return statements;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cableItem(link: FlsLink, items: readonly BeanObject[], ports: readonly BeanValue[][]): BeanObject {
|
||||||
|
const [first, second] = link.ends;
|
||||||
|
const end = (reference: { node: number; port: number }) => ports[reference.node]![reference.port]!;
|
||||||
|
const cable = object('filius.hardware.Kabel', [
|
||||||
|
{ set: 'anschluesse', value: { kind: 'array', componentClass: 'filius.hardware.Port', items: [end(first), end(second)] } },
|
||||||
|
...(link.wireless ? [{ set: 'wireless', value: true }] : []),
|
||||||
|
]);
|
||||||
|
return object('filius.gui.netzwerksicht.GUIKabelItem', [
|
||||||
|
{ set: 'dasKabel', value: cable },
|
||||||
|
{
|
||||||
|
get: 'kabelpanel',
|
||||||
|
statements: [
|
||||||
|
{ set: 'ziel1', value: items[first.node]! },
|
||||||
|
{ set: 'ziel2', value: items[second.node]! },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function documentationItem(item: FlsDocumentationItem): BeanObject {
|
||||||
|
const statements: BeanStatement[] = [];
|
||||||
|
if (item.color !== null) statements.push({ set: 'color', value: object('java.awt.Color', [], item.color.map(int)) });
|
||||||
|
if (item.font !== null) {
|
||||||
|
statements.push({ set: 'font', value: object('java.awt.Font', [], [item.font.name, int(item.font.style), int(item.font.size)]) });
|
||||||
|
}
|
||||||
|
statements.push(
|
||||||
|
{ set: 'height', value: int(item.height) },
|
||||||
|
{ set: 'text', value: item.text },
|
||||||
|
{ set: 'type', value: int(item.type) },
|
||||||
|
{ set: 'width', value: int(item.width) },
|
||||||
|
{ set: 'x', value: int(item.x) },
|
||||||
|
{ set: 'y', value: int(item.y) },
|
||||||
|
);
|
||||||
|
return object('filius.gui.netzwerksicht.GUIDocuItem', statements);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -296,6 +296,47 @@
|
|||||||
],
|
],
|
||||||
"errors": []
|
"errors": []
|
||||||
},
|
},
|
||||||
|
"references.xml": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"class": "java.util.LinkedList",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"class": "filius.hardware.NetzwerkInterface",
|
||||||
|
"properties": {
|
||||||
|
"dns": {
|
||||||
|
"string": ""
|
||||||
|
},
|
||||||
|
"gateway": {
|
||||||
|
"string": ""
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"string": "10.0.0.1"
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"string": "8E:A8:7E:96:38:62"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"id": 2,
|
||||||
|
"class": "filius.hardware.Port",
|
||||||
|
"properties": {}
|
||||||
|
},
|
||||||
|
"subnetzMaske": {
|
||||||
|
"string": "255.255.255.0"
|
||||||
|
},
|
||||||
|
"wireless": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ref": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
"shared.xml": {
|
"shared.xml": {
|
||||||
"values": [
|
"values": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<java version="17" class="java.beans.XMLDecoder">
|
||||||
|
<object class="java.util.LinkedList">
|
||||||
|
<void method="add">
|
||||||
|
<object class="filius.hardware.NetzwerkInterface">
|
||||||
|
<void property="ip">
|
||||||
|
<string>10.0.0.1</string>
|
||||||
|
</void>
|
||||||
|
<void id="Port0" property="port"/>
|
||||||
|
</object>
|
||||||
|
</void>
|
||||||
|
<void method="add">
|
||||||
|
<object idref="Port0"/>
|
||||||
|
</void>
|
||||||
|
</object>
|
||||||
|
</java>
|
||||||
@@ -1044,9 +1044,9 @@ describe('importProject rules', () => {
|
|||||||
topology({
|
topology({
|
||||||
nodes: [flsNode('Switch', { portCount: 24 }), flsNode('Rechner', { interfaces: [nic('10.0.0.1')] })],
|
nodes: [flsNode('Switch', { portCount: 24 }), flsNode('Rechner', { interfaces: [nic('10.0.0.1')] })],
|
||||||
links: [
|
links: [
|
||||||
{ ends: [{ node: 0, port: 0 }, { node: 0, port: 1 }], wireless: false },
|
{ ends: [{ node: 0, port: 0 }, { node: 0, port: 1 }], wireless: false, panel: [null, null] },
|
||||||
{ ends: [{ node: 1, port: 0 }, { node: 0, port: 2 }], wireless: false },
|
{ ends: [{ node: 1, port: 0 }, { node: 0, port: 2 }], wireless: false, panel: [null, null] },
|
||||||
{ ends: [{ node: 0, port: 3 }, { node: 1, port: 0 }], wireless: false },
|
{ ends: [{ node: 0, port: 3 }, { node: 1, port: 0 }], wireless: false, panel: [null, null] },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
counter(),
|
counter(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
int,
|
int,
|
||||||
long,
|
long,
|
||||||
object,
|
object,
|
||||||
|
reference,
|
||||||
type BeanObject,
|
type BeanObject,
|
||||||
type BeanValue,
|
type BeanValue,
|
||||||
} from '../src/javaBeansWriter.js';
|
} from '../src/javaBeansWriter.js';
|
||||||
@@ -42,7 +43,23 @@ const file = object('filius.software.system.Datei', [
|
|||||||
{ set: 'dateiInhalt', value: 'a.de. A 3600 1.2.3.4\n' },
|
{ set: 'dateiInhalt', value: 'a.de. A 3600 1.2.3.4\n' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const port = reference('Port');
|
||||||
|
|
||||||
const CASES: Record<string, { values: BeanValue[]; expected: unknown[] }> = {
|
const CASES: Record<string, { values: BeanValue[]; expected: unknown[] }> = {
|
||||||
|
references: {
|
||||||
|
values: [
|
||||||
|
object('java.util.LinkedList', [
|
||||||
|
{ call: 'add', args: [object('filius.hardware.NetzwerkInterface', [{ set: 'ip', value: '10.0.0.1' }, { get: 'port', statements: [], as: port }])] },
|
||||||
|
{ call: 'add', args: [port] },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
expected: [
|
||||||
|
items('java.util.LinkedList', [
|
||||||
|
bean('filius.hardware.NetzwerkInterface', { ip: text('10.0.0.1'), port: { id: 2, class: 'filius.hardware.Port' } }),
|
||||||
|
{ ref: 2 },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
strings: {
|
strings: {
|
||||||
values: [list('java.util.LinkedList', STRINGS)],
|
values: [list('java.util.LinkedList', STRINGS)],
|
||||||
expected: [items('java.util.LinkedList', STRINGS.map(text))],
|
expected: [items('java.util.LinkedList', STRINGS.map(text))],
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
|
|||||||
cables: topology.links.map((link) => ({
|
cables: topology.links.map((link) => ({
|
||||||
ends: link.ends.map((end) => [end.node, end.port]),
|
ends: link.ends.map((end) => [end.node, end.port]),
|
||||||
wireless: link.wireless,
|
wireless: link.wireless,
|
||||||
|
panel: link.panel,
|
||||||
})),
|
})),
|
||||||
documentation: topology.documentation.map((item) => ({
|
documentation: topology.documentation.map((item) => ({
|
||||||
type: item.type,
|
type: item.type,
|
||||||
@@ -305,7 +306,7 @@ describe('readTopology rules', () => {
|
|||||||
</object>`);
|
</object>`);
|
||||||
|
|
||||||
expect(nodes.map((node) => node.javaClass)).toEqual(['Switch']);
|
expect(nodes.map((node) => node.javaClass)).toEqual(['Switch']);
|
||||||
expect(links).toEqual([{ ends: [{ node: 0, port: 0 }, { node: 0, port: 1 }], wireless: false }]);
|
expect(links).toEqual([{ ends: [{ node: 0, port: 0 }, { node: 0, port: 1 }], wireless: false, panel: [null, null] }]);
|
||||||
expect(warnings).toEqual([
|
expect(warnings).toEqual([
|
||||||
'node 1: unknown device class filius.hardware.knoten.Drucker, left out',
|
'node 1: unknown device class filius.hardware.knoten.Drucker, left out',
|
||||||
'cable 2: an end is not a port of a device, left out',
|
'cable 2: an end is not a port of a device, left out',
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// Every corpus file, read and written again: Java FILIUS loads the written
|
||||||
|
// file (tools/java-oracle, written.java.json) to the same topology as the
|
||||||
|
// original (corpus.java.json) — devices, names, positions, interfaces,
|
||||||
|
// cables and documentation. Java against Java: no mapping of this package
|
||||||
|
// stands between the two.
|
||||||
|
//
|
||||||
|
// UPDATE_WRITTEN=1 writes the files to test/written/ (not checked in); the
|
||||||
|
// Java oracle then loads them and records each file's SHA-256, which must
|
||||||
|
// match what the writer makes now.
|
||||||
|
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||||
|
import { dirname, join, relative } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { decodeJavaBeans, readFlsArchive, readTopology, writeFlsArchive } from '../src/index.js';
|
||||||
|
import { writeTopologyXml, type TopologyToWrite } from '../src/topologyWriter.js';
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const corpusRoot = join(here, '../../../corpus');
|
||||||
|
const writtenRoot = join(here, 'written');
|
||||||
|
|
||||||
|
function flsFiles(directory: string): string[] {
|
||||||
|
return readdirSync(directory).flatMap((name) => {
|
||||||
|
const path = join(directory, name);
|
||||||
|
if (statSync(path).isDirectory()) return flsFiles(path);
|
||||||
|
return name.endsWith('.fls') ? [path] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type JavaNode = { class: string; name: string | null; bounds: number[]; useIpAsName: boolean; useMacAsName: boolean; ports: unknown };
|
||||||
|
type JavaFile = { success: boolean; nodes: JavaNode[]; cables: unknown[]; documentation: unknown[]; sha256?: string };
|
||||||
|
|
||||||
|
const original = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<string, JavaFile>;
|
||||||
|
const written = JSON.parse(readFileSync(join(here, 'written.java.json'), 'utf8')) as Record<string, JavaFile>;
|
||||||
|
|
||||||
|
/** The topology of Java's view: the label's position, not its size, which Java works out itself. */
|
||||||
|
function topologyOf(view: JavaFile) {
|
||||||
|
return {
|
||||||
|
success: view.success,
|
||||||
|
nodes: view.nodes.map((node) => ({
|
||||||
|
class: node.class,
|
||||||
|
name: node.name,
|
||||||
|
position: node.bounds.slice(0, 2),
|
||||||
|
useIpAsName: node.useIpAsName,
|
||||||
|
useMacAsName: node.useMacAsName,
|
||||||
|
ports: node.ports,
|
||||||
|
})),
|
||||||
|
cables: view.cables,
|
||||||
|
documentation: view.documentation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('the topology written again, as Java loads it', () => {
|
||||||
|
for (const file of flsFiles(corpusRoot)) {
|
||||||
|
const name = relative(corpusRoot, file);
|
||||||
|
it(name, () => {
|
||||||
|
const topology = readTopology(decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml));
|
||||||
|
const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(topology)));
|
||||||
|
if (process.env.UPDATE_WRITTEN === '1') {
|
||||||
|
mkdirSync(dirname(join(writtenRoot, name)), { recursive: true });
|
||||||
|
writeFileSync(join(writtenRoot, name), archive);
|
||||||
|
}
|
||||||
|
const java = written[name];
|
||||||
|
expect(java?.sha256, 'written.java.json is not about this output: UPDATE_WRITTEN=1, then npm run oracle:java').toBe(
|
||||||
|
createHash('sha256').update(archive).digest('hex'),
|
||||||
|
);
|
||||||
|
expect(topologyOf(java!)).toEqual(topologyOf(original[name]!));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** What the corpus does not have: a name set to null, useMACAsName, fewer ports than the constructor makes, more interfaces. */
|
||||||
|
const SYNTHETIC: TopologyToWrite = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
javaClass: 'Rechner',
|
||||||
|
name: null,
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
useIpAsName: false,
|
||||||
|
useMacAsName: true,
|
||||||
|
interfaces: [{ ip: '10.0.0.1', mask: '255.0.0.0', mac: 'AA:BB:CC:DD:EE:01', gateway: null, dns: '10.0.0.53' }],
|
||||||
|
portCount: 1,
|
||||||
|
},
|
||||||
|
{ javaClass: 'Switch', name: 'S', x: 100, y: 20, useIpAsName: false, useMacAsName: false, interfaces: [], portCount: 3 },
|
||||||
|
{
|
||||||
|
javaClass: 'Vermittlungsrechner',
|
||||||
|
name: 'R',
|
||||||
|
x: 200,
|
||||||
|
y: 20,
|
||||||
|
useIpAsName: false,
|
||||||
|
useMacAsName: false,
|
||||||
|
interfaces: ['1', '2', '3'].map((octet) => ({
|
||||||
|
ip: `10.0.${octet}.1`,
|
||||||
|
mask: '255.255.255.0',
|
||||||
|
mac: `AA:BB:CC:DD:EE:1${octet}`,
|
||||||
|
gateway: '',
|
||||||
|
dns: '',
|
||||||
|
})),
|
||||||
|
portCount: 3,
|
||||||
|
},
|
||||||
|
{ javaClass: 'Modem', name: 'M', x: 300, y: 20, useIpAsName: false, useMacAsName: false, interfaces: [], portCount: 1 },
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{ ends: [{ node: 0, port: 0 }, { node: 1, port: 2 }], wireless: false, panel: [0, 1] },
|
||||||
|
{ ends: [{ node: 2, port: 2 }, { node: 1, port: 0 }], wireless: true, panel: [2, 1] },
|
||||||
|
],
|
||||||
|
documentation: [{ type: 1, text: null, x: 5, y: 6, width: 70, height: 80, color: null, font: null }],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('a topology beyond the corpus, as Java loads it', () => {
|
||||||
|
it('keeps what the corpus never sets', () => {
|
||||||
|
const name = 'synthetic/synthetic.fls';
|
||||||
|
const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(SYNTHETIC)));
|
||||||
|
if (process.env.UPDATE_WRITTEN === '1') {
|
||||||
|
mkdirSync(join(writtenRoot, 'synthetic'), { recursive: true });
|
||||||
|
writeFileSync(join(writtenRoot, name), archive);
|
||||||
|
}
|
||||||
|
const java = written[name];
|
||||||
|
expect(java?.sha256, 'written.java.json is not about this output: UPDATE_WRITTEN=1, then npm run oracle:java').toBe(
|
||||||
|
createHash('sha256').update(archive).digest('hex'),
|
||||||
|
);
|
||||||
|
const plainInterface = (octet: string) => ({ ip: `10.0.${octet}.1`, mask: '255.255.255.0', mac: `AA:BB:CC:DD:EE:1${octet}`, gateway: '', dns: '' });
|
||||||
|
expect(topologyOf(java!)).toEqual({
|
||||||
|
success: true,
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
class: 'Rechner',
|
||||||
|
name: null,
|
||||||
|
position: [10, 20],
|
||||||
|
useIpAsName: false,
|
||||||
|
useMacAsName: true,
|
||||||
|
ports: [{ ip: '10.0.0.1', mask: '255.0.0.0', mac: 'AA:BB:CC:DD:EE:01', gateway: null, dns: '10.0.0.53' }],
|
||||||
|
},
|
||||||
|
{ class: 'Switch', name: 'S', position: [100, 20], useIpAsName: undefined, useMacAsName: undefined, ports: [{}, {}, {}] },
|
||||||
|
{
|
||||||
|
class: 'Vermittlungsrechner',
|
||||||
|
name: 'R',
|
||||||
|
position: [200, 20],
|
||||||
|
useIpAsName: undefined,
|
||||||
|
useMacAsName: undefined,
|
||||||
|
ports: ['1', '2', '3'].map(plainInterface),
|
||||||
|
},
|
||||||
|
{ class: 'Modem', name: 'M', position: [300, 20], useIpAsName: undefined, useMacAsName: undefined, ports: [{}] },
|
||||||
|
],
|
||||||
|
cables: [
|
||||||
|
{ ends: [[0, 0], [1, 2]], wireless: false, panel: [0, 1] },
|
||||||
|
{ ends: [[2, 2], [1, 0]], wireless: true, panel: [2, 1] },
|
||||||
|
],
|
||||||
|
documentation: [{ type: 1, text: null, bounds: [5, 6, 70, 80], color: null, font: null }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,17 @@ wird leer, der Eintrag bleibt trotzdem; Typen außer A/MX/NS (auch `a`,
|
|||||||
`CNAME`, eine Kommentarzeile) gelten als gültig; eine TTL, die kein `int`
|
`CNAME`, eine Kommentarzeile) gelten als gültig; eine TTL, die kein `int`
|
||||||
ist, wird still 3600.
|
ist, wird still 3600.
|
||||||
|
|
||||||
|
## Was Java aus unseren Dateien lädt
|
||||||
|
|
||||||
|
`run.sh` lädt auch die Dateien unter `packages/fls/test/written/`, die
|
||||||
|
`topologyWriter.test.ts` mit `UPDATE_WRITTEN=1` schreibt (nicht
|
||||||
|
eingecheckt), mit derselben Lademethode wie den Korpus, und schreibt
|
||||||
|
`packages/fls/test/written.java.json` samt SHA-256 jeder Datei
|
||||||
|
(`--sha256`), damit der Test merkt, wenn der Befund nicht mehr zur Ausgabe
|
||||||
|
passt. Je Kabel steht auch `panel`: die Geräte, zwischen denen die
|
||||||
|
Oberfläche es zeichnet (`JCablePanel.ziel1/ziel2`) — im Korpus bei allen 183
|
||||||
|
Kabeln die Geräte, die es verbindet.
|
||||||
|
|
||||||
## Was Java aus unserem XML macht
|
## Was Java aus unserem XML macht
|
||||||
|
|
||||||
`BeansDump` liest jede `*.xml` unter `packages/fls/test/beans/` mit
|
`BeansDump` liest jede `*.xml` unter `packages/fls/test/beans/` mit
|
||||||
|
|||||||
@@ -88,3 +88,13 @@ beans="$root/packages/fls/test/beans"
|
|||||||
rm -f "$beans/beans.java.json"
|
rm -f "$beans/beans.java.json"
|
||||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" BeansDump "$beans" "$beans/beans.java.json" 2>&1 | grep -v 'logback\|^$' || true
|
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" BeansDump "$beans" "$beans/beans.java.json" 2>&1 | grep -v 'logback\|^$' || true
|
||||||
test -s "$beans/beans.java.json"
|
test -s "$beans/beans.java.json"
|
||||||
|
|
||||||
|
# What Java loads from the files packages/fls writes (topologyWriter.test.ts
|
||||||
|
# writes them with UPDATE_WRITTEN=1; the files are not checked in, their
|
||||||
|
# SHA-256 is).
|
||||||
|
written="$root/packages/fls/test/written"
|
||||||
|
if [ -d "$written" ]; then
|
||||||
|
rm -f "$written.java.json"
|
||||||
|
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" FlsOracle "$written" "$written.java.json" --sha256 2>&1 | grep -v 'logback\|^$' || true
|
||||||
|
test -s "$written.java.json"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import filius.gui.netzwerksicht.GUIDocuItem;
|
import filius.gui.netzwerksicht.GUIDocuItem;
|
||||||
import filius.gui.netzwerksicht.GUIKabelItem;
|
import filius.gui.netzwerksicht.GUIKabelItem;
|
||||||
|
import filius.gui.netzwerksicht.JCablePanel;
|
||||||
import filius.gui.netzwerksicht.GUIKnotenItem;
|
import filius.gui.netzwerksicht.GUIKnotenItem;
|
||||||
import filius.hardware.Kabel;
|
import filius.hardware.Kabel;
|
||||||
import filius.hardware.NetzwerkInterface;
|
import filius.hardware.NetzwerkInterface;
|
||||||
@@ -72,7 +73,9 @@ public class FlsOracle {
|
|||||||
for (Path file : files) {
|
for (Path file : files) {
|
||||||
String name = root.relativize(file).toString();
|
String name = root.relativize(file).toString();
|
||||||
try {
|
try {
|
||||||
result.put(name, read(file));
|
Map<String, Object> view = read(file);
|
||||||
|
if (args.length > 2 && args[2].equals("--sha256")) view.put("sha256", sha256(Files.readAllBytes(file)));
|
||||||
|
result.put(name, view);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
Throwable cause = error instanceof java.lang.reflect.InvocationTargetException && error.getCause() != null
|
Throwable cause = error instanceof java.lang.reflect.InvocationTargetException && error.getCause() != null
|
||||||
? error.getCause()
|
? error.getCause()
|
||||||
@@ -96,6 +99,19 @@ public class FlsOracle {
|
|||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The position of an item in a list by identity, null when absent. */
|
||||||
|
static Integer indexOf(List<?> list, Object item) {
|
||||||
|
if (item == null) return null;
|
||||||
|
for (int index = 0; index < list.size(); index++) if (list.get(index) == item) return index;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String sha256(byte[] bytes) throws Exception {
|
||||||
|
StringBuilder hex = new StringBuilder();
|
||||||
|
for (byte value : java.security.MessageDigest.getInstance("SHA-256").digest(bytes)) hex.append(String.format("%02x", value));
|
||||||
|
return hex.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the file twice. What FILIUS does not find in a file it rolls at
|
* Loads the file twice. What FILIUS does not find in a file it rolls at
|
||||||
* random (MAC addresses, a switch's SSID) — such values differ between
|
* random (MAC addresses, a switch's SSID) — such values differ between
|
||||||
@@ -208,6 +224,10 @@ public class FlsOracle {
|
|||||||
}
|
}
|
||||||
json.put("ends", ends);
|
json.put("ends", ends);
|
||||||
json.put("wireless", cable != null && cable.getWireless());
|
json.put("wireless", cable != null && cable.getWireless());
|
||||||
|
// The nodes the editor draws the cable between (JCablePanel).
|
||||||
|
JCablePanel panel = item.getKabelpanel();
|
||||||
|
json.put("panel", panel == null ? null
|
||||||
|
: Arrays.asList(indexOf(nodes, panel.getZiel1()), indexOf(nodes, panel.getZiel2())));
|
||||||
cableList.add(json);
|
cableList.add(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user