/* * read_zip.c * Stephanie Taylor * Fall 2025 * * Read in an uncompressed zip file that contains text files. * Then print out the names and contents of those text files. * * This code is intended to illustrate binary file I/O. * And why not de-mystify ZIP files along the way? * * I got the ZIP file format information from Wikipedia: https://en.wikipedia.org/wiki/ZIP_(file_format) * */ #include #include #include // This is the struct that stores the info we are looking for typedef struct { char *fn; char *text; } TextFile; /* Read the header information and content for one of the text files in * the ZIP file. Start by checking the magic number, and if it is wrong, then * back up so that the next function called can re-read it. * Return a TextFile* if the magic number is correct (and there is a file to read) * or NULL if it isn't. */ TextFile* read_sub_file( FILE *fp, int verbose ) { // Offset (byes), size (bytes), description // 0 4 Magic number. Must be 50 4B 03 04. unsigned char magic[4]; unsigned char magic_should_be[4] = {0x50, 0x4B, 0x03, 0x04}; size_t size; size = fread( &magic, 1, 4, fp ); if (verbose) printf( "Read %lu magic %02x %02x %02x %02x\n", size, magic[0], magic[1], magic[2], magic[3] ); for (int i = 0; i < 4; i++) { if (magic[i] != magic_should_be[i]) { // probably, we just found the magic number for the file directory options, and that means // we are done with the files themselves. // so back up 4 bytes for the next phase and return NULL so the calling code knows to move on fseek( fp, -4, SEEK_CUR ); return NULL; } } // 4 2 Version needed to extract (minimum). unsigned short int version; size = fread( &version, 2, 1, fp ); if (verbose) printf( "Need unzip version >= %d\n", version ); // 6 2 General purpose bit flag. unsigned short int flags; size = fread( &flags, 2, 1, fp ); if (verbose) printf( "Bit flags are %02x\n", flags ); // 8 2 Compression method; e.g. none = 0, DEFLATE = 8 (or "\0x08\0x00"). unsigned short int compression_method; size = fread( &compression_method, 2, 1, fp ); if (verbose) printf( "Compression method is %d\n", compression_method ); if (compression_method != 0) { fprintf( stderr, "This code doesn't support compressed ZIP files. Sorry!\n" ); return NULL; } // 10 2 File last modification time. // 12 2 File last modification date. unsigned int ztime; unsigned int zdate; size = fread( &ztime, 2, 1, fp ); size = fread( &zdate, 2, 1, fp ); if (verbose) printf( "ztime is %04x, zdate is %04x\n", ztime, zdate ); // 14 4 CRC-32 of uncompressed data. unsigned char crc32[4]; size = fread( &crc32, 1, 4, fp ); if (verbose) printf( "CRC-32 is %02x %02x %02x %02x\n", crc32[0], crc32[1], crc32[2], crc32[3] ); // 18 4 Compressed size (or FF FF FF FF for ZIP64). // 22 4 Uncompressed size (or FF FF FF FF for ZIP64). unsigned int compressed_size; unsigned int uncompressed_size; size = fread( &compressed_size, 4, 1, fp ); size = fread( &uncompressed_size, 4, 1, fp ); if (verbose) printf( "Uncompressed size is %d, compressed size of %d\n", compressed_size, uncompressed_size ); // 26 2 File name length (n). // 28 2 Extra field length (m). unsigned short filename_length; unsigned short extra_field_length; size = fread( &filename_length, 2, 1, fp ); size = fread( &extra_field_length, 2, 1, fp ); if (verbose) printf( "filename length is %d, extra field length of %d\n", filename_length, extra_field_length ); // 30 n File name. char *filename = malloc( sizeof(char)*(filename_length+1) ); size = fread( filename, filename_length, 1, fp ); filename[filename_length] = '\0'; if (verbose) printf( "filename is |%s|\n", filename ); // 30+n m Extra field. // ignore it by skipping extra_field_length bytes, starting from the current position fseek( fp, extra_field_length, SEEK_CUR ); // Now is the content of the file. // Here, I am assuming it is a text file. // how do we know how many bytes that is? Ahh! It is the compressed size!! char *file_contents = malloc( sizeof(char)*(compressed_size+1) ); fread( file_contents, compressed_size, 1, fp ); file_contents[compressed_size] = '\0'; TextFile *tf = (TextFile *)(malloc( sizeof(TextFile) ) ); tf->fn = filename; tf->text = file_contents; return tf; } /* * Read a central directory file header. There should be one for each subfile. Read the magic number * and if it is correct, seek to the end of this header. If it is not correct, then back up so the * next function can read the magic number. * * Return 1 if we detect and read a central directory file header and 0 if we do not. */ int read_central_directory_file_header( FILE *fp, int verbose ) { // 0 4 Magic number. Must be 50 4B 01 02. unsigned char magic[4]; unsigned char magic_should_be[4] = {0x50, 0x4B, 0x01, 0x02}; size_t size; size = fread( &magic, 1, 4, fp ); if (verbose) printf( "Read %lu magic %02x %02x %02x %02x\n", size, magic[0], magic[1], magic[2], magic[3] ); for (int i = 0; i < 4; i++) { if (magic[i] != magic_should_be[i]) { // probably, we just found the magic number for the End of central directory record, and that means // we are done with the file headers. // so back up 4 bytes for the next phase and return NULL so the calling code knows to move on fseek( fp, -4, SEEK_CUR ); return 0; } } // 4 2 Version made by. // 6 2 Version needed to extract (minimum). // 8 2 General purpose bit flag. // 10 2 Compression method. // 12 2 File last modification time. // 14 2 File last modification date. // 16 4 CRC-32 of uncompressed data. // 20 4 Compressed size (or FF FF FF FF for ZIP64). // 24 4 Uncompressed size (or FF FF FF FF for ZIP64). // 28 2 File name length (n). // 30 2 Extra field length (m). // 32 2 File comment length (k). fseek( fp, 28-4, SEEK_CUR ); // To get to offset 28 from beginning of record, we need to move 24 more bytes (because owe read the magic number already) unsigned short filename_length; unsigned short extra_field_length; unsigned short file_comment_length; size = fread( &filename_length, 2, 1, fp ); size = fread( &extra_field_length, 2, 1, fp ); size = fread( &file_comment_length, 2, 1, fp ); // 34 2 Disk number where file starts (or FF FF for ZIP64). // 36 2 Internal file attributes. // 38 4 External file attributes. // 42 4 Relative offset of local file header (or FF FF FF FF for ZIP64). This is the number of bytes between the start of the first disk on which the file occurs, and the start of the local file header. This allows software reading the central directory to locate the position of the file inside the ZIP file. // 46 n File name. // 46+n m Extra field. // 46+n+m k File comment. // to move to the end of this record we need to move from offset 34 (where we finished // reading the file comment length) to the beginning of the variable-length parts + all those // variable lengths. fseek( fp, 46-34+filename_length+extra_field_length+file_comment_length, SEEK_CUR ); if (verbose) printf( "Finished reading a directory record\n" ); return 1; } /* * Read a the end of central directory file record. There should be one at the end of the ZIP file. * Read the magic number and if it is correct, seek to the end of this header. If it is not correct, * then we have a problem. * * Return 1 if we detect and read the end of central directory file record and 0 if we do not. */ int read_end_of_central_directory_record( FILE *fp, int verbose ) { // 0 4 Magic number. Must be 50 4B 05 06. unsigned char magic[4]; unsigned char magic_should_be[4] = {0x50, 0x4B, 0x05, 0x06}; size_t size; size = fread( &magic, 1, 4, fp ); if (verbose) printf( "Read %lu magic %02x %02x %02x %02x\n", size, magic[0], magic[1], magic[2], magic[3] ); for (int i = 0; i < 4; i++) { if (magic[i] != magic_should_be[i]) { // This really needs to match the magic number, so it is an error if we haven't fprintf( stderr, "Magic byte %d should have been %02x but was %02x\n", i, magic[0], magic_should_be[1] ); return 0; } } // 4 2 Number of this disk (or FF FF for ZIP64). // 6 2 Disk where central directory starts (or FF FF for ZIP64). // 8 2 Number of central directory records on this disk (or FF FF for ZIP64). // 10 2 Total number of central directory records (or FF FF for ZIP64). // 12 4 Size of central directory in bytes (or FF FF FF FF for ZIP64). // 16 4 Offset of start of central directory, relative to start of archive (or FF FF FF FF for ZIP64). // 20 2 Comment length (n). fseek (fp, 20-4, SEEK_CUR ); // move from offset 4 to offset 20 unsigned short comment_length; size = fread( &comment_length, 2, 1, fp ); if (verbose) printf( "comment length at end is %d\n", comment_length ); // 22 n Comment. if (comment_length > 0) fseek( fp, comment_length, SEEK_CUR ); // we should now be at the end of file. success if we are. // but we need to attempt to read past the end of file, to tell getc(fp); return feof( fp ); } /* Read in simple.zip */ int main() { int verbose = 0; FILE *fp; fp = fopen( "simple.zip", "rb" ); if (fp == NULL) { fprintf( stderr, "Error opening simple.zip\n" ); return 1; } // Overall format for a zip file with n files in it // local file 1 header // local file 1 data // local file 2 header // local file 2 data // ... // local file n header // local file n data // central directory file header 1 // central directory file header 2 // ... // central directory file header n // end of central directory file header // after this, there is a section for each file in the archive. while (1) { TextFile *tf = read_sub_file( fp, verbose ); if (tf == NULL) { break; } printf( "Found a file named %s with contents:\n%s", tf->fn, tf->text ); } while (1) { int ret = read_central_directory_file_header( fp, verbose ); if (ret == 0) { break; } } int succ = read_end_of_central_directory_record( fp, verbose ); if (succ) printf( "Successfully read uncompressed ZIP file!\n" ); }