Files
SwipeAnything/lib/zip-list.js
T
ilia ce43c5de6d
CI / Gitleaks (pull_request) Successful in 17s
CI / Unit tests (pull_request) Successful in 21s
Add organize folders, details sheet, and richer previews.
Map keys 0–9 to destinations, inspect files before deciding, and preview
PDF/RAW/ZIP/video with an OLED dark UI polish plus demo seed and tests.
2026-07-26 21:38:36 -04:00

99 lines
3.7 KiB
JavaScript

'use strict';
// Lightweight ZIP central-directory reader — list entry names + sizes without
// extracting. No npm deps. Supports store/deflate archives (standard .zip).
// Not a full unzipper: encrypted, zip64, and split archives are skipped/partial.
const fs = require('fs');
const fsp = fs.promises;
const EOCD_SIG = 0x06054b50;
const CEN_SIG = 0x02014b50;
const MAX_SCAN = 64 * 1024; // EOCD lives in the last 64KiB (+comment)
const MAX_ENTRIES = 200;
const MAX_NAME_BYTES = 1024;
/**
* @returns {Promise<{ entries: Array<{ name: string, size: number, compressedSize: number, isDir: boolean }>, truncated: boolean, totalEntries: number }>}
*/
async function listZipEntries(absPath) {
const stat = await fsp.stat(absPath);
const fd = await fsp.open(absPath, 'r');
try {
const scanLen = Math.min(stat.size, MAX_SCAN);
const tail = Buffer.alloc(scanLen);
await fd.read(tail, 0, scanLen, stat.size - scanLen);
let eocd = -1;
for (let i = tail.length - 22; i >= 0; i -= 1) {
if (tail.readUInt32LE(i) === EOCD_SIG) {
eocd = i;
break;
}
}
if (eocd < 0) throw new Error('Not a zip archive (EOCD not found)');
const totalEntries = tail.readUInt16LE(eocd + 10);
const cenSize = tail.readUInt32LE(eocd + 12);
const cenOffset = tail.readUInt32LE(eocd + 16);
// Zip64 / huge archives: offsets of 0xffffffff mean we can't parse simply.
if (cenOffset === 0xffffffff || cenSize === 0xffffffff) {
throw new Error('Zip64 archives are not supported for listing');
}
const cen = Buffer.alloc(cenSize);
const { bytesRead } = await fd.read(cen, 0, cenSize, cenOffset);
if (bytesRead !== cenSize) throw new Error('Truncated zip central directory');
const entries = [];
let offset = 0;
let truncated = false;
while (offset + 46 <= cen.length && entries.length < MAX_ENTRIES) {
if (cen.readUInt32LE(offset) !== CEN_SIG) break;
const compressedSize = cen.readUInt32LE(offset + 20);
const size = cen.readUInt32LE(offset + 24);
const nameLen = cen.readUInt16LE(offset + 28);
const extraLen = cen.readUInt16LE(offset + 30);
const commentLen = cen.readUInt16LE(offset + 32);
const nameStart = offset + 46;
const nameEnd = nameStart + Math.min(nameLen, MAX_NAME_BYTES);
let name = cen.slice(nameStart, nameEnd).toString('utf8');
// Prefer UTF-8 when the language-encoding flag is set; otherwise still try utf8.
const isDir = name.endsWith('/');
entries.push({ name, size, compressedSize, isDir });
offset = nameStart + nameLen + extraLen + commentLen;
}
if (totalEntries > entries.length) truncated = true;
return { entries, truncated, totalEntries: totalEntries || entries.length };
} finally {
await fd.close();
}
}
function formatZipListing(listing) {
const lines = [];
const files = listing.entries.filter((e) => !e.isDir);
const dirs = listing.entries.filter((e) => e.isDir);
lines.push(`${listing.totalEntries} entr${listing.totalEntries === 1 ? 'y' : 'ies'} (${files.length} files${dirs.length ? `, ${dirs.length} folders` : ''})`);
lines.push('');
for (const entry of listing.entries) {
if (entry.isDir) {
lines.push(` [dir] ${entry.name}`);
} else {
lines.push(` ${formatSize(entry.size).padStart(8)} ${entry.name}`);
}
}
if (listing.truncated) lines.push('', `… listing capped at ${MAX_ENTRIES} entries`);
return lines.join('\n');
}
function formatSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
module.exports = { listZipEntries, formatZipListing, MAX_ENTRIES };