feat(fls): export the editor model's files

exportProject.ts, step X3: the files of a PC or notebook from the model's
virtual file system under Java's root. A file's type follows from its kind:
text by a known extension (else "text"), an image or binary data by its
media type. Images and binary data are Base64 as FILIUS's
Base64.encodeBytes (iHarder 2.x) writes it: a line break after every 76
characters, also after the last full line. Without that, all 31 images of
the corpus came back with other content. A new oracle tool, Base64Cases,
has Java encode 19 lengths around the line length; javaBase64 matches all.
A router or home router keeps the files its constructor makes.

Checked by Java: every corpus file, imported, exported and written, loads
with the files of the original, byte for byte, up to named departures of
the model: files in its order (directories first, then by name), with the
type of their kind rather than the one the file named (Java itself names
them unevenly), and no router files beyond the constructor's (www.conf,
16 times). The synthetic model adds nested directories, text with and
without a known extension, a PNG, a JPEG, binary data with and without a
media type and 57 bytes, one Base64 line exactly. 10 mutation probes, each
loaded by Java, were all caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 09:11:39 +00:00
co-authored by Claude Opus 5.5
parent d3f476269a
commit 698a775c37
8 changed files with 1777 additions and 106 deletions
+65 -4
View File
@@ -15,7 +15,13 @@
// model has no setting for is what Java's constructors make. A remote link
// pair becomes modems on the port its identifier names (12345 when it names
// none): the first of a pair in node order serves, the others connect to
// localhost — the model's pairs live in one project. System software, files and programs
// localhost — the model's pairs live in one project.
//
// The files of a PC or notebook: the model's directories and files under
// Java's root, each file a Datei of the type Java's importer reads it by —
// text by its extension (else "text"), an image or binary data by its media
// type, in Base64 as FILIUS's Base64.encodeBytes writes it (javaBase64). A router or home
// router keeps the files its constructor makes; the model holds none for it. System software, files and programs
// follow in later steps.
//
// What the model keeps is written as it keeps it, and so differs from the
@@ -31,12 +37,16 @@
// - a documentation item always has a text and a font, bold or plain: where
// Java has none, the empty text and Dialog 12 the model gives it;
// - a switch's SSID the file lacked (Java rolls one) is the one the model
// derives from the node's id.
// derives from the node's id;
// - files in the model's order (directories first, then by name) and of the
// type their kind says, not the one the file named; a router's own files
// (a web server's www.conf) are not in the model.
//
// Checked by Java: every corpus file, imported, exported and written, loads in
// Java FILIUS to the topology of the original up to those departures.
import {
base64Encode,
defaultSwitchConfiguration,
dhcpServerConfiguration,
effectiveMacAddress,
@@ -60,8 +70,9 @@ import {
type NodeKind,
type TopologyGraph,
type TopologyNode,
type VirtualFileSystem,
} from '@mdg-filius/core';
import type { FlsDhcpServer, FlsFirewall } from './systemSoftware.js';
import type { FlsDhcpServer, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsLink, FlsNodeClass } from './topology.js';
import type { NodeToWrite, SystemToWrite, TopologyToWrite } from './topologyWriter.js';
@@ -79,6 +90,7 @@ export interface ProjectToExport {
readonly dhcpServerConfigurations: ReadonlyMap<Id, DhcpServerConfiguration>;
readonly portForwardingRowsByNodeId: ReadonlyMap<Id, readonly GatewayPortForwardingRow[]>;
readonly remoteLinkConfigurations: ReadonlyMap<Id, RemoteLinkConfiguration>;
readonly virtualFileSystems: ReadonlyMap<Id, VirtualFileSystem>;
readonly documentationItems: readonly DocumentationItem[];
}
@@ -206,7 +218,12 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
case 'pc':
case 'notebook': {
const ssid = project.hostWirelessConfigurations.get(id)?.ssid ?? '';
return { ...system, ssid: ssid === '' ? null : ssid };
const files = project.virtualFileSystems.get(id);
return {
...system,
ssid: ssid === '' ? null : ssid,
fileSystem: { kind: 'directory', name: 'root', children: files === undefined ? [] : fileTrees(files, '/') },
};
}
case 'router':
return {
@@ -228,6 +245,50 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
}
}
/** The text types Java's importer reads as text, named by a file's extension. */
const TEXT_EXTENSIONS = new Set(['txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
function fileTrees(files: VirtualFileSystem, directory: string): FlsFileTree[] {
return files.entries(directory).map((entry): FlsFileTree => {
const name = entry.path.slice(entry.path.lastIndexOf('/') + 1);
const file = (type: string | null, content: string): FlsFileTree => ({ kind: 'file', name, type, content, children: [] });
switch (entry.content.kind) {
case 'directory':
return { kind: 'directory', name, children: fileTrees(files, entry.path) };
case 'text': {
const dot = name.lastIndexOf('.');
const extension = dot < 0 ? '' : name.slice(dot + 1).toLowerCase();
return file(TEXT_EXTENSIONS.has(extension) ? extension : 'text', entry.content.text);
}
case 'image':
return file(subtype(entry.content.mediaType), javaBase64(entry.content.data));
case 'binary':
return file(entry.content.mediaType === undefined ? '' : subtype(entry.content.mediaType), javaBase64(entry.content.data));
}
});
}
/**
* FILIUS's Base64.encodeBytes (iHarder Base64 2.x): a line break after every
* 76 characters — also after the last full line — then the padded rest.
*/
export function javaBase64(bytes: Uint8Array): string {
const full = bytes.length - (bytes.length % 3);
let text = '';
for (let offset = 0; offset < full; offset += 57) {
const end = Math.min(offset + 57, full);
text += base64Encode(bytes.subarray(offset, end));
if (end - offset === 57) text += '\n';
}
return text + base64Encode(bytes.subarray(full));
}
/** "image/png" → "png", "image/jpeg" → "jpg", as Java names them. */
function subtype(mediaType: string): string {
const name = mediaType.slice(mediaType.indexOf('/') + 1);
return name === 'jpeg' ? 'jpg' : name;
}
function exportDhcpServer(server: DhcpServerConfiguration): FlsDhcpServer {
return {
isActive: server.isActive,