]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/uri.py
uri.py: fix two interface violations in verifier URI classes. refs #1474
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / uri.py
index 1d09e62d0a1b1dba5e5c247e1a20be3350ebd965..62bf9b9aabd1708bd11dbc2cc3bb708255b15bb0 100644 (file)
@@ -2,48 +2,67 @@
 import re, urllib
 from zope.interface import implements
 from twisted.python.components import registerAdapter
-from allmydata import storage
+from allmydata.storage.server import si_a2b, si_b2a
 from allmydata.util import base32, hashutil
-from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IVerifierURI, \
-     IMutableFileURI, INewDirectoryURI, IReadonlyNewDirectoryURI
+from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IImmutableFileURI, \
+    IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI, \
+    MustBeDeepImmutableError, MustBeReadonlyError, CapConstraintError
 
-# the URI shall be an ascii representation of the file. It shall contain
-# enough information to retrieve and validate the contents. It shall be
-# expressed in a limited character set (namely [TODO]).
+class BadURIError(CapConstraintError):
+    pass
+
+# The URI shall be an ASCII representation of a reference to the file/directory.
+# It shall contain enough information to retrieve and validate the contents.
+# It shall be expressed in a limited character set (currently base32 plus ':' and
+# capital letters, but future URIs might use a larger charset).
+
+# TODO:
+#  - rename all of the *URI classes/interfaces to *Cap
+#  - make variable and method names consistently use _uri for an URI string,
+#    and _cap for a Cap object (decoded URI)
+#  - remove the human_encoding methods?
 
 BASE32STR_128bits = '(%s{25}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_3bits)
 BASE32STR_256bits = '(%s{51}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_1bits)
 
 SEP='(?::|%3A)'
 NUMBER='([0-9]+)'
+NUMBER_IGNORE='(?:[0-9]+)'
+OPTIONAL_EXTENSION_FIELD = '(' + SEP + '[0-9' + SEP + ']+|)'
 
-# URIs (soon to be renamed "caps") are always allowed to come with a leading
-# 'http://127.0.0.1:8123/uri/' that will be ignored.
-OPTIONALHTTPLEAD=r'(?:https?://(?:127.0.0.1|localhost):8123/uri/)?'
+# "human-encoded" URIs are allowed to come with a leading
+# 'http://127.0.0.1:(8123|3456)/uri/' that will be ignored.
+# Note that nothing in the Tahoe code currently uses the human encoding.
+OPTIONALHTTPLEAD=r'(?:https?://(?:[^:/]+)(?::%s)?/uri/)?' % NUMBER_IGNORE
 
 
 class _BaseURI:
     def __hash__(self):
         return self.to_string().__hash__()
+
     def __eq__(self, them):
         if isinstance(them, _BaseURI):
             return self.to_string() == them.to_string()
         else:
             return False
+
     def __ne__(self, them):
         if isinstance(them, _BaseURI):
             return self.to_string() != them.to_string()
         else:
             return True
+
     def to_human_encoding(self):
-        return 'http://127.0.0.1:8123/uri/'+self.to_string()
+        return 'http://127.0.0.1:3456/uri/'+self.to_string()
 
     def get_storage_index(self):
         return self.storage_index
 
+
 class CHKFileURI(_BaseURI):
-    implements(IURI, IFileURI)
+    implements(IURI, IImmutableFileURI)
 
+    BASE_STRING='URI:CHK:'
     STRING_RE=re.compile('^URI:CHK:'+BASE32STR_128bits+':'+
                          BASE32STR_256bits+':'+NUMBER+':'+NUMBER+':'+NUMBER+
                          '$')
@@ -59,21 +78,22 @@ class CHKFileURI(_BaseURI):
         self.total_shares = total_shares
         self.size = size
         self.storage_index = hashutil.storage_index_hash(self.key)
-        assert len(self.storage_index) == 16
-        self.storage_index = hashutil.storage_index_hash(key)
-        assert len(self.storage_index) == 16 # sha256 hash truncated to 128
+        if not len(self.storage_index) == 16: # sha256 hash truncated to 128
+            raise BadURIError("storage index must be 16 bytes long")
 
     @classmethod
     def init_from_human_encoding(cls, uri):
         mo = cls.HUMAN_RE.search(uri)
-        assert mo, uri
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
 
     @classmethod
     def init_from_string(cls, uri):
         mo = cls.STRING_RE.search(uri)
-        assert mo, uri
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
 
@@ -91,15 +111,17 @@ class CHKFileURI(_BaseURI):
 
     def is_readonly(self):
         return True
+
     def is_mutable(self):
         return False
+
     def get_readonly(self):
         return self
 
     def get_size(self):
         return self.size
 
-    def get_verifier(self):
+    def get_verify_cap(self):
         return CHKFileVerifierURI(storage_index=self.storage_index,
                                   uri_extension_hash=self.uri_extension_hash,
                                   needed_shares=self.needed_shares,
@@ -109,6 +131,7 @@ class CHKFileURI(_BaseURI):
 class CHKFileVerifierURI(_BaseURI):
     implements(IVerifierURI)
 
+    BASE_STRING='URI:CHK-Verifier:'
     STRING_RE=re.compile('^URI:CHK-Verifier:'+BASE32STR_128bits+':'+
                          BASE32STR_256bits+':'+NUMBER+':'+NUMBER+':'+NUMBER)
     HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'CHK-Verifier'+SEP+
@@ -127,15 +150,17 @@ class CHKFileVerifierURI(_BaseURI):
     @classmethod
     def init_from_human_encoding(cls, uri):
         mo = cls.HUMAN_RE.search(uri)
-        assert mo, uri
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
 
     @classmethod
     def init_from_string(cls, uri):
         mo = cls.STRING_RE.search(uri)
-        assert mo, (uri, cls, cls.STRING_RE)
-        return cls(storage.si_a2b(mo.group(1)), base32.a2b(mo.group(2)),
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+        return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)),
                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
 
     def to_string(self):
@@ -144,33 +169,49 @@ class CHKFileVerifierURI(_BaseURI):
         assert isinstance(self.size, (int,long))
 
         return ('URI:CHK-Verifier:%s:%s:%d:%d:%d' %
-                (storage.si_b2a(self.storage_index),
+                (si_b2a(self.storage_index),
                  base32.b2a(self.uri_extension_hash),
                  self.needed_shares,
                  self.total_shares,
                  self.size))
 
+    def is_readonly(self):
+        return True
+
+    def is_mutable(self):
+        return False
+
+    def get_readonly(self):
+        return self
+
+    def get_verify_cap(self):
+        return self
+
 
 class LiteralFileURI(_BaseURI):
-    implements(IURI, IFileURI)
+    implements(IURI, IImmutableFileURI)
 
+    BASE_STRING='URI:LIT:'
     STRING_RE=re.compile('^URI:LIT:'+base32.BASE32STR_anybytes+'$')
     HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'LIT'+SEP+base32.BASE32STR_anybytes+'$')
 
     def __init__(self, data=None):
         if data is not None:
+            assert isinstance(data, str)
             self.data = data
 
     @classmethod
     def init_from_human_encoding(cls, uri):
         mo = cls.HUMAN_RE.search(uri)
-        assert mo, uri
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)))
 
     @classmethod
     def init_from_string(cls, uri):
         mo = cls.STRING_RE.search(uri)
-        assert mo, uri
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)))
 
     def to_string(self):
@@ -178,20 +219,24 @@ class LiteralFileURI(_BaseURI):
 
     def is_readonly(self):
         return True
+
     def is_mutable(self):
         return False
+
     def get_readonly(self):
         return self
+
     def get_storage_index(self):
         return None
 
-    def get_verifier(self):
+    def get_verify_cap(self):
         # LIT files need no verification, all the data is present in the URI
         return None
 
     def get_size(self):
         return len(self.data)
 
+
 class WriteableSSKFileURI(_BaseURI):
     implements(IURI, IMutableFileURI)
 
@@ -211,13 +256,15 @@ class WriteableSSKFileURI(_BaseURI):
     @classmethod
     def init_from_human_encoding(cls, uri):
         mo = cls.HUMAN_RE.search(uri)
-        assert mo, uri
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
 
     @classmethod
     def init_from_string(cls, uri):
         mo = cls.STRING_RE.search(uri)
-        assert mo, (uri, cls)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
 
     def to_string(self):
@@ -232,15 +279,27 @@ class WriteableSSKFileURI(_BaseURI):
     def abbrev(self):
         return base32.b2a(self.writekey[:5])
 
+    def abbrev_si(self):
+        return base32.b2a(self.storage_index)[:5]
+
     def is_readonly(self):
         return False
+
     def is_mutable(self):
         return True
+
     def get_readonly(self):
         return ReadonlySSKFileURI(self.readkey, self.fingerprint)
-    def get_verifier(self):
+
+    def get_verify_cap(self):
         return SSKVerifierURI(self.storage_index, self.fingerprint)
 
+    def get_extension_params(self):
+        return []
+
+    def set_extension_params(self, params):
+        pass
+
 class ReadonlySSKFileURI(_BaseURI):
     implements(IURI, IMutableFileURI)
 
@@ -257,13 +316,15 @@ class ReadonlySSKFileURI(_BaseURI):
     @classmethod
     def init_from_human_encoding(cls, uri):
         mo = cls.HUMAN_RE.search(uri)
-        assert mo, uri
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
 
     @classmethod
     def init_from_string(cls, uri):
         mo = cls.STRING_RE.search(uri)
-        assert mo, uri
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
 
     def to_string(self):
@@ -278,15 +339,27 @@ class ReadonlySSKFileURI(_BaseURI):
     def abbrev(self):
         return base32.b2a(self.readkey[:5])
 
+    def abbrev_si(self):
+        return base32.b2a(self.storage_index)[:5]
+
     def is_readonly(self):
         return True
+
     def is_mutable(self):
         return True
+
     def get_readonly(self):
         return self
-    def get_verifier(self):
+
+    def get_verify_cap(self):
         return SSKVerifierURI(self.storage_index, self.fingerprint)
 
+    def get_extension_params(self):
+        return []
+
+    def set_extension_params(self, params):
+        pass
+
 class SSKVerifierURI(_BaseURI):
     implements(IVerifierURI)
 
@@ -302,22 +375,242 @@ class SSKVerifierURI(_BaseURI):
     @classmethod
     def init_from_human_encoding(cls, uri):
         mo = cls.HUMAN_RE.search(uri)
-        assert mo, uri
-        return cls(storage.si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+        return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
 
     @classmethod
     def init_from_string(cls, uri):
         mo = cls.STRING_RE.search(uri)
-        assert mo, (uri, cls)
-        return cls(storage.si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+        return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
 
     def to_string(self):
         assert isinstance(self.storage_index, str)
         assert isinstance(self.fingerprint, str)
-        return 'URI:SSK-Verifier:%s:%s' % (storage.si_b2a(self.storage_index),
+        return 'URI:SSK-Verifier:%s:%s' % (si_b2a(self.storage_index),
                                            base32.b2a(self.fingerprint))
 
-class _NewDirectoryBaseURI(_BaseURI):
+    def is_readonly(self):
+        return True
+
+    def is_mutable(self):
+        return False
+
+    def get_readonly(self):
+        return self
+
+    def get_verify_cap(self):
+        return self
+
+    def get_extension_params(self):
+        return []
+
+    def set_extension_params(self, params):
+        pass
+
+class WriteableMDMFFileURI(_BaseURI):
+    implements(IURI, IMutableFileURI)
+
+    BASE_STRING='URI:MDMF:'
+    STRING_RE=re.compile('^'+BASE_STRING+BASE32STR_128bits+':'+BASE32STR_256bits+OPTIONAL_EXTENSION_FIELD+'$')
+    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'MDMF'+SEP+BASE32STR_128bits+SEP+BASE32STR_256bits+OPTIONAL_EXTENSION_FIELD+'$')
+
+    def __init__(self, writekey, fingerprint, params=[]):
+        self.writekey = writekey
+        self.readkey = hashutil.ssk_readkey_hash(writekey)
+        self.storage_index = hashutil.ssk_storage_index_hash(self.readkey)
+        assert len(self.storage_index) == 16
+        self.fingerprint = fingerprint
+        self.extension = params
+
+    @classmethod
+    def init_from_human_encoding(cls, uri):
+        mo = cls.HUMAN_RE.search(uri)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+        params = filter(lambda x: x != '', re.split(SEP, mo.group(3)))
+        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)), params)
+
+    @classmethod
+    def init_from_string(cls, uri):
+        mo = cls.STRING_RE.search(uri)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+        params = mo.group(3)
+        params = filter(lambda x: x != '', params.split(":"))
+        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)), params)
+
+    def to_string(self):
+        assert isinstance(self.writekey, str)
+        assert isinstance(self.fingerprint, str)
+        ret = 'URI:MDMF:%s:%s' % (base32.b2a(self.writekey),
+                                  base32.b2a(self.fingerprint))
+        if self.extension:
+            ret += ":"
+            ret += ":".join(self.extension)
+
+        return ret
+
+    def __repr__(self):
+        return "<%s %s>" % (self.__class__.__name__, self.abbrev())
+
+    def abbrev(self):
+        return base32.b2a(self.writekey[:5])
+
+    def abbrev_si(self):
+        return base32.b2a(self.storage_index)[:5]
+
+    def is_readonly(self):
+        return False
+
+    def is_mutable(self):
+        return True
+
+    def get_readonly(self):
+        return ReadonlyMDMFFileURI(self.readkey, self.fingerprint, self.extension)
+
+    def get_verify_cap(self):
+        return MDMFVerifierURI(self.storage_index, self.fingerprint, self.extension)
+
+    def get_extension_params(self):
+        return self.extension
+
+    def set_extension_params(self, params):
+        params = map(str, params)
+        self.extension = params
+
+class ReadonlyMDMFFileURI(_BaseURI):
+    implements(IURI, IMutableFileURI)
+
+    BASE_STRING='URI:MDMF-RO:'
+    STRING_RE=re.compile('^' +BASE_STRING+BASE32STR_128bits+':'+BASE32STR_256bits+OPTIONAL_EXTENSION_FIELD+'$')
+    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'MDMF-RO'+SEP+BASE32STR_128bits+SEP+BASE32STR_256bits+OPTIONAL_EXTENSION_FIELD+'$')
+
+    def __init__(self, readkey, fingerprint, params=[]):
+        self.readkey = readkey
+        self.storage_index = hashutil.ssk_storage_index_hash(self.readkey)
+        assert len(self.storage_index) == 16
+        self.fingerprint = fingerprint
+        self.extension = params
+
+    @classmethod
+    def init_from_human_encoding(cls, uri):
+        mo = cls.HUMAN_RE.search(uri)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+        params = mo.group(3)
+        params = filter(lambda x: x!= '', re.split(SEP, params))
+        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)), params)
+
+    @classmethod
+    def init_from_string(cls, uri):
+        mo = cls.STRING_RE.search(uri)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+
+        params = mo.group(3)
+        params = filter(lambda x: x != '', params.split(":"))
+        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)), params)
+
+    def to_string(self):
+        assert isinstance(self.readkey, str)
+        assert isinstance(self.fingerprint, str)
+        ret = 'URI:MDMF-RO:%s:%s' % (base32.b2a(self.readkey),
+                                     base32.b2a(self.fingerprint))
+        if self.extension:
+            ret += ":"
+            ret += ":".join(self.extension)
+
+        return ret
+
+    def __repr__(self):
+        return "<%s %s>" % (self.__class__.__name__, self.abbrev())
+
+    def abbrev(self):
+        return base32.b2a(self.readkey[:5])
+
+    def abbrev_si(self):
+        return base32.b2a(self.storage_index)[:5]
+
+    def is_readonly(self):
+        return True
+
+    def is_mutable(self):
+        return True
+
+    def get_readonly(self):
+        return self
+
+    def get_verify_cap(self):
+        return MDMFVerifierURI(self.storage_index, self.fingerprint, self.extension)
+
+    def get_extension_params(self):
+        return self.extension
+
+    def set_extension_params(self, params):
+        params = map(str, params)
+        self.extension = params
+
+class MDMFVerifierURI(_BaseURI):
+    implements(IVerifierURI)
+
+    BASE_STRING='URI:MDMF-Verifier:'
+    STRING_RE=re.compile('^'+BASE_STRING+BASE32STR_128bits+':'+BASE32STR_256bits+OPTIONAL_EXTENSION_FIELD+'$')
+    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'MDMF-Verifier'+SEP+BASE32STR_128bits+SEP+BASE32STR_256bits+OPTIONAL_EXTENSION_FIELD+'$')
+
+    def __init__(self, storage_index, fingerprint, params=[]):
+        assert len(storage_index) == 16
+        self.storage_index = storage_index
+        self.fingerprint = fingerprint
+        self.extension = params
+
+    @classmethod
+    def init_from_human_encoding(cls, uri):
+        mo = cls.HUMAN_RE.search(uri)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+        params = mo.group(3)
+        params = filter(lambda x: x != '', re.split(SEP, params))
+        return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)), params)
+
+    @classmethod
+    def init_from_string(cls, uri):
+        mo = cls.STRING_RE.search(uri)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
+        params = mo.group(3)
+        params = filter(lambda x: x != '', params.split(":"))
+        return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)), params)
+
+    def to_string(self):
+        assert isinstance(self.storage_index, str)
+        assert isinstance(self.fingerprint, str)
+        ret = 'URI:MDMF-Verifier:%s:%s' % (si_b2a(self.storage_index),
+                                           base32.b2a(self.fingerprint))
+        if self.extension:
+            ret += ':'
+            ret += ":".join(self.extension)
+
+        return ret
+
+    def is_readonly(self):
+        return True
+
+    def is_mutable(self):
+        return False
+
+    def get_readonly(self):
+        return self
+
+    def get_verify_cap(self):
+        return self
+
+    def get_extension_params(self):
+        return self.extension
+
+class _DirectoryBaseURI(_BaseURI):
     implements(IURI, IDirnodeURI)
     def __init__(self, filenode_uri=None):
         self._filenode_uri = filenode_uri
@@ -328,7 +621,8 @@ class _NewDirectoryBaseURI(_BaseURI):
     @classmethod
     def init_from_string(cls, uri):
         mo = cls.BASE_STRING_RE.search(uri)
-        assert mo, (uri, cls)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         bits = uri[mo.end():]
         fn = cls.INNER_URI_CLASS.init_from_string(
             cls.INNER_URI_CLASS.BASE_STRING+bits)
@@ -337,7 +631,8 @@ class _NewDirectoryBaseURI(_BaseURI):
     @classmethod
     def init_from_human_encoding(cls, uri):
         mo = cls.BASE_HUMAN_RE.search(uri)
-        assert mo, (uri, cls)
+        if not mo:
+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
         bits = uri[mo.end():]
         while bits and bits[-1] == '/':
             bits = bits[:-1]
@@ -355,20 +650,26 @@ class _NewDirectoryBaseURI(_BaseURI):
     def abbrev(self):
         return self._filenode_uri.to_string().split(':')[2][:5]
 
-    def get_filenode_uri(self):
-        return self._filenode_uri
+    def abbrev_si(self):
+        si = self._filenode_uri.get_storage_index()
+        if si is None:
+            return "<LIT>"
+        return base32.b2a(si)[:5]
 
     def is_mutable(self):
         return True
 
-    def get_verifier(self):
-        return NewDirectoryURIVerifier(self._filenode_uri.get_verifier())
+    def get_filenode_cap(self):
+        return self._filenode_uri
+
+    def get_verify_cap(self):
+        return DirectoryURIVerifier(self._filenode_uri.get_verify_cap())
 
     def get_storage_index(self):
         return self._filenode_uri.get_storage_index()
 
-class NewDirectoryURI(_NewDirectoryBaseURI):
-    implements(INewDirectoryURI)
+class DirectoryURI(_DirectoryBaseURI):
+    implements(IDirectoryURI)
 
     BASE_STRING='URI:DIR2:'
     BASE_STRING_RE=re.compile('^'+BASE_STRING)
@@ -378,16 +679,17 @@ class NewDirectoryURI(_NewDirectoryBaseURI):
     def __init__(self, filenode_uri=None):
         if filenode_uri:
             assert not filenode_uri.is_readonly()
-        _NewDirectoryBaseURI.__init__(self, filenode_uri)
+        _DirectoryBaseURI.__init__(self, filenode_uri)
 
     def is_readonly(self):
         return False
 
     def get_readonly(self):
-        return ReadonlyNewDirectoryURI(self._filenode_uri.get_readonly())
+        return ReadonlyDirectoryURI(self._filenode_uri.get_readonly())
+
 
-class ReadonlyNewDirectoryURI(_NewDirectoryBaseURI):
-    implements(IReadonlyNewDirectoryURI)
+class ReadonlyDirectoryURI(_DirectoryBaseURI):
+    implements(IReadonlyDirectoryURI)
 
     BASE_STRING='URI:DIR2-RO:'
     BASE_STRING_RE=re.compile('^'+BASE_STRING)
@@ -397,7 +699,132 @@ class ReadonlyNewDirectoryURI(_NewDirectoryBaseURI):
     def __init__(self, filenode_uri=None):
         if filenode_uri:
             assert filenode_uri.is_readonly()
-        _NewDirectoryBaseURI.__init__(self, filenode_uri)
+        _DirectoryBaseURI.__init__(self, filenode_uri)
+
+    def is_readonly(self):
+        return True
+
+    def get_readonly(self):
+        return self
+
+
+class _ImmutableDirectoryBaseURI(_DirectoryBaseURI):
+    def __init__(self, filenode_uri=None):
+        if filenode_uri:
+            assert isinstance(filenode_uri, self.INNER_URI_CLASS), filenode_uri
+            assert not filenode_uri.is_mutable()
+        _DirectoryBaseURI.__init__(self, filenode_uri)
+
+    def is_readonly(self):
+        return True
+
+    def is_mutable(self):
+        return False
+
+    def get_readonly(self):
+        return self
+
+
+class ImmutableDirectoryURI(_ImmutableDirectoryBaseURI):
+    BASE_STRING='URI:DIR2-CHK:'
+    BASE_STRING_RE=re.compile('^'+BASE_STRING)
+    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK'+SEP)
+    INNER_URI_CLASS=CHKFileURI
+
+    def get_verify_cap(self):
+        vcap = self._filenode_uri.get_verify_cap()
+        return ImmutableDirectoryURIVerifier(vcap)
+
+
+class LiteralDirectoryURI(_ImmutableDirectoryBaseURI):
+    BASE_STRING='URI:DIR2-LIT:'
+    BASE_STRING_RE=re.compile('^'+BASE_STRING)
+    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-LIT'+SEP)
+    INNER_URI_CLASS=LiteralFileURI
+
+    def get_verify_cap(self):
+        # LIT caps have no verifier, since they aren't distributed
+        return None
+
+
+class MDMFDirectoryURI(_DirectoryBaseURI):
+    implements(IDirectoryURI)
+
+    BASE_STRING='URI:DIR2-MDMF:'
+    BASE_STRING_RE=re.compile('^'+BASE_STRING)
+    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-MDMF'+SEP)
+    INNER_URI_CLASS=WriteableMDMFFileURI
+
+    def __init__(self, filenode_uri=None):
+        if filenode_uri:
+            assert not filenode_uri.is_readonly()
+        _DirectoryBaseURI.__init__(self, filenode_uri)
+
+    def is_readonly(self):
+        return False
+
+    def get_readonly(self):
+        return ReadonlyMDMFDirectoryURI(self._filenode_uri.get_readonly())
+
+    def get_verify_cap(self):
+        return MDMFDirectoryURIVerifier(self._filenode_uri.get_verify_cap())
+
+
+class ReadonlyMDMFDirectoryURI(_DirectoryBaseURI):
+    implements(IReadonlyDirectoryURI)
+
+    BASE_STRING='URI:DIR2-MDMF-RO:'
+    BASE_STRING_RE=re.compile('^'+BASE_STRING)
+    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-MDMF-RO'+SEP)
+    INNER_URI_CLASS=ReadonlyMDMFFileURI
+
+    def __init__(self, filenode_uri=None):
+        if filenode_uri:
+            assert filenode_uri.is_readonly()
+        _DirectoryBaseURI.__init__(self, filenode_uri)
+
+    def is_readonly(self):
+        return True
+
+    def get_readonly(self):
+        return self
+
+    def get_verify_cap(self):
+        return MDMFDirectoryURIVerifier(self._filenode_uri.get_verify_cap())
+
+def wrap_dirnode_cap(filecap):
+    if isinstance(filecap, WriteableSSKFileURI):
+        return DirectoryURI(filecap)
+    if isinstance(filecap, ReadonlySSKFileURI):
+        return ReadonlyDirectoryURI(filecap)
+    if isinstance(filecap, CHKFileURI):
+        return ImmutableDirectoryURI(filecap)
+    if isinstance(filecap, LiteralFileURI):
+        return LiteralDirectoryURI(filecap)
+    if isinstance(filecap, WriteableMDMFFileURI):
+        return MDMFDirectoryURI(filecap)
+    if isinstance(filecap, ReadonlyMDMFFileURI):
+        return ReadonlyMDMFDirectoryURI(filecap)
+    assert False, "cannot interpret as a directory cap: %s" % filecap.__class__
+
+class MDMFDirectoryURIVerifier(_DirectoryBaseURI):
+    implements(IVerifierURI)
+
+    BASE_STRING='URI:DIR2-MDMF-Verifier:'
+    BASE_STRING_RE=re.compile('^'+BASE_STRING)
+    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-MDMF-Verifier'+SEP)
+    INNER_URI_CLASS=MDMFVerifierURI
+
+    def __init__(self, filenode_uri=None):
+        if filenode_uri:
+            assert IVerifierURI.providedBy(filenode_uri)
+        self._filenode_uri = filenode_uri
+
+    def get_filenode_cap(self):
+        return self._filenode_uri
+
+    def is_mutable(self):
+        return False
 
     def is_readonly(self):
         return True
@@ -405,7 +832,8 @@ class ReadonlyNewDirectoryURI(_NewDirectoryBaseURI):
     def get_readonly(self):
         return self
 
-class NewDirectoryURIVerifier(_NewDirectoryBaseURI):
+
+class DirectoryURIVerifier(_DirectoryBaseURI):
     implements(IVerifierURI)
 
     BASE_STRING='URI:DIR2-Verifier:'
@@ -415,67 +843,191 @@ class NewDirectoryURIVerifier(_NewDirectoryBaseURI):
 
     def __init__(self, filenode_uri=None):
         if filenode_uri:
-            filenode_uri = IVerifierURI(filenode_uri)
+            assert IVerifierURI.providedBy(filenode_uri)
         self._filenode_uri = filenode_uri
 
-    def get_filenode_uri(self):
+    def get_filenode_cap(self):
         return self._filenode_uri
 
+    def is_mutable(self):
+        return False
+
+    def is_readonly(self):
+        return True
+
+    def get_readonly(self):
+        return self
+
+
+class ImmutableDirectoryURIVerifier(DirectoryURIVerifier):
+    implements(IVerifierURI)
+    BASE_STRING='URI:DIR2-CHK-Verifier:'
+    BASE_STRING_RE=re.compile('^'+BASE_STRING)
+    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK-VERIFIER'+SEP)
+    INNER_URI_CLASS=CHKFileVerifierURI
+
+
+class UnknownURI:
+    def __init__(self, uri, error=None):
+        self._uri = uri
+        self._error = error
+
+    def to_string(self):
+        return self._uri
+
+    def get_readonly(self):
+        return None
+
+    def get_error(self):
+        return self._error
+
+    def get_verify_cap(self):
+        return None
 
 
-def from_string(s):
-    if s.startswith('URI:CHK:'):
-        return CHKFileURI.init_from_string(s)
-    elif s.startswith('URI:CHK-Verifier:'):
-        return CHKFileVerifierURI.init_from_string(s)
-    elif s.startswith('URI:LIT:'):
-        return LiteralFileURI.init_from_string(s)
-    elif s.startswith('URI:SSK:'):
-        return WriteableSSKFileURI.init_from_string(s)
-    elif s.startswith('URI:SSK-RO:'):
-        return ReadonlySSKFileURI.init_from_string(s)
-    elif s.startswith('URI:SSK-Verifier:'):
-        return SSKVerifierURI.init_from_string(s)
-    elif s.startswith('URI:DIR2:'):
-        return NewDirectoryURI.init_from_string(s)
-    elif s.startswith('URI:DIR2-RO:'):
-        return ReadonlyNewDirectoryURI.init_from_string(s)
-    elif s.startswith('URI:DIR2-Verifier:'):
-        return NewDirectoryURIVerifier.init_from_string(s)
-    else:
-        raise TypeError("unknown URI type: %s.." % s[:12])
-
-registerAdapter(from_string, str, IURI)
+ALLEGED_READONLY_PREFIX = 'ro.'
+ALLEGED_IMMUTABLE_PREFIX = 'imm.'
+
+def from_string(u, deep_immutable=False, name=u"<unknown name>"):
+    if not isinstance(u, str):
+        raise TypeError("unknown URI type: %s.." % str(u)[:100])
+
+    # We allow and check ALLEGED_READONLY_PREFIX or ALLEGED_IMMUTABLE_PREFIX
+    # on all URIs, even though we would only strictly need to do so for caps of
+    # new formats (post Tahoe-LAFS 1.6). URIs that are not consistent with their
+    # prefix are treated as unknown. This should be revisited when we add the
+    # new cap formats. See <http://allmydata.org/trac/tahoe/ticket/833#comment:31>.
+    s = u
+    can_be_mutable = can_be_writeable = not deep_immutable
+    if s.startswith(ALLEGED_IMMUTABLE_PREFIX):
+        can_be_mutable = can_be_writeable = False
+        s = s[len(ALLEGED_IMMUTABLE_PREFIX):]
+    elif s.startswith(ALLEGED_READONLY_PREFIX):
+        can_be_writeable = False
+        s = s[len(ALLEGED_READONLY_PREFIX):]
+
+    error = None
+    kind = "cap"
+    try:
+        if s.startswith('URI:CHK:'):
+            return CHKFileURI.init_from_string(s)
+        elif s.startswith('URI:CHK-Verifier:'):
+            return CHKFileVerifierURI.init_from_string(s)
+        elif s.startswith('URI:LIT:'):
+            return LiteralFileURI.init_from_string(s)
+        elif s.startswith('URI:SSK:'):
+            if can_be_writeable:
+                return WriteableSSKFileURI.init_from_string(s)
+            kind = "URI:SSK file writecap"
+        elif s.startswith('URI:SSK-RO:'):
+            if can_be_mutable:
+                return ReadonlySSKFileURI.init_from_string(s)
+            kind = "URI:SSK-RO readcap to a mutable file"
+        elif s.startswith('URI:SSK-Verifier:'):
+            return SSKVerifierURI.init_from_string(s)
+        elif s.startswith('URI:MDMF:'):
+            if can_be_writeable:
+                return WriteableMDMFFileURI.init_from_string(s)
+            kind = "URI:MDMF file writecap"
+        elif s.startswith('URI:MDMF-RO:'):
+            if can_be_mutable:
+                return ReadonlyMDMFFileURI.init_from_string(s)
+            kind = "URI:MDMF-RO readcap to a mutable file"
+        elif s.startswith('URI:MDMF-Verifier:'):
+            return MDMFVerifierURI.init_from_string(s)
+        elif s.startswith('URI:DIR2:'):
+            if can_be_writeable:
+                return DirectoryURI.init_from_string(s)
+            kind = "URI:DIR2 directory writecap"
+        elif s.startswith('URI:DIR2-RO:'):
+            if can_be_mutable:
+                return ReadonlyDirectoryURI.init_from_string(s)
+            kind = "URI:DIR2-RO readcap to a mutable directory"
+        elif s.startswith('URI:DIR2-Verifier:'):
+            return DirectoryURIVerifier.init_from_string(s)
+        elif s.startswith('URI:DIR2-CHK:'):
+            return ImmutableDirectoryURI.init_from_string(s)
+        elif s.startswith('URI:DIR2-CHK-Verifier:'):
+            return ImmutableDirectoryURIVerifier.init_from_string(s)
+        elif s.startswith('URI:DIR2-LIT:'):
+            return LiteralDirectoryURI.init_from_string(s)
+        elif s.startswith('URI:DIR2-MDMF:'):
+            if can_be_writeable:
+                return MDMFDirectoryURI.init_from_string(s)
+            kind = "URI:DIR2-MDMF directory writecap"
+        elif s.startswith('URI:DIR2-MDMF-RO:'):
+            if can_be_mutable:
+                return ReadonlyMDMFDirectoryURI.init_from_string(s)
+            kind = "URI:DIR2-MDMF-RO readcap to a mutable directory"
+        elif s.startswith('URI:DIR2-MDMF-Verifier:'):
+            return MDMFDirectoryURIVerifier.init_from_string(s)
+        elif s.startswith('x-tahoe-future-test-writeable:') and not can_be_writeable:
+            # For testing how future writeable caps would behave in read-only contexts.
+            kind = "x-tahoe-future-test-writeable: testing cap"
+        elif s.startswith('x-tahoe-future-test-mutable:') and not can_be_mutable:
+            # For testing how future mutable readcaps would behave in immutable contexts.
+            kind = "x-tahoe-future-test-mutable: testing cap"
+        else:
+            return UnknownURI(u)
+
+        # We fell through because a constraint was not met.
+        # Prefer to report the most specific constraint.
+        if not can_be_mutable:
+            error = MustBeDeepImmutableError(kind + " used in an immutable context", name)
+        else:
+            error = MustBeReadonlyError(kind + " used in a read-only context", name)
+
+    except BadURIError, e:
+        error = e
+
+    return UnknownURI(u, error=error)
 
 def is_uri(s):
     try:
-        uri = from_string(s)
+        from_string(s, deep_immutable=False)
         return True
     except (TypeError, AssertionError):
         return False
 
-def from_string_dirnode(s):
-    u = from_string(s)
+def is_literal_file_uri(s):
+    if not isinstance(s, str):
+        return False
+    return (s.startswith('URI:LIT:') or
+            s.startswith(ALLEGED_READONLY_PREFIX + 'URI:LIT:') or
+            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:'))
+
+def has_uri_prefix(s):
+    if not isinstance(s, str):
+        return False
+    return (s.startswith("URI:") or
+            s.startswith(ALLEGED_READONLY_PREFIX + 'URI:') or
+            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:'))
+
+
+# These take the same keyword arguments as from_string above.
+
+def from_string_dirnode(s, **kwargs):
+    u = from_string(s, **kwargs)
     assert IDirnodeURI.providedBy(u)
     return u
 
 registerAdapter(from_string_dirnode, str, IDirnodeURI)
 
-def from_string_filenode(s):
-    u = from_string(s)
+def from_string_filenode(s, **kwargs):
+    u = from_string(s, **kwargs)
     assert IFileURI.providedBy(u)
     return u
 
 registerAdapter(from_string_filenode, str, IFileURI)
 
-def from_string_mutable_filenode(s):
-    u = from_string(s)
+def from_string_mutable_filenode(s, **kwargs):
+    u = from_string(s, **kwargs)
     assert IMutableFileURI.providedBy(u)
     return u
 registerAdapter(from_string_mutable_filenode, str, IMutableFileURI)
 
-def from_string_verifier(s):
-    u = from_string(s)
+def from_string_verifier(s, **kwargs):
+    u = from_string(s, **kwargs)
     assert IVerifierURI.providedBy(u)
     return u
 registerAdapter(from_string_verifier, str, IVerifierURI)