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>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user