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>
66 lines
3.1 KiB
TypeScript
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);
|
|
}
|