💾 Archived View for it.omarpolo.com › articoli › estrarre-file-da-uno-zip.gmi captured on 2021-11-30 at 20:18:30. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Estrarre file da uno zip

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?)

pledge(2) manpage

capsicum(4) manpage

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 $