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:
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user