#!/usr/bin/python2

"""mkshar

Generate a shar (with a zip file payload) for a Switch Light installer.
"""

import sys, os, shutil
import subprocess
import hashlib
import optparse

HELP = """\
%prog [OPTIONS] SHAR-OUTPUT SFX-FRAGMENT INSTALL-SCRIPT [ARG ...]
"""

parser = optparse.OptionParser(usage=HELP)
parser.add_option('--blocksize',
                  type=int, default=512,
                  help="Set block size for e.g. 'dd'")
parser.add_option('--bzip',
                  action='store_true',
                  help="Enable bzip2")
parser.add_option('--lazy',
                  action='store_true',
                  help="Enable lazy unpacking")
parser.add_option('--unzip-sfx',
                  action='store_true',
                  help="Use 'unzip' to natively handle SFX preamble")
parser.add_option('--unzip-pipe',
                  action='store_true',
                  help="Enable 'dd' to handle SFX preamble")
parser.add_option('--unzip-loop',
                  action='store_true',
                  help="Enable 'losetup' to handle SFX preamble")
parser.add_option('--unzip-pad',
                  action='store_true',
                  help="Special pad options for deficient unzip")
parser.add_option('--inplace',
                  action='store_true',
                  help="Perform fixups in-place")
parser.add_option('--fixup-perms',
                  type=str,
                  help="Post-unpack shell script to fix permissions")

opts, args = parser.parse_args()

ZIP = "/usr/bin/zip"
ZIP_OPTS = ['-v',
            '-y',
            '-n', '.zip:.loader:.swi:.jar:.gz:.bz:.bz2:.xz',]

# enable (experimental) bzip2, make sure that
# busybox unzip supports it
# Ha ha, using '-9' not only cranks up the compression,
# but *ignores* the suffix list for 'store-only'.
if opts.bzip:
    ZIP_COMPRESS_OPTS = ['-Z', 'bzip2', '-8',]
else:
    ZIP_COMPRESS_OPTS = ['-8',]

shar = args.pop(0)
sfx = args.pop(0)
install = args.pop(0)

import logging
logging.basicConfig()
logger = logging.getLogger("mkshar")
logger.setLevel(logging.DEBUG)

logger.info("initializing ZIP")
zipFile = os.path.abspath(shar + ".zip.in")
if os.path.exists(zipFile):
    logger.debug("+ /bin/rm %s", zipFile)
    os.unlink(zipFile)

def _addfile(path, *opts):
    logger.info("adding file %s with opts %s",
                path, " ".join(opts))
    cmd = [ZIP,] + ZIP_OPTS + list(opts) + ['-u', zipFile, path,]
    subprocess.check_call(cmd)

def _addfiles(paths, *opts):
    logger.info("adding %d files with opts %s",
                len(paths), " ".join(opts))
    cmd = [ZIP,] + ZIP_OPTS + list(opts) + [zipFile,] + paths
    subprocess.check_call(cmd)

def _zipopts(path):
    ext = os.path.splitext(path)[1].lower()
    if ext in ('.loader', '.zip', '.swi',):
        return ['-0',]
    else:
        return ZIP_COMPRESS_OPTS

# block to 512 bytes
buf = open(sfx).read()
sz = len(buf)
logger.info("script size is %d bytes", sz)
rem = sz % opts.blocksize
pad = opts.blocksize - rem

# extra block for house keeping
if opts.unzip_pad:
    pad = pad + opts.blocksize

if pad == 1:
    buf += "\n"
elif pad > 1:
    pad -= 1
    buf = buf + ('#' * pad) + "\n"

logger.info("padded script size is %d bytes", len(buf))

if opts.unzip_pad:
    logger.info("adding SFX pad to beginning as archive item")
    fd = open("pad.bin", "w")
    fd.write(('#' * (len(buf)-1)))
    fd.write('\n')
    fd.close()
    _addfile("pad.bin", '-0', '-j')
    os.unlink("pad.bin")

_addfile(install, '-0')

if opts.fixup_perms:
    _addfile(opts.fixup_perms, '-0')

logger.info("adding other payload items")
o = ['-r',] + ZIP_COMPRESS_OPTS
_addfiles(args, *o)

def _splice(tag, val):
    global buf
    pat = tag + '='
    p = buf.find(pat)
    q = buf.find("\n", p+1)
    if p < 0:
        raise ValueError("cannot find tag %s" % repr(tag))
    llen = q - p
    line = "%s=%s #" % (tag, str(val),)
    if len(line) > llen:
        raise SystemExit("cannot insert byte count marker")
    llen -= len(line)
    line = line + ('#' * llen)
    buf = buf[:p] + line + buf[q:]

def _spliceMaybe(tag, val):
    val = "${%s-\"%s\"}" % (tag, val,)
    _splice(tag, val)

logger.info("prepping SFX")

_splice('SFX_BYTES', len(buf))
_splice('SFX_BLOCKSIZE', opts.blocksize)
_splice('SFX_INSTALL', os.path.split(install)[1])
_splice('SFX_CHECKSUM', '')
if opts.lazy:
    _splice('SFX_LAZY', '1')
else:
    _splice('SFX_LAZY', '')
if opts.unzip_sfx:
    _spliceMaybe('SFX_UNZIP', '1')
else:
    _spliceMaybe('SFX_UNZIP', '')
if opts.unzip_pipe:
    _spliceMaybe('SFX_PIPE', '1')
else:
    _spliceMaybe('SFX_PIPE', '')
if opts.unzip_loop:
    _spliceMaybe('SFX_LOOP', '1')
else:
    _spliceMaybe('SFX_LOOP', '')
if opts.unzip_pad:
    _splice('SFX_PAD', 'pad.bin')
else:
    _splice('SFX_PAD', '')
if opts.fixup_perms:
    _splice('SFX_PERMS', opts.fixup_perms)
if opts.inplace:
    _spliceMaybe('SFX_INPLACE', '1')
else:
    _spliceMaybe('SFX_INPLACE', '')

# remember the checksum offset
ckStart = buf.find("SFX_CHECKSUM=")
ckEnd = buf.find("\n", ckStart+1)

logger.info("generating shar")

if opts.unzip_pad:

    wfd = open(shar, "w+")
    rfd = open(zipFile, "r")
    shutil.copyfileobj(rfd, wfd)
    rfd.close()

    logger.info("capturing first block")
    wfd.seek(0, 0)
    magic = wfd.read(opts.blocksize)

    logger.info("splicing in SFX")
    wfd.seek(0, 0)
    wfd.write(buf)

    logger.info("saving first block")
    wfd.seek(len(buf)-opts.blocksize, 0)
    wfd.write(magic)

    wfd.close()

    # adjust buf to include magic
    buf = buf[:-opts.blocksize] + magic

else:

    wfd = open(shar, "w")
    wfd.write(buf)
    rfd = open(zipFile, "r")
    shutil.copyfileobj(rfd, wfd)
    rfd.close()
    wfd.close()

    logger.info("adjusting SFX")
    cmd = [ZIP,] + ZIP_OPTS + ['-A', shar,]
    subprocess.check_call(cmd)

# compute the actual checksum
ckf = hashlib.md5()
ckf.update(buf[:ckStart])
ckf.update(buf[ckEnd+1:])

rfd = open(shar, "r")
rfd.seek(len(buf), 0)
while True:
    buf = rfd.read(10*1024)
    if not buf: break
    ckf.update(buf)
rfd.close()

logger.info("adjusting checksum")
wfd = open(shar, "r+")
wfd.seek(ckStart, 0)
wfd.write("SFX_CHECKSUM=%s #" % ckf.hexdigest())
wfd.close()

logger.info("+ /bin/chmod +x %s", shar)
os.chmod(shar, 0755)

logger.debug("+ /bin/rm %s", zipFile)
os.unlink(zipFile)
