372 lines
20 KiB
TypeScript
372 lines
20 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import { inflateRawSync } from 'node:zlib';
|
|
import { artifactPathCollisionKey, normalizeArtifactRelativePath } from '../../src/domain/artifact-path';
|
|
import type { ImportResourcePolicy } from '../../src/importers/resource-policy';
|
|
import {
|
|
resolveImportResourcePolicy,
|
|
validateImportSourceResources
|
|
} from '../../src/importers/resource-policy';
|
|
import { ImportError } from '../../src/shared/errors';
|
|
|
|
export interface SafeZipInventoryEntry {
|
|
index: number;
|
|
relativePath: string;
|
|
kind: 'file' | 'directory';
|
|
compressionMethod: 0 | 8;
|
|
compressedBytes: number;
|
|
uncompressedBytes: number;
|
|
crc32: string;
|
|
sha256?: string;
|
|
}
|
|
|
|
export interface SafeZipInventory {
|
|
version: 1;
|
|
containerSha256: string;
|
|
compressedBytes: number;
|
|
uncompressedBytes: number;
|
|
entries: SafeZipInventoryEntry[];
|
|
}
|
|
|
|
export interface VerifiedZipFile {
|
|
entry: SafeZipInventoryEntry & { kind: 'file'; sha256: string };
|
|
bytes: Uint8Array;
|
|
}
|
|
|
|
interface CentralEntry extends SafeZipInventoryEntry {
|
|
flags: number;
|
|
expectedCrc32: number;
|
|
localHeaderOffset: number;
|
|
encodedName: Uint8Array;
|
|
externalAttributes: number;
|
|
madeBySystem: number;
|
|
}
|
|
|
|
const centralHeaderSignature = 0x02014b50;
|
|
const localHeaderSignature = 0x04034b50;
|
|
const endSignature = 0x06054b50;
|
|
const allowedFlags = 0x080e;
|
|
|
|
function zipError(code: string, message: string, details: Record<string, string | number> = {}): never {
|
|
throw new ImportError(message, [{ severity: 'error', code, message, stage: 'format-decoding', details }]);
|
|
}
|
|
|
|
function sha256(bytes: Uint8Array): string {
|
|
return createHash('sha256').update(bytes).digest('hex');
|
|
}
|
|
|
|
function crc32(bytes: Uint8Array): number {
|
|
let value = 0xffffffff;
|
|
for (const byte of bytes) {
|
|
value ^= byte;
|
|
for (let bit = 0; bit < 8; bit += 1) value = (value >>> 1) ^ (0xedb88320 & -(value & 1));
|
|
}
|
|
return (value ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function findEndRecord(bytes: Uint8Array): number {
|
|
const minimumOffset = Math.max(0, bytes.length - 65_557);
|
|
for (let offset = bytes.length - 22; offset >= minimumOffset; offset -= 1) {
|
|
if (bytes[offset] === 0x50 && bytes[offset + 1] === 0x4b
|
|
&& bytes[offset + 2] === 0x05 && bytes[offset + 3] === 0x06) return offset;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function decodeEntryName(bytes: Uint8Array, flags: number, index: number): string {
|
|
if (!(flags & 0x0800) && bytes.some(byte => byte > 0x7f)) {
|
|
zipError('zip-entry-name-encoding-rejected', 'Nicht-ASCII-Archivpfade benötigen eine eindeutige UTF-8-Kennzeichnung.', { entryIndex: index });
|
|
}
|
|
try {
|
|
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
} catch {
|
|
zipError('zip-entry-name-encoding-rejected', 'Ein Archivpfad ist nicht gültig UTF-8-kodiert.', { entryIndex: index });
|
|
}
|
|
}
|
|
|
|
function canonicalEntryPath(rawName: string, index: number): { path: string; kind: 'file' | 'directory' } {
|
|
if (rawName.includes('\0')) zipError('zip-entry-path-rejected', 'Archivpfade dürfen keine Nullzeichen enthalten.', { entryIndex: index });
|
|
const slashNormalized = rawName.replaceAll('\\', '/');
|
|
const kind = slashNormalized.endsWith('/') ? 'directory' : 'file';
|
|
const candidate = kind === 'directory' ? slashNormalized.slice(0, -1) : slashNormalized;
|
|
const path = normalizeArtifactRelativePath(candidate);
|
|
if (!path) zipError('zip-entry-path-rejected', 'Ein Archivpfad ist unsicher oder nicht kanonisch.', { entryIndex: index });
|
|
return { path, kind };
|
|
}
|
|
|
|
function validateExternalType(entry: CentralEntry): void {
|
|
if (entry.madeBySystem !== 3) return;
|
|
const unixMode = entry.externalAttributes >>> 16;
|
|
const fileType = unixMode & 0xf000;
|
|
if (fileType === 0xa000) zipError('zip-link-rejected', 'Symbolische Links in Archiven werden nicht verarbeitet.', { entryIndex: entry.index });
|
|
if (fileType && fileType !== 0x8000 && fileType !== 0x4000) {
|
|
zipError('zip-special-file-rejected', 'Spezialdateien in Archiven werden nicht verarbeitet.', { entryIndex: entry.index });
|
|
}
|
|
if (fileType && (fileType === 0x4000) !== (entry.kind === 'directory')) {
|
|
zipError('zip-entry-type-conflict', 'Dateiname und externer Archivtyp widersprechen sich.', { entryIndex: entry.index });
|
|
}
|
|
}
|
|
|
|
function parseCentralDirectory(bytes: Uint8Array, policy: ImportResourcePolicy): { entries: CentralEntry[]; directoryOffset: number } {
|
|
if (bytes.length < 22) zipError('zip-directory-missing', 'Der ZIP-Container besitzt kein gültiges Zentralverzeichnis.', { sizeBytes: bytes.length });
|
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
const endOffset = findEndRecord(bytes);
|
|
if (endOffset < 0 || view.getUint32(endOffset, true) !== endSignature) {
|
|
zipError('zip-directory-missing', 'Der ZIP-Container besitzt kein gültiges Zentralverzeichnis.', { sizeBytes: bytes.length });
|
|
}
|
|
const commentLength = view.getUint16(endOffset + 20, true);
|
|
if (endOffset + 22 + commentLength !== bytes.length) {
|
|
zipError('zip-trailing-data-rejected', 'Daten außerhalb des abschließenden ZIP-Verzeichniseintrags werden nicht akzeptiert.', { endOffset, sizeBytes: bytes.length });
|
|
}
|
|
const diskNumber = view.getUint16(endOffset + 4, true);
|
|
const directoryDisk = view.getUint16(endOffset + 6, true);
|
|
const entriesOnDisk = view.getUint16(endOffset + 8, true);
|
|
const entryCount = view.getUint16(endOffset + 10, true);
|
|
const directorySize = view.getUint32(endOffset + 12, true);
|
|
const directoryOffset = view.getUint32(endOffset + 16, true);
|
|
if (diskNumber || directoryDisk || entriesOnDisk !== entryCount) {
|
|
zipError('zip-multidisk-rejected', 'Mehrteilige ZIP-Archive werden nicht verarbeitet.', { diskNumber, directoryDisk });
|
|
}
|
|
if (entryCount === 0xffff || directorySize === 0xffffffff || directoryOffset === 0xffffffff) {
|
|
zipError('zip64-not-accepted', 'ZIP64-Archive werden innerhalb der aktuellen Importgrenzen nicht akzeptiert.', { entryCount });
|
|
}
|
|
if (!entryCount) zipError('zip-empty-rejected', 'Leere ZIP-Archive enthalten kein inventarisierbares GPO-Artefakt.');
|
|
if (entryCount > policy.maximumZipEntries) {
|
|
zipError('zip-entry-limit', 'Das Archiv überschreitet die zulässige Eintragsanzahl.', { entryCount, limit: policy.maximumZipEntries });
|
|
}
|
|
if (directoryOffset + directorySize !== endOffset) {
|
|
zipError('zip-directory-invalid', 'Das ZIP-Zentralverzeichnis besitzt eine inkonsistente Größe oder Position.', { directoryOffset, directorySize, endOffset });
|
|
}
|
|
|
|
const entries: CentralEntry[] = [];
|
|
const pathKeys = new Map<string, number>();
|
|
let offset = directoryOffset;
|
|
let declaredTotal = 0;
|
|
for (let index = 0; index < entryCount; index += 1) {
|
|
if (offset + 46 > endOffset || view.getUint32(offset, true) !== centralHeaderSignature) {
|
|
zipError('zip-directory-invalid', 'Ein ZIP-Zentralverzeichniseintrag ist beschädigt.', { entryIndex: index });
|
|
}
|
|
const madeBySystem = view.getUint16(offset + 4, true) >>> 8;
|
|
const flags = view.getUint16(offset + 8, true);
|
|
const compressionMethod = view.getUint16(offset + 10, true);
|
|
const expectedCrc32 = view.getUint32(offset + 16, true);
|
|
const compressedBytes = view.getUint32(offset + 20, true);
|
|
const uncompressedBytes = view.getUint32(offset + 24, true);
|
|
const nameLength = view.getUint16(offset + 28, true);
|
|
const extraLength = view.getUint16(offset + 30, true);
|
|
const entryCommentLength = view.getUint16(offset + 32, true);
|
|
const startDisk = view.getUint16(offset + 34, true);
|
|
const externalAttributes = view.getUint32(offset + 38, true);
|
|
const localHeaderOffset = view.getUint32(offset + 42, true);
|
|
const nextOffset = offset + 46 + nameLength + extraLength + entryCommentLength;
|
|
if (nextOffset > endOffset) zipError('zip-directory-invalid', 'Ein Zentralverzeichniseintrag liegt außerhalb seiner Grenzen.', { entryIndex: index });
|
|
if (flags & 0x0001 || flags & ~allowedFlags) {
|
|
zipError('zip-flags-rejected', 'Der Archivinhalt verwendet Verschlüsselung oder nicht unterstützte ZIP-Flags.', { entryIndex: index, flags });
|
|
}
|
|
if (compressionMethod !== 0 && compressionMethod !== 8) {
|
|
zipError('zip-compression-method-rejected', 'Das Archiv verwendet ein nicht zugelassenes Kompressionsverfahren.', { entryIndex: index, compressionMethod });
|
|
}
|
|
if (compressionMethod === 0 && (flags & 0x0006)) {
|
|
zipError('zip-flags-rejected', 'Unkomprimierte ZIP-Einträge dürfen keine Deflate-Optionsflags tragen.', { entryIndex: index, flags });
|
|
}
|
|
if (compressedBytes === 0xffffffff || uncompressedBytes === 0xffffffff || localHeaderOffset === 0xffffffff) {
|
|
zipError('zip64-not-accepted', 'ZIP64-Einträge werden innerhalb der aktuellen Importgrenzen nicht akzeptiert.', { entryIndex: index });
|
|
}
|
|
if (startDisk) zipError('zip-multidisk-rejected', 'Ein Archivinhalt verweist auf einen anderen Datenträger.', { entryIndex: index, startDisk });
|
|
if (uncompressedBytes > policy.maximumZipEntryUncompressedBytes) {
|
|
zipError('zip-entry-size-limit', 'Ein einzelner Archivinhalt überschreitet das Entpacklimit.', { entryIndex: index, uncompressedBytes, limit: policy.maximumZipEntryUncompressedBytes });
|
|
}
|
|
declaredTotal += uncompressedBytes;
|
|
if (declaredTotal > policy.maximumZipUncompressedBytes) {
|
|
zipError('zip-uncompressed-size-limit', 'Die deklarierte entpackte Archivgröße überschreitet das Limit.', { uncompressedBytes: declaredTotal, limit: policy.maximumZipUncompressedBytes });
|
|
}
|
|
const encodedName = bytes.slice(offset + 46, offset + 46 + nameLength);
|
|
const canonical = canonicalEntryPath(decodeEntryName(encodedName, flags, index), index);
|
|
const collisionKey = artifactPathCollisionKey(canonical.path);
|
|
if (!collisionKey) zipError('zip-entry-path-rejected', 'Ein Archivpfad ist nicht sicher normalisierbar.', { entryIndex: index });
|
|
const collidingIndex = pathKeys.get(collisionKey);
|
|
if (collidingIndex !== undefined) {
|
|
zipError('zip-entry-path-collision', 'Archivpfade kollidieren nach Unicode-/Case-Normalisierung.', { entryIndex: index, collidingIndex });
|
|
}
|
|
pathKeys.set(collisionKey, index);
|
|
const entry: CentralEntry = {
|
|
index,
|
|
relativePath: canonical.path,
|
|
kind: canonical.kind,
|
|
compressionMethod: compressionMethod as 0 | 8,
|
|
compressedBytes,
|
|
uncompressedBytes,
|
|
crc32: expectedCrc32.toString(16).padStart(8, '0'),
|
|
flags,
|
|
expectedCrc32,
|
|
localHeaderOffset,
|
|
encodedName,
|
|
externalAttributes,
|
|
madeBySystem
|
|
};
|
|
validateExternalType(entry);
|
|
entries.push(entry);
|
|
offset = nextOffset;
|
|
}
|
|
if (offset !== endOffset) zipError('zip-directory-invalid', 'Das Zentralverzeichnis wurde nicht vollständig und eindeutig gelesen.', { actualEnd: offset, expectedEnd: endOffset });
|
|
|
|
const fileKeys = new Set(entries.filter(entry => entry.kind === 'file').map(entry => artifactPathCollisionKey(entry.relativePath)!));
|
|
for (const entry of entries) {
|
|
const segments = entry.relativePath.split('/');
|
|
for (let count = 1; count < segments.length; count += 1) {
|
|
if (fileKeys.has(artifactPathCollisionKey(segments.slice(0, count).join('/'))!)) {
|
|
zipError('zip-entry-path-collision', 'Ein Archivpfad verwendet eine Datei zugleich als Verzeichnis.', { entryIndex: entry.index });
|
|
}
|
|
}
|
|
}
|
|
return { entries, directoryOffset };
|
|
}
|
|
|
|
function inflateEntries(
|
|
bytes: Uint8Array,
|
|
entries: CentralEntry[],
|
|
directoryOffset: number,
|
|
policy: ImportResourcePolicy,
|
|
onFile?: (entry: SafeZipInventoryEntry & { kind: 'file'; sha256: string }, content: Uint8Array) => void
|
|
): SafeZipInventoryEntry[] {
|
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
const ordered = [...entries].sort((left, right) => left.localHeaderOffset - right.localHeaderOffset);
|
|
let actualTotal = 0;
|
|
let totalCompressed = 0;
|
|
if (ordered[0]?.localHeaderOffset !== 0) zipError('zip-leading-data-rejected', 'Daten vor dem ersten lokalen ZIP-Dateikopf werden nicht akzeptiert.');
|
|
for (let orderIndex = 0; orderIndex < ordered.length; orderIndex += 1) {
|
|
const entry = ordered[orderIndex];
|
|
const offset = entry.localHeaderOffset;
|
|
if (offset + 30 > directoryOffset || view.getUint32(offset, true) !== localHeaderSignature) {
|
|
zipError('zip-local-header-invalid', 'Ein lokaler ZIP-Dateikopf ist beschädigt oder fehlt.', { entryIndex: entry.index });
|
|
}
|
|
const localFlags = view.getUint16(offset + 6, true);
|
|
const localMethod = view.getUint16(offset + 8, true);
|
|
const localCrc32 = view.getUint32(offset + 14, true);
|
|
const localCompressedBytes = view.getUint32(offset + 18, true);
|
|
const localUncompressedBytes = view.getUint32(offset + 22, true);
|
|
const localNameLength = view.getUint16(offset + 26, true);
|
|
const localExtraLength = view.getUint16(offset + 28, true);
|
|
const localName = bytes.subarray(offset + 30, offset + 30 + localNameLength);
|
|
const dataOffset = offset + 30 + localNameLength + localExtraLength;
|
|
const dataEnd = dataOffset + entry.compressedBytes;
|
|
const nextBoundary = ordered[orderIndex + 1]?.localHeaderOffset ?? directoryOffset;
|
|
const descriptorFieldsValid = entry.flags & 0x0008
|
|
? (localCrc32 === 0 || localCrc32 === entry.expectedCrc32)
|
|
&& (localCompressedBytes === 0 || localCompressedBytes === entry.compressedBytes)
|
|
&& (localUncompressedBytes === 0 || localUncompressedBytes === entry.uncompressedBytes)
|
|
: localCrc32 === entry.expectedCrc32
|
|
&& localCompressedBytes === entry.compressedBytes
|
|
&& localUncompressedBytes === entry.uncompressedBytes;
|
|
if (localFlags !== entry.flags || localMethod !== entry.compressionMethod || !descriptorFieldsValid
|
|
|| localName.length !== entry.encodedName.length
|
|
|| !localName.every((byte, index) => byte === entry.encodedName[index])) {
|
|
zipError('zip-header-conflict', 'Lokaler Dateikopf und Zentralverzeichnis widersprechen sich.', { entryIndex: entry.index });
|
|
}
|
|
if (dataOffset > directoryOffset || dataEnd > nextBoundary) {
|
|
zipError('zip-entry-range-invalid', 'Komprimierte Archivdaten überlappen andere ZIP-Strukturen.', { entryIndex: entry.index });
|
|
}
|
|
if (entry.flags & 0x0008) {
|
|
const descriptorSize = nextBoundary - dataEnd;
|
|
const descriptorOffset = descriptorSize === 16 && view.getUint32(dataEnd, true) === 0x08074b50 ? dataEnd + 4 : dataEnd;
|
|
if (![12, 16].includes(descriptorSize)
|
|
|| view.getUint32(descriptorOffset, true) !== entry.expectedCrc32
|
|
|| view.getUint32(descriptorOffset + 4, true) !== entry.compressedBytes
|
|
|| view.getUint32(descriptorOffset + 8, true) !== entry.uncompressedBytes) {
|
|
zipError('zip-data-descriptor-invalid', 'Der ZIP-Datendeskriptor fehlt oder widerspricht dem Zentralverzeichnis.', { entryIndex: entry.index });
|
|
}
|
|
} else if (dataEnd !== nextBoundary) {
|
|
zipError('zip-entry-range-invalid', 'Nicht zugeordnete Daten zwischen ZIP-Einträgen werden nicht akzeptiert.', { entryIndex: entry.index });
|
|
}
|
|
const compressed = bytes.subarray(dataOffset, dataEnd);
|
|
let uncompressed: Uint8Array;
|
|
try {
|
|
uncompressed = entry.compressionMethod === 0
|
|
? compressed
|
|
: inflateRawSync(compressed, { maxOutputLength: Math.max(1, Math.min(policy.maximumZipEntryUncompressedBytes, policy.maximumZipUncompressedBytes - actualTotal)) });
|
|
} catch {
|
|
zipError('zip-entry-inflate-failed', 'Ein Archivinhalt konnte innerhalb der Ressourcenlimits nicht entpackt werden.', { entryIndex: entry.index });
|
|
}
|
|
if (entry.compressionMethod === 0 && entry.compressedBytes !== entry.uncompressedBytes) {
|
|
zipError('zip-entry-size-mismatch', 'Ein unkomprimierter Archivinhalt besitzt widersprüchliche Größenangaben.', { entryIndex: entry.index });
|
|
}
|
|
if (uncompressed.byteLength !== entry.uncompressedBytes) {
|
|
zipError('zip-entry-size-mismatch', 'Deklarierte und tatsächliche entpackte Größe stimmen nicht überein.', { entryIndex: entry.index, declaredBytes: entry.uncompressedBytes, actualBytes: uncompressed.byteLength });
|
|
}
|
|
const entryRatio = uncompressed.byteLength / Math.max(1, entry.compressedBytes);
|
|
if (entryRatio > policy.maximumZipCompressionRatio) {
|
|
zipError('zip-compression-ratio-limit', 'Das tatsächliche Kompressionsverhältnis eines Archivinhalts ist ungewöhnlich hoch.', { entryIndex: entry.index, ratio: Math.round(entryRatio * 100) / 100, limit: policy.maximumZipCompressionRatio });
|
|
}
|
|
actualTotal += uncompressed.byteLength;
|
|
totalCompressed += entry.compressedBytes;
|
|
if (actualTotal > policy.maximumZipUncompressedBytes) {
|
|
zipError('zip-uncompressed-size-limit', 'Die tatsächliche entpackte Archivgröße überschreitet das Limit.', { uncompressedBytes: actualTotal, limit: policy.maximumZipUncompressedBytes });
|
|
}
|
|
if (crc32(uncompressed) !== entry.expectedCrc32) zipError('zip-entry-crc-mismatch', 'Die CRC-Prüfsumme eines Archivinhalts ist ungültig.', { entryIndex: entry.index });
|
|
if (entry.kind === 'file' && uncompressed.length >= 4 && uncompressed[0] === 0x50 && uncompressed[1] === 0x4b
|
|
&& [[0x03, 0x04], [0x05, 0x06], [0x07, 0x08]].some(signature => uncompressed[2] === signature[0] && uncompressed[3] === signature[1])) {
|
|
zipError('zip-nested-archive-rejected', 'Verschachtelte ZIP-Archive werden nicht verarbeitet.', { entryIndex: entry.index });
|
|
}
|
|
entry.sha256 = entry.kind === 'file' ? sha256(uncompressed) : undefined;
|
|
if (entry.kind === 'file' && entry.sha256) {
|
|
onFile?.({
|
|
index: entry.index, relativePath: entry.relativePath, kind: 'file', compressionMethod: entry.compressionMethod,
|
|
compressedBytes: entry.compressedBytes, uncompressedBytes: entry.uncompressedBytes, crc32: entry.crc32, sha256: entry.sha256
|
|
}, uncompressed);
|
|
}
|
|
}
|
|
const ratio = actualTotal / Math.max(1, totalCompressed);
|
|
if (ratio > policy.maximumZipCompressionRatio) {
|
|
zipError('zip-compression-ratio-limit', 'Das tatsächliche Kompressionsverhältnis des Archivs ist ungewöhnlich hoch.', { ratio: Math.round(ratio * 100) / 100, limit: policy.maximumZipCompressionRatio });
|
|
}
|
|
return entries.map(({ flags: _flags, expectedCrc32: _expectedCrc32, localHeaderOffset: _localHeaderOffset,
|
|
encodedName: _encodedName, externalAttributes: _externalAttributes, madeBySystem: _madeBySystem, ...entry }) => entry);
|
|
}
|
|
|
|
export function inventoryZipArchive(bytes: Uint8Array, overrides?: Partial<ImportResourcePolicy>): SafeZipInventory {
|
|
const policy = resolveImportResourcePolicy(overrides);
|
|
validateImportSourceResources({ metadata: { name: 'server-container.zip', sizeBytes: bytes.byteLength, lastModified: 0 }, bytes }, policy);
|
|
const { entries, directoryOffset } = parseCentralDirectory(bytes, policy);
|
|
const inventoried = inflateEntries(bytes, entries, directoryOffset, policy);
|
|
return {
|
|
version: 1,
|
|
containerSha256: sha256(bytes),
|
|
compressedBytes: bytes.byteLength,
|
|
uncompressedBytes: inventoried.reduce((sum, entry) => sum + entry.uncompressedBytes, 0),
|
|
entries: inventoried
|
|
};
|
|
}
|
|
|
|
export function readVerifiedZipFiles(
|
|
bytes: Uint8Array,
|
|
relativePaths: string[],
|
|
overrides?: Partial<ImportResourcePolicy>,
|
|
maximumSelectedBytes = 8 * 1024 * 1024
|
|
): VerifiedZipFile[] {
|
|
const policy = resolveImportResourcePolicy(overrides);
|
|
if (!Number.isSafeInteger(maximumSelectedBytes) || maximumSelectedBytes < 1
|
|
|| maximumSelectedBytes > policy.maximumZipUncompressedBytes) {
|
|
zipError('zip-selected-size-limit-invalid', 'Das Auswahllimit für ZIP-Inhalte ist ungültig.');
|
|
}
|
|
validateImportSourceResources({ metadata: { name: 'server-container.zip', sizeBytes: bytes.byteLength, lastModified: 0 }, bytes }, policy);
|
|
const selectedKeys = new Set<string>();
|
|
for (const requestedPath of relativePaths) {
|
|
const normalized = normalizeArtifactRelativePath(requestedPath);
|
|
const key = artifactPathCollisionKey(requestedPath);
|
|
if (!normalized || !key || normalized !== requestedPath || selectedKeys.has(key)) {
|
|
zipError('zip-selection-path-rejected', 'Die ZIP-Inhaltsauswahl enthält einen unsicheren oder doppelten Pfad.');
|
|
}
|
|
selectedKeys.add(key);
|
|
}
|
|
const { entries, directoryOffset } = parseCentralDirectory(bytes, policy);
|
|
const selected: VerifiedZipFile[] = [];
|
|
let selectedBytes = 0;
|
|
inflateEntries(bytes, entries, directoryOffset, policy, (entry, content) => {
|
|
if (!selectedKeys.has(artifactPathCollisionKey(entry.relativePath)!)) return;
|
|
selectedBytes += content.byteLength;
|
|
if (selectedBytes > maximumSelectedBytes) {
|
|
zipError('zip-selected-size-limit', 'Die ausgewählten ZIP-Inhalte überschreiten das Verarbeitungslimit.', { selectedBytes, limit: maximumSelectedBytes });
|
|
}
|
|
selected.push({ entry, bytes: content.slice() });
|
|
});
|
|
return selected.sort((left, right) => left.entry.index - right.entry.index);
|
|
} |