infocalypse

(djk)
2009-07-20: First pass support for fn-pull'ing with a usk hash value and command

First pass support for fn-pull'ing with a usk hash value and command line trust map editing. e.g. hg fn-pull --hash 2220b02cf7ee hg fn-fmsread --trust --fmsid djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks --hash 2220b02cf7ee hg fn-fmsread --untrust --fmsid djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks --hash 2220b02cf7ee Still needs pylint cleanup and doc updates.

diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py
--- a/infocalypse/__init__.py
+++ b/infocalypse/__init__.py
@@ -295,10 +295,11 @@ from infcmds import get_config_info, exe
      execute_push, execute_setup, execute_copy, execute_reinsert, \
      execute_info
 
-from fmscmds import execute_fmsread, execute_fmsnotify
+from fmscmds import execute_fmsread, execute_fmsnotify, get_uri_from_hash
 
 from sitecmds import execute_putsite, execute_genkey
 from config import read_freesite_cfg
+from validate import is_hex_string, is_fms_id
 
 def set_target_version(ui_, repo, opts, params, msg_fmt):
     """ INTERNAL: Update TARGET_VERSION in params. """
@@ -390,7 +391,19 @@ def infocalypse_pull(ui_, repo, **opts):
     """ Pull from an Infocalypse repository in Freenet.
      """
     params, stored_cfg = get_config_info(ui_, opts)
-    request_uri = opts['uri']
+
+    if opts['hash']:
+        # Use FMS to lookup the uri from the repo hash.
+        if opts['uri'] != '':
+            ui_.warn("Ignoring --uri because --hash is set!\n")
+        if len(opts['hash']) != 1:
+            raise util.Abort("Only one --hash value is allowed.")
+        params['FMSREAD_HASH'] = opts['hash'][0]
+        params['FMSREAD_ONLYTRUSTED'] = bool(opts['onlytrusted'])
+        request_uri = get_uri_from_hash(ui_, repo, params, stored_cfg)
+    else:
+        request_uri = opts['uri']
+
     if request_uri == '':
         request_uri = stored_cfg.get_request_uri(repo.root)
         if not request_uri:
@@ -444,6 +457,44 @@ def infocalypse_info(ui_, repo, **opts):
     params['REQUEST_URI'] = request_uri
     execute_info(ui_, params, stored_cfg)
 
+def parse_trust_args(params, opts):
+    """ INTERNAL: Helper function to parse  --hash and --fmsid. """
+    if opts.get('hash', []) == []:
+        raise util.Abort("Use --hash to set the USK hash.")
+    if len(opts['hash']) != 1:
+        raise util.Abort("Only one --hash value is allowed.")
+    if not is_hex_string(opts['hash'][0]):
+        raise util.Abort("[%s] doesn't look like a USK hash." %
+                         opts['hash'][0])
+
+    if opts.get('fmsid', []) == []:
+        raise util.Abort("Use --fmsid to set the FMS id.")
+    if len(opts['fmsid']) != 1:
+        raise util.Abort("Only one --fmsid value is allowed.")
+    if not is_fms_id(opts['fmsid'][0]):
+        raise util.Abort("[%s] doesn't look like an FMS id."
+                         % opts['fmsid'][0])
+
+    params['FMSREAD_HASH'] = opts['hash'][0]
+    params['FMSREAD_FMSID'] = opts['fmsid'][0]
+
+def parse_fmsread_subcmd(params, opts):
+    """ INTERNAL: Parse subcommand for fmsread."""
+    if opts['listall']:
+        params['FMSREAD'] = 'listall'
+    elif opts['list']:
+        params['FMSREAD'] = 'list'
+    elif opts['showtrust']:
+        params['FMSREAD'] = 'showtrust'
+    elif opts['trust']:
+        params['FMSREAD'] = 'trust'
+        parse_trust_args(params, opts)
+    elif opts['untrust']:
+        params['FMSREAD'] = 'untrust'
+        parse_trust_args(params, opts)
+    else:
+        params['FMSREAD'] = 'update'
+
 def infocalypse_fmsread(ui_, repo, **opts):
     """ Read repository update information from fms.
     """
@@ -457,13 +508,7 @@ def infocalypse_fmsread(ui_, repo, **opt
         if not request_uri:
             ui_.status("There is no stored request URI for this repo.\n")
             request_uri = None
-
-    if opts['listall']:
-        params['FMSREAD'] = 'listall'
-    elif opts['list']:
-        params['FMSREAD'] = 'list'
-    else:
-        params['FMSREAD'] = 'update'
+    parse_fmsread_subcmd(params, opts)
     params['DRYRUN'] = opts['dryrun']
     params['REQUEST_URI'] = request_uri
     execute_fmsread(ui_, params, stored_cfg)
@@ -555,7 +600,10 @@ NOSEARCH_OPT = [('', 'nosearch', None, '
 
 cmdtable = {
     "fn-pull": (infocalypse_pull,
-                [('', 'uri', '', 'request URI to pull from'),]
+                [('', 'uri', '', 'request URI to pull from'),
+                 ('', 'hash', [], 'repo hash of repository to pull from'),
+                 ('', 'onlytrusted', None, 'only use repo announcements from '
+                  + 'known users')]
                 + FCP_OPTS
                 + NOSEARCH_OPT
                 + AGGRESSIVE_OPT,
@@ -596,9 +644,14 @@ cmdtable = {
 
     "fn-fmsread": (infocalypse_fmsread,
                    [('', 'uri', '', 'request URI'),
+                    ('', 'hash', [], 'repo hash to modify trust for'),
+                    ('', 'fmsid', [], 'FMS id to modify trust for'),
                     ('', 'list', None, 'show repo USKs from trusted '
                      + 'fms identities'),
                     ('', 'listall', None, 'show all repo USKs'),
+                    ('', 'showtrust', None, 'show the trust map'),
+                    ('', 'trust', None, 'add an entry to the trust map'),
+                    ('', 'untrust', None, 'remove an entry from the trust map'),
                     ('', 'dryrun', None, "don't update the index cache"),],
                    "[options]"),
 
diff --git a/infocalypse/config.py b/infocalypse/config.py
--- a/infocalypse/config.py
+++ b/infocalypse/config.py
@@ -20,7 +20,6 @@
     Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
 """
 
-
 import os
 import sys
 
@@ -28,6 +27,9 @@ from fcpclient import get_usk_hash, is_u
      get_usk_for_usk_version
 from knownrepos import DEFAULT_TRUST, DEFAULT_GROUPS, \
      DEFAULT_NOTIFICATION_GROUP
+
+from validate import is_hex_string, is_fms_id
+
 from mercurial import util
 
 # Similar hack is used in fms.py.
@@ -202,6 +204,15 @@ class Config:
     #def get_key_alias(self, alias, is_public):
     #    pass
 
+    def trusted_notifiers(self, repo_hash):
+        """ Return the list of FMS ids trusted to modify the repo with
+            repository hash repo_hash. """
+        ret = []
+        for fms_id in self.fmsread_trust_map:
+            if repo_hash in self.fmsread_trust_map[fms_id]:
+                ret.append(fms_id)
+        return ret
+
     @classmethod
     def update_defaults(cls, parser, cfg):
         """ INTERNAL: Helper function to simplify from_file. """
@@ -264,12 +275,21 @@ class Config:
                 fields = parser.get('fmsread_trust_map',
                                     ordinal).strip().split('|')
                 # REDFLAG: better validation for fms_id, hashes?
-                if fields[0].find('@') == -1:
+                if not is_fms_id(fields[0]):
                     raise ValueError("%s doesn't look like an fms id." %
                                      fields[0])
                 if len(fields) < 2:
                     raise ValueError("No USK hashes for fms id: %s?" %
                                      fields[0])
+                for value in fields[1:]:
+                    if not is_hex_string(value):
+                        raise ValueError("%s doesn't look like a repo hash." %
+                                         value)
+
+                if fields[0] in cfg.fmsread_trust_map:
+                    raise ValueError(("%s appears more than once in the "
+                                      + "[fmsread_trust_map] section.") %
+                                         fields[0])
                 cfg.fmsread_trust_map[fields[0]] = tuple(fields[1:])
 
 
@@ -401,3 +421,48 @@ def read_freesite_cfg(ui_, repo, params,
     if not params['SITE_KEY'].startswith('SSK@'):
         raise util.Abort("Stored site key not an SSK?")
 
+def known_hashes(trust_map):
+    """ Return all repo hashes in the trust map. """
+    ret = set([])
+    for fms_id in trust_map:
+        ret |=  set(trust_map[fms_id])
+    return tuple(ret)
+
+# REMEMBER that hashes are stored in a tuple not a list!
+def trust_id_for_repo(trust_map, fms_id, repo_hash):
+    """ Accept index updates from an fms id for a repo."""
+    assert is_fms_id(fms_id)
+    assert is_hex_string(repo_hash)
+
+    hashes = trust_map.get(fms_id, ())
+    if repo_hash in hashes:
+        return False
+    hashes = list(hashes)
+    hashes.append(repo_hash)
+    trust_map[fms_id] = tuple(hashes)
+
+    return True
+
+# See above.
+def untrust_id_for_repo(trust_map, fms_id, repo_hash):
+    """ Stop accepting index updates from an fms id for a repo."""
+    assert is_fms_id(fms_id)
+    assert is_hex_string(repo_hash)
+
+    if not fms_id in trust_map:
+        return False
+
+    hashes = list(trust_map[fms_id])
+    # Paranoia. There shouldn't be duplicates.
+    count = 0
+    while repo_hash in hashes:
+        hashes.remove(repo_hash)
+        count += 1
+
+    if len(hashes) == 0:
+        del trust_map[fms_id]
+        return True
+
+    trust_map[fms_id] = tuple(hashes)
+    return count > 0
+
diff --git a/infocalypse/fms.py b/infocalypse/fms.py
--- a/infocalypse/fms.py
+++ b/infocalypse/fms.py
@@ -26,8 +26,7 @@ import StringIO
 from fcpclient import get_usk_hash, get_version, is_usk_file, \
      get_usk_for_usk_version
 
-# Hmmm... This dependency doesn't really belong here.
-from knownrepos import KNOWN_REPOS
+from validate import is_hex_string
 
 # Similar HACK is used in config.py
 import knownrepos # Just need a module to read __file__ from
@@ -95,14 +94,18 @@ class IFmsMessageSink:
 
             items is an nntplib xover items tuple.
             """
-        raise NotImplementedError()
+        # Avoid pylint R0922
+        # raise NotImplementedError()
+        pass
 
     def recv_fms_msg(self, group, items, lines):
         """ Handle an fms message.
 
             items is an nntplib xover items tuple.
         """
-        raise NotImplementedError()
+        # Avoid pylint R0922
+        # raise NotImplementedError()
+        pass
 
 def recv_msgs(fms_host, fms_port, msg_sink, groups):
     """ Read messages from fms. """
@@ -139,6 +142,10 @@ def recv_msgs(fms_host, fms_port, msg_si
 ############################################################
 # Infocalypse specific stuff.
 ############################################################
+# REDFLAG: LATER, move this into fmscmd.py?
+
+# REDFLAG: Research, when exactly?
+# Can sometimes see fms ids w/o  human readable part.
 def clean_nym(fms_id):
     """ Returns the line noise part of an fms id, after the '@'. """
     pos = fms_id.index('@')
@@ -233,7 +240,6 @@ def parse(text, is_lines=False):
     announcements.sort()
     return (tuple(updates), tuple(announcements))
 
-
 def strip_names(trust_map):
     """ Returns a trust map without human readable names in the keys. """
     clean = {}
@@ -246,142 +252,202 @@ def strip_names(trust_map):
                                   + clean.get(cleaned, [])))
     return clean
 
-# REDFLAG: Trust map ids are w/o names
-# 'isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks', not
-# 'djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks'
-class USKIndexUpdateParser(IFmsMessageSink):
-    """ Class which accumulates USK index update notifications
-        from fms messages. """
-    def __init__(self, trust_map, keep_untrusted=False):
+# REDFLAG: Trust map ids are w/o names.
+#
+# clean_fms_id -> (set(uri,..), hash-> index, set(full_fms_id, ...))
+class USKNotificationParser(IFmsMessageSink):
+    """ IFmsMessageSink reads and saves all updates and announcements """
+    def __init__(self, trust_map=None):
         IFmsMessageSink.__init__(self)
-        self.trust_map = strip_names(trust_map)
-        self.updates = {}
-        self.untrusted = None
-        if keep_untrusted:
-            self.untrusted = {}
+        self.table = {}
+        self.trust_map = trust_map
 
     def wants_msg(self, dummy, items):
-        """ IFmsMessageSink implementation. """
+        """ Return True if the message should be passed to recv_fms_msg,
+            False, otherwise.
+
+            items is an nntplib xover items tuple.
+            """
+        # Skip replies, accept everything else.
         if len(items[5]) != 0:
-            # Skip replies
-            return False
-
-        if not self.untrusted is None:
-            return True
-
-        if clean_nym(items[2]) not in self.trust_map:
-            #print "Not trusted: ", items[2]
-            # Sender not authoritative on any USK.
-            return False
-
-        return True
-
-    def recv_fms_msg(self, dummy, items, lines):
-        """ IFmsMessageSink implementation. """
-        allowed_hashes = self.trust_map.get(clean_nym(items[2]), ())
-
-        #print "---\nSender: %s\nSubject: %s\n" % (items[2], items[1])
-        for update in parse(lines, True)[0]:
-            if update[0] in allowed_hashes:
-                # Only update if the nym is trusted *for the specific USK*.
-                #print "UPDATING ---\nSender: %s\nSubject:
-                # %s\n" % (items[2], items[1])
-                self.handle_trusted_update(update)
-            else:
-                self.handle_untrusted_update(items[2], update)
-
-    def handle_trusted_update(self, update):
-        """ INTERNAL: Handle a single update. """
-        index = update[1]
-        value = self.updates.get(update[0], index)
-        if index >= value:
-            self.updates[update[0]] = index
-
-    def handle_untrusted_update(self, sender, update):
-        """ INTERNAL: Handle a single untrusted update. """
-        entry = self.untrusted.get(update[0], [])
-        if not sender in entry:
-            entry.append(sender)
-        self.untrusted[update[0]] = entry
-
-    def updated(self, previous=None):
-        """ Returns a USK hash -> index map for USKs which
-            have been updated. """
-        if previous is None:
-            previous = {}
-        ret = {}
-        for usk_hash in self.updates:
-            if not usk_hash in previous:
-                ret[usk_hash] = self.updates[usk_hash]
-                continue
-            if self.updates[usk_hash] > previous[usk_hash]:
-                ret[usk_hash] = self.updates[usk_hash]
-
-        return ret
-
-class USKAnnouncementParser(IFmsMessageSink):
-    """ Class which accumulates USK announcement notifications
-        from fms messages. """
-    # None means accept all announcements.
-    def __init__(self, trust_map = None, include_defaults=False):
-        IFmsMessageSink.__init__(self)
-        if not trust_map is None:
-            trust_map = strip_names(trust_map)
-        self.trust_map = trust_map
-        self.usks = {}
-        if include_defaults:
-            for owner, usk in KNOWN_REPOS:
-                if ((not trust_map is None) and
-                    (not clean_nym(owner) in trust_map)):
-                    continue
-                self.handle_announcement(owner, usk)
-
-    def wants_msg(self, dummy, items):
-        """ IFmsMessageSink implementation. """
-        if len(items[5]) != 0:
-            # Skip replies
             return False
 
         if self.trust_map is None:
             return True
 
-        if clean_nym(items[2]) not in self.trust_map:
-            #print "Not trusted: ", items[2]
-            # Sender not authoritative on any USK.
-            return False
-
-        return True
+        return items[2] in self.trust_map
 
     def recv_fms_msg(self, dummy, items, lines):
         """ IFmsMessageSink implementation. """
         #print "---\nSender: %s\nSubject: %s\n" % (items[2], items[1])
-        for usk in parse(lines, True)[1]:
-            self.handle_announcement(items[2], usk)
+        clean_id = clean_nym(items[2])
+        new_updates, new_announcements = parse(lines, True)
+        #if len(new_updates) > 0 or len(new_announcements) > 0:
+        #    print "---\nSender: %s\nSubject: %s\n" % (items[2], items[1])
 
-    def handle_announcement(self, sender, usk):
-        """ INTERNAL: Handle a single announcement """
-        usk = get_usk_for_usk_version(usk, 0)
-        entry = self.usks.get(usk, [])
-        if not sender in entry:
-            entry.append(sender)
-        self.usks[usk] = entry
+        for update in new_updates:
+            self.handle_update(clean_id, items[2], update[0], update[1])
 
-HEX_CHARS = frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
-                       'a', 'b', 'c', 'd', 'e', 'f'])
+        for announcement in new_announcements:
+            self.handle_announcement(clean_id, items[2], announcement)
 
-# Really no library function to do this?
-# REQUIRES: Lowercase!
-def is_hex_string(value, length=12):
-    """ Returns True if value is a lowercase hex digit string,
-        False otherwise. """
-    if not length is None:
-        if len(value) != length:
-            raise ValueError("Expected hex string of length: %i" % length)
-    for char in value:
-        if not char in HEX_CHARS:
-            return False
-    return True
+    def add_default_repos(self, default_repos):
+        """ Add table entries from a [(fms_id, usk), ...] list. """
+        for repo_entry in default_repos:
+            clean_id = clean_nym(repo_entry[0])
+            usk_hash = get_usk_hash(repo_entry[1])
+            self.handle_announcement(clean_id, repo_entry[0], repo_entry[1])
+            # Implicit in announcement
+            self.handle_update(clean_id, repo_entry[0], usk_hash,
+                               get_version(repo_entry[1]))
 
+    # parse() handles implicit updates in annoucements
+    def handle_announcement(self, clean_id, fms_id, usk):
+        """ INTERNAL: process a single announcement. """
+        entry = self.table.get(clean_id, (set([]), {}, set([])))
+        entry[0].add(get_usk_for_usk_version(usk, 0))
+        entry[2].add(fms_id)
+
+        self.table[clean_id] = entry
+
+    def handle_update(self, clean_id, fms_id, usk_hash, index):
+        """ INTERNAL: process a single update. """
+        if index < 0:
+            print "handle_update -- skipped negative index!"
+            return
+
+        entry = self.table.get(clean_id, (set([]), {}, set([])))
+        prev_index = entry[1].get(usk_hash, 0)
+        if index > prev_index:
+            prev_index = index
+        entry[1][usk_hash] = prev_index
+        entry[2].add(fms_id)
+
+        self.table[clean_id] = entry
+
+    # REDFLAG: Doesn't deep copy. Passing out refs to stuff in table.
+    def invert_table(self):
+        """ INTERNAL: Return (clean_id -> fms_id,  usk->set(clean_id, ...),
+            repo_hash->usk->set(clean_id, ...)) """
+        # clean_id -> fms_id
+        fms_id_map = {}
+        # usk -> clean_id
+        announce_map = {}
+        # repo_hash -> clean_id
+        update_map = {}
+
+        for clean_id in self.table:
+            table_entry = self.table[clean_id]
+
+            # Backmap to the human readable fms ids
+            fms_id_map[clean_id] = self.get_human_name(clean_id, table_entry)
+
+            # Accumulate all announcements
+            for usk in table_entry[0]:
+                entry = announce_map.get(usk, set([]))
+                entry.add(clean_id)
+                announce_map[usk] = entry
+
+            # Accumulate all updates
+            for usk_hash in table_entry[1]:
+                entry = update_map.get(usk_hash, set([]))
+                entry.add(clean_id)
+                update_map[usk_hash] = entry
+
+        return (fms_id_map, announce_map, update_map)
+
+    def get_human_name(self, clean_id, table_entry=None):
+        """ INTERNAL: Return a full FMS id from a clean_id. """
+        ret = None
+        if table_entry is None:
+            table_entry = self.table[clean_id]
+
+        for fms_id in table_entry[2]:
+            fields = fms_id.split('@')
+            if len(fields[0].strip()) > 0:
+                ret = fms_id
+                break # break inner loop.
+        if ret is None:
+            # REDFLAG: Nail down when this can happen.
+            print "??? saw an fms id with no human readable part ???"
+            print list(table_entry[2])[0]
+            ret = list(table_entry[2])[0]
+        return ret
+
+    # changed, untrusted
+    def get_updated(self, trust_map, version_table):
+        """ Returns trusted and untrusted changes with respect to
+            the version table. """
+
+        clean_trust = strip_names(trust_map)
+        # usk_hash -> index
+        max_trusted = {}
+        # usk_hash -> (index, fms_id)
+        max_untrusted = {}
+        for clean_id in self.table:
+            table_entry = self.table[clean_id]
+            for usk_hash in table_entry[1]:
+                if not usk_hash in version_table:
+                    continue # Not a repo we care about.
+
+                index = table_entry[1][usk_hash]
+                if index <= version_table[usk_hash]:
+                    continue # Not news. Already know about that index.
+
+                if (clean_id in clean_trust and
+                    usk_hash in clean_trust[clean_id]):
+                    # Trusted update
+                    if not usk_hash in max_trusted:
+                        max_trusted[usk_hash] = index
+                    elif index > max_trusted[usk_hash]:
+                        max_trusted[usk_hash] = index
+                else:
+                    # Untrusted update
+                    fms_id = self.get_human_name(clean_id, table_entry)
+                    if not usk_hash in max_untrusted:
+                        max_untrusted[usk_hash] = (index, fms_id)
+                    elif index > max_untrusted[usk_hash]:
+                        max_untrusted[usk_hash] = (index, fms_id)
+
+        changed = {}
+        untrusted = {}
+        for usk_hash in version_table:
+            if usk_hash in max_trusted:
+                changed[usk_hash] = max_trusted[usk_hash]
+
+            if usk_hash in max_untrusted:
+                if usk_hash in changed:
+                    if max_untrusted[usk_hash][0] > changed[usk_hash]:
+                        # There was a trusted update, but the untrusted one
+                        # was higher.
+                        untrusted[usk_hash] = max_untrusted[usk_hash]
+                else:
+                    # No trusted updated
+                    untrusted[usk_hash] =  max_untrusted[usk_hash]
+
+        # changed is usk_hash->index
+        # untrusted is usk_hash->(index, fms_id)
+        return (changed, untrusted)
+
+def show_table(parser, out_func):
+    """ Dump the announcements and updates in a human readable format. """
+    fms_id_map, announce_map, update_map = parser.invert_table()
+
+    usks = announce_map.keys()
+    usks.sort()
+
+    for usk in usks:
+        usk_hash = get_usk_hash(usk)
+        out_func("USK Hash: %s\n" % usk_hash)
+        out_func("USK: %s\n" % usk)
+        out_func("Announced by:\n")
+        for clean_id in announce_map[usk]:
+            out_func("   %s\n" % fms_id_map[clean_id])
+        out_func("Updated by:\n")
+        for clean_id in update_map[usk_hash]:
+            out_func("   %i:%s\n" % (parser.table[clean_id][1][usk_hash],
+                                     fms_id_map[clean_id]))
+        out_func("\n")
 ############################################################
 
 DEFAULT_SUBJECT = 'Ignore'
@@ -409,31 +475,8 @@ Group  : %s
 
 def smoke_test():
     """ Smoke test the functions in this module. """
-    #    trust_map = {'djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks':
-    #                 ('be68e8feccdd', ),}
 
-
-    trust_map = {'falafel@IxVqeqM0LyYdTmYAf5z49SJZUxr7NtQkOqVYG0hvITw':
-                 ('1' * 12, ),
-                 'SDiZ@17fy9sQtAvZI~nwDt5xXJkTZ9KlXon1ucEakK0vOFTc':
-                 ('2' * 12, ),
-                 }
-
-    parser = USKIndexUpdateParser(trust_map)
-    recv_msgs('127.0.0.1', 11119, parser, ('test',))
-    print
-    print "fms updates:"
-    print parser.updated()
-    print
-    print
-    parser = USKAnnouncementParser(trust_map)
-    recv_msgs('127.0.0.1', 11119, parser, ('test',))
-    print
-    print "fms announcements:"
-    print parser.usks
-    print
-    print
-
+    # REDFLAG: tests for USKNotificationParser ???
     values0 = ((('be68e8feccdd', 23), ('e246cc31bc42', 3)),
                ('USK@kRM~jJVREwnN2qnA8R0Vt8HmpfRzBZ0j4rHC2cQ-0hw,'
                 + '2xcoQVdQLyqfTpF2DpkdUIbHFCeL4W~2X1phUYymnhM,AQACAAE/'
diff --git a/infocalypse/fmscmds.py b/infocalypse/fmscmds.py
--- a/infocalypse/fmscmds.py
+++ b/infocalypse/fmscmds.py
@@ -19,68 +19,86 @@
 
     Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
 """
+from mercurial import util
 
 from fcpclient import get_usk_hash
 
-from fms import USKAnnouncementParser, USKIndexUpdateParser, recv_msgs, \
-     to_msg_string, MSG_TEMPLATE, send_msgs
+from knownrepos import KNOWN_REPOS
 
-from config import Config
+from fms import recv_msgs, to_msg_string, MSG_TEMPLATE, send_msgs, \
+     USKNotificationParser, show_table
+
+from config import Config, trust_id_for_repo, untrust_id_for_repo, known_hashes
 from infcmds import do_key_setup, setup, cleanup
 
 def handled_list(ui_, params, stored_cfg):
-    """ INTERNAL: Helper function to simplify execute_fmsread. """
-    if params['FMSREAD'] != 'list' and params['FMSREAD'] != 'listall':
+    """ INTERNAL: HACKED"""
+    if params['FMSREAD'] != 'listall' and params['FMSREAD'] != 'list':
         return False
 
-    if params['FMSREAD'] == 'listall':
-        parser = USKAnnouncementParser(None, True)
-        if params['VERBOSITY'] >= 2:
-            ui_.status('Listing all repo USKs.\n')
-    else:
+    trust_map = None
+    if params['FMSREAD'] == 'list':
         trust_map = stored_cfg.fmsread_trust_map.copy() # paranoid copy
-        if params['VERBOSITY'] >= 2:
-            fms_ids = trust_map.keys()
-            fms_ids.sort()
-            ui_.status(("Only listing repo USKs from trusted "
-                            + "fms IDs:\n%s\n\n") % '\n'.join(fms_ids))
-        parser = USKAnnouncementParser(trust_map, True)
+        fms_ids = trust_map.keys()
+        fms_ids.sort()
+        ui_.status(("Only listing repo USKs from trusted "
+                    + "FMS IDs:\n   %s\n\n") % '\n   '.join(fms_ids))
 
+    parser = USKNotificationParser(trust_map)
+    parser.add_default_repos(KNOWN_REPOS)
     recv_msgs(stored_cfg.defaults['FMS_HOST'],
               stored_cfg.defaults['FMS_PORT'],
               parser,
               stored_cfg.fmsread_groups)
-
-    if len(parser.usks) == 0:
-        ui_.status("No USKs found.\n")
-        return True
-
-    ui_.status("\n")
-    for usk in parser.usks:
-        usk_entry = parser.usks[usk]
-        ui_.status("USK Hash: %s\n%s\n%s\n\n" %
-                   (get_usk_hash(usk), usk,
-                    '\n'.join(usk_entry)))
+    show_table(parser, ui_.status)
 
     return True
 
-def dump_trust_map(ui_, params, trust_map):
+def dump_trust_map(ui_, params, trust_map, force=False):
     """ Show verbose trust map information. """
-    if params['VERBOSITY'] < 2:
+    if  not force and params['VERBOSITY'] < 2:
         return
 
-    if not params['REQUEST_URI'] is None:
-        ui_.status("USK Hash: %s\n" % get_usk_hash(params['REQUEST_URI']))
+    if not force and not params['REQUEST_URI'] is None:
+        ui_.status("USK hash for local repository: %s\n" %
+                   get_usk_hash(params['REQUEST_URI']))
     fms_ids = trust_map.keys()
     fms_ids.sort()
     ui_.status("Update Trust Map:\n")
     for fms_id in fms_ids:
-        ui_.status("   %s: %s\n" % (fms_id,
-                                    ' '.join(trust_map[fms_id])))
+        ui_.status("   %s\n      %s\n" % (fms_id,
+                                         '\n      '.join(trust_map[fms_id])))
     ui_.status("\n")
 
+def handled_trust_cmd(ui_, params, stored_cfg):
+    """ INTERNAL: Handle --trust, --untrust and --showtrust. """
+    if params['FMSREAD'] == 'trust':
+        if trust_id_for_repo(stored_cfg.fmsread_trust_map,
+                             params['FMSREAD_FMSID'],
+                             params['FMSREAD_HASH']):
+            ui_.status("Updated the trust map.\n")
+            Config.to_file(stored_cfg)
+        return True
+    elif params['FMSREAD'] == 'untrust':
+        if untrust_id_for_repo(stored_cfg.fmsread_trust_map,
+                               params['FMSREAD_FMSID'],
+                               params['FMSREAD_HASH']):
+            ui_.status("Updated the trust map.\n")
+            Config.to_file(stored_cfg)
+        return True
+
+    elif params['FMSREAD'] == 'showtrust':
+        dump_trust_map(ui_, params, stored_cfg.fmsread_trust_map, True)
+        return True
+
+    return False
+
 def execute_fmsread(ui_, params, stored_cfg):
     """ Run the fmsread command. """
+
+    if handled_trust_cmd(ui_, params, stored_cfg):
+        return
+
     if params['VERBOSITY'] >= 2:
         ui_.status(('Connecting to fms on %s:%i\n'
                     + 'Searching groups: %s\n') %
@@ -93,30 +111,34 @@ def execute_fmsread(ui_, params, stored_
         return
 
     # Updating Repo USK indices for repos which are
-    # listed int the fmsread_trust_map section of the
+    # listed in the fmsread_trust_map section of the
     # config file.
     trust_map = stored_cfg.fmsread_trust_map.copy() # paranoid copy
 
     dump_trust_map(ui_, params, trust_map)
 
     ui_.status("Raking through fms messages. This may take a while...\n")
-    parser = USKIndexUpdateParser(trust_map, True)
+    parser = USKNotificationParser()
     recv_msgs(stored_cfg.defaults['FMS_HOST'],
               stored_cfg.defaults['FMS_PORT'],
               parser,
               stored_cfg.fmsread_groups)
-    changed = parser.updated(stored_cfg.version_table)
 
-    if params['VERBOSITY'] >= 2:
-        if parser.untrusted and len(parser.untrusted) > 0:
-            text = 'Skipped Untrusted Updates:\n'
-            for usk_hash in parser.untrusted:
-                text += usk_hash + ':\n'
-                fms_ids = parser.untrusted[usk_hash]
-                for fms_id in fms_ids:
-                    text += '   ' + fms_id + '\n'
-            text += '\n'
-            ui_.status(text)
+    # IMPORTANT: Must include versions that are in the trust map
+    #            but which we haven't seen before.
+    full_version_table = stored_cfg.version_table.copy()
+    for usk_hash in known_hashes(trust_map):
+        if not usk_hash in full_version_table:
+            full_version_table[usk_hash] = None # works
+
+    changed, untrusted = parser.get_updated(trust_map, full_version_table)
+
+    if params['VERBOSITY'] >= 2 and len(untrusted) > 0:
+        text = 'Skipped untrusted updates:\n'
+        for usk_hash in untrusted:
+            text += "   %i:%s\n" % (untrusted[usk_hash], usk_hash)
+        text += '\n'
+        ui_.status(text)
 
     if len(changed) == 0:
         ui_.status('No updates found.\n')
@@ -221,3 +243,106 @@ def execute_fmsnotify(ui_, repo, params,
     finally:
         cleanup(update_sm)
 
+def check_trust_map(ui_, stored_cfg, repo_hash, notifiers, trusted_notifiers):
+    """ INTERNAL: Function to interactively update the trust map. """
+    if len(trusted_notifiers) > 0:
+        return
+    ui_.warn("\nYou MUST trust at least one FMS Id to "
+             + "provide update notifications.\n\n")
+
+    added = False
+    fms_ids = notifiers.keys()
+    fms_ids.sort()
+
+    done = False
+    for fms_id in fms_ids:
+        if done:
+            break
+        ui_.status("Trust notifications from %s\n" % fms_id)
+        while not done:
+            result = ui_.prompt("(y)es, (n)o, (d)one, (a)bort?").lower()
+            if result is None:
+                raise util.Abort("Interactive input required.")
+            elif result == 'y':
+                trust_id_for_repo(stored_cfg.fmsread_trust_map, fms_id,
+                                  repo_hash)
+                added = True
+                break
+            elif result == 'n':
+                break
+            elif result == 'd':
+                done = True
+                break
+            elif result == 'a':
+                raise util.Abort("User aborted editing trust map.")
+
+    if not added:
+        raise util.Abort("No trusted notifiers!")
+
+    Config.to_file(stored_cfg)
+    ui_.status("Saved updated config file.\n\n")
+
+def get_uri_from_hash(ui_, dummy, params, stored_cfg):
+    """ Use FMS to get the URI for a repo hash. """
+    if params['VERBOSITY'] >= 2:
+        ui_.status(('Connecting to fms on %s:%i\n'
+                    + 'Searching groups: %s\n') %
+                   (stored_cfg.defaults['FMS_HOST'],
+                    stored_cfg.defaults['FMS_PORT'],
+                    ' '.join(stored_cfg.fmsread_groups)))
+    trust_map = None
+    if params['FMSREAD_ONLYTRUSTED']:
+        # HACK to deal with spam of the announcement group.'
+        trust_map = stored_cfg.fmsread_trust_map.copy() # paranoid copy
+        fms_ids = trust_map.keys()
+        fms_ids.sort()
+        ui_.status(("Only using announcements from trusted "
+                    + "FMS IDs:\n   %s\n\n") % '\n   '.join(fms_ids))
+
+    parser = USKNotificationParser(trust_map)
+    parser.add_default_repos(KNOWN_REPOS)
+    ui_.status("Raking through fms messages. This may take a while...\n")
+    recv_msgs(stored_cfg.defaults['FMS_HOST'],
+              stored_cfg.defaults['FMS_PORT'],
+              parser,
+              stored_cfg.fmsread_groups)
+
+    target_hash = params['FMSREAD_HASH']
+    target_usk = None
+    fms_id_map, announce_map, update_map = parser.invert_table()
+
+    # Find URI
+    for usk in announce_map:
+        if target_hash == get_usk_hash(usk):
+            # We don't care who announced. The hash matches.
+            target_usk = usk
+            break
+
+    if target_usk is None:
+        raise util.Abort(("No announcement found for [%s]. "
+                          +"Use --uri to set the URI.") % target_hash)
+
+    if params['VERBOSITY'] >= 2:
+        ui_.status("Found URI announcement:\n%s\n" % target_usk)
+
+    trusted_notifiers = stored_cfg.trusted_notifiers(target_hash)
+
+    notifiers = {}
+    for clean_id in update_map[target_hash]:
+        notifiers[fms_id_map[clean_id]] = parser.table[clean_id][1][target_hash]
+
+    fms_ids = notifiers.keys()
+    fms_ids.sort()
+
+    ui_.status("Found Updates:\n")
+    for fms_id in fms_ids:
+        if fms_id in trusted_notifiers:
+            value = "trusted"
+        else:
+            value = "untrusted"
+        ui_.status("   [%s]:%i:%s\n" % (value, notifiers[fms_id], fms_id))
+
+    check_trust_map(ui_, stored_cfg, target_hash, notifiers, trusted_notifiers)
+
+    return target_usk
+
diff --git a/infocalypse/infcmds.py b/infocalypse/infcmds.py
--- a/infocalypse/infcmds.py
+++ b/infocalypse/infcmds.py
@@ -44,6 +44,7 @@ from updatesm import UpdateStateMachine,
      INSERTING_URI, FAILING, REQUESTING_URI_4_COPY, CANCELING, CleaningUp
 
 from config import Config, DEFAULT_CFG_PATH, FORMAT_VERSION, normalize
+
 from knownrepos import DEFAULT_TRUST, DEFAULT_GROUPS
 
 DEFAULT_PARAMS = {
@@ -668,10 +669,16 @@ def execute_pull(ui_, repo, params, stor
         if not params['NO_SEARCH'] and is_usk_file(params['REQUEST_URI']):
             index = stored_cfg.get_index(params['REQUEST_URI'])
             if not index is None:
-                # Update index to the latest known value
-                # for the --uri case.
-                params['REQUEST_URI'] = get_usk_for_usk_version(
-                    params['REQUEST_URI'], index)
+                if index >= get_version(params['REQUEST_URI']):
+                    # Update index to the latest known value
+                    # for the --uri case.
+                    params['REQUEST_URI'] = get_usk_for_usk_version(
+                        params['REQUEST_URI'], index)
+                else:
+                    ui_.status(("Cached index [%i] < index in USK [%i].  "
+                                + "Using the index from the USK.\n"
+                                + "You're sure that index exists, right?\n") %
+                               (index, get_version(params['REQUEST_URI'])))
 
         update_sm = setup(ui_, repo, params, stored_cfg)
         ui_.status("%sRequest URI:\n%s\n" % (is_redundant(params[
@@ -693,12 +700,15 @@ def execute_pull(ui_, repo, params, stor
     finally:
         cleanup(update_sm)
 
-NO_INFO_FMT = """There's no stored information about that USK.
+NO_INFO_FMT = """There's no stored information about this USK.
 USK hash: %s
 """
 
 INFO_FMT = """USK hash: %s
-index   : %i
+Index   : %i
+
+Trusted Notifiers:
+%s
 
 Request URI:
 %s
@@ -724,8 +734,14 @@ def execute_info(ui_, params, stored_cfg
     # fix index
     request_uri = get_usk_for_usk_version(request_uri, max_index)
 
+    trusted = stored_cfg.trusted_notifiers(usk_hash)
+    if not trusted:
+        trusted = '   None'
+    else:
+        trusted = '   ' + '\n   '.join(trusted)
+
     ui_.status(INFO_FMT %
-               (usk_hash, max_index or -1, request_uri, insert_uri))
+               (usk_hash, max_index or -1, trusted, request_uri, insert_uri))
 
 def setup_tmp_dir(ui_, tmp):
     """ INTERNAL: Setup the temp directory. """
diff --git a/infocalypse/knownrepos.py b/infocalypse/knownrepos.py
--- a/infocalypse/knownrepos.py
+++ b/infocalypse/knownrepos.py
@@ -25,15 +25,18 @@
 from fcpclient import get_usk_hash
 
 # Not sure about this. Flat text file instead?
+INFOCALYPSE_INDEX = 38
+FRED_STAGING_INDEX = 96
+
 KNOWN_REPOS = (
     ('djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks',
      'USK@kRM~jJVREwnN2qnA8R0Vt8HmpfRzBZ0j4rHC2cQ-0hw,'
      + '2xcoQVdQLyqfTpF2DpkdUIbHFCeL4W~2X1phUYymnhM,AQACAAE/'
-     + 'infocalypse.hgext.R1/31'), # This code.
+     + 'infocalypse.hgext.R1/%i' % INFOCALYPSE_INDEX), # This code.
     ('djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks',
      'USK@kRM~jJVREwnN2qnA8R0Vt8HmpfRzBZ0j4rHC2cQ-0hw,'
      + '2xcoQVdQLyqfTpF2DpkdUIbHFCeL4W~2X1phUYymnhM,AQACAAE/'
-     + 'fred_staging.R1/24'), # Experimental git->hg mirror
+     + 'fred_staging.R1/%i' % FRED_STAGING_INDEX), # Experimental git->hg mirror
     )
 
 
diff --git a/infocalypse/validate.py b/infocalypse/validate.py
new file mode 100644
--- /dev/null
+++ b/infocalypse/validate.py
@@ -0,0 +1,61 @@
+""" Helper functions to validate input from fms and config file.
+
+    Copyright (C) 2009 Darrell Karbott
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public
+    License as published by the Free Software Foundation; either
+    version 2.0 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    General Public License for more details.
+
+    You should have received a copy of the GNU General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+"""
+
+HEX_CHARS = frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+                       'a', 'b', 'c', 'd', 'e', 'f'])
+
+# Really no library function to do this?
+# REQUIRES: Lowercase!
+def is_hex_string(value, length=12):
+    """ Returns True if value is a lowercase hex digit string,
+        False otherwise. """
+    if not length is None:
+        if len(value) != length:
+            return False
+
+    for char in value:
+        if not char in HEX_CHARS:
+            return False
+    return True
+
+# http://wiki.freenetproject.org/Base64
+FREENET_BASE64_CHARS = frozenset(
+    [ chr(c) for c in
+      (range(ord('0'), ord('9') + 1)
+       + range(ord('a'), ord('z') + 1)
+       + range(ord('A'), ord('Z') + 1)
+       + [ord('~'), ord('-')])
+      ])
+
+def is_fms_id(value):
+    """ Returns True if value looks like a plausible FMS id."""
+    fields = value.split('@')
+    if len(fields) != 2:
+        return False
+
+    # REDFLAG: Faster way? Does it matter?
+    for character in fields[1]:
+        if not character in FREENET_BASE64_CHARS:
+            return False
+
+    return True
+
+