💾 Archived View for it.omarpolo.com › articoli › estrarre-file-da-uno-zip.gmi captured on 2022-04-29 at 12:27:14. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-11-30)
-=-=-=-=-=-=-
Questa è una traduzione di un articolo pubblicato nel mio blog in inglese:
gemini://gemini.omarpolo.com/post/extracting-from-zips.gmi
Parte prima: “Elencare i file di uno zip”
Il codice per l’intera serie; vedi ‘zipview.c’ per questo articolo in particolare.
Si ringraziano Stefan Sperling per aver trovato un errore nella versione originale della funzione “next” e nytpu per avermi fatto notare che la specifica epub richieda che il file “mimetype” non sia compresso.
Dopo aver visto come navigare in un archivio zip, è ora di provare ad estrarre dei file. Prima di soffermarci sulle funzioni di decompressione (spoiler: servirà zlib, assicurarsi di averla installata!) vorrei fare un po’ di refactoring, per dei motivi che saranno chiari a breve.
La funzione “next” ritorna un puntatore al prossimo file record nella directory centrale, o NULL se non ne trova:
void * next(uint8_t *zip, size_t len, uint8_t *entry) { uint16_t flen, xlen, clen; uint8_t *next, *end; memcpy(&flen, entry + 28, sizeof(flen)); memcpy(&xlen, entry + 28 + 2, sizeof(xlen)); memcpy(&clen, entry + 28 + 2 + 2, sizeof(xlen)); flen = le16toh(flen); xlen = le16toh(xlen); clen = le16toh(clen); next = entry + 46 + flen + xlen + clen; end = zip + len; if (next >= end - 46 || memcmp(next, "\x50\x4b\x01\x02", 4) != 0) return NULL; return next; }
È molto simile al codice che c’era nella funzione “ls”: calcola il puntatore al prossimo file e fa un po’ di validazione.
La funzione “filename” estrae il nome del file dato un puntatore al suo record:
void filename(uint8_t *zip, size_t len, uint8_t *entry, char *buf, size_t size) { uint16_t flen; size_t s; memcpy(&flen, entry + 28, sizeof(flen)); flen = le16toh(flen); s = MIN(size-1, flen); memcpy(buf, entry + 46, s); buf[s] = '\0'; }
Con queste due funzioni è possibile riscrivere la “ls” in modo più coinciso:
void ls(uint8_t *zip, size_t len, uint8_t *cd) { char name[PATH_MAX]; do { filename(zip, len, cd, name, sizeof(name)); printf("%s\n", name); } while ((cd = next(zip, len, cd)) != NULL); }
Aggiorno anche la funzione main:
int main(int argc, char **argv) { int i, fd; void *zip, *cd; size_t len; if (argc < 2) { fprintf(stderr, "Usage: %s archive.zip [files...]", *argv); return 1; } if ((fd = open(argv[1], O_RDONLY)) == -1) err(1, "can't open %s", argv[1]); zip = map_file(fd, &len); #ifdef __OpenBSD__ if (pledge("stdio", NULL) == -1) err(1, "pledge"); #endif if ((cd = find_central_directory(zip, len)) == NULL) errx(1, "can't find the central directory"); if (argc == 2) ls(zip, len, cd); else { for (i = 2; i < argc; ++i) extract_file(zip, len, cd, argv[i]); } munmap(zip, len); close(fd); return 0; }
La differenza è che adesso accetta un numero variabile di file da estrarre dopo il nome dell’archivio da considerare.
Dato che sono un po’ un fanboy di OpenBSD, ho aggiunto anche una chiamata a pledge(2) prima della logica principale del programma: in questo modo, anche se uno zip corrotto riesca a ingannare il programma a fare cose che non dovrebbe (leggere file, aprire connessioni di rete…), il kernel non permetterà altro che scrivere su file già aperti e nulla di più. Su FreeBSD una chiamata a capsicum(4) produce più o meno lo stesso risultato in questo caso. Su linux, è lasciato come esercizio al lettore la scrittura di un filtro seccomp(2) che funzioni, buona fortuna!
(Ho già detto di essere un fanboy vero?)
Comparing sandboxing techniques
Per implementare “extract_file” ho usato una piccola funzione “find_file” che, dato il nome di un file, ritorna un puntatore alla sua entry nella central directory. Assomiglia al codice di “ls”:
void * find_file(uint8_t *zip, size_t len, uint8_t *cd, const char *target) { char name[PATH_MAX]; do { filename(zip, len, cd, name, sizeof(name)); if (!strcmp(name, target)) return cd; } while ((cd = next(zip, len, cd)) != NULL); return NULL; }
Quindi “extract_file” è davvero semplice:
int extract_file(uint8_t *zip, size_t len, uint8_t *cd, const char *target) { if ((cd = find_file(zip, len, cd, target)) == NULL) return -1; unzip(zip, len, cd); return 0; }
OK, ho imbrogliato, questa non è la funzione di decompressione ma solo un wrapper per ‘unzip’. Inizialmente chiamavo ‘unzip’ da ‘ls’ ma era un po’ confusionario, questa è la motivazione per il refactoring.
Piccolo riassunto della situazione: l’entry dei file nella central directory di uno zip contiene un puntatore al record del file all’interno dell’archivio stesso. Il file record è formato da un header e dai dati del file, generalmente compressi. Un aspetto interessante dei file zip è che nello stesso archivio i file possono essere compressi con algoritmi diversi, incluso nessuno.
Il lato positivo è che la magigor parte delle applicazioni usano l’algoritmo deflate, che è l’unico che andrò a supportare anch’io, oltre ai file non compressi, dato che sono semplici da gestire.
I due metodi di compressione sono identificati dai seguenti valori:
#define COMPRESSION_NONE 0x00 #define COMPRESSION_DEFLATE 0x08
Gli altri algoritmi e i loro codici sono lungamente descritti nella documentazione di PKZip.
La funzione “unzip” prende lo zip e un puntatore all’entry del file nella central directory, quindi calcola l’offset del file e il puntatore all’inizio dei dati. Si ricorda che l’intestazione di un file ha lunghezza variabile: sono 46 byte seguiti da due campi a lunghezza variabile.
Per sapere quale algoritmo di decompressione usare bisogna leggere il campo “compression”. (si veda l’articolo precedente o la documentazione ufficiale per la struttura degli header)
void unzip(uint8_t *zip, size_t len, uint8_t *entry) { uint32_t size, crc, off; uint16_t compression; uint16_t flen, xlen; uint8_t *data, *offset; /* legge l’offset del record del file */ memcpy(&off, entry + 42, sizeof(off)); offset = zip + le32toh(off); if (offset > zip + len - 46 || memcmp(offset, "\x50\x4b\x03\x04", 4) != 0) errx(1, "invalid offset or file header signature"); memcpy(&compression, offset + 8, sizeof(compression)); compression = le16toh(compression); memcpy(&crc, entry + 16, sizeof(crc)); memcpy(&size, entry + 20, sizeof(size)); crc = le32toh(crc); size = le32toh(size); memcpy(&flen, offset + 26, sizeof(flen)); memcpy(&xlen, offset + 28, sizeof(xlen)); flen = le16toh(flen); xlen = le16toh(xlen); data = offset + 30 + flen + xlen; if (data + size > zip + len) errx(1, "corrupted zip, offset out of file"); switch (compression) { case COMPRESSION_NONE: unzip_none(data, size, crc); break; case COMPRESSION_DEFLATE: unzip_deflate(data, size, crc); break; default: errx(1, "unknown compression method 0x%02x", compression); } }
“unzip_none” gestisce il caso di un file salvato senza compressione. Copia semplicemente i dati su standard output e controlla il CRC32.
CRC significa “Cyclic Redundancy Check” (controllo di ridondanza ciclico) e viene largamente usato per proteggersi da corruzioni accidentali. La matematica dietro è molto interessante, è basata sui campi finiti e ha delle proprietà molto interessanti. È anche facile da calcolare, anche a mano, ma dato che stiamo già usando zlib, ho optato per usare la funzione “crc32” fornita dalla libreria.
“Cyclic Redundancy Check” su Wikipedia
void unzip_none(uint8_t *data, size_t size, unsigned long ocrc) { unsigned long crc = 0; fwrite(data, 1, size, stdout); crc = crc32(0, data, size); if (crc != ocrc) errx(1, "CRC mismatch"); }
“unzip_deflate” gestisce il caso di un file compresso con l’algoritmo “deflate”, implementato da zlib.
Al meno per la decompressione, zlib non sembra troppo male da usare. (Non so perché ma ho sempre avuto quest’impressione che zlib avesse delle API terribili… Nonostante non siano le più belle mai viste, non mi sembrano neanche esageratamente brutte)
Bisogna preparare un “oggetto” z_stream con inflateInit, chiamare a ripetizione inflate fino a quando tutto l’input non è stato processato e poi liberare la memoria interna usata da zlib con inflateEnd.
Per tornare a quello che stavo blaterando prima riguardo le API, zlib ha un modo strano di convogliare alcune informazioni. Un semplice inflateInit assume che i dati siano in formato zlib o gz, mentre gli archivi zip usano uno stream deflate “puro”. Il modo di informare zlib è chiamando inflateInit2 usando un numero negativo tra -15 e -8 come grandezza della sliding window. Esatto, un numero negativo come grandezza indica un flusso deflate puro. (Il modo di richiedere un header gz è anche abbastanza carino, bisogna sommare 16 alla grandezza della sliding window desiderata…)
Quando stavo scrivendo questa funzione per la prima volta mi sono bloccato un po’ su questa problematica, in quanto non è molto intuitiva secondo me.
Comunque, il problema adesso diventa quale grandezza scegliere per la sliding window. Da quello che ho capito va calcolata come:
size = log2(file_size) if (size < 8) size = 8 if (size > 15) size = 15; return -1 * size
Ma per il file zip che sto usando come test non funziona. Ho trovato che passare -15 in modo incondizionato sembra funzionare in ogni caso: dovrebbe usare un po’ di memoria in più, ma è anche il valore di default altrimenti quindi non credo sia una brutta scelta.
Se conosci di più riguardo quest’aspetto sentiti libero di correggermi in modo che possa aggiornare l’articolo!
void unzip_deflate(uint8_t *data, size_t size, unsigned long ocrc) { z_stream stream; size_t have; unsigned long crc = 0; char buf[BUFSIZ]; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; stream.next_in = data; stream.avail_in = size; stream.next_out = Z_NULL; stream.avail_out = 0; if (inflateInit2(&stream, -15) != Z_OK) err(1, "inflateInit failed"); do { stream.next_out = buf; stream.avail_out = sizeof(buf); switch (inflate(&stream, Z_BLOCK)) { case Z_STREAM_ERROR: errx(1, "stream error"); case Z_NEED_DICT: errx(1, "need dict"); case Z_DATA_ERROR: errx(1, "data error: %s", stream.msg); case Z_MEM_ERROR: errx(1, "memory error"); } have = sizeof(buf) - stream.avail_out; fwrite(buf, 1, have, stdout); crc = crc32(crc, buf, have); } while (stream.avail_out == 0); inflateEnd(&stream); if (crc != ocrc) errx(1, "CRC mismatch"); }
Si noti anche la bellezza del CRC: può essere calcolato pezzo per pezzo. Lo svantaggio è che non siamo in grado di capire se il file è corrotto o meno fino a che non è stato estratto tutto: si potrebbe eseguire il loop due volte, ma credo sarebbe solo uno spreco (in modo particolare per file molto grandi.)
Per testare quanto scritto fin’ora:
% cc zipview.c -o zipview -lz % ./zipview star_maker_olaf_stapledon.gpub metadata.txt title: Star Maker author: William Olaf Stapledon published: 1937 language: en gpubVersion: 0.0.1 %
Funziona!
Nel prossimo articolo andrò ad aggiungere il supporto per gli ZIP64 e qualche considerazione finale.
$BlogIt: estrarre-file-da-uno-zip.gmi,v 1.1 2021/10/20 07:41:39 op Exp $