💾 Archived View for it.omarpolo.com › articoli › elencare-i-file-di-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

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

Elencare i file di uno zip

Questa è una traduzione di un articolo pubblicato nel mio blog in inglese:

gemini://gemini.omarpolo.com/post/inspecting-zips.gmi

Parte seconda: “Estrarer file da uno zip”

Il codice per l’intera serie; vedi ‘zipls.c’ per questo articolo in particolare.

Dovuta precisazione: prima di oggi non sapevo nulla su come fossero fatti gli zip, quindi potrebbero esserci imprecisioni nel testo. Il lato positivo è che il codice che ho scritto sembra essere coerente con quanto letto online e funzionare su veri file zip.

Vorrei aggiungere il supporto per i gempub a Telescope, il client Gemini che sviluppo. I gempub sono fondamentalmente una cartella di file text/gemini (ma non solo, c’è anche un metadata.txt e alcune immagini in genere) “zippati” in un singolo archivio.

gempub: a new eBook format based on text/gemini

Telescope

Ci sono molte librerie disponibili per gestire gli zip, ma ho deciso di provare a scrivere qualcosa da zero. In fondo non ho bisogno di modificare archivi o fare nulla di particolare, devo solo leggere alcuni file.

Per iniziare, in quest’articolo vedremo come estrarre la lista di file contenuta in un archivio zip. Forse in futuro ci saranno altri articoli sugli zip!

Da quello che ho capito da APPNOTE.TXT e altre fondi, un file zip è una serie di “record” (un header seguito dal contenuto del file) e una “central directory” finale che contiene le informazioni su tutti i file.

APPNOTE.TXT

The structure of a PKZip file

ZIP (Wikipedia)

Avere la central directory alla fine del file invece che all’inizio sembra essere una soluzione per far perdere tempo^W^W^W permettere di includere gli zip dentro altri tipi di file, come GIFs o EXE. Credo che in alcuni casi questa possa essere una proprietà molto comoda (come per degli installer).

Nota: dopo un po’ di riflessione un altro vantaggio di avere la directory in fondo al file è che rende possibile costruire lo zip al volo, magari scrivendolo su standard output o simili dispositivi non “seekable”, senza dover costruire tutto lo zip in memoria.

Si potrebbe pensare che si possa scansionare uno zip leggendo i record per i file uno dopo l’altro, ma fortunatamente non è il caso. L’unica fonte di verità sui file dell’archivio è la directory in fondo al file; le applicazioni che modificano gli zip possono ri-usare o lasciare header dei file invalidi in giro, in modo particolare se cancellano o rimpiazzano dei file.

Ad aggravare la situazione, non è ovvio trovare l’inizio della directory centrale. Sono proprio belli gli ZIP :) Credo che aggiungere 4 byte di puntatore in fondo al file per l’inizio della directory centrale non fosse così brutto, ma siamo un po’ in ritardo.

La directory centrale è una sequenza di record che identificano i file nell’archivio seguita da una firma digitale, due campi per ZIP64 e il record di fine directory. Non ho ancora ben capito a cosa serva la firma digitale e i campi per ZIP64, ma non sembrano necessari per accedere alla lista dei file.

L’ultima parte della cartella centrale, il record finale, contiene un comodo puntatore all’inizio della directory. Sfortunatamente contiene anche un commento finale a lunghezza variabile che complica un po’ le cose.

Ma basta con le parole, vediamo il codice. Dato che Telescope è scritto in C, il programmino oggetto di questo post sarà anch’esso in C. La funzione main è abbastanza ovvia:

int
main(int argc, char **argv)
{
	int	 fd;
	void	*zip, *cd;
	size_t	 len;

	if (argc != 2)
		errx(1, "missing file to inspect");

	if ((fd = open(argv[1], O_RDONLY)) == -1)
		err(1, "open %s", argv[1]);

	zip = map_file(fd, &len);
	if ((cd = find_central_directory(zip, len)) == NULL)
		errx(1, "can't find central directory");

	ls(zip, len, cd);

	munmap(zip, len);
	close(fd);

	return 0;
}

Credo sia più semplice usare mappare il file in memoria piuttosto che muoversi avanti e indietro con lseek(2). map_file è un piccolo wrapper intorno a mmap(2):

void *
map_file(int fd, size_t *len)
{
	off_t	 jump;
	void	*addr;

	if ((jump = lseek(fd, 0, SEEK_END)) == -1)
		err(1, "lseek");

	if (lseek(fd, 0, SEEK_SET) == -1)
		err(1, "lseek");

	if ((addr = mmap(NULL, jump, PROT_READ, MAP_PRIVATE, fd, 0))
	    == MAP_FAILED)
                err(1, "mmap");

	*len = jump;
	return addr;
}

Come visto prima, per trovare la cartella centrale bisogna prima trovare il record finale, la cui struttura è

signature[4] disk_number[2] disk_cd_number[2] disk_entries[2]
total_entrie[2] central_directory_size[4] cd_offset[4]
comment_len[2] comment…

La firma (“signature”) è sempre “\x50\x4b\x05\x06”, il che aiuta non poco. Bisogna comunque fare attenzione, dato che nulla vieta che quella particolare sequenza di byte compaia dentro il commento.

Per essere sicuri di aver trovato il vero record finale c’è un controllo esplicito: la lunghezza del commento sommata alla lunghezza della parte non-variabile del record dev’essere uguale alla distanza che abbiamo percorso dalla fine del file. Certo, non è una soluzione completamente solida, dato che zip malvagi potrebbero includere nel commento una sequenza di byte che sembri il record di fine directory, ma non sono sicuro di come potremmo proteggerci meglio da queste tipologie di file.

Nota: come sempre, non considero i dati letti dai file come fidati e cerco di includere tutti i controlli possibili. Non voglio che zip malformati (per qualunque ragione), facciano crashare il programma dopo tutto.

Un’ultima cosa: non mi dispiace usare in modo leggero e sparso i ‘goto’. Nella seguente funzione uso un ‘goto again’ per continuare a cercare se trovo una falsa firma dentro un commento. Un ciclo while potrebbe essere una soluzione, ma credo sarebbe più brutta.

void *
find_central_directory(uint8_t *addr, size_t len)
{
	uint32_t	 offset;
	uint16_t	 clen;
	uint8_t		*p, *end;

	/*
	 * At -22 bytes from the end there is the end of the central
	 * directory assuming an empty comment.  It's a sensible place
	 * from which start.
	 */
	if (len < 22)
		return NULL;
	end = addr + len;
	p = end - 22;

again:
	for (; p > addr; --p)
		if (memcmp(p, "\x50\x4b\x05\x06", 4) == 0)
			break;

	if (p == addr)
		return NULL;

	/* read comment length */
	memcpy(&clen, p + 20, sizeof(clen));
	clen = le16toh(clen);

	/* false signature inside a comment? */
	if (clen + 22 != end - p) {
		p--;
		goto again;
	}

	/* read the offset for the central directory */
	memcpy(&offset, p + 16, sizeof(offset));
	offset = le32toh(offset);

	if (addr + offset > p)
		return NULL;

	return addr + offset;
}

C’è spazio per una piccola ottimizzazione: il record finale deve essere negli ultimi 64kb (circa), quindi per file molto grandi non serve continuare a cercare fino all’inizio. Perché 64Kb? La lunghezza del commento è un intero da 16 bit, quindi la lunghezza massima per il record finale è di 22 byte più 64kb di commento.

Se tutto è andato bene, abbiamo trovato un puntatore all’inizio della cartella centrale. È composta da una sequenza di record contenente informazioni sui file secondo il seguente formato:

signature[4] version[2] vers_needed[2] flags[2] compression[2]
mod_time[2] mod_date[2] crc32[4]
compressed_size[4] uncompressed_size[4]
filename_len[2] extra_field_len[2] file_comment_len[2]
disk_number[2] internal_attrs[2] offset[4]
filename… extra_field… file_comment…

La firma è sempre "\x50\x4b\x01\x02", che è diversa dalle altre firme fortunatamente! Per elencare i file basta leggere tutte queste intestazioni, sono una di seguito all’altra, finchè non troviamo un record che inizia con una firma diversa:

void
ls(uint8_t *zip, size_t len, uint8_t *cd)
{
	uint32_t	 offset;
	uint16_t	 flen, xlen, clen;
	uint8_t		*end;
	char		 filename[PATH_MAX];

	end = zip + len;
	while (cd < end - 4 && memcmp(cd, "\x50\x4b\x01\x02", 4) == 0) {
		memcpy(&flen, cd + 28, sizeof(flen));
		memcpy(&xlen, cd + 28 + 2, sizeof(xlen));
		memcpy(&clen, cd + 28 + 2 + 2, sizeof(xlen));

		flen = le16toh(flen);
		xlen = le16toh(xlen);
		clen = le16toh(clen);

		memcpy(&offset, cd + 42, sizeof(offset));
		offset = le32toh(offset);

		memset(filename, 0, sizeof(filename));
		memcpy(filename, cd + 46, MIN(sizeof(filename)-1, flen));

		printf("%s [%d]\n", filename, offset);

                cd += 46 + flen + xlen + clen;
	}
}

Come sempre, ci sono alcuni numeri “hardcodati” quando un programma vero userebbe delle costanti, ma essendo un giocattolino per me va bene com’è. Si noti anche la pedanteria nel controllare di non andare a leggere fuori dai margini del file per evitare che zip malformati provochino accessi di memoria invalidi.

Per compilare ed eseguire:

% cc zipls.c -o zipls && ./zipls star_maker_olaf_stapledon.gpub
0_preface.gmi [0]
chapter_1_1_the_starting_point.gmi [2957]
chapter_1_2_earth_among_the_stars.gmi [6932]
chapter_2_1_interstellar_travel.gmi [11041]
chapter_3_1_on_the_other_earth.gmi [20382]
…

e funziona!

La maggior parte dei numeri sono scritti in formato little-endian, anche se ci sono delle eccezioni, come per la data e l’ora in formato MSDOS, quindi conviene sempre controllare la documentazione. La prima versione del codice non aveva le dovute chiamate a leXYtoh() da ‘endian.h’.

Per concludere, sono abbastanza contento del risultato. In poco tempo sono riuscito a passare dal non sapere nulla sugli zip a riuscire ad almeno ispezionarli, usando solo la libreria standard. Non sono troppo difficili da maneggiare come tipo di file alla fine. Lascerò l’estrazione dei file per una prossima volta però!

$BlogIt: elencare-i-file-di-uno-zip.gmi,v 1.1 2021/10/20 07:41:39 op Exp $