VPY Annotations

Below is asciilifeform's original prototype V with my annotations.

#!/usr/bin/python

##############################################################################
# Quick Intro:
# 1) Create '.wot' in your home directory. Fill it with public keys from 'wot'.
# 2) Create '.seals' in your home directory. Place all signatures there from 'sigs'.
# 3) Create a 'patches' directory somewhere where 'v' can find it. Or use this one.
# 4) ./v.py patches command
# e.g.,
# ./v.py patches w
#                  ^^ displays WoT
#  ./v.py patches p patches/asciilifeform_add_verifyall_option.vpatch asciis_bleedingedge
#                  ^^ this 'presses' (creates the actual tree)
#                  ^^ approximately like a 'checkout' in your vanilla flavoured shithub.

##############################################################################

import os, sys, shutil, argparse, re, tempfile, gnupg

##############################################################################
vver = 1001 # This program's Kelvin version.

## HOW YOU CAN HELP: ##

# * TESTS plox, ty!
#
# Report findings in #bitcoin-assets on Freenode.

##############################################################################

prolog =  '''\
(C) 2015 NoSuchlAbs.
You do not have, nor can you ever acquire the right to use, copy or distribute
this software ; Should you use this software for any purpose, or copy and
distribute it to anyone or in any manner, you are breaking the laws of whatever
soi-disant jurisdiction, and you promise to continue doing so for the indefinite
future. In any case, please always : read and understand any software ;
verify any PGP signatures that you use - for any purpose.2
'''

intro = "V (ver. {0}K)\n".format(vver)

##############################################################################
def toposort3(unsorted):
    sorted = []
    unsorted = dict(unsorted)
    while unsorted:
        acyclic = False
        for node, edges in unsorted.items():
            for edge in edges:
                if edge in unsorted:
                    break
            else:
                acyclic = True
                del unsorted[node]
                sorted.append((node, edges))
        if not acyclic:
            fatal("Cyclic graph!")
    return sorted
##############################################################################

verbose = False

def fatal4(msg):
    sys.stderr.write(msg + "\n")
    exit(1)

def spew5(msg):
    if verbose:
        print msg

# List of files in a directory, in lexical order.
def dir_files6(dir):
    return sorted([os.path.join(dir, fn) for fn in next(os.walk(dir))[2]])

# GPG is retarded and insists on 'keychain.'
# This will be a temp dir, because we don't do any crypto.
gpgtmp = tempfile.mkdtemp()
gpg = gnupg.GPG(gnupghome=gpgtmp)
gpg.encoding = 'utf-8'7

# Known WoT public keys.
pubkeys = {}

# The subset of vpatches that are considered valid.
patches = []

# Banners (i.e. vpatches mapped to their guarantors)
banners = {}

# Roots (i.e. vpatches parented by thin air)
roots = []

# Table mapping file hash to originating vpatch
desc = {}
desc['false' ] = 'false'

# Grep for diff magics, and memoize
def vpdata8(path, exp, cache):
    l = cache.get(path)
    if not l:
        l = []
        patch = open(path, 'r').read()
        for m in re.findall(exp, patch, re.MULTILINE):
            l += [{'p':m[0], 'h':m[1]}]
        cache[path] = l
    return l

# Get parents of a vpatch
pcache = {}
def parents9(vpatch):
    parents = vpdata(vpatch, r'^--- (\S+) (\S+)$', pcache)
    if not parents:
        fatal("{0} is INVALID, check whether it IS a vpatch!".format(vpatch))
    return parents

# Get children of a vpatch
ccache = {}
def children10(vpatch):
    children = vpdata(vpatch, r'^\+\+\+ (\S+) (\S+)$', ccache)
    if not children:
        fatal("{0} is INVALID, check whether it IS a vpatch!".format(vpatch))
    # Record descendents:
    for child in children:
        h = child['h']
        if h != 'false':
            desc[h] = vpatch
    return children

# It is entirely possible to have more than one root!
# ... exactly how, is left as an exercise for readers.
def find_roots11(patchset):
    rset = []
    # Walk, find roots
    for p in patchset:
        if all(p['h'] == 'false' for p in parents(p)):
            rset += [p]
            spew("Found a Root: '{0}'".format(p))
    return rset

# Get antecedents.
def get_ante12(vpatch):
    ante = {}
    for p in parents(vpatch):
        pp = desc.get(p['h']) # Patch where this appears
        if not ante.get(pp):
            ante[pp] = []
        ante[pp] += [p['p']]
    return ante

# Get descendants.
def get_desc13(vpatch):
    des = {}
    for p in patches:
        ante = get_ante(p)
        if vpatch in ante.keys():
            des[p] = ante[vpatch]
    return des

##############################################################################

# Print name of patch and its guarantors, or 'WILD' if none known.
def disp_vp14(vpatch):
    seals = ', '.join(map(str, banners[vpatch]))
    if seals == '':
        seals = 'WILD'
    return "{0} ({1})".format(vpatch, seals)

##############################################################################

# Command: WoT
def c_wot15(args):
    for k in pubkeys.values():
        print "{0}:{1} ({2})".format(k['handle'], k['fp'], k['id'])

# Command: Flow
def c_flow16(args):
    for p in patches:
        print disp_vp(p)

# Command: Roots.
def c_roots17(args):
    for r in roots:
        print "Root: " + disp_vp(r)

# Command: Antecedents.
def c_ante18(args):
    ante = get_ante(args.query)
    for p in ante.keys():
        if p != 'false':
            print "{0} [{1}]".format(disp_vp(p), '; '.join(map(str, ante[p])))

# Command: Descendants
def c_desc19(args):
    des = get_desc(args.query)
    for d in des.keys():
        print "Descendant: {0} [{1}]".format(disp_vp(d), '; '.join(map(str, des[d])))

# Command: Press.
def c_press20(args):
    print "Pressing using head: {0} to path: '{1}'".format(args.head, args.dest)
    headpos = patches.index(args.head)
    seq = patches[:headpos + 1]
    os.mkdir(args.dest)
    for p in seq:
        print "Using: {0}".format(disp_vp(p))
        os.system("patch -E --dir {0} -p1 < {1}".format(args.dest, p))
    print "Completed Pressing using head: {0} to path: '{1}'".format(args.head, args.dest)

# Command: Origin.
def c_origin21(args):
    o = desc.get(args.query)
    if o:
        print disp_vp(o)
    else:
        print "No origin known."

##############################################################################

##############################################################################
# Command line parameter processor.22
parser = argparse.ArgumentParser(description=intro, epilog=prolog)

# Print paths, etc
parser.add_argument('-v', dest='verbose', default=False,
                    action="store_true", help='Verbose.')

# Permit the use of patches no one has yet sealed. Use this ONLY for own dev work!
parser.add_argument('-wild', dest='wild', default=False,
                    action="store_true", help='Permit wild (UNSEALED!) vpatches.')

# Glom keyid (short fingerprint) onto every WoT handle.
parser.add_argument('-fingers', dest='fingers', default=False,
                    action="store_true", help='Prefix keyid to all WoT handles.')

# Default path of WoT public keys is /home/yourusername/.wot
# This dir must exist. Alternatively, you may specify another.
parser.add_argument('--wot', dest='wot', default=os.path.join(os.path.expanduser('~'), '.wot'),
                    action="store", help='Use WoT in given directory. (Default: ~/.wot)')

# Default path of the seals (PGP signatures) is /home/yourusername/.seals
# This dir must exist. Alternatively, you may specify another.
parser.add_argument('--seals', dest='seals', default=os.path.join(os.path.expanduser('~'), '.seals'),
                    action="store", help='Use Seals in given directory. (Default: ~/.seals)')

# REQUIRED: Path of directory with vpatches.23
parser.add_argument('vpatches', help='Vpatch directory to operate on. [REQUIRED]')

# REQUIRED: Command.24
subparsers = parser.add_subparsers(help='Command [REQUIRED]')

parser_w = subparsers.add_parser('w', help='Display WoT.')
parser_w.set_defaults(f=c_wot)

parser_r = subparsers.add_parser('r', help='Display Roots.')
parser_r.set_defaults(f=c_roots)

parser_a = subparsers.add_parser('a', help='Display Antecedents [PATCH]')
parser_a.set_defaults(f=c_ante)
parser_a.add_argument('query', action="store", help='Patch.')

parser_d = subparsers.add_parser('d', help='Display Descendants [PATCH]')
parser_d.set_defaults(f=c_desc)
parser_d.add_argument('query', action="store", help='Patch.')

parser_l = subparsers.add_parser('f', help='Compute Flow.')
parser_l.set_defaults(f=c_flow)

parser_p = subparsers.add_parser('p', help='Press [HEADPATCH AND DESTINATION]')
parser_p.set_defaults(f=c_press)
parser_p.add_argument('head', action="store", help='Head patch.')
parser_p.add_argument('dest', action="store", help='Destionation directory.')

parser_o = subparsers.add_parser('o', help='Find Origin [SHA512]')
parser_o.set_defaults(f=c_origin)
parser_o.add_argument('query', action="store", help='SHA512 to search for.')

##############################################################################

# V cannot operate without vpatches, WoT, and Seals datasets.
def reqdir25(path):
    if (not (os.path.isdir(path))):
        fatal("Directory '{0}' does not exist!".format(path))
    return path

def main26 ():
    global verbose, pubkeys, patches, roots, banners

    args = parser.parse_args()
    verbose = args.verbose

    # Patch and Sigs dirs
    pdir = reqdir(args.vpatches)
    sdir = reqdir(args.seals)
    wdir = reqdir(args.wot)

    spew("Using patches from:" + pdir)
    spew("Using signatures from:" + sdir)
    spew("Using wot from:" + wdir)

    pfiles = dir_files(pdir)
    sfiles = dir_files(sdir)
    wfiles = dir_files(wdir)

    # Build WoT from pubkeys
    handle = {}
    for w in wfiles:
        pubkey = open(w, 'r').read()
        impkey = gpg.import_keys(pubkey)
        for fp in impkey.fingerprints:
            handle[fp] = os.path.splitext(os.path.basename(w))[0]

    for k in gpg.list_keys():
        name = handle[k['fingerprint']]
        if args.fingers:
            name += '-' + k['keyid']
        pubkeys[k['keyid']] = {'fp':k['fingerprint'],
                               'id':', '.join(map(str, k['uids'])),
                               'handle':name}

    # Validate seals
    for p in pfiles:
        pt = os.path.basename(p)
        banners[p] = []
        for s in sfiles:
            sig = os.path.basename(s)
            # All seals must take the form patchtitle.vpatch.yourname.sig
            if sig.find(pt) == 0: # substring of sig filename up through '.vpatch'
                v = gpg.verify_file(open(s, 'r'), data_filename=p)
                if v.valid:
                    banners[p] += [pubkeys[v.key_id]['handle']]
                else:
                    fatal("---------------------------------------------------------------------\n" +
                          "WARNING: {0} is an INVALID seal for {1} !\n".format(sig, pt) +
                          "Check that this user is in your WoT, and that this key has not expired.\n" +
                          "Otherwise remove the invalid seal from your SEALS directory.\n" +
                          "---------------------------------------------------------------------")

    # Select the subset of vpatches currently in use.
    for p in pfiles:
        if banners.get(p) or args.wild:
            patches += [p]
            children(p) # Memoize.
            parents(p) # Memoize.

    roots = find_roots(patches)
    if not roots:
        fatal('No roots found!')

    # Topological ordering of flow graph
    l = []
    for p in patches:
        l += [(p, get_desc(p).keys())]
    s = map(lambda x:x[0], toposort(l))
    patches = s[::-1]

    # Run command
    args.f(args)

    # Remove temporary keychain
    shutil.rmtree(gpgtmp)

##############################################################################

if __name__ == '__main__' :
    main()

##############################################################################
  1. In Kelvin versioning subsequent version numbers decrease until one has working code, hopefully before version 0. Instead of growing indefinitely like a tumor, software written using this versioning system is meant to crystallize into a program that does one job well. []
  2. This is the software liscense of ~TMSR. The subtext is software is owned by the person running it. []
  3. This is Dijkstra's algorithm for topological sort. I wrote my own version here. Stan starts with an unsorted dictionary of nodes mapped to their outgoing edges. The function iterates through unsorted until it is empty. On each iteration, if a node has no outgoing edges that point to a node in unsorted, the node is appended to the list sorted. If the toposort goes through this iteration without finding a leaf node, then the graph must be cyclic and the toposort fails. Note that Stan's toposort returns the reverse of what is conventionally returned by topological sort. []
  4. A simple function that takes a msg and writes it to standard error while existing the program returning a 1 symbolizing the program failed. []
  5. Prints the given message if verbose is set to true. []
  6. The comment for this function describes it perfectly. os.walk will return an iterator that yields a value for every sub directory. The magical 2 index is the list of file names inside of a given directory in the directory tree. []
  7. ben_vulpes describes why the three lines above are done. gpg by default saves the state of imported keys. For V, however, we only want to use the keys that are in the wot directory at the time of running a command. Setting up this temporary directory is thus done as an implementation detail for running gpg with only the keys found in the wot dir. []
  8. This is an auxilary method used to find children and parents of vpatches. Its parameters include a path to a vpatch and a regular expression. The regex is used to match to the lines found in a vpatch that look like:
    +++ filepath filehash
    or
    --- filepath filehash

    The third parameter, cache, is the dictionary used for memoization. []
  9. Returns a list of the parents of a vpatch using vpdata. Returns an error if it finds none. It's important to note that a parent refers to the filepath/filehash pair that is found after the "---" lines in a vpatch. They are not to be confused with antecedents, which are vpatches. The same goes for children, which are the filepath/filehash pairs found after "+++" lines in a vpatch. []
  10. Returns a list of the children of a vpatch. After finding the children, the function maps the child's filehash to the vpatch in the dictionary desc. It does not count children who have a filehash of 'false' since those are files that have been deleted. []
  11. Filters a given list of patches for the ones that are roots. A genesis patch is a root, but a root is not necessarily a genesis patch. Stan leaves an exercise for the reader to show how one could have more than one root. The answer: a vpatch set will have multiple roots if there are at least two vpatches that do not modify or delete any existing files and instead only create files. []
  12. Iterates through the parents of a vpatch, and then finds the corresponding vpatch that introduced the files in the parents. Returns a dictionary mapping the vpatch's antecedents to the filepath's of the intersection of the antecedent's children and the vpatch's parents. []
  13. Iterates through all patches, getting their antecedents. If the given vpatch is in the antecedents of another patch, then that patch is a descendant of the given vpatch. Returns a dictionary mapping the vpatch's descendants to the filepath's of the intersection of the vpatch's children and the descendant's parents. []
  14. Self explanatory. Although, the comment is technically incorrect- the function just returns the string with the name of the patch and its guarantors / wild if unknown, it does not print the string. []
  15. Prints the list of the keys in the wot directory. []
  16. Prints all the patches in the patch directory with their guarantors as per disp_vp. []
  17. Prints all the roots. These are usually only genesis patches, but could technically be any patch that doesn't modify/delete any existing files. []
  18. Takes a vpatch and prints all of its antecedents. []
  19. Takes a vpatch and prints all of its descendants. []
  20. Presses to a given head vpatch and outputs the result in a given directory. Applies the already topologically sorted vpatches in order up to and including the given head vpatch. If the head is in a vtree that diverges, there is no guarentee that a divergent branch that does not include the head will be pressed as well. This may be considered a bug; vpy needs to be used with care. []
  21. Takes a filehash and prints the vpatch that generated the corresponding file or "No origin known.". []
  22. The code in this section sets up the command line parameter processor. First Stan sets up the optional arguments: verbose, wild, fingers, wot, and seals. The add_argument command used for this takes the parameters: dest, a string that will determine how to access the argument; default, the default value for the parameter; action, either sets the dest to true or sets the dest to the argument given; help, gives a description of the optional parameter.
    -verbose makes the code liberally print what it's doing, -wild lets one patch with unsigned patches, -fingers appends a short fingerprint to every wot handle, --wot takes a path to a directory of wotkeys to replace the default /home/username/.wot, --seals takes a path to a directory of signatures of vpatches to replace the default /home/username/.seals []
  23. Stan's code requires you provide the path to the vpatches on every run, other implementations I've seen have a default patches directory just like .wot and .seals. []
  24. Sets up the list of commands. These fire off the functions that have a prefix c_ in their name, their jobs are described above. The commands are: w (displays WOT), r (displays Roots), a (displays the antecedents of a given vpatch), d (displays the descendants of a given vpatch), f (displays the patches topologically sorted), p (presses a vtree), o (gets the vpatch that generated the given filehash) []
  25. Fails the program if any of the vpatches, wot, or seals directory do not exist. []
  26. The main function stores the arguments from the parser, and then does various setup such as loading the vpatches while checking for their seals and doing the topological sort on the vpatches. Once done initializing it runs the command given by the user and then cleans up by removing the temporary directory it had to make for gnupg. []

8 Responses to “VPY Annotations”

  1. Please use the ?b&e=#select selection mechanism instead.

  2. Jacob Welsh says:

    Perhaps a topic for another time, but at least on my display this code well overflows its box, resulting in a horizontal scroll region, and with the scroll bar well out of reach at the bottom. Kinda why I gave up on the Kubrick theme, pleasing as its curves and colors may be. I'm curious how you do the syntax highlighting though!

  3. whaack says:

    And now realized my stupidity coming from having not done any investigation of what mp meant in his comment. My link will be fixed once I digest the post that explains the new selection mechanism.

  4. whaack says:

    @Jacob Welsh

    I agree with you that this theme, although perhaps pleasing to the eye for vanilla text posts, is very impractical given its small width.

    For the syntax highlighting I threw the code into http://hilite.me/ But I didn't vet the tool. And given the selection debacle, i think it's safe to say using anything one doesn't understand is The Wrong Thing.

  5. [...] I read mp's comment "Please use the ?b&e=#select selection mechanism [...]

  6. [...] before you open emacs or vim and get started on your ~371 line program, there's a few tasks that your new environment demands you take care of. You'll have to figure out [...]

Leave a Reply