#include "private.h" #include <stdio.h> #include <string.h> #include <stdlib.h> enum { // finding the directory CD_SIGNATURE = 0x06054b50, EOCD_LEN = 22, // EndOfCentralDir len, excl. comment MAX_COMMENT_LEN = 65535, MAX_EOCD_SEARCH = MAX_COMMENT_LEN + EOCD_LEN, // central directory entries ENTRY_SIGNATURE = 0x02014b50, ENTRY_LEN = 46, // CentralDirEnt len, excl. var fields // local file header LFH_SIZE = 30, }; unsigned int read_le_int(const unsigned char* buf) { return buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24); } unsigned int read_le_short(const unsigned char* buf) { return buf[0] | (buf[1] << 8); } static int read_central_dir_values(Zipfile* file, const unsigned char* buf, int len) { if (len < EOCD_LEN) { // looks like ZIP file got truncated fprintf(stderr, " Zip EOCD: expected >= %d bytes, found %d\n", EOCD_LEN, len); return -1; } file->disknum = read_le_short(&buf[0x04]); file->diskWithCentralDir = read_le_short(&buf[0x06]); file->entryCount = read_le_short(&buf[0x08]); file->totalEntryCount = read_le_short(&buf[0x0a]); file->centralDirSize = read_le_int(&buf[0x0c]); file->centralDirOffest = read_le_int(&buf[0x10]); file->commentLen = read_le_short(&buf[0x14]); if (file->commentLen > 0) { if (EOCD_LEN + file->commentLen > len) { fprintf(stderr, "EOCD(%d) + comment(%d) exceeds len (%d)\n", EOCD_LEN, file->commentLen, len); return -1; } file->comment = buf + EOCD_LEN; } return 0; } static int read_central_directory_entry(Zipfile* file, Zipentry* entry, const unsigned char** buf, ssize_t* len) { const unsigned char* p; unsigned short versionMadeBy; unsigned short versionToExtract; unsigned short gpBitFlag; unsigned short compressionMethod; unsigned short lastModFileTime; unsigned short lastModFileDate; unsigned long crc32; unsigned short extraFieldLength; unsigned short fileCommentLength; unsigned short diskNumberStart; unsigned short internalAttrs; unsigned long externalAttrs; unsigned long localHeaderRelOffset; const unsigned char* extraField; const unsigned char* fileComment; unsigned int dataOffset; unsigned short lfhExtraFieldSize; p = *buf; if (*len < ENTRY_LEN) { fprintf(stderr, "cde entry not large enough\n"); return -1; } if (read_le_int(&p[0x00]) != ENTRY_SIGNATURE) { fprintf(stderr, "Whoops: didn't find expected signature\n"); return -1; } versionMadeBy = read_le_short(&p[0x04]); versionToExtract = read_le_short(&p[0x06]); gpBitFlag = read_le_short(&p[0x08]); entry->compressionMethod = read_le_short(&p[0x0a]); lastModFileTime = read_le_short(&p[0x0c]); lastModFileDate = read_le_short(&p[0x0e]); crc32 = read_le_int(&p[0x10]); entry->compressedSize = read_le_int(&p[0x14]); entry->uncompressedSize = read_le_int(&p[0x18]); entry->fileNameLength = read_le_short(&p[0x1c]); extraFieldLength = read_le_short(&p[0x1e]); fileCommentLength = read_le_short(&p[0x20]); diskNumberStart = read_le_short(&p[0x22]); internalAttrs = read_le_short(&p[0x24]); externalAttrs = read_le_int(&p[0x26]); localHeaderRelOffset = read_le_int(&p[0x2a]); p += ENTRY_LEN; // filename if (entry->fileNameLength != 0) { entry->fileName = p; } else { entry->fileName = NULL; } p += entry->fileNameLength; // extra field if (extraFieldLength != 0) { extraField = p; } else { extraField = NULL; } p += extraFieldLength; // comment, if any if (fileCommentLength != 0) { fileComment = p; } else { fileComment = NULL; } p += fileCommentLength; *buf = p; // the size of the extraField in the central dir is how much data there is, // but the one in the local file header also contains some padding. p = file->buf + localHeaderRelOffset; extraFieldLength = read_le_short(&p[0x1c]); dataOffset = localHeaderRelOffset + LFH_SIZE + entry->fileNameLength + extraFieldLength; entry->data = file->buf + dataOffset; #if 0 printf("file->buf=%p entry->data=%p dataOffset=%x localHeaderRelOffset=%d " "entry->fileNameLength=%d extraFieldLength=%d\n", file->buf, entry->data, dataOffset, localHeaderRelOffset, entry->fileNameLength, extraFieldLength); #endif return 0; } /* * Find the central directory and read the contents. * * The fun thing about ZIP archives is that they may or may not be * readable from start to end. In some cases, notably for archives * that were written to stdout, the only length information is in the * central directory at the end of the file. * * Of course, the central directory can be followed by a variable-length * comment field, so we have to scan through it backwards. The comment * is at most 64K, plus we have 18 bytes for the end-of-central-dir stuff * itself, plus apparently sometimes people throw random junk on the end * just for the fun of it. * * This is all a little wobbly. If the wrong value ends up in the EOCD * area, we're hosed. This appears to be the way that everbody handles * it though, so we're in pretty good company if this fails. */ int read_central_dir(Zipfile *file) { int err; const unsigned char* buf = file->buf; ssize_t bufsize = file->bufsize; const unsigned char* eocd; const unsigned char* p; const unsigned char* start; ssize_t len; int i; // too small to be a ZIP archive? if (bufsize < EOCD_LEN) { fprintf(stderr, "Length is %d -- too small\n", bufsize); goto bail; } // find the end-of-central-dir magic if (bufsize > MAX_EOCD_SEARCH) { start = buf + bufsize - MAX_EOCD_SEARCH; } else { start = buf; } p = buf + bufsize - 4; while (p >= start) { if (*p == 0x50 && read_le_int(p) == CD_SIGNATURE) { eocd = p; break; } p--; } if (p < start) { fprintf(stderr, "EOCD not found, not Zip\n"); goto bail; } // extract eocd values err = read_central_dir_values(file, eocd, (buf+bufsize)-eocd); if (err != 0) { goto bail; } if (file->disknum != 0 || file->diskWithCentralDir != 0 || file->entryCount != file->totalEntryCount) { fprintf(stderr, "Archive spanning not supported\n"); goto bail; } // Loop through and read the central dir entries. p = buf + file->centralDirOffest; len = (buf+bufsize)-p; for (i=0; i < file->totalEntryCount; i++) { Zipentry* entry = malloc(sizeof(Zipentry)); memset(entry, 0, sizeof(Zipentry)); err = read_central_directory_entry(file, entry, &p, &len); if (err != 0) { fprintf(stderr, "read_central_directory_entry failed\n"); free(entry); goto bail; } // add it to our list entry->next = file->entries; file->entries = entry; } return 0; bail: return -1; }