Implementation de la feature de gestion des fichiers sur les investissements
This commit is contained in:
1 parent
f6829dec9d
commit
8953d0bb35
21 files changed
+1729
-181
No files matched your search
@@ -28,6 +28,15 @@ function dosDateTime(d = new Date()) {
|
||||
return { time, date };
|
||||
}
|
||||
|
||||
// ── Nettoyage d'un segment de chemin ZIP ────────────────────────────────────
|
||||
// Retire les caractères interdits dans un nom de fichier/dossier cross-platform
|
||||
// (\ / : * ? " < > | + caractères de contrôle) tout en conservant accents et
|
||||
// espaces. Partagé par tous les exports ZIP de l'app (documents, investissements...)
|
||||
// pour que la même règle de nommage s'applique partout.
|
||||
export function sanitizeZipPart(name) {
|
||||
return String(name).replace(/[\\/:*?"<>|\u0000-\u001F]/g, ' ').replace(/\s+/g, ' ').trim() || 'Sans nom';
|
||||
}
|
||||
|
||||
/**
|
||||
* createZip(entries) → Buffer
|
||||
* entries: [{ name: string, data: Buffer|string }]
|
||||
@@ -54,7 +63,14 @@ export function createZip(entries) {
|
||||
const lh = Buffer.alloc(30 + nameBuf.length);
|
||||
lh.writeUInt32LE(0x04034b50, 0);
|
||||
lh.writeUInt16LE(20, 4);
|
||||
lh.writeUInt16LE(0, 6);
|
||||
// Bit 11 (0x0800) = "Language encoding flag (EFS)" — indique que le nom
|
||||
// de fichier est encodé en UTF-8. Sans ce bit, les extracteurs qui ne
|
||||
// devinent pas l'UTF-8 par défaut (Windows Explorer notamment) réinterprètent
|
||||
// les octets UTF-8 des noms accentués avec la page de code système, d'où le
|
||||
// mojibake constaté par Olivier ("Billet_électronique" -> "Billet_ Ⓡlectronique").
|
||||
// Les noms sont toujours écrits en UTF-8 ci-dessus (Buffer.from(name, 'utf8')),
|
||||
// ce bit est donc toujours correct à poser.
|
||||
lh.writeUInt16LE(0x0800, 6);
|
||||
lh.writeUInt16LE(method, 8);
|
||||
lh.writeUInt16LE(modTime, 10);
|
||||
lh.writeUInt16LE(modDate, 12);
|
||||
@@ -77,7 +93,7 @@ export function createZip(entries) {
|
||||
cd.writeUInt32LE(0x02014b50, 0);
|
||||
cd.writeUInt16LE(20, 4);
|
||||
cd.writeUInt16LE(20, 6);
|
||||
cd.writeUInt16LE(0, 8);
|
||||
cd.writeUInt16LE(0x0800, 8); // même flag UTF-8 (EFS) que l'en-tête local, cf. commentaire ci-dessus
|
||||
cd.writeUInt16LE(h.method, 10);
|
||||
cd.writeUInt16LE(h.modTime, 12);
|
||||
cd.writeUInt16LE(h.modDate, 14);
|
||||
@@ -113,6 +129,25 @@ export function createZip(entries) {
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
// ── Protection contre les "zip bombs" ───────────────────────────────────
|
||||
// readZip() est utilisé pour relire des zips produits en interne par l'appli
|
||||
// (exports/sauvegardes admin — contenu maîtrisé) MAIS AUSSI pour des zips
|
||||
// envoyés directement par un utilisateur (import de dossier d'investissement,
|
||||
// import PFU/plateformes/référentiel) — donc potentiellement malveillants.
|
||||
// Un zip conçu avec des données très répétitives peut annoncer quelques Ko
|
||||
// compressés pour plusieurs Go décompressés (ratio extrême), ce qui saturerait
|
||||
// la mémoire du process si rien ne le limite. Deux garde-fous complémentaires :
|
||||
// 1. Rejet précoce si la taille décompressée ANNONCÉE dans l'en-tête central
|
||||
// dépasse déjà la limite (évite même de lancer la décompression).
|
||||
// 2. `maxOutputLength` passé à zlib : filet de sécurité si l'en-tête ment sur
|
||||
// la taille annoncée — zlib interrompt la décompression dès que la sortie
|
||||
// réelle dépasse la limite, sans jamais allouer plus que nécessaire.
|
||||
// Limites généreuses par rapport à l'usage réel de l'appli (sauvegarde complète
|
||||
// = base + logos/icons/documents, quelques dizaines de Mo en pratique) tout en
|
||||
// bornant strictement le pire cas.
|
||||
export const MAX_ZIP_ENTRY_UNCOMPRESSED_BYTES = 500 * 1024 * 1024; // 500 Mo par entrée
|
||||
export const MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES = 1024 * 1024 * 1024; // 1 Go cumulé par zip
|
||||
|
||||
/**
|
||||
* readZip(buffer) → [{ name: string, data: Buffer }]
|
||||
*/
|
||||
@@ -129,6 +164,7 @@ export function readZip(buffer) {
|
||||
|
||||
const entries = [];
|
||||
let pos = cdOffset;
|
||||
let totalUncompressed = 0;
|
||||
|
||||
for (let i = 0; i < entryCount; i++) {
|
||||
if (buffer.readUInt32LE(pos) !== 0x02014b50) throw new Error('ZIP invalide : signature Central Directory incorrecte');
|
||||
@@ -141,13 +177,23 @@ export function readZip(buffer) {
|
||||
const localOffset = buffer.readUInt32LE(pos + 42);
|
||||
const name = buffer.toString('utf8', pos + 46, pos + 46 + nameLen);
|
||||
|
||||
if (uncompSize > MAX_ZIP_ENTRY_UNCOMPRESSED_BYTES) {
|
||||
throw new Error(`ZIP invalide : l'entrée "${name}" dépasse la taille décompressée maximale autorisée (${Math.round(MAX_ZIP_ENTRY_UNCOMPRESSED_BYTES / 1024 / 1024)} Mo).`);
|
||||
}
|
||||
totalUncompressed += uncompSize;
|
||||
if (totalUncompressed > MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES) {
|
||||
throw new Error(`ZIP invalide : la taille décompressée totale dépasse la limite autorisée (${Math.round(MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES / 1024 / 1024)} Mo).`);
|
||||
}
|
||||
|
||||
// Read local file header to get actual extra field length
|
||||
const localNameLen = buffer.readUInt16LE(localOffset + 26);
|
||||
const localExtraLen = buffer.readUInt16LE(localOffset + 28);
|
||||
const dataStart = localOffset + 30 + localNameLen + localExtraLen;
|
||||
|
||||
const compData = buffer.subarray(dataStart, dataStart + compSize);
|
||||
const data = method === 0 ? compData : zlib.inflateRawSync(compData);
|
||||
// maxOutputLength : filet de sécurité si l'en-tête ment sur uncompSize —
|
||||
// voir commentaire au-dessus des constantes MAX_ZIP_*.
|
||||
const data = method === 0 ? compData : zlib.inflateRawSync(compData, { maxOutputLength: MAX_ZIP_ENTRY_UNCOMPRESSED_BYTES });
|
||||
|
||||
entries.push({ name, data: Buffer.from(data) });
|
||||
pos += 46 + nameLen + extraLen + commentLen;
|
||||
|
||||
Reference in new issue
Block a user