diff -uNr a/v/manifest b/v/manifest --- a/v/manifest false +++ b/v/manifest 213e6ee9c388c260463ff127c129b88531438e26f02e49cf5e38822a7bfa493feede975eccfd3924c23cbad8cb8e6c21ae0a59e6a07d180ba9acff9cf8de57af @@ -0,0 +1 @@ +543800 v99 Canonical version of asciilifeform's v.py, the original V proof of concept diff -uNr a/v/v.py b/v/v.py --- a/v/v.py false +++ b/v/v.py 2a837416675b312acffbe3df80d0b0d099d1d9f2768148476d31e25502b20ab0713a8110b4f4a97455ecc29a444cd8e92f03534211307a9ae5a5595dcb1f0ad2 @@ -0,0 +1,371 @@ +#!/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 = 100 # 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. +''' + +intro = "V (ver. {0}K)\n".format(vver) + +############################################################################## +def toposort(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 fatal(msg): + sys.stderr.write(msg + "\n") + exit(1) + +def spew(msg): + if verbose: + print msg + +# List of files in a directory, in lexical order. +def dir_files(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' + +# 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 vpdata(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 parents(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 children(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_roots(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_ante(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_desc(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_vp(vpatch): + seals = ', '.join(map(str, banners[vpatch])) + if seals == '': + seals = 'WILD' + return "{0} ({1})".format(vpatch, seals) + +############################################################################## + +# Command: WoT +def c_wot(args): + for k in pubkeys.values(): + print "{0}:{1} ({2})".format(k['handle'], k['fp'], k['id']) + +# Command: Flow +def c_flow(args): + for p in patches: + print disp_vp(p) + +# Command: Roots. +def c_roots(args): + for r in roots: + print "Root: " + disp_vp(r) + +# Command: Antecedents. +def c_ante(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_desc(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_press(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_origin(args): + o = desc.get(args.query) + if o: + print disp_vp(o) + else: + print "No origin known." + +############################################################################## + +############################################################################## +# Command line parameter processor. +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. +parser.add_argument('vpatches', help='Vpatch directory to operate on. [REQUIRED]') + +# REQUIRED: Command. +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 reqdir(path): + if (not (os.path.isdir(path))): + fatal("Directory '{0}' does not exist!".format(path)) + return path + + +def main(): + 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() + +##############################################################################