CFHeader.java

/*
 * Copyright 2019 Emmanuel Bourg
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.jsign.mscab;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SeekableByteChannel;
import java.security.MessageDigest;

import static net.jsign.ChannelUtils.*;

/**
 * Cabinet File Header structure (CFHEADER):
 *
 * <pre>
 * signature                           4 bytes  (0x4643534d: 'MSCF')
 * reserved1 (former header checksum)  4 bytes
 * size of the cabinet file            4 bytes
 * reserved2 (former folders checksum) 4 bytes
 * offset  of the first CFFILE entry   4 bytes
 * reserved3 (former files checksum)   4 bytes
 * minor format version                1 byte
 * major format version                1 byte
 * number of CFFOLDER entries          2 bytes
 * number of CFFILE entries            2 bytes
 * flags                               2 bytes
 * set identifier                      2 bytes
 * cabinet sequential number           2 bytes
 * size of per-cabinet reserve         2 bytes  (optional)
 * size of per-folder reserve          1 byte   (optional)
 * size of per-datablock reserve       1 byte   (optional)
 * reserve                             (variable size, optional)
 * file name of the previous cabinet   (variable size, optional)
 * media with the previous cabinet     (variable size, optional)
 * file name of the next cabinet       (variable size, optional)
 * media with the next cabinet         (variable size, optional)
 * </pre>
 *
 * @since 4.0
 */
class CFHeader {

    public static final int SIGNATURE = 0x4643534d; // MSCF

    public long csumHeader;     // u4
    public long cbCabinet;      // u4
    public long csumFolders;    // u4
    public long coffFiles;      // u4
    public long csumFiles;      // u4
    public byte versionMinor;   // u1
    public byte versionMajor;   // u1
    public int cFolders;        // u2
    public int cFiles;          // u2
    public int flags;           // u2
    public int setID;           // u2
    public int iCabinet;        // u2
    public int cbCFHeader;      // u2
    public short cbCFFolder;    // u1
    public short cbCFData;      // u1
    public CFReserve reserve;
    public byte[] szCabinetPrev;
    public byte[] szDiskPrev;
    public byte[] szCabinetNext;
    public byte[] szDiskNext;

    /**
     * FLAG_PREV_CABINET is set if this cabinet file is not the first in a set
     * of cabinet files. When this bit is set, the szCabinetPrev and szDiskPrev
     * fields are present in this CFHEADER.
     */
    public static final int FLAG_PREV_CABINET    = 0b00000001;

    /**
     * FLAG_NEXT_CABINET is set if this cabinet file is not the last in a set
     * of cabinet files. When this bit is set, the szCabinetNext and szDiskNext
     * fields are present in this CFHEADER.
     */
    public static final int FLAG_NEXT_CABINET    = 0b00000010;

    /**
     * FLAG_RESERVE_PRESENT is set if this cabinet file contains any reserved
     * fields. When this bit is set, the cbCFHeader, cbCFFolder, and cbCFData
     * fields are present in this CFHEADER.
     */
    public static final int FLAG_RESERVE_PRESENT = 0b00000100;

    /** Base size of the header (with no optional fields) */
    public static final int BASE_SIZE = 36;

    public CFHeader() {
    }

    public CFHeader(CFHeader header) {
        this.csumHeader = header.csumHeader;
        this.cbCabinet = header.cbCabinet;
        this.csumFolders = header.csumFolders;
        this.coffFiles = header.coffFiles;
        this.csumFiles = header.csumFiles;
        this.versionMinor = header.versionMinor;
        this.versionMajor = header.versionMajor;
        this.cFolders = header.cFolders;
        this.cFiles = header.cFiles;
        this.flags = header.flags;
        this.setID = header.setID;
        this.iCabinet = header.iCabinet;
        this.cbCFHeader = header.cbCFHeader;
        this.cbCFFolder = header.cbCFFolder;
        this.cbCFData = header.cbCFData;
        this.reserve = header.reserve != null ? new CFReserve(header.reserve) : null;
        this.szCabinetPrev = header.szCabinetPrev;
        this.szDiskPrev = header.szDiskPrev;
        this.szCabinetNext = header.szCabinetNext;
        this.szDiskNext = header.szDiskNext;
    }

    public void read(SeekableByteChannel channel) throws IOException {
        if ((channel.size()) < BASE_SIZE + CFFolder.BASE_SIZE) {
            throw new IOException("MSCabinet file too short");
        }
        ByteBuffer buffer = ByteBuffer.allocate(BASE_SIZE).order(ByteOrder.LITTLE_ENDIAN);
        channel.read(buffer);
        buffer.flip();

        int signature = buffer.getInt();
        if (signature != SIGNATURE) {
            throw new IOException("Invalid MSCabinet header signature " + String.format("0x%04x", signature & 0xFFFFFFFFL));
        }

        this.csumHeader = buffer.getInt() & 0xFFFFFFFFL;  // u4
        this.cbCabinet = buffer.getInt() & 0xFFFFFFFFL;   // u4 H
        this.csumFolders = buffer.getInt() & 0xFFFFFFFFL; // u4 H
        this.coffFiles = buffer.getInt() & 0xFFFFFFFFL;   // u4 H
        this.csumFiles = buffer.getInt() & 0xFFFFFFFFL;   // u4 H
        this.versionMinor = buffer.get();                 // u1 H
        this.versionMajor = buffer.get();                 // u1 H
        this.cFolders = buffer.getShort() & 0xFFFF;       // u2 H
        this.cFiles = buffer.getShort() & 0xFFFF;         // u2 H
        this.flags = buffer.getShort() & 0xFFFF;          // u2 H
        this.setID = buffer.getShort();                   // u2 H
        this.iCabinet = buffer.getShort() & 0xFFFF;       // u2
        this.reserve = null;

        if (isReservePresent()) {
            buffer.clear();
            buffer.limit(4);
            channel.read(buffer);
            buffer.flip();

            this.cbCFHeader = buffer.getShort() & 0xFFFF;    // u2
            this.cbCFFolder = (short) (buffer.get() & 0xFF); // u1
            this.cbCFData = (short) (buffer.get() & 0xFF);   // u1
            if (this.cbCFHeader > 0) {
                byte[] abReserve = new byte[this.cbCFHeader];
                channel.read(ByteBuffer.wrap(abReserve));
                reserve = new CFReserve();
                reserve.read(abReserve);
            }
        }

        if (hasPreviousCabinet()) {
            szCabinetPrev = readNullTerminatedString(channel);
            szDiskPrev = readNullTerminatedString(channel);
        }

        if (hasNextCabinet()) {
            szCabinetNext = readNullTerminatedString(channel);
            szDiskNext = readNullTerminatedString(channel);
        }
    }

    public void write(SeekableByteChannel channel) throws IOException {
        channel.position(0);
        ByteBuffer buffer = ByteBuffer.allocate(getHeaderSize()).order(ByteOrder.LITTLE_ENDIAN);
        write(buffer);
        buffer.flip();
        channel.write(buffer);
    }

    public void write(ByteBuffer buffer) {
        buffer.putInt(SIGNATURE);
        buffer.putInt((int) this.csumHeader);
        buffer.putInt((int) this.cbCabinet);
        buffer.putInt((int) this.csumFolders);
        buffer.putInt((int) this.coffFiles);
        buffer.putInt((int) this.csumFiles);
        buffer.put(this.versionMinor);
        buffer.put(this.versionMajor);
        buffer.putShort((short) this.cFolders);
        buffer.putShort((short) this.cFiles);
        buffer.putShort((short) this.flags);
        buffer.putShort((short) this.setID);
        buffer.putShort((short) this.iCabinet);
        if (isReservePresent()) {
            buffer.putShort((short) this.cbCFHeader);
            buffer.put((byte) this.cbCFFolder);
            buffer.put((byte) this.cbCFData);
            if (this.cbCFHeader > 0) {
                buffer.put(reserve.toBuffer());
            }
        }
        if (hasPreviousCabinet()) {
            buffer.put(szCabinetPrev);
            buffer.put(szDiskPrev);
        }
        if (hasNextCabinet()) {
            buffer.put(szCabinetNext);
            buffer.put(szDiskNext);
        }
    }

    public int getHeaderSize() {
        int size = BASE_SIZE;
        if (isReservePresent()) {
            size += 4 + this.cbCFHeader;
        }
        if (hasPreviousCabinet()) {
            size += szCabinetPrev.length;
            size += szDiskPrev.length;
        }
        if (hasNextCabinet()) {
            size += szCabinetNext.length;
            size += szDiskNext.length;
        }
        return size;
    }

    public void headerDigestUpdate(MessageDigest digest) {
        ByteBuffer buffer = ByteBuffer.allocate(BASE_SIZE).order(ByteOrder.LITTLE_ENDIAN);

        buffer.putInt(SIGNATURE);
        // the checksum of the header is skipped
        buffer.putInt((int) this.cbCabinet);
        buffer.putInt((int) this.csumFolders);
        buffer.putInt((int) this.coffFiles);
        buffer.putInt((int) this.csumFiles);
        buffer.put(this.versionMinor);
        buffer.put(this.versionMajor);
        buffer.putShort((short) this.cFolders);
        buffer.putShort((short) this.cFiles);
        buffer.putShort((short) this.flags);
        buffer.putShort((short) this.setID);
        buffer.putShort((short) this.iCabinet);

        buffer.flip();
        digest.update(buffer);

        if (reserve != null && !reserve.isEmpty()) {
            reserve.digest(digest);
        }

        if (hasPreviousCabinet()) {
            digest.update(szCabinetPrev);
            digest.update(szDiskPrev);
        }

        if (hasNextCabinet()) {
            digest.update(szCabinetNext);
            digest.update(szDiskNext);
        }
    }

    public boolean hasPreviousCabinet() {
        return (FLAG_PREV_CABINET & flags) != 0;
    }

    public boolean hasNextCabinet() {
        return (FLAG_NEXT_CABINET & flags) != 0;
    }

    public boolean isReservePresent() {
        return (FLAG_RESERVE_PRESENT & flags) != 0;
    }

    public boolean hasSignature() {
        return this.reserve != null && this.reserve.structure2.length >= CABSignature.SIZE;
    }

    public void setReserve(CFReserve reserve) {
        int previousSize = getHeaderSize();

        this.reserve = reserve;
        this.cbCFHeader = reserve != null ? reserve.size() : 0;

        // update the reserve flag
        if (cbCFHeader != 0 || cbCFFolder != 0 || cbCFData != 0) {
            this.flags |= FLAG_RESERVE_PRESENT;
        } else {
            this.flags &= ~FLAG_RESERVE_PRESENT;
        }

        // adjust the size of the cabinet and the offset of the first file
        int currentSize = getHeaderSize();
        int shift = currentSize - previousSize;
        this.cbCabinet += shift;
        this.coffFiles += shift;
    }
}