Subversion Repositories planix.SVN

Rev

Blame | Last modification | View Log | RSS feed

#include <u.h>
#include <libc.h>
#include <bio.h>
#include <flate.h>
#include "zip.h"

enum
{
        BufSize = 4096
};

static  int     cheader(Biobuf *bin, ZipHead *zh);
static  int     copyout(int ofd, Biobuf *bin, long len);
static  int     crcwrite(void *ofd, void *buf, int n);
static  int     findCDir(Biobuf *bin, char *file);
static  int     get1(Biobuf *b);
static  int     get2(Biobuf *b);
static  ulong   get4(Biobuf *b);
static  char    *getname(Biobuf *b, int len);
static  int     header(Biobuf *bin, ZipHead *zh);
static  long    msdos2time(int time, int date);
static  int     sunzip(Biobuf *bin);
static  int     sunztable(Biobuf *bin);
static  void    trailer(Biobuf *bin, ZipHead *zh);
static  int     unzip(Biobuf *bin, char *file);
static  int     unzipEntry(Biobuf *bin, ZipHead *czh);
static  int     unztable(Biobuf *bin, char *file);
static  int     wantFile(char *file);

static  void    *emalloc(ulong);
static  void    error(char*, ...);
#pragma varargck        argpos  error   1

static  Biobuf  bin;
static  ulong   crc;
static  ulong   *crctab;
static  int     debug;
static  char    *delfile;
static  int     lower;
static  int     nwant;
static  ulong   rlen;
static  int     settimes;
static  int     stdout;
static  int     verbose;
static  char    **want;
static  int     wbad;
static  ulong   wlen;
static  jmp_buf zjmp;
static  jmp_buf seekjmp;
static  int     autodir;

static void
usage(void)
{
        fprint(2, "usage: unzip [-cistTvD] [-f zipfile] [file ...]\n");
        exits("usage");
}

void
main(int argc, char *argv[])
{
        char *zfile;
        int fd, ok, table, stream;

        table = 0;
        stream = 0;
        zfile = nil;
        ARGBEGIN{
        case 'a':
                autodir++;
                break;
        case 'D':
                debug++;
                break;
        case 'c':
                stdout++;
                break;
        case 'i':
                lower++;
                break;
        case 'f':
                zfile = ARGF();
                if(zfile == nil)
                        usage();
                break;
        case 's':
                stream++;
                break;
        case 't':
                table++;
                break;
        case 'T':
                settimes++;
                break;
        case 'v':
                verbose++;
                break;
        default:
                usage();
                break;
        }ARGEND

        nwant = argc;
        want = argv;

        crctab = mkcrctab(ZCrcPoly);
        ok = inflateinit();
        if(ok != FlateOk)
                sysfatal("inflateinit failed: %s", flateerr(ok));

        if(zfile == nil){
                Binit(&bin, 0, OREAD);
                zfile = "<stdin>";
        }else{
                fd = open(zfile, OREAD);
                if(fd < 0)
                        sysfatal("can't open %s: %r", zfile);
                Binit(&bin, fd, OREAD);
        }

        if(setjmp(seekjmp)){
                fprint(2, "trying to re-run assuming -s\n");
                stream = 1;
                Bseek(&bin, 0, 0);
        }

        if(table){
                if(stream)
                        ok = sunztable(&bin);
                else
                        ok = unztable(&bin, zfile);
        }else{
                if(stream)
                        ok = sunzip(&bin);
                else
                        ok = unzip(&bin, zfile);
        }

        exits(ok ? nil: "errors");
}

/*
 * print the table of contents from the "central directory structure"
 */
static int
unztable(Biobuf *bin, char *file)
{
        ZipHead zh;
        int entries;

        entries = findCDir(bin, file);
        if(entries < 0)
                return 0;

        if(verbose > 1)
                print("%d items in the archive\n", entries);
        while(entries-- > 0){
                if(setjmp(zjmp)){
                        free(zh.file);
                        return 0;
                }

                memset(&zh, 0, sizeof(zh));
                if(!cheader(bin, &zh))
                        return 1;

                if(wantFile(zh.file)){
                        if(verbose)
                                print("%-32s %10lud %s", zh.file, zh.uncsize, ctime(msdos2time(zh.modtime, zh.moddate)));
                        else
                                print("%s\n", zh.file);

                        if(verbose > 1){
                                print("\tmade by os %d vers %d.%d\n", zh.madeos, zh.madevers/10, zh.madevers % 10);
                                print("\textract by os %d vers %d.%d\n", zh.extos, zh.extvers/10, zh.extvers % 10);
                                print("\tflags %x\n", zh.flags);
                                print("\tmethod %d\n", zh.meth);
                                print("\tmod time %d\n", zh.modtime);
                                print("\tmod date %d\n", zh.moddate);
                                print("\tcrc %lux\n", zh.crc);
                                print("\tcompressed size %lud\n", zh.csize);
                                print("\tuncompressed size %lud\n", zh.uncsize);
                                print("\tinternal attributes %ux\n", zh.iattr);
                                print("\texternal attributes %lux\n", zh.eattr);
                                print("\tstarts at %ld\n", zh.off);
                        }
                }

                free(zh.file);
                zh.file = nil;
        }

        return 1;
}

/*
 * print the "local file header" table of contents
 */
static int
sunztable(Biobuf *bin)
{
        ZipHead zh;
        vlong off;
        ulong hcrc, hcsize, huncsize;
        int ok, err;

        ok = 1;
        for(;;){
                if(setjmp(zjmp)){
                        free(zh.file);
                        return 0;
                }

                memset(&zh, 0, sizeof(zh));
                if(!header(bin, &zh))
                        return ok;

                hcrc = zh.crc;
                hcsize = zh.csize;
                huncsize = zh.uncsize;

                wlen = 0;
                rlen = 0;
                crc = 0;
                wbad = 0;

                if(zh.meth == 0){
                        if(!copyout(-1, bin, zh.csize))
                                error("reading data for %s failed: %r", zh.file);
                }else if(zh.meth == 8){
                        off = Boffset(bin);
                        err = inflate((void*)-1, crcwrite, bin, (int(*)(void*))Bgetc);
                        if(err != FlateOk)
                                error("inflate %s failed: %s", zh.file, flateerr(err));
                        rlen = Boffset(bin) - off;
                }else
                        error("can't handle compression method %d for %s", zh.meth, zh.file);

                trailer(bin, &zh);

                if(wantFile(zh.file)){
                        if(verbose)
                                print("%-32s %10lud %s", zh.file, zh.uncsize, ctime(msdos2time(zh.modtime, zh.moddate)));
                        else
                                print("%s\n", zh.file);

                        if(verbose > 1){
                                print("\textract by os %d vers %d.%d\n", zh.extos, zh.extvers / 10, zh.extvers % 10);
                                print("\tflags %x\n", zh.flags);
                                print("\tmethod %d\n", zh.meth);
                                print("\tmod time %d\n", zh.modtime);
                                print("\tmod date %d\n", zh.moddate);
                                print("\tcrc %lux\n", zh.crc);
                                print("\tcompressed size %lud\n", zh.csize);
                                print("\tuncompressed size %lud\n", zh.uncsize);
                                if((zh.flags & ZTrailInfo) && (hcrc || hcsize || huncsize)){
                                        print("\theader crc %lux\n", zh.crc);
                                        print("\theader compressed size %lud\n", zh.csize);
                                        print("\theader uncompressed size %lud\n", zh.uncsize);
                                }
                        }
                }

                if(zh.crc != crc)
                        error("crc mismatch for %s", zh.file);
                if(zh.uncsize != wlen)
                        error("output size mismatch for %s", zh.file);
                if(zh.csize != rlen)
                        error("input size mismatch for %s", zh.file);


                free(zh.file);
                zh.file = nil;
        }
}

/*
 * extract files using the info in the central directory structure
 */
static int
unzip(Biobuf *bin, char *file)
{
        ZipHead zh;
        vlong off;
        int ok, eok, entries;

        entries = findCDir(bin, file);
        if(entries < 0)
                return 0;

        ok = 1;
        while(entries-- > 0){
                if(setjmp(zjmp)){
                        free(zh.file);
                        return 0;
                }
                memset(&zh, 0, sizeof(zh));
                if(!cheader(bin, &zh))
                        return ok;


                off = Boffset(bin);
                if(wantFile(zh.file)){
                        if(Bseek(bin, zh.off, 0) < 0){
                                fprint(2, "unzip: can't seek to start of %s, skipping\n", zh.file);
                                ok = 0;
                        }else{
                                eok = unzipEntry(bin, &zh);
                                if(eok <= 0){
                                        fprint(2, "unzip: skipping %s\n", zh.file);
                                        ok = 0;
                                }
                        }
                }

                free(zh.file);
                zh.file = nil;

                if(Bseek(bin, off, 0) < 0){
                        fprint(2, "unzip: can't seek to start of next entry, terminating extraction\n");
                        return 0;
                }
        }

        return ok;
}

/*
 * extract files using the info the "local file headers"
 */
static int
sunzip(Biobuf *bin)
{
        int eok;

        for(;;){
                eok = unzipEntry(bin, nil);
                if(eok == 0)
                        return 1;
                if(eok < 0)
                        return 0;
        }
}

static int mkdirs(char *);

/*
 * if any directories leading up to path don't exist, create them.
 * modifies but restores path.
 */
static int
mkpdirs(char *path)
{
        int rv = 0;
        char *sl = strrchr(path, '/');
print("%s\n", path);
        if (sl != nil) {
                *sl = '\0';
                rv = mkdirs(path);
                *sl = '/';
        }
        return rv;
}

/*
 * if path or any directories leading up to it don't exist, create them.
 * modifies but restores path.
 */
static int
mkdirs(char *path)
{
        int fd;

        if (access(path, AEXIST) >= 0)
                return 0;

        /* make presumed-missing intermediate directories */
        if (mkpdirs(path) < 0)
                return -1;

        /* make final directory */
        fd = create(path, OREAD, 0755|DMDIR);
        if (fd < 0)
                /*
                 * we may have lost a race; if the directory now exists,
                 * it's okay.
                 */
                return access(path, AEXIST) < 0? -1: 0;
        close(fd);
        return 0;
}


/*
 * extracts a single entry from a zip file
 * czh is the optional corresponding central directory entry
 */
static int
unzipEntry(Biobuf *bin, ZipHead *czh)
{
        Dir *d;
        ZipHead zh;
        char *p;
        vlong off;
        int fd, isdir, ok, err;

        zh.file = nil;
        if(setjmp(zjmp)){
                delfile = nil;
                free(zh.file);
                return -1;
        }

        memset(&zh, 0, sizeof(zh));
        if(!header(bin, &zh))
                return 0;

        ok = 1;
        isdir = 0;

        fd = -1;
        if(wantFile(zh.file)){
                if(verbose)
                        fprint(2, "extracting %s\n", zh.file);

                if(czh != nil && czh->extos == ZDos){
                        isdir = czh->eattr & ZDDir;
                        if(isdir && zh.uncsize != 0)
                                fprint(2, "unzip: ignoring directory data for %s\n", zh.file);
                }
                if(zh.meth == 0 && zh.uncsize == 0){
                        p = strchr(zh.file, '\0');
                        if(p > zh.file && p[-1] == '/')
                                isdir = 1;
                }

                if(stdout){
                        if(ok && !isdir)
                                fd = 1;
                }else if(isdir){
                        fd = create(zh.file, OREAD, DMDIR | 0775);
                        if(fd < 0){
                                d = dirstat(zh.file);
                                if(d == nil || (d->mode & DMDIR) != DMDIR){
                                        fprint(2, "unzip: can't create directory %s: %r\n", zh.file);
                                        ok = 0;
                                }
                                free(d);
                        }
                }else if(ok){
                        if(autodir)
                                mkpdirs(zh.file);
                        fd = create(zh.file, OWRITE, 0664);
                        if(fd < 0){
                                fprint(2, "unzip: can't create %s: %r\n", zh.file);
                                ok = 0;
                        }else
                                delfile = zh.file;
                }
        }

        wlen = 0;
        rlen = 0;
        crc = 0;
        wbad = 0;

        if(zh.meth == 0){
                if(!copyout(fd, bin, zh.csize))
                        error("copying data for %s failed: %r", zh.file);
        }else if(zh.meth == 8){
                off = Boffset(bin);
                err = inflate((void*)fd, crcwrite, bin, (int(*)(void*))Bgetc);
                if(err != FlateOk)
                        error("inflate failed: %s", flateerr(err));
                rlen = Boffset(bin) - off;
        }else
                error("can't handle compression method %d for %s", zh.meth, zh.file);

        trailer(bin, &zh);

        if(zh.crc != crc)
                error("crc mismatch for %s", zh.file);
        if(zh.uncsize != wlen)
                error("output size mismatch for %s", zh.file);
        if(zh.csize != rlen)
                error("input size mismatch for %s", zh.file);

        delfile = nil;
        free(zh.file);

        if(fd >= 0 && !stdout){
                if(settimes){
                        d = dirfstat(fd);
                        if(d != nil){
                                d->mtime = msdos2time(zh.modtime, zh.moddate);
                                if(d->mtime)
                                        dirfwstat(fd, d);
                        }
                }
                close(fd);
        }

        return ok;
}

static int
wantFile(char *file)
{
        int i, n;

        if(nwant == 0)
                return 1;
        for(i = 0; i < nwant; i++){
                if(strcmp(want[i], file) == 0)
                        return 1;
                n = strlen(want[i]);
                if(strncmp(want[i], file, n) == 0 && file[n] == '/')
                        return 1;
        }
        return 0;
}

/*
 * find the start of the central directory
 * returns the number of entries in the directory,
 * or -1 if there was an error
 */
static int
findCDir(Biobuf *bin, char *file)
{
        vlong ecoff;
        long off, size, m;
        int entries, zclen, dn, ds, de;

        ecoff = Bseek(bin, -ZECHeadSize, 2);
        if(ecoff < 0){
                fprint(2, "unzip: can't seek to contents of %s\n", file);
                longjmp(seekjmp, 1);
                return -1;
        }
        if(setjmp(zjmp))
                return -1;

        if((m=get4(bin)) != ZECHeader){
                fprint(2, "unzip: bad magic number for table of contents of %s: %#.8lx\n", file, m);
                longjmp(seekjmp, 1);
                return -1;
        }
        dn = get2(bin);
        ds = get2(bin);
        de = get2(bin);
        entries = get2(bin);
        size = get4(bin);
        off = get4(bin);
        zclen = get2(bin);
        while(zclen-- > 0)
                get1(bin);

        if(verbose > 1){
                print("table starts at %ld for %ld bytes\n", off, size);
                if(ecoff - size != off)
                        print("\ttable should start at %lld-%ld=%lld\n", ecoff, size, ecoff-size);
                if(dn || ds || de != entries)
                        print("\tcurrent disk=%d start disk=%d table entries on this disk=%d\n", dn, ds, de);
        }

        if(Bseek(bin, off, 0) != off){
                fprint(2, "unzip: can't seek to start of contents of %s\n", file);
                longjmp(seekjmp, 1);
                return -1;
        }

        return entries;
}

static int
cheader(Biobuf *bin, ZipHead *zh)
{
        ulong v;
        int flen, xlen, fclen;

        v = get4(bin);
        if(v != ZCHeader){
                if(v == ZECHeader)
                        return 0;
                error("bad magic number %lux", v);
        }
        zh->madevers = get1(bin);
        zh->madeos = get1(bin);
        zh->extvers = get1(bin);
        zh->extos = get1(bin);
        zh->flags = get2(bin);
        zh->meth = get2(bin);
        zh->modtime = get2(bin);
        zh->moddate = get2(bin);
        zh->crc = get4(bin);
        zh->csize = get4(bin);
        zh->uncsize = get4(bin);
        flen = get2(bin);
        xlen = get2(bin);
        fclen = get2(bin);
        get2(bin);              /* disk number start */
        zh->iattr = get2(bin);
        zh->eattr = get4(bin);
        zh->off = get4(bin);

        zh->file = getname(bin, flen);

        while(xlen-- > 0)
                get1(bin);

        while(fclen-- > 0)
                get1(bin);

        return 1;
}

static int
header(Biobuf *bin, ZipHead *zh)
{
        ulong v;
        int flen, xlen;

        v = get4(bin);
        if(v != ZHeader){
                if(v == ZCHeader)
                        return 0;
                error("bad magic number %lux at %lld", v, Boffset(bin)-4);
        }
        zh->extvers = get1(bin);
        zh->extos = get1(bin);
        zh->flags = get2(bin);
        zh->meth = get2(bin);
        zh->modtime = get2(bin);
        zh->moddate = get2(bin);
        zh->crc = get4(bin);
        zh->csize = get4(bin);
        zh->uncsize = get4(bin);
        flen = get2(bin);
        xlen = get2(bin);

        zh->file = getname(bin, flen);

        while(xlen-- > 0)
                get1(bin);

        return 1;
}

static void
trailer(Biobuf *bin, ZipHead *zh)
{
        if(zh->flags & ZTrailInfo){
                zh->crc = get4(bin);
                zh->csize = get4(bin);
                zh->uncsize = get4(bin);
        }
}

static char*
getname(Biobuf *bin, int len)
{
        char *s;
        int i, c;

        s = emalloc(len + 1);
        for(i = 0; i < len; i++){
                c = get1(bin);
                if(lower)
                        c = tolower(c);
                s[i] = c;
        }
        s[i] = '\0';
        return s;
}

static int
crcwrite(void *out, void *buf, int n)
{
        int fd, nw;

        wlen += n;
        crc = blockcrc(crctab, crc, buf, n);
        fd = (int)(uintptr)out;
        if(fd < 0)
                return n;
        nw = write(fd, buf, n);
        if(nw != n)
                wbad = 1;
        return nw;
}

static int
copyout(int ofd, Biobuf *bin, long len)
{
        char buf[BufSize];
        int n;

        for(; len > 0; len -= n){
                n = len;
                if(n > BufSize)
                        n = BufSize;
                n = Bread(bin, buf, n);
                if(n <= 0)
                        return 0;
                rlen += n;
                if(crcwrite((void*)ofd, buf, n) != n)
                        return 0;
        }
        return 1;
}

static ulong
get4(Biobuf *b)
{
        ulong v;
        int i, c;

        v = 0;
        for(i = 0; i < 4; i++){
                c = Bgetc(b);
                if(c < 0)
                        error("unexpected eof reading file information");
                v |= c << (i * 8);
        }
        return v;
}

static int
get2(Biobuf *b)
{
        int i, c, v;

        v = 0;
        for(i = 0; i < 2; i++){
                c = Bgetc(b);
                if(c < 0)
                        error("unexpected eof reading file information");
                v |= c << (i * 8);
        }
        return v;
}

static int
get1(Biobuf *b)
{
        int c;

        c = Bgetc(b);
        if(c < 0)
                error("unexpected eof reading file information");
        return c;
}

static long
msdos2time(int time, int date)
{
        Tm tm;

        tm.hour = time >> 11;
        tm.min = (time >> 5) & 63;
        tm.sec = (time & 31) << 1;
        tm.year = 80 + (date >> 9);
        tm.mon = ((date >> 5) & 15) - 1;
        tm.mday = date & 31;
        tm.zone[0] = '\0';
        tm.yday = 0;

        return tm2sec(&tm);
}

static void*
emalloc(ulong n)
{
        void *p;

        p = malloc(n);
        if(p == nil)
                sysfatal("out of memory");
        return p;
}

static void
error(char *fmt, ...)
{
        va_list arg;

        fprint(2, "unzip: ");
        va_start(arg, fmt);
        vfprint(2, fmt, arg);
        va_end(arg);
        fprint(2, "\n");

        if(delfile != nil){
                fprint(2, "unzip: removing output file %s\n", delfile);
                remove(delfile);
                delfile = nil;
        }

        longjmp(zjmp, 1);
}