-
+ 2A837416675B312ACFFBE3DF80D0B0D099D1D9F2768148476D31E25502B20AB0713A8110B4F4A97455ECC29A444CD8E92F03534211307A9AE5A5595DCB1F0AD2
v/v.py
(0 . 0)(1 . 371)
10 #!/usr/bin/python
11
12 ##############################################################################
13 # Quick Intro:
14 # 1) Create '.wot' in your home directory. Fill it with public keys from 'wot'.
15 # 2) Create '.seals' in your home directory. Place all signatures there from 'sigs'.
16 # 3) Create a 'patches' directory somewhere where 'v' can find it. Or use this one.
17 # 4) ./v.py patches command
18 # e.g.,
19 # ./v.py patches w
20 # ^^ displays WoT
21 # ./v.py patches p patches/asciilifeform_add_verifyall_option.vpatch asciis_bleedingedge
22 # ^^ this 'presses' (creates the actual tree)
23 # ^^ approximately like a 'checkout' in your vanilla flavoured shithub.
24
25 ##############################################################################
26
27 import os, sys, shutil, argparse, re, tempfile, gnupg
28
29 ##############################################################################
30 vver = 100 # This program's Kelvin version.
31
32 ## HOW YOU CAN HELP: ##
33
34 # * TESTS plox, ty!
35 #
36 # Report findings in #bitcoin-assets on Freenode.
37
38 ##############################################################################
39
40 prolog = '''\
41 (C) 2015 NoSuchlAbs.
42 You do not have, nor can you ever acquire the right to use, copy or distribute
43 this software ; Should you use this software for any purpose, or copy and
44 distribute it to anyone or in any manner, you are breaking the laws of whatever
45 soi-disant jurisdiction, and you promise to continue doing so for the indefinite
46 future. In any case, please always : read and understand any software ;
47 verify any PGP signatures that you use - for any purpose.
48 '''
49
50 intro = "V (ver. {0}K)\n".format(vver)
51
52 ##############################################################################
53 def toposort(unsorted):
54 sorted = []
55 unsorted = dict(unsorted)
56 while unsorted:
57 acyclic = False
58 for node, edges in unsorted.items():
59 for edge in edges:
60 if edge in unsorted:
61 break
62 else:
63 acyclic = True
64 del unsorted[node]
65 sorted.append((node, edges))
66 if not acyclic:
67 fatal("Cyclic graph!")
68 return sorted
69 ##############################################################################
70
71 verbose = False
72
73 def fatal(msg):
74 sys.stderr.write(msg + "\n")
75 exit(1)
76
77 def spew(msg):
78 if verbose:
79 print msg
80
81 # List of files in a directory, in lexical order.
82 def dir_files(dir):
83 return sorted([os.path.join(dir, fn) for fn in next(os.walk(dir))[2]])
84
85 # GPG is retarded and insists on 'keychain.'
86 # This will be a temp dir, because we don't do any crypto.
87 gpgtmp = tempfile.mkdtemp()
88 gpg = gnupg.GPG(gnupghome=gpgtmp)
89 gpg.encoding = 'utf-8'
90
91 # Known WoT public keys.
92 pubkeys = {}
93
94 # The subset of vpatches that are considered valid.
95 patches = []
96
97 # Banners (i.e. vpatches mapped to their guarantors)
98 banners = {}
99
100 # Roots (i.e. vpatches parented by thin air)
101 roots = []
102
103 # Table mapping file hash to originating vpatch
104 desc = {}
105 desc['false'] = 'false'
106
107
108 # Grep for diff magics, and memoize
109 def vpdata(path, exp, cache):
110 l = cache.get(path)
111 if not l:
112 l = []
113 patch = open(path, 'r').read()
114 for m in re.findall(exp, patch, re.MULTILINE):
115 l += [{'p':m[0], 'h':m[1]}]
116 cache[path] = l
117 return l
118
119 # Get parents of a vpatch
120 pcache = {}
121 def parents(vpatch):
122 parents = vpdata(vpatch, r'^--- (\S+) (\S+)$', pcache)
123 if not parents:
124 fatal("{0} is INVALID, check whether it IS a vpatch!".format(vpatch))
125 return parents
126
127 # Get children of a vpatch
128 ccache = {}
129 def children(vpatch):
130 children = vpdata(vpatch, r'^\+\+\+ (\S+) (\S+)$', ccache)
131 if not children:
132 fatal("{0} is INVALID, check whether it IS a vpatch!".format(vpatch))
133 # Record descendents:
134 for child in children:
135 h = child['h']
136 if h != 'false':
137 desc[h] = vpatch
138 return children
139
140 # It is entirely possible to have more than one root!
141 # ... exactly how, is left as an exercise for readers.
142 def find_roots(patchset):
143 rset = []
144 # Walk, find roots
145 for p in patchset:
146 if all(p['h'] == 'false' for p in parents(p)):
147 rset += [p]
148 spew("Found a Root: '{0}'".format(p))
149 return rset
150
151 # Get antecedents.
152 def get_ante(vpatch):
153 ante = {}
154 for p in parents(vpatch):
155 pp = desc.get(p['h']) # Patch where this appears
156 if not ante.get(pp):
157 ante[pp] = []
158 ante[pp] += [p['p']]
159 return ante
160
161 # Get descendants.
162 def get_desc(vpatch):
163 des = {}
164 for p in patches:
165 ante = get_ante(p)
166 if vpatch in ante.keys():
167 des[p] = ante[vpatch]
168 return des
169
170 ##############################################################################
171
172 # Print name of patch and its guarantors, or 'WILD' if none known.
173 def disp_vp(vpatch):
174 seals = ', '.join(map(str, banners[vpatch]))
175 if seals == '':
176 seals = 'WILD'
177 return "{0} ({1})".format(vpatch, seals)
178
179 ##############################################################################
180
181 # Command: WoT
182 def c_wot(args):
183 for k in pubkeys.values():
184 print "{0}:{1} ({2})".format(k['handle'], k['fp'], k['id'])
185
186 # Command: Flow
187 def c_flow(args):
188 for p in patches:
189 print disp_vp(p)
190
191 # Command: Roots.
192 def c_roots(args):
193 for r in roots:
194 print "Root: " + disp_vp(r)
195
196 # Command: Antecedents.
197 def c_ante(args):
198 ante = get_ante(args.query)
199 for p in ante.keys():
200 if p != 'false':
201 print "{0} [{1}]".format(disp_vp(p), '; '.join(map(str, ante[p])))
202
203 # Command: Descendants
204 def c_desc(args):
205 des = get_desc(args.query)
206 for d in des.keys():
207 print "Descendant: {0} [{1}]".format(disp_vp(d), '; '.join(map(str, des[d])))
208
209 # Command: Press.
210 def c_press(args):
211 print "Pressing using head: {0} to path: '{1}'".format(args.head, args.dest)
212 headpos = patches.index(args.head)
213 seq = patches[:headpos + 1]
214 os.mkdir(args.dest)
215 for p in seq:
216 print "Using: {0}".format(disp_vp(p))
217 os.system("patch -E --dir {0} -p1 < {1}".format(args.dest, p))
218 print "Completed Pressing using head: {0} to path: '{1}'".format(args.head, args.dest)
219
220 # Command: Origin.
221 def c_origin(args):
222 o = desc.get(args.query)
223 if o:
224 print disp_vp(o)
225 else:
226 print "No origin known."
227
228 ##############################################################################
229
230 ##############################################################################
231 # Command line parameter processor.
232 parser = argparse.ArgumentParser(description=intro, epilog=prolog)
233
234 # Print paths, etc
235 parser.add_argument('-v', dest='verbose', default=False,
236 action="store_true", help='Verbose.')
237
238 # Permit the use of patches no one has yet sealed. Use this ONLY for own dev work!
239 parser.add_argument('-wild', dest='wild', default=False,
240 action="store_true", help='Permit wild (UNSEALED!) vpatches.')
241
242 # Glom keyid (short fingerprint) onto every WoT handle.
243 parser.add_argument('-fingers', dest='fingers', default=False,
244 action="store_true", help='Prefix keyid to all WoT handles.')
245
246 # Default path of WoT public keys is /home/yourusername/.wot
247 # This dir must exist. Alternatively, you may specify another.
248 parser.add_argument('--wot', dest='wot', default=os.path.join(os.path.expanduser('~'), '.wot'),
249 action="store", help='Use WoT in given directory. (Default: ~/.wot)')
250
251 # Default path of the seals (PGP signatures) is /home/yourusername/.seals
252 # This dir must exist. Alternatively, you may specify another.
253 parser.add_argument('--seals', dest='seals', default=os.path.join(os.path.expanduser('~'), '.seals'),
254 action="store", help='Use Seals in given directory. (Default: ~/.seals)')
255
256 # REQUIRED: Path of directory with vpatches.
257 parser.add_argument('vpatches', help='Vpatch directory to operate on. [REQUIRED]')
258
259 # REQUIRED: Command.
260 subparsers = parser.add_subparsers(help='Command [REQUIRED]')
261
262 parser_w = subparsers.add_parser('w', help='Display WoT.')
263 parser_w.set_defaults(f=c_wot)
264
265 parser_r = subparsers.add_parser('r', help='Display Roots.')
266 parser_r.set_defaults(f=c_roots)
267
268 parser_a = subparsers.add_parser('a', help='Display Antecedents [PATCH]')
269 parser_a.set_defaults(f=c_ante)
270 parser_a.add_argument('query', action="store", help='Patch.')
271
272 parser_d = subparsers.add_parser('d', help='Display Descendants [PATCH]')
273 parser_d.set_defaults(f=c_desc)
274 parser_d.add_argument('query', action="store", help='Patch.')
275
276 parser_l = subparsers.add_parser('f', help='Compute Flow.')
277 parser_l.set_defaults(f=c_flow)
278
279 parser_p = subparsers.add_parser('p', help='Press [HEADPATCH AND DESTINATION]')
280 parser_p.set_defaults(f=c_press)
281 parser_p.add_argument('head', action="store", help='Head patch.')
282 parser_p.add_argument('dest', action="store", help='Destionation directory.')
283
284 parser_o = subparsers.add_parser('o', help='Find Origin [SHA512]')
285 parser_o.set_defaults(f=c_origin)
286 parser_o.add_argument('query', action="store", help='SHA512 to search for.')
287
288
289 ##############################################################################
290
291 # V cannot operate without vpatches, WoT, and Seals datasets.
292 def reqdir(path):
293 if (not (os.path.isdir(path))):
294 fatal("Directory '{0}' does not exist!".format(path))
295 return path
296
297
298 def main():
299 global verbose, pubkeys, patches, roots, banners
300
301 args = parser.parse_args()
302 verbose = args.verbose
303
304 # Patch and Sigs dirs
305 pdir = reqdir(args.vpatches)
306 sdir = reqdir(args.seals)
307 wdir = reqdir(args.wot)
308
309 spew("Using patches from:" + pdir)
310 spew("Using signatures from:" + sdir)
311 spew("Using wot from:" + wdir)
312
313 pfiles = dir_files(pdir)
314 sfiles = dir_files(sdir)
315 wfiles = dir_files(wdir)
316
317 # Build WoT from pubkeys
318 handle = {}
319 for w in wfiles:
320 pubkey = open(w, 'r').read()
321 impkey = gpg.import_keys(pubkey)
322 for fp in impkey.fingerprints:
323 handle[fp] = os.path.splitext(os.path.basename(w))[0]
324
325 for k in gpg.list_keys():
326 name = handle[k['fingerprint']]
327 if args.fingers:
328 name += '-' + k['keyid']
329 pubkeys[k['keyid']] = {'fp':k['fingerprint'],
330 'id':', '.join(map(str, k['uids'])),
331 'handle':name}
332
333 # Validate seals
334 for p in pfiles:
335 pt = os.path.basename(p)
336 banners[p] = []
337 for s in sfiles:
338 sig = os.path.basename(s)
339 # All seals must take the form patchtitle.vpatch.yourname.sig
340 if sig.find(pt) == 0: # substring of sig filename up through '.vpatch'
341 v = gpg.verify_file(open(s, 'r'), data_filename=p)
342 if v.valid:
343 banners[p] += [pubkeys[v.key_id]['handle']]
344 else:
345 fatal("---------------------------------------------------------------------\n" +
346 "WARNING: {0} is an INVALID seal for {1} !\n".format(sig, pt) +
347 "Check that this user is in your WoT, and that this key has not expired.\n" +
348 "Otherwise remove the invalid seal from your SEALS directory.\n" +
349 "---------------------------------------------------------------------")
350
351 # Select the subset of vpatches currently in use.
352 for p in pfiles:
353 if banners.get(p) or args.wild:
354 patches += [p]
355 children(p) # Memoize.
356 parents(p) # Memoize.
357
358 roots = find_roots(patches)
359 if not roots:
360 fatal('No roots found!')
361
362 # Topological ordering of flow graph
363 l = []
364 for p in patches:
365 l += [(p, get_desc(p).keys())]
366 s = map(lambda x:x[0], toposort(l))
367 patches = s[::-1]
368
369 # Run command
370 args.f(args)
371
372 # Remove temporary keychain
373 shutil.rmtree(gpgtmp)
374
375 ##############################################################################
376
377 if __name__ == '__main__' :
378 main()
379
380 ##############################################################################