feat(fls): keep a .fls's own applications through the editor model

The archive's entries besides the configuration — the project's own Java
applications under projekt/anwendungen (.java, .class,
EigeneAnwendungen.txt) — now go through the editor model:
openProjectArchive reads a whole .fls into the model and keeps them in its
archiveEntries, saveProjectArchive writes a whole .fls from the model with
them unchanged. They cannot run here, but must not be lost.

Checked on every corpus file: each entry comes back byte for byte (7 files
carry applications of their own); a probe that drops them is caught.
Installing such applications on a host is the next step; Java loads those
only through its full loading path, which the oracle is to use first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 09:29:21 +00:00
co-authored by Claude Opus 5.5
parent 8ab8850b20
commit 75c3da2bd1
5 changed files with 49 additions and 15 deletions
+11 -1
View File
@@ -111,11 +111,12 @@ import {
type TopologyNode,
type VirtualFileSystem,
} from '@mdg-filius/core';
import { writeFlsArchive } from './archive.js';
import { EMAIL_CLIENT_FILE, EMAIL_SERVER_FILE, type JavaAddressEntry, type JavaContact, type JavaEmail } from './javaEmail.js';
import { PROGRAM_OF_CLASS } from './importProject.js';
import type { FlsApplicationClass, FlsDhcpServer, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNodeClass } from './topology.js';
import type { ApplicationToWrite, NodeToWrite, SystemToWrite, TopologyToWrite } from './topologyWriter.js';
import { writeTopologyXml, type ApplicationToWrite, type NodeToWrite, type SystemToWrite, type TopologyToWrite } from './topologyWriter.js';
/** The parts of the editor model the topology is made of. */
export interface ProjectToExport {
@@ -142,6 +143,8 @@ export interface ProjectToExport {
readonly emailClientConfigurations: ReadonlyMap<Id, EmailClientConfiguration>;
readonly emailServerConfigurations: ReadonlyMap<Id, EmailServerConfiguration>;
readonly documentationItems: readonly DocumentationItem[];
/** The archive's other entries (the project's own applications), written unchanged. */
readonly archiveEntries?: ReadonlyMap<string, Uint8Array>;
}
export interface ExportedTopology {
@@ -195,6 +198,13 @@ export function exportProject(project: ProjectToExport): ExportedTopology {
};
}
/** A whole .fls: the model's configuration, as Java's view written, and the archive's other entries unchanged. */
export function saveProjectArchive(project: ProjectToExport): { readonly archive: Uint8Array; readonly warnings: string[] } {
const { topology, warnings } = exportProject(project);
const configuration = new TextEncoder().encode(writeTopologyXml(topology));
return { archive: writeFlsArchive(configuration, project.archiveEntries ?? new Map()), warnings };
}
function exportNode(node: TopologyNode, project: ProjectToExport, servingModems: ReadonlySet<Id>, warnings: string[]): NodeToWrite {
const javaClass = CLASS_OF_KIND[node.kind as Exclude<NodeKind, 'unsupported'>];
const interfaces = exportInterfaces(node, project);
+21 -2
View File
@@ -97,10 +97,12 @@ import {
type TopologyNode,
type VirtualFileContent,
} from '@mdg-filius/core';
import { readFlsArchive } from './archive.js';
import { dnsRecordsOf, javaHostsFileContent, readDnsHosts } from './dnsHosts.js';
import { decodeJavaBeans } from './javabeans.js';
import { emailClientConfiguration, emailServerConfiguration } from './importEmail.js';
import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
import { readTopology, type FlsDocumentationItem, type FlsInterface, type FlsNode, type FlsNodeClass, type FlsTopology } from './topology.js';
export interface ImportedProject {
versionLine: string | null;
@@ -132,6 +134,12 @@ export interface ImportedProject {
simpleClientConfigurations: Map<Id, SimpleClientConfiguration>;
/** In render order: rectangles below text, then by order. */
documentationItems: DocumentationItem[];
/**
* The archive's entries besides the configuration — the project's own Java
* applications (projekt/anwendungen) — carried unchanged to the next save:
* they cannot run here, but must not be lost.
*/
archiveEntries: ReadonlyMap<string, Uint8Array>;
/** The topology's warnings, then this step's. */
warnings: string[];
}
@@ -150,7 +158,11 @@ export const NODE_KIND_OF_CLASS: Readonly<Record<FlsNodeClass, NodeKind>> = {
* (Swift uses random UUIDs; tests pass a counter). Documentation ids are
* derived from the item, as in Swift.
*/
export function importProject(topology: FlsTopology, newId: () => Id): ImportedProject {
export function importProject(
topology: FlsTopology,
newId: () => Id,
archiveEntries: ReadonlyMap<string, Uint8Array> = new Map(),
): ImportedProject {
const warnings = [...topology.warnings];
const deviceConfigurations = new Map<Id, DeviceConfiguration>();
const interfaceConfigurations = new Map<InterfaceKey, InterfaceConfiguration>();
@@ -236,10 +248,17 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
interfaceConfigurations,
...system,
documentationItems: inDeterministicRenderOrder(documentationItems),
archiveEntries,
warnings,
};
}
/** A whole .fls: its archive, Java's view of its configuration, the editor model with the archive's other entries. */
export function openProjectArchive(archive: Uint8Array, newId: () => Id): ImportedProject {
const contents = readFlsArchive(archive);
return importProject(readTopology(decodeJavaBeans(contents.configurationXml)), newId, contents.supplementalEntries);
}
function hostLabelPolicy(node: FlsNode): HostLabelPolicy {
if (node.useIpAsName && node.useMacAsName) return 'ipAndMAC';
if (node.useIpAsName) return 'ipAddress';
+8 -3
View File
@@ -30,7 +30,9 @@ import {
exportProject,
importProject,
javaBase64,
openProjectArchive,
readFlsArchive,
saveProjectArchive,
readTopology,
writeFlsArchive,
type FlsFileTree,
@@ -324,10 +326,13 @@ describe('the editor model exported, 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 project = importProject(topology, counter());
const bytes = new Uint8Array(readFileSync(file));
const topology = readTopology(decodeJavaBeans(readFlsArchive(bytes).configurationXml));
const project = openProjectArchive(bytes, counter());
const { topology: toWrite } = exportProject(project);
const archive = writeFlsArchive(new TextEncoder().encode(writeTopologyXml(toWrite)));
const { archive } = saveProjectArchive(project);
// The project's own applications, and every other entry, byte for byte.
expect(readFlsArchive(archive).supplementalEntries).toEqual(readFlsArchive(bytes).supplementalEntries);
if (process.env.UPDATE_EXPORTED === '1') {
mkdirSync(dirname(join(exportedRoot, name)), { recursive: true });
writeFileSync(join(exportedRoot, name), archive);
+7 -7
View File
@@ -8230,7 +8230,7 @@
}
],
"documentation": [],
"sha256": "e0f906a2f02c2fdb9b309a6fc0c5625113270ffa95a7a1ce8be11f93e146bd2c"
"sha256": "955edc9322d1de6bae59ce05221b94ab18983c46193db034cf37fd18f0146a48"
},
"java-examples/dhcp_test_scenario.fls": {
"success": true,
@@ -8650,7 +8650,7 @@
}
],
"documentation": [],
"sha256": "76d8317e6d352e8686663957f0bbc74f1945b0c419a80ac94992e40590e75e8c"
"sha256": "24841ef8e02136eef19247c1a53850c4644232d97a016815bd2338e8190bd357"
},
"java-examples/dns_server.fls": {
"success": true,
@@ -10024,7 +10024,7 @@
}
],
"documentation": [],
"sha256": "ebd1aad2945a178470cc22ce327460d09c456814dbcd155eacced456f242661b"
"sha256": "bc7b13a0ada9522a211583371d9b838c4731642032efa1d49d8c217903e4b989"
},
"java-examples/email_komplett.fls": {
"success": true,
@@ -13451,7 +13451,7 @@
}
],
"documentation": [],
"sha256": "35e84042bc6b04062cb943127a85900ebbc947d5148e9fbf4978bb128a013ff3"
"sha256": "b860d1bdf773a92b90f63ff6c7c76e9d0ee2c9b342e6042c3e39f61acf6697e3"
},
"java-examples/nat.fls": {
"success": true,
@@ -17079,7 +17079,7 @@
}
],
"documentation": [],
"sha256": "f9f829093450eda122c1a5c01d66b1492fd426769652d1f138791a56641d1fea"
"sha256": "ff743751be0d5ecdd84d1db935a332c248e39d1f6f7d3882efdb108c419a0a38"
},
"java-examples/zwei_rechnernetze_komplett.fls": {
"success": true,
@@ -18359,14 +18359,14 @@
}
],
"documentation": [],
"sha256": "ebd1aad2945a178470cc22ce327460d09c456814dbcd155eacced456f242661b"
"sha256": "bc7b13a0ada9522a211583371d9b838c4731642032efa1d49d8c217903e4b989"
},
"java-tests/empty.fls": {
"success": true,
"nodes": [],
"cables": [],
"documentation": [],
"sha256": "49855a61d8576af64e50c211807e658d69daa924a411ced654a3452070b1722c"
"sha256": "aec202d78dc3a05cd83471744050a102cb2cf448b273b053df6b4c640f1ac6e9"
},
"synthetic/model.fls": {
"success": true,