Files
mdg-filius/packages/fls/src/archive.ts
T
julian.kiedaischandClaude Opus 5.5 750cc72c55 feat(fls): ZIP layer of the .fls codec
Reader and writer ported from filius-ipad with the same path policy,
limits and error codes; DEFLATE via fflate, cut at the declared size
like Apple Compression. 54 tests: the Swift archive rejection tests,
DEFLATE edge cases, and all 23 corpus files cross-checked against
fflate.unzipSync and round-tripped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:03:07 +00:00

66 lines
3.1 KiB
TypeScript

// Ported from filius-ipad (GPLv2/GPLv3):
// ios/FiliusPad/TopologyEditor/Persistence/TopologyFLSArchive.swift
// importFiliusArchiveDocument, exportFiliusArchiveWithReport (the archive part)
// Java counterpart: filius.rahmenprogramm.SzenarioVerwaltung (its save and load methods).
//
// A .fls holds the project in `projekt/konfiguration.xml` and the list of the
// user's own Java applications in `projekt/anwendungen/EigeneAnwendungen.txt`
// (plus their sources and classes). Everything besides the configuration is
// carried through unchanged: this port cannot run custom Java applications,
// but it must not destroy them either.
import { canonicalEntryKey, FlsArchiveError, readZipEntries, writeZipEntries } from './zip.js';
export const CONFIGURATION_ENTRY_PATH = 'projekt/konfiguration.xml';
export const APPLICATIONS_ENTRY_PATH = 'projekt/anwendungen/EigeneAnwendungen.txt';
export interface FlsArchiveContents {
/** `projekt/konfiguration.xml`, as bytes. */
readonly configurationXml: Uint8Array;
/** Every other entry, carried through to the next save. */
readonly supplementalEntries: ReadonlyMap<string, Uint8Array>;
/** All entry paths of the archive, sorted. */
readonly entryPaths: readonly string[];
}
/** Opens a .fls: checks the archive and separates the configuration from the rest. */
export function readFlsArchive(archive: Uint8Array): FlsArchiveContents {
const entries = readZipEntries(archive);
const configurationXml = entries.get(CONFIGURATION_ENTRY_PATH);
if (configurationXml === undefined) {
throw new FlsArchiveError('missingConfiguration', CONFIGURATION_ENTRY_PATH, 'The required configuration entry is missing.');
}
const supplementalEntries = new Map(entries);
supplementalEntries.delete(CONFIGURATION_ENTRY_PATH);
return {
configurationXml,
supplementalEntries,
entryPaths: [...entries.keys()].sort(),
};
}
/**
* Writes a .fls from a configuration and the entries carried along. A
* carried entry may not pose as one of the two FILIUS entries under another
* spelling (`PROJEKT/Konfiguration.xml`); the application list is added
* empty when missing, as Java always writes it.
*/
export function writeFlsArchive(
configurationXml: Uint8Array,
supplementalEntries: ReadonlyMap<string, Uint8Array> = new Map(),
): Uint8Array {
const criticalConfigurationKey = canonicalEntryKey(CONFIGURATION_ENTRY_PATH);
const criticalApplicationsKey = canonicalEntryKey(APPLICATIONS_ENTRY_PATH);
for (const path of supplementalEntries.keys()) {
const key = canonicalEntryKey(path);
const isExactCriticalPath = path === CONFIGURATION_ENTRY_PATH || path === APPLICATIONS_ENTRY_PATH;
if ((key === criticalConfigurationKey || key === criticalApplicationsKey) && !isExactCriticalPath) {
throw new FlsArchiveError('duplicateEntry', path, 'Supplemental entry conflicts with a critical FILIUS archive path.');
}
}
const entries = new Map(supplementalEntries);
if (!entries.has(APPLICATIONS_ENTRY_PATH)) entries.set(APPLICATIONS_ENTRY_PATH, new Uint8Array());
entries.set(CONFIGURATION_ENTRY_PATH, configurationXml);
return writeZipEntries(entries);
}