/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* ** Copyright (c) 2017 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** */ /** This copy has been modified slightly, and expanded, for use with the libfossil project. */ #include "fossil-scm/fossil-internal.h" #include "fossil-scm/fossil-checkout.h" #include #include #include /* atoi() and friends */ #include /* memset() */ #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) /* Write a 16- or 32-bit integer as little-endian into the given buffer. */ static void fzip_put16(char *z, int v){ z[0] = v & 0xff; z[1] = (v>>8) & 0xff; } static void fzip_put32(char *z, int v){ z[0] = v & 0xff; z[1] = (v>>8) & 0xff; z[2] = (v>>16) & 0xff; z[3] = (v>>24) & 0xff; } /** Set the date and time values from an ISO8601 date string. */ static void fzip_timestamp_from_str(fsl_zip_writer *z, const char *zDate){ int y, m, d; int H, M, S; y = atoi(zDate); m = atoi(&zDate[5]); d = atoi(&zDate[8]); H = atoi(&zDate[11]); M = atoi(&zDate[14]); S = atoi(&zDate[17]); z->dosTime = (H<<11) + (M<<5) + (S>>1); z->dosDate = ((y-1980)<<9) + (m<<5) + d; } fsl_buffer const * fsl_zip_body( fsl_zip_writer const * const z ){ return z ? &z->body : NULL; } void fsl_zip_timestamp_set_julian(fsl_zip_writer * const z, double rDate){ char buf[20] = {0}; fsl_julian_to_iso8601(rDate, buf, 0); fzip_timestamp_from_str(z, buf); z->unixTime = (fsl_time_t)((rDate - 2440587.5)*86400.0); } void fsl_zip_timestamp_set_unix(fsl_zip_writer * const z, fsl_time_t epochTime){ char buf[20] = {0}; fsl_julian_to_iso8601(fsl_unix_to_julian(epochTime), buf, 0); fzip_timestamp_from_str(z, buf); z->unixTime = epochTime; } /** Adds all directories for the given file to the zip if they are not in there already. Returns 0 on success, non-0 on error (namely OOM). */ static int fzip_mkdir(fsl_zip_writer * const z, char const *zName); /** Adds a file entry to zw's zip output. zName is the virtual name of the file or directory. If pSrc is NULL then it is assumed that we are creating a directory, otherwise the zip's entry is populated from pSrc. mPerms specify the fossil-specific permission flags from the fsl_fileperm_e enum. If doMkDirs is true then fzip_mkdir() is called to create the directory entries for zName, otherwise they are not. */ static int fzip_file_add(fsl_zip_writer * const zw, char const * zName, fsl_buffer const * pSrc, int mPerm, char doMkDirs){ int rc = 0; z_stream stream; fsl_size_t nameLen; int toOut = 0; int iStart; int iCRC = 0; int nByte = 0; int nByteCompr = 0; int nBlob; /* Size of the blob */ int iMethod; /* Compression method. */ int iMode = 0644; /* Access permissions */ char *z; char zHdr[30]; char zExTime[13]; char zBuf[100]; char zOutBuf[/*historical: 100000*/ 1024 * 16]; /* Fill in as much of the header as we know. */ nBlob = pSrc ? (int)pSrc->used : 0; if( pSrc ){ /* a file entry */ iMethod = pSrc->used ? 8 : 0 /* don't compress 0-byte files */; switch( mPerm ){ case FSL_FILE_PERM_LINK: iMode = 0120755; break; case FSL_FILE_PERM_EXE: iMode = 0100755; break; default: iMode = 0100644; break; } }else{ /* a directory entry */ iMethod = 0; iMode = 040755; } if(doMkDirs){ rc = fzip_mkdir(zw, zName) /* This causes an extraneous run of fzip_mkdir(), but it is harmless other than the waste of search time */; if(rc) return rc; } if(zw->rootDir){ zw->scratch.used = 0; rc = fsl_buffer_appendf(&zw->scratch, "%s%s", zw->rootDir, zName); if(rc){ assert(FSL_RC_OOM==rc); return rc; } zName = fsl_buffer_cstr(&zw->scratch); } nameLen = fsl_strlen(zName); memset(zHdr, 0, sizeof(zHdr)); fzip_put32(&zHdr[0], 0x04034b50); fzip_put16(&zHdr[4], 0x000a); fzip_put16(&zHdr[6], 0x0800); fzip_put16(&zHdr[8], iMethod); fzip_put16(&zHdr[10], zw->dosTime); fzip_put16(&zHdr[12], zw->dosDate); fzip_put16(&zHdr[26], nameLen); fzip_put16(&zHdr[28], 13); fzip_put16(&zExTime[0], 0x5455); fzip_put16(&zExTime[2], 9); zExTime[4] = 3; fzip_put32(&zExTime[5], zw->unixTime); fzip_put32(&zExTime[9], zw->unixTime); /* Write the header and filename. */ iStart = (int)zw->body.used; fsl_buffer_append(&zw->body, zHdr, 30); fsl_buffer_append(&zw->body, zName, nameLen); fsl_buffer_append(&zw->body, zExTime, 13); if( nBlob>0 ){ /* Write the compressed file. Compute the CRC as we progress. */ stream.zalloc = (alloc_func)0; stream.zfree = (free_func)0; stream.opaque = 0; stream.avail_in = pSrc->used; stream.next_in = /* (unsigned char*) */pSrc->mem; stream.avail_out = sizeof(zOutBuf); stream.next_out = (unsigned char*)zOutBuf; deflateInit2(&stream, 9, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY); iCRC = crc32(0, stream.next_in, stream.avail_in); while( stream.avail_in>0 ){ deflate(&stream, 0); toOut = sizeof(zOutBuf) - stream.avail_out; fsl_buffer_append(&zw->body, zOutBuf, toOut); stream.avail_out = sizeof(zOutBuf); stream.next_out = (unsigned char*)zOutBuf; } do{ stream.avail_out = sizeof(zOutBuf); stream.next_out = (unsigned char*)zOutBuf; deflate(&stream, Z_FINISH); toOut = sizeof(zOutBuf) - stream.avail_out; fsl_buffer_append(&zw->body, zOutBuf, toOut); }while( stream.avail_out==0 ); nByte = stream.total_in; nByteCompr = stream.total_out; deflateEnd(&stream); /* Go back and write the header, now that we know the compressed file size. */ z = (char *)zw->body.mem + iStart/* &blob_buffer(&body)[iStart] */; fzip_put32(&z[14], iCRC); fzip_put32(&z[18], nByteCompr); fzip_put32(&z[22], nByte); } /* Make an entry in the tables of contents */ memset(zBuf, 0, sizeof(zBuf)); fzip_put32(&zBuf[0], 0x02014b50); fzip_put16(&zBuf[4], 0x0317); fzip_put16(&zBuf[6], 0x000a); fzip_put16(&zBuf[8], 0x0800); fzip_put16(&zBuf[10], iMethod); fzip_put16(&zBuf[12], zw->dosTime); fzip_put16(&zBuf[14], zw->dosDate); fzip_put32(&zBuf[16], iCRC); fzip_put32(&zBuf[20], nByteCompr); fzip_put32(&zBuf[24], nByte); fzip_put16(&zBuf[28], nameLen); fzip_put16(&zBuf[30], 9); fzip_put16(&zBuf[32], 0); fzip_put16(&zBuf[34], 0); fzip_put16(&zBuf[36], 0); fzip_put32(&zBuf[38], ((unsigned)iMode)<<16); fzip_put32(&zBuf[42], iStart); fsl_buffer_append(&zw->toc, zBuf, 46); fsl_buffer_append(&zw->toc, zName, nameLen); fzip_put16(&zExTime[2], 5); fsl_buffer_append(&zw->toc, zExTime, 9); ++zw->entryCount; return rc; } int fzip_mkdir(fsl_zip_writer * const z, char const *zName){ fsl_size_t i; fsl_size_t j; int rc = 0; char const * dirName; fsl_size_t nDir = z->dirs.used; for(i=0; zName[i]; i++){ if( zName[i]=='/' ){ while(zName[i+1]=='/') ++i /* Skip extra slashes */; for(j=0; jdirs.list[j]; if( fsl_strncmp(zName, dirName, i)==0 ) break; } if( j>=nDir ){ char * cp = fsl_strndup(zName, (fsl_int_t)i+1); rc = cp ? fsl_list_append(&z->dirs, cp) : FSL_RC_OOM; if(cp && rc){ fsl_free(cp); }else{ rc = fzip_file_add(z, cp, NULL, 0, 0); } } } } return rc; } int fsl_zip_file_add(fsl_zip_writer * const z, char const * zName, fsl_buffer const * pSrc, int mPerm){ return fzip_file_add(z, zName, pSrc, mPerm, 1); } int fsl_zip_root_set(fsl_zip_writer * const z, char const * zRoot ){ if(!z) return FSL_RC_MISUSE; else if(zRoot && *zRoot && fsl_is_absolute_path(zRoot)){ return FSL_RC_RANGE; }else{ fsl_free(z->rootDir); z->rootDir = NULL; if(zRoot && *zRoot){ /* Problem: we have to mkdir zRoot before we assign z->rootDir to avoid an interesting ROOT/ROOT dir entry on an otherwise empty ZIP. We create the dirs here, instead of during the first file insertion (after z->rootDir is set), to work around that. */ char * cp; fsl_size_t n = fsl_strlen(zRoot); if('/'==zRoot[n-1]){ /* Keep the slash */ cp = fsl_strndup(zRoot, (fsl_int_t)n); }else{ /* Add a slash to our copy... */ cp = (char *)fsl_malloc(n+2); if(cp){ memcpy( cp, zRoot, n ); cp[n] = '/'; cp[n+1] = 0; ++n; } } if(!cp) return FSL_RC_OOM; else{ int rc; n = fsl_file_simplify_name(cp, (fsl_int_t)n, 1); assert(n); assert('/'==cp[n-1]); cp[n-1] = 0; rc = fsl_is_simple_pathname(cp, 1); cp[n-1] = '/'; rc = rc ? fzip_mkdir(z, cp) : FSL_RC_RANGE; z->rootDir = cp /* transfer ownership on error as well and let normal downstream clean it up. */; return rc; } } return 0; } } static void fsl_zip_finalize_impl(fsl_zip_writer * const z, bool alsoBody){ if(z){ fsl_buffer_clear(&z->toc); fsl_buffer_clear(&z->scratch); fsl_list_visit_free(&z->dirs, 1); assert(NULL==z->dirs.list); fsl_free(z->rootDir); if(alsoBody){ fsl_buffer_clear(&z->body); *z = fsl_zip_writer_empty; }else{ fsl_buffer cp = z->body; *z = fsl_zip_writer_empty; z->body = cp; } } } void fsl_zip_finalize(fsl_zip_writer * const z){ fsl_zip_finalize_impl(z, 1); } int fsl_zip_end( fsl_zip_writer * const z ){ int rc; fsl_int_t iTocStart; fsl_int_t iTocEnd; char zBuf[30]; iTocStart = (fsl_int_t)z->body.used; rc = fsl_buffer_append(&z->body, z->toc.mem, z->toc.used); if(rc) return rc; fsl_buffer_clear(&z->toc); iTocEnd = (fsl_int_t)z->body.used; memset(zBuf, 0, sizeof(zBuf)); fzip_put32(&zBuf[0], 0x06054b50); fzip_put16(&zBuf[4], 0); fzip_put16(&zBuf[6], 0); fzip_put16(&zBuf[8], (int)z->entryCount); fzip_put16(&zBuf[10], (int)z->entryCount); fzip_put32(&zBuf[12], iTocEnd - iTocStart); fzip_put32(&zBuf[16], iTocStart); fzip_put16(&zBuf[20], 0); rc = fsl_buffer_append(&z->body, zBuf, 22); fsl_zip_finalize_impl(z, 0); assert(z->body.used); return rc; } int fsl_zip_end_take( fsl_zip_writer * const z, fsl_buffer * dest ){ if(!z) return FSL_RC_MISUSE; else{ int rc; if(!dest){ rc = FSL_RC_MISUSE; }else{ rc = fsl_zip_end(z); if(!rc){ fsl_buffer_swap( &z->body, dest ); } } fsl_zip_finalize( z ); return rc; } } int fsl_zip_end_to_filename( fsl_zip_writer * const z, char const * filename ){ if(!z) return FSL_RC_MISUSE; else{ int rc; if(!filename || !*filename){ rc = FSL_RC_MISUSE; }else{ rc = fsl_zip_end(z); if(!rc){ rc = fsl_buffer_to_filename(&z->body, filename); } } fsl_zip_finalize( z ); return rc; } } struct ZipState{ fsl_cx * f; fsl_id_t vid; fsl_card_F_visitor_f progress; void * progressState; fsl_zip_writer z; fsl_buffer cbuf; }; typedef struct ZipState ZipState; static const ZipState ZipState_empty = { NULL, 0, NULL, NULL, fsl_zip_writer_empty_m, fsl_buffer_empty_m }; static int fsl_card_F_visitor_zip(fsl_card_F const * fc, void * state){ ZipState * zs = (ZipState *)state; fsl_id_t frid; int rc = 0; if(!fc->uuid) return 0 /* file was removed in this (delta) manifest */; else if(zs->progress){ rc = (*zs->progress)(fc, zs->progressState); if(rc) return rc; }else if(FSL_FILE_PERM_LINK == fc->perm){ return fsl_cx_err_set(zs->f, FSL_RC_NYI, "Symlinks are not yet supported " "in ZIP output."); } frid = fsl_uuid_to_rid(zs->f, fc->uuid); if(frid<0){ rc = zs->f->error.code; }else if(!frid){ assert(zs->f->error.code); rc = zs->f->error.code; }else{ fsl_time_t mTime = 0; rc = fsl_mtime_of_manifest_file(zs->f, zs->vid, frid, &mTime); if(!rc){ fsl_zip_timestamp_set_unix(&zs->z, mTime); zs->cbuf.used = 0; rc = fsl_content_get(zs->f, frid, &zs->cbuf); if(!rc){ rc = fsl_zip_file_add(&zs->z, fc->name, &zs->cbuf, FSL_FILE_PERM_REGULAR); if(rc){ fsl_cx_err_set(zs->f, rc, "Error %s adding file [%s] " "to zip.", fsl_rc_cstr(rc), fc->name); } } } } return rc; } int fsl_repo_zip_sym_to_filename( fsl_cx * const f, char const * sym, char const * rootDir, char const * fileName, fsl_card_F_visitor_f progress, void * progressState ){ int rc; fsl_deck mf = fsl_deck_empty; ZipState zs = ZipState_empty; if(!f || !sym || !fileName || !*sym || !*fileName) return FSL_RC_MISUSE; else if(!fsl_needs_repo(f)) return FSL_RC_NOT_A_REPO; rc = fsl_deck_load_sym( f, &mf, sym, FSL_SATYPE_CHECKIN ); if(rc) goto end; if(rootDir && *rootDir){ fsl_time_t rootTime = 0; rc = fsl_mtime_of_manifest_file(f, mf.rid, 0, &rootTime); if(rc) return rc; fsl_zip_timestamp_set_unix(&zs.z, rootTime); rc = fsl_zip_root_set( &zs.z, rootDir ); if(rc) goto end; } rc = fsl_deck_F_rewind(&mf); if(rc) goto end; zs.f = f; zs.vid = mf.rid; zs.progress = progress; zs.progressState = progressState; rc = fsl_deck_F_foreach( &mf, fsl_card_F_visitor_zip, &zs); if(!rc){ if(!zs.z.entryCount){ if(rootDir && *rootDir){ rc = fsl_zip_file_add( &zs.z, rootDir, NULL, FSL_FILE_PERM_REGULAR ); }else{ rc = fsl_cx_err_set(f, FSL_RC_RANGE, "Cowardly refusing to create " "empty ZIP file for repo version [%s].", sym); } if(rc) goto end; } } /** Always write the manifest files to the zip, regardless of the repo-level settings. This decision is up for debate. */ if(rc) goto end; else { fsl_buffer * const bManifest = &f->cache.fileContent; fsl_buffer * const bHash = fsl__cx_scratchpad(f); fsl_buffer * const bTags = fsl__cx_scratchpad(f); fsl_buffer_reuse(bManifest); rc = fsl_repo_manifest_write(f, mf.rid, bManifest, bHash, bTags); if(rc) goto mf_end; rc = fsl_zip_file_add(&zs.z, "manifest", bManifest, FSL_FILE_PERM_REGULAR); if(rc) goto mf_end; rc = fsl_zip_file_add(&zs.z, "manifest.uuid", bHash, FSL_FILE_PERM_REGULAR); if(rc) goto mf_end; rc = fsl_zip_file_add(&zs.z, "manifest.tags", bTags, FSL_FILE_PERM_REGULAR); mf_end: fsl_buffer_reuse(bManifest); fsl__cx_scratchpad_yield(f, bHash); fsl__cx_scratchpad_yield(f, bTags); } if(rc) goto end; rc = fsl_zip_end( &zs.z ); if(!rc) rc = fsl_buffer_to_filename( fsl_zip_body(&zs.z), fileName ); end: if(rc && !f->error.code){ fsl_cx_err_set(f, rc, "Error #%d (%s) during ZIP.", rc, fsl_rc_cstr(rc)); } fsl_buffer_clear(&zs.cbuf); fsl_zip_finalize(&zs.z); fsl_deck_clean(&mf); return rc; } #undef MARKER