structures.js 6.72 KB
'use strict';
var binary = require('binary');

var convertDateTime = function (dosDate, dosTime) {
    var year = ((dosDate >> 9) & 0x7F) + 1980;
    var month = (dosDate >> 5) & 0x0F;
    var day = dosDate & 0x1F;

    var hour = (dosTime >> 11);
    var minute = (dosTime >> 5) & 0x3F;
    var second = (dosTime & 0x1F) * 2;

    var result = new Date(year, month - 1, day, hour, minute, second, 0);

    return result;
};

var convertGeneralPurposeFlags = function (value) {
    var bits = [];

    for (var i = 0; i < 16; i++) {
        bits[i] = (value >> i) & 1;
    }

    return {
        encrypted: !!bits[0],
        compressionFlag1: !!bits[1],
        compressionFlag2: !!bits[2],
        useDataDescriptor: !!bits[3],
        enhancedDeflating: !!bits[4],
        compressedPatched: !!bits[5],
        strongEncryption: !!bits[6],
        utf8: !!bits[11],
        encryptedCD: !!bits[13]
    };
};

var parseExternalFileAttributes = function (externalAttributes, platform) {
    var types = {
        // In theory, any of these could be set. Realistically, though, it will
        // be regular, directory or symlink
        1: 'NamedPipe',
        2: 'Character',
        4: 'Directory',
        6: 'Block',
        8: 'File',
        10: 'SymbolicLink',
        12: 'Socket'
    };

    switch (platform) {

    case 3: // Unix
        return {
            platform: 'Unix',
            type: types[(externalAttributes >> 28) & 0x0F],
            mode: (externalAttributes >> 16) & 0xFFF
        };

    // case 0: // MSDOS
    default:
        if (platform !== 0) {
            console.warn('Possibly unsupported ZIP platform type, ' + platform);
        }

        var attribs = {
            A: (externalAttributes >> 5) & 0x01,
            D: (externalAttributes >> 4) & 0x01,
            V: (externalAttributes >> 3) & 0x01,
            S: (externalAttributes >> 2) & 0x01,
            H: (externalAttributes >> 1) & 0x01,
            R: externalAttributes & 0x01
        };

        // With no better guidance we'll make the default permissions ugo+r
        var mode = parseInt('0444', 8);

        if (attribs.D) {
            mode |= parseInt('0111', 8); // Set the execute bit
        }

        if (!attribs.R) {
            mode |= parseInt('0222', 8); // Set the write bit
        }

        mode &= ~process.umask();

        return {
            platform: 'DOS',
            type: attribs.D ? 'Directory' : 'File',
            mode: mode
        };
    }
};

var readEndRecord = function (buffer) {
    var data = binary.parse(buffer)
    .word32lu('signature')
    .word16lu('diskNumber')
    .word16lu('directoryStartDisk')
    .word16lu('directoryEntryCountDisk')
    .word16lu('directoryEntryCount')
    .word32lu('directorySize')
    .word32lu('directoryOffset')
    .word16lu('commentLength')
    .buffer('comment', 'commentLength')
    .vars;

    data.comment = data.comment.toString();

    return data;
};

var directorySort = function (a, b) {
    return a.relativeOffsetOfLocalHeader - b.relativeOffsetOfLocalHeader;
};

var readDirectory = function (buffer) {
    var directory = [];
    var current;
    var index = 0;

    while (index < buffer.length) {
        current = binary.parse(buffer.slice(index, index + 46))
        .word32lu('signature')
        .word8lu('creatorSpecVersion')
        .word8lu('creatorPlatform')
        .word8lu('requiredSpecVersion')
        .word8lu('requiredPlatform')
        .word16lu('generalPurposeBitFlag')
        .word16lu('compressionMethod')
        .word16lu('lastModFileTime')
        .word16lu('lastModFileDate')
        .word32lu('crc32')
        .word32lu('compressedSize')
        .word32lu('uncompressedSize')
        .word16lu('fileNameLength')
        .word16lu('extraFieldLength')
        .word16lu('fileCommentLength')
        .word16lu('diskNumberStart')
        .word16lu('internalFileAttributes')
        .word32lu('externalFileAttributes')
        .word32lu('relativeOffsetOfLocalHeader')
        .vars;

        index += 46;

        current.generalPurposeFlags = convertGeneralPurposeFlags(current.generalPurposeBitFlag);
        current.fileAttributes = parseExternalFileAttributes(current.externalFileAttributes, current.creatorPlatform);

        current.modifiedTime = convertDateTime(current.lastModFileDate, current.lastModFileTime);
        current.fileName = current.extraField = current.fileComment = '';
        current.headerLength = 46 + current.fileNameLength + current.extraFieldLength + current.fileCommentLength;

        if (current.fileNameLength > 0) {
            current.fileName = buffer.slice(index, index + current.fileNameLength).toString();
            index += current.fileNameLength;
        }

        if (current.extraFieldLength > 0) {
            current.extraField = buffer.slice(index, index + current.extraFieldLength).toString();
            index += current.extraFieldLength;
        }

        if (current.fileCommentLength > 0) {
            current.fileComment = buffer.slice(index, index + current.fileCommentLength).toString();
            index += current.fileCommentLength;
        }

        if (current.fileAttributes.type !== 'Directory' && current.fileName.substr(-1) === '/') {
            // TODO: check that this is a reasonable check
            current.fileAttributes.type = 'Directory';
        }

        directory.push(current);
    }

    directory.sort(directorySort);

    return directory;
};

var readFileEntry = function (buffer) {
    var index = 0;

    var fileEntry = binary.parse(buffer.slice(index, 30))
    .word32lu('signature')
    .word16lu('versionNeededToExtract')
    .word16lu('generalPurposeBitFlag')
    .word16lu('compressionMethod')
    .word16lu('lastModFileTime')
    .word16lu('lastModFileDate')
    .word32lu('crc32')
    .word32lu('compressedSize')
    .word32lu('uncompressedSize')
    .word16lu('fileNameLength')
    .word16lu('extraFieldLength')
    .vars;

    index += 30;

    fileEntry.fileName = fileEntry.extraField = '';

    fileEntry.entryLength = 30 + fileEntry.fileNameLength + fileEntry.extraFieldLength;

    if (fileEntry.entryLength > structures.maxFileEntrySize) {
        throw new Error('File entry unexpectedly large: ' + fileEntry.entryLength + ' (max: ' + structures.maxFileEntrySize + ')');
    }

    if (fileEntry.fileNameLength > 0) {
        fileEntry.fileName = buffer.slice(index, index + fileEntry.fileNameLength).toString();
        index += fileEntry.fileNameLength;
    }

    if (fileEntry.extraFieldLength > 0) {
        fileEntry.extraField = buffer.slice(index, index + fileEntry.extraFieldLength).toString();
        index += fileEntry.extraFieldLength;
    }

    return fileEntry;
};

var structures = module.exports = {
    readEndRecord: readEndRecord,
    readDirectory: readDirectory,
    readFileEntry: readFileEntry,
    maxFileEntrySize: 4096
};