#!/usr/bin/python
##
## Simple wrapper around sha1sum
##
## - When creating a new manifest, it scans
##   directories recursively so the resulting
##   manifest describes the whole tree.
##
## - When creating a manifest, it will skip
##   symlinks.
##
## - When checking, it complains about any stray
##   files, though it accepts exceptions.
##
## - Tries hard to be portable and self-contained
##   so casual users on OSX or Windows can use it.
##
## - It still uses sha1sum - installed by default
##   on OSX and modern Linuxen. Windows users
##   can find precompiled versions googling for
##   sha1sum.exe - please use one you can trust.
##
## - checksums are racy - put the data in a tmpdir
##   you control first
##
## Author: Martin Langhoff <martin@laptop.org>
## Copyright: One Laptop per Child
## License: GPLv2
##
##
##
##
##
import sys, os, os.path, subprocess
from optparse import OptionParser

from gpgwrapper import check_sig, SignatureError

# In this brave new world, with
# different platforms, modes and ACLs
# the proof of readability is in the
# pudding.
#
# Where's my trusty '-e' test?
#
def file_readable(fpath):

    # catch all errors as
    # no, not readable
    try:
        assert os.path.exists(fpath)
        fh = os.open(fpath,os.O_RDONLY)
        os.close(fh)
    except:
        return False

    return True



def gen_checksums(options):
    """Recursively generate sha1 checksums for a directory's contents,
    with output to stdout.  The output file names are relative to the
    given directory, which is found in options.directory.

    (Like `sha1sum -r $DIR` if it existed)."""
    # canonicalise and chdir...
    options.directory = os.path.realpath(options.directory)
    os.chdir(options.directory)
    filepaths = []
    for root, dirs, files in os.walk(options.directory):
        for fname in files:
            fpath = os.path.join(root, fname)
            if ( os.path.isfile(fpath)
                 and not os.path.islink(fpath)
                 and file_readable(fpath) ):
                # strip the leading path
                fpath = fpath[len(options.directory)+1:]
                filepaths.append(fpath)

    # sys.stdout.write("\n".join(filepaths))

    # Each platform has a different limit WRT max args
    # we can use - so keep it conservative
    fplen = len(filepaths)
    maxargs=100
    for n in range(0,fplen, maxargs+1 ):
        cmd = ['sha1sum']
        cmd.extend( filepaths[n:n+maxargs] )
        try:
            subprocess.check_call(cmd)
        except OSError:
            sys.stderr.write("Error: sha1sum is not installed or is not in the path\n");
            sys.exit(1)
        except subprocess.CalledProcessError, e:
            print >> sys.stderr, "sha1sum returned an error (%s)" % e
            sys.exit(1)

def check(options):
    """Check that all the files in a directory match the sha1
    signatures listed in a manifest file.

    options.directory: the directory to check
    options.checkf:    the manifest file
    options.verbose:   chattiness switch
    """
    options.directory = os.path.realpath(options.directory)
    os.chdir(options.directory)

    #check_sig will raise SignatureError if the signature is bad.
    try:
        ok = check_sig(options.checkf)
        if ok is None and options.strict:
            print >> sys.stderr, "Error: security is turned off, and we're in strict mode."
            sys.exit(1)
    except SignatureError, e:
        print >> sys.stderr, "Error: either the manifest, an installed key, or the signature is bad."
        print >> sys.stderr, e
        sys.exit(1)

    # check for stray files
    manifestf = file(options.checkf, 'r')
    knownpaths = []
    for fpath in manifestf:
        #sys.stdout.write(fpath)
        fpath = fpath[42:]
        fpath = fpath.strip()
        knownpaths.append(fpath)

    strayfiles=False
    for root, dirs, files in os.walk(options.directory):
        for fname in files:
            fpath = os.path.join(root, fname)
            fpath = fpath[len(options.directory)+1:]
            if fpath not in knownpaths:
                strayfiles=True
                sys.stderr.write("File %s is not in the manifest\n" % fpath)

    if strayfiles:
        sys.exit(1)

    # run the sha1sum check
    try:
        # sha1sum doesn't have interesting error codes
        # unfortunately -
        cmd = ['sha1sum', '-c', options.checkf]
        if not options.verbose:
            cmd.append('--status')
        subprocess.check_call(cmd)
    except OSError:
        sys.stderr.write("Error: sha1sum is not installed or is not in the path\n");
        sys.exit(1)
    except subprocess.CalledProcessError:
        sys.stderr.write("Error: sha1sum reported errors matching the files and manifest.\n")
        sys.exit(1)





def main():
    parser = OptionParser(usage='%prog [-c manifest.sha1] [-d directory ]')
    parser.add_option('-c', '--check', dest='checkf', help='manifest file to check against')
    parser.add_option('-d', '--directory', dest='directory', default=os.getcwd(), help='directory to check (default: .)')
    parser.add_option('-v', '--verbose',   dest='verbose',   action='store_true', help='be more verbose')
    parser.add_option('-S', '--strict',   dest='strict',   action='store_true', help='fail if a signature check is attempted in insecure mode')

    (options, Null) = parser.parse_args()

    if (options.checkf):
        options.checkf = os.path.realpath(options.checkf)
        check(options)
    else:
        gen_checksums(options)



if __name__ == '__main__': main ()
