#!/usr/bin/env python3
"""mkcab: make a Microsoft cabinet, the format Windows 95 was installed from.

    mkcab OUT.CAB file1 file2 ...

One folder, MSZIP compression, every file stored under its own base name. The
format is Microsoft's own, documented in [MS-CAB], so what this writes opens
in cabextract, in 7-Zip, and in Windows itself - and this system reads it with
cab.c, over the same inflate that decodes PNGs.

MSZIP is DEFLATE in blocks of at most 32 KiB of output, each introduced by the
two letters "CK". A block may refer back into the one before it; these do
not - each is compressed on its own and finished - which costs a percent or
two of size and means a reader can decode any block without its predecessor.
Readers that keep the history, as Microsoft's does, read them just the same.
"""
import os, struct, sys, time, zlib

BLOCK = 32768


def dos_date_time(seconds):
    t = time.localtime(seconds)
    date = ((t.tm_year - 1980) << 9) | (t.tm_mon << 5) | t.tm_mday
    clock = (t.tm_hour << 11) | (t.tm_min << 5) | (t.tm_sec // 2)
    return date & 0xFFFF, clock & 0xFFFF


def main():
    if len(sys.argv) < 3:
        sys.exit(__doc__)
    out, paths = sys.argv[1], sys.argv[2:]

    names = [os.path.basename(p).upper() for p in paths]
    if len(set(names)) != len(names):
        sys.exit("mkcab: two files would have the same name in the cabinet")

    data = b"".join(open(p, "rb").read() for p in paths)
    blocks = []
    for at in range(0, len(data), BLOCK):
        chunk = data[at:at + BLOCK]
        packer = zlib.compressobj(9, zlib.DEFLATED, -15)
        packed = b"CK" + packer.compress(chunk) + packer.flush(zlib.Z_FINISH)
        blocks.append((packed, len(chunk)))
    if not blocks:
        blocks.append((b"CK\x03\x00", 0))

    header_size = 36
    folder_size = 8
    files = b""
    offset = 0
    for path, name in zip(paths, names):
        size = os.path.getsize(path)
        # SOURCE_DATE_EPOCH, when it is set, rather than when the file was
        # last written: a build writes every file whether or not it changed,
        # and a cabinet that differs only in its dates is one that dosget
        # downloads again for nothing.
        stamp = os.environ.get("SOURCE_DATE_EPOCH")
        date, clock = dos_date_time(int(stamp) if stamp else os.path.getmtime(path))
        files += struct.pack("<IIHHHH", size, offset, 0, date, clock, 0x20)
        files += name.encode("ascii") + b"\0"
        offset += size
    data_start = header_size + folder_size + len(files)
    data_blocks = b"".join(struct.pack("<IHH", 0, len(p), n) + p for p, n in blocks)
    total = data_start + len(data_blocks)

    header = struct.pack("<4sIIIIIBBHHHHH", b"MSCF", 0, total, 0,
                         header_size + folder_size, 0, 3, 1, 1, len(paths),
                         0, 0x4B4F, 0)
    folder = struct.pack("<IHH", data_start, len(blocks), 1)   # 1 = MSZIP
    with open(out, "wb") as target:
        target.write(header + folder + files + data_blocks)
    print("%s: %d files, %d bytes -> %d" % (out, len(paths), len(data), total))


if __name__ == "__main__":
    main()
