Ticket #833: all-diff.txt

File all-diff.txt, 240.0 KB (added by davidsarah, at 2010-01-23T13:06:14Z)

Diff for everything (code + tests + docs)

Line 
1diff -rN -u old-tahoe/src/allmydata/client.py new-tahoe/src/allmydata/client.py
2--- old-tahoe/src/allmydata/client.py   2010-01-23 12:59:08.664000000 +0000
3+++ new-tahoe/src/allmydata/client.py   2010-01-23 12:59:11.145000000 +0000
4@@ -471,13 +471,16 @@
5     # dirnodes. The first takes a URI and produces a filenode or (new-style)
6     # dirnode. The other three create brand-new filenodes/dirnodes.
7 
8-    def create_node_from_uri(self, writecap, readcap=None):
9-        # this returns synchronously.
10-        return self.nodemaker.create_from_cap(writecap, readcap)
11+    def create_node_from_uri(self, write_uri, read_uri=None, deep_immutable=False, name="<unknown name>"):
12+        # This returns synchronously.
13+        # Note that it does *not* validate the write_uri and read_uri; instead we
14+        # may get an opaque node if there were any problems.
15+        return self.nodemaker.create_from_cap(write_uri, read_uri, deep_immutable=deep_immutable, name=name)
16 
17     def create_dirnode(self, initial_children={}):
18         d = self.nodemaker.create_new_mutable_directory(initial_children)
19         return d
20+
21     def create_immutable_dirnode(self, children, convergence=None):
22         return self.nodemaker.create_immutable_directory(children, convergence)
23 
24diff -rN -u old-tahoe/src/allmydata/control.py new-tahoe/src/allmydata/control.py
25--- old-tahoe/src/allmydata/control.py  2010-01-23 12:59:08.685000000 +0000
26+++ new-tahoe/src/allmydata/control.py  2010-01-23 12:59:11.158000000 +0000
27@@ -5,7 +5,7 @@
28 from twisted.internet import defer
29 from twisted.internet.interfaces import IConsumer
30 from foolscap.api import Referenceable
31-from allmydata.interfaces import RIControlClient
32+from allmydata.interfaces import RIControlClient, IFileNode
33 from allmydata.util import fileutil, mathutil
34 from allmydata.immutable import upload
35 from twisted.python import log
36@@ -67,7 +67,9 @@
37         return d
38 
39     def remote_download_from_uri_to_file(self, uri, filename):
40-        filenode = self.parent.create_node_from_uri(uri)
41+        filenode = self.parent.create_node_from_uri(uri, name=filename)
42+        if not IFileNode.providedBy(filenode):
43+            raise AssertionError("The URI does not reference a file.")
44         c = FileWritingConsumer(filename)
45         d = filenode.read(c)
46         d.addCallback(lambda res: filename)
47@@ -199,6 +201,8 @@
48             if i >= self.count:
49                 return
50             n = self.parent.create_node_from_uri(self.uris[i])
51+            if not IFileNode.providedBy(n):
52+                raise AssertionError("The URI does not reference a file.")
53             if n.is_mutable():
54                 d1 = n.download_best_version()
55             else:
56diff -rN -u old-tahoe/src/allmydata/dirnode.py new-tahoe/src/allmydata/dirnode.py
57--- old-tahoe/src/allmydata/dirnode.py  2010-01-23 12:59:08.694000000 +0000
58+++ new-tahoe/src/allmydata/dirnode.py  2010-01-23 12:59:11.166000000 +0000
59@@ -5,13 +5,13 @@
60 from twisted.internet import defer
61 from foolscap.api import fireEventually
62 import simplejson
63-from allmydata.mutable.common import NotMutableError
64+from allmydata.mutable.common import NotWriteableError
65 from allmydata.mutable.filenode import MutableFileNode
66-from allmydata.unknown import UnknownNode
67+from allmydata.unknown import UnknownNode, strip_prefix_for_ro
68 from allmydata.interfaces import IFilesystemNode, IDirectoryNode, IFileNode, \
69      IImmutableFileNode, IMutableFileNode, \
70      ExistingChildError, NoSuchChildError, ICheckable, IDeepCheckable, \
71-     CannotPackUnknownNodeError
72+     MustBeDeepImmutableError, CapConstraintError
73 from allmydata.check_results import DeepCheckResults, \
74      DeepCheckAndRepairResults
75 from allmydata.monitor import Monitor
76@@ -23,6 +23,11 @@
77 from pycryptopp.cipher.aes import AES
78 from allmydata.util.dictutil import AuxValueDict
79 
80+
81+# TODO: {Deleter,MetadataSetter,Adder}.modify all start by unpacking the
82+# contents and end by repacking them. It might be better to apply them to
83+# the unpacked contents.
84+
85 class Deleter:
86     def __init__(self, node, name, must_exist=True):
87         self.node = node
88@@ -40,6 +45,7 @@
89         new_contents = self.node._pack_contents(children)
90         return new_contents
91 
92+
93 class MetadataSetter:
94     def __init__(self, node, name, metadata):
95         self.node = node
96@@ -75,6 +81,11 @@
97         for (name, (child, new_metadata)) in self.entries.iteritems():
98             precondition(isinstance(name, unicode), name)
99             precondition(IFilesystemNode.providedBy(child), child)
100+
101+            # Strictly speaking this is redundant because we would raise the
102+            # error again in pack_children.
103+            child.raise_error()
104+
105             if name in children:
106                 if not self.overwrite:
107                     raise ExistingChildError("child '%s' already exists" % name)
108@@ -123,25 +134,21 @@
109         new_contents = self.node._pack_contents(children)
110         return new_contents
111 
112-def _encrypt_rwcap(filenode, rwcap):
113-    assert isinstance(rwcap, str)
114+def _encrypt_rw_uri(filenode, rw_uri):
115+    assert isinstance(rw_uri, str)
116     writekey = filenode.get_writekey()
117     if not writekey:
118         return ""
119-    salt = hashutil.mutable_rwcap_salt_hash(rwcap)
120+    salt = hashutil.mutable_rwcap_salt_hash(rw_uri)
121     key = hashutil.mutable_rwcap_key_hash(salt, writekey)
122     cryptor = AES(key)
123-    crypttext = cryptor.process(rwcap)
124+    crypttext = cryptor.process(rw_uri)
125     mac = hashutil.hmac(key, salt + crypttext)
126     assert len(mac) == 32
127     return salt + crypttext + mac
128     # The MAC is not checked by readers in Tahoe >= 1.3.0, but we still
129     # produce it for the sake of older readers.
130 
131-class MustBeDeepImmutable(Exception):
132-    """You tried to add a non-deep-immutable node to a deep-immutable
133-    directory."""
134-
135 def pack_children(filenode, children, deep_immutable=False):
136     """Take a dict that maps:
137          children[unicode_name] = (IFileSystemNode, metadata_dict)
138@@ -152,7 +159,7 @@
139     time.
140 
141     If deep_immutable is True, I will require that all my children are deeply
142-    immutable, and will raise a MustBeDeepImmutable exception if not.
143+    immutable, and will raise a MustBeDeepImmutableError if not.
144     """
145 
146     has_aux = isinstance(children, AuxValueDict)
147@@ -161,25 +168,25 @@
148         assert isinstance(name, unicode)
149         entry = None
150         (child, metadata) = children[name]
151-        if deep_immutable and child.is_mutable():
152-            # TODO: consider adding IFileSystemNode.is_deep_immutable()
153-            raise MustBeDeepImmutable("child '%s' is mutable" % (name,))
154+        child.raise_error()
155+        if deep_immutable and not child.is_allowed_in_immutable_directory():
156+            raise MustBeDeepImmutableError("child '%s' is not allowed in an immutable directory" % (name,), name)
157         if has_aux:
158             entry = children.get_aux(name)
159         if not entry:
160             assert IFilesystemNode.providedBy(child), (name,child)
161             assert isinstance(metadata, dict)
162-            rwcap = child.get_uri() # might be RO if the child is not writeable
163-            if rwcap is None:
164-                rwcap = ""
165-            assert isinstance(rwcap, str), rwcap
166-            rocap = child.get_readonly_uri()
167-            if rocap is None:
168-                rocap = ""
169-            assert isinstance(rocap, str), rocap
170+            rw_uri = child.get_write_uri()
171+            if rw_uri is None:
172+                rw_uri = ""
173+            assert isinstance(rw_uri, str), rw_uri
174+            ro_uri = child.get_readonly_uri()
175+            if ro_uri is None:
176+                ro_uri = ""
177+            assert isinstance(ro_uri, str), ro_uri
178             entry = "".join([netstring(name.encode("utf-8")),
179-                             netstring(rocap),
180-                             netstring(_encrypt_rwcap(filenode, rwcap)),
181+                             netstring(strip_prefix_for_ro(ro_uri, deep_immutable)),
182+                             netstring(_encrypt_rw_uri(filenode, rw_uri)),
183                              netstring(simplejson.dumps(metadata))])
184         entries.append(netstring(entry))
185     return "".join(entries)
186@@ -230,38 +237,66 @@
187         plaintext = cryptor.process(crypttext)
188         return plaintext
189 
190-    def _create_node(self, rwcap, rocap):
191-        return self._nodemaker.create_from_cap(rwcap, rocap)
192+    def _create_and_validate_node(self, rw_uri, ro_uri, name):
193+        #print "mutable? %r\n" % self.is_mutable()
194+        #print "_create_and_validate_node(rw_uri=%r, ro_uri=%r, name=%r)\n" % (rw_uri, ro_uri, name)
195+        node = self._nodemaker.create_from_cap(rw_uri, ro_uri,
196+                                               deep_immutable=not self.is_mutable(),
197+                                               name=name)
198+        node.raise_error()
199+        return node
200 
201     def _unpack_contents(self, data):
202         # the directory is serialized as a list of netstrings, one per child.
203-        # Each child is serialized as a list of four netstrings: (name,
204-        # rocap, rwcap, metadata), in which the name,rocap,metadata are in
205-        # cleartext. The 'name' is UTF-8 encoded. The rwcap is formatted as:
206-        # pack("16ss32s", iv, AES(H(writekey+iv), plaintextrwcap), mac)
207+        # Each child is serialized as a list of four netstrings: (name, ro_uri,
208+        # rwcapdata, metadata), in which the name, ro_uri, metadata are in
209+        # cleartext. The 'name' is UTF-8 encoded. The rwcapdata is formatted as:
210+        # pack("16ss32s", iv, AES(H(writekey+iv), plaintext_rw_uri), mac)
211         assert isinstance(data, str), (repr(data), type(data))
212         # an empty directory is serialized as an empty string
213         if data == "":
214             return AuxValueDict()
215         writeable = not self.is_readonly()
216+        mutable = self.is_mutable()
217         children = AuxValueDict()
218         position = 0
219         while position < len(data):
220             entries, position = split_netstring(data, 1, position)
221             entry = entries[0]
222-            (name, rocap, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
223+            (name, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
224             name = name.decode("utf-8")
225-            rwcap = None
226+            rw_uri = ""
227             if writeable:
228-                rwcap = self._decrypt_rwcapdata(rwcapdata)
229-            if not rwcap:
230-                rwcap = None # rwcap is None or a non-empty string
231-            if not rocap:
232-                rocap = None # rocap is None or a non-empty string
233-            child = self._create_node(rwcap, rocap)
234-            metadata = simplejson.loads(metadata_s)
235-            assert isinstance(metadata, dict)
236-            children.set_with_aux(name, (child, metadata), auxilliary=entry)
237+                rw_uri = self._decrypt_rwcapdata(rwcapdata)
238+            #print "mutable=%r, writeable=%r, rw_uri=%r, ro_uri=%r, name=%r" % (mutable, writeable, rw_uri, ro_uri, name)
239+
240+            # Since the encryption uses CTR mode, it currently leaks the length of the
241+            # plaintext rw_uri -- and therefore whether it is present, i.e. whether the
242+            # dirnode is writable (ticket #925). By stripping spaces in Tahoe >= 1.6.0,
243+            # we may make it easier for future versions to plug this leak.
244+            rw_uri = rw_uri.strip()
245+            if not rw_uri:
246+                rw_uri = None  # rw_uri is None or a non-empty string
247+
248+            # Treat ro_uri in the same way for consistency.
249+            ro_uri = ro_uri.strip()
250+            if not ro_uri:
251+                ro_uri = None  # ro_uri is None or a non-empty string
252+
253+            try:
254+                child = self._create_and_validate_node(rw_uri, ro_uri, name)
255+                #print "%r.is_allowed_in_immutable_directory() = %r" % (child, child.is_allowed_in_immutable_directory())
256+                if mutable or child.is_allowed_in_immutable_directory():
257+                    metadata = simplejson.loads(metadata_s)
258+                    assert isinstance(metadata, dict)
259+                    children[name] = (child, metadata)
260+                    children.set_with_aux(name, (child, metadata), auxilliary=entry)
261+            except CapConstraintError, e:
262+                #print "unmet constraint: (%s, %s)" % (e.args[0], e.args[1].encode("utf-8"))
263+                log.msg(format="unmet constraint on cap for child '%(name)s' unpacked from a directory:\n"
264+                               "%(message)s", message=e.args[0], name=e.args[1].encode("utf-8"),
265+                               facility="tahoe.webish", level=log.UNUSUAL)
266+
267         return children
268 
269     def _pack_contents(self, children):
270@@ -270,21 +305,39 @@
271 
272     def is_readonly(self):
273         return self._node.is_readonly()
274+
275     def is_mutable(self):
276         return self._node.is_mutable()
277 
278+    def is_unknown(self):
279+        return False
280+
281+    def is_allowed_in_immutable_directory(self):
282+        return not self._node.is_mutable()
283+
284+    def raise_error(self):
285+        pass
286+
287     def get_uri(self):
288         return self._uri.to_string()
289 
290+    def get_write_uri(self):
291+        if self.is_readonly():
292+            return None
293+        return self._uri.to_string()
294+
295     def get_readonly_uri(self):
296         return self._uri.get_readonly().to_string()
297 
298     def get_cap(self):
299         return self._uri
300+
301     def get_readcap(self):
302         return self._uri.get_readonly()
303+
304     def get_verify_cap(self):
305         return self._uri.get_verify_cap()
306+
307     def get_repair_cap(self):
308         if self._node.is_readonly():
309             return None # readonly (mutable) dirnodes are not yet repairable
310@@ -350,7 +403,7 @@
311     def set_metadata_for(self, name, metadata):
312         assert isinstance(name, unicode)
313         if self.is_readonly():
314-            return defer.fail(NotMutableError())
315+            return defer.fail(NotWriteableError())
316         assert isinstance(metadata, dict)
317         s = MetadataSetter(self, name, metadata)
318         d = self._node.modify(s.modify)
319@@ -398,14 +451,10 @@
320         precondition(isinstance(name, unicode), name)
321         precondition(isinstance(writecap, (str,type(None))), writecap)
322         precondition(isinstance(readcap, (str,type(None))), readcap)
323-        child_node = self._create_node(writecap, readcap)
324-        if isinstance(child_node, UnknownNode):
325-            # don't be willing to pack unknown nodes: we might accidentally
326-            # put some write-authority into the rocap slot because we don't
327-            # know how to diminish the URI they gave us. We don't even know
328-            # if they gave us a readcap or a writecap.
329-            msg = "cannot pack unknown node as child %s" % str(name)
330-            raise CannotPackUnknownNodeError(msg)
331+           
332+        # We now allow packing unknown nodes, provided they are valid
333+        # for this type of directory.
334+        child_node = self._create_and_validate_node(writecap, readcap, name)
335         d = self.set_node(name, child_node, metadata, overwrite)
336         d.addCallback(lambda res: child_node)
337         return d
338@@ -423,10 +472,10 @@
339                 writecap, readcap, metadata = e
340             precondition(isinstance(writecap, (str,type(None))), writecap)
341             precondition(isinstance(readcap, (str,type(None))), readcap)
342-            child_node = self._create_node(writecap, readcap)
343-            if isinstance(child_node, UnknownNode):
344-                msg = "cannot pack unknown node as child %s" % str(name)
345-                raise CannotPackUnknownNodeError(msg)
346+           
347+            # We now allow packing unknown nodes, provided they are valid
348+            # for this type of directory.
349+            child_node = self._create_and_validate_node(writecap, readcap, name)
350             a.set_node(name, child_node, metadata)
351         d = self._node.modify(a.modify)
352         d.addCallback(lambda ign: self)
353@@ -439,12 +488,12 @@
354         same name.
355 
356         If this directory node is read-only, the Deferred will errback with a
357-        NotMutableError."""
358+        NotWriteableError."""
359 
360         precondition(IFilesystemNode.providedBy(child), child)
361 
362         if self.is_readonly():
363-            return defer.fail(NotMutableError())
364+            return defer.fail(NotWriteableError())
365         assert isinstance(name, unicode)
366         assert IFilesystemNode.providedBy(child), child
367         a = Adder(self, overwrite=overwrite)
368@@ -456,7 +505,7 @@
369     def set_nodes(self, entries, overwrite=True):
370         precondition(isinstance(entries, dict), entries)
371         if self.is_readonly():
372-            return defer.fail(NotMutableError())
373+            return defer.fail(NotWriteableError())
374         a = Adder(self, entries, overwrite=overwrite)
375         d = self._node.modify(a.modify)
376         d.addCallback(lambda res: self)
377@@ -470,10 +519,10 @@
378         the operation completes."""
379         assert isinstance(name, unicode)
380         if self.is_readonly():
381-            return defer.fail(NotMutableError())
382+            return defer.fail(NotWriteableError())
383         d = self._uploader.upload(uploadable)
384-        d.addCallback(lambda results: results.uri)
385-        d.addCallback(self._nodemaker.create_from_cap)
386+        d.addCallback(lambda results:
387+                      self._create_and_validate_node(results.uri, None, name))
388         d.addCallback(lambda node:
389                       self.set_node(name, node, metadata, overwrite))
390         return d
391@@ -483,7 +532,7 @@
392         fires (with the node just removed) when the operation finishes."""
393         assert isinstance(name, unicode)
394         if self.is_readonly():
395-            return defer.fail(NotMutableError())
396+            return defer.fail(NotWriteableError())
397         deleter = Deleter(self, name)
398         d = self._node.modify(deleter.modify)
399         d.addCallback(lambda res: deleter.old_child)
400@@ -493,7 +542,7 @@
401                             mutable=True):
402         assert isinstance(name, unicode)
403         if self.is_readonly():
404-            return defer.fail(NotMutableError())
405+            return defer.fail(NotWriteableError())
406         if mutable:
407             d = self._nodemaker.create_new_mutable_directory(initial_children)
408         else:
409@@ -515,7 +564,7 @@
410         Deferred that fires when the operation finishes."""
411         assert isinstance(current_child_name, unicode)
412         if self.is_readonly() or new_parent.is_readonly():
413-            return defer.fail(NotMutableError())
414+            return defer.fail(NotWriteableError())
415         if new_child_name is None:
416             new_child_name = current_child_name
417         assert isinstance(new_child_name, unicode)
418diff -rN -u old-tahoe/src/allmydata/immutable/filenode.py new-tahoe/src/allmydata/immutable/filenode.py
419--- old-tahoe/src/allmydata/immutable/filenode.py       2010-01-23 12:59:08.893000000 +0000
420+++ new-tahoe/src/allmydata/immutable/filenode.py       2010-01-23 12:59:11.317000000 +0000
421@@ -17,6 +17,9 @@
422 class _ImmutableFileNodeBase(object):
423     implements(IImmutableFileNode, ICheckable)
424 
425+    def get_write_uri(self):
426+        return None
427+
428     def get_readonly_uri(self):
429         return self.get_uri()
430 
431@@ -26,6 +29,15 @@
432     def is_readonly(self):
433         return True
434 
435+    def is_unknown(self):
436+        return False
437+
438+    def is_allowed_in_immutable_directory(self):
439+        return True
440+
441+    def raise_error(self):
442+        pass
443+
444     def __hash__(self):
445         return self.u.__hash__()
446     def __eq__(self, other):
447diff -rN -u old-tahoe/src/allmydata/interfaces.py new-tahoe/src/allmydata/interfaces.py
448--- old-tahoe/src/allmydata/interfaces.py       2010-01-23 12:59:08.923000000 +0000
449+++ new-tahoe/src/allmydata/interfaces.py       2010-01-23 12:59:11.366000000 +0000
450@@ -426,6 +426,7 @@
451         """Return True if the data can be modified by *somebody* (perhaps
452         someone who has a more powerful URI than this one)."""
453 
454+    # TODO: rename to get_read_cap()
455     def get_readonly():
456         """Return another IURI instance, which represents a read-only form of
457         this one. If is_readonly() is True, this returns self."""
458@@ -456,7 +457,6 @@
459 class IDirnodeURI(Interface):
460     """I am a URI which represents a dirnode."""
461 
462-
463 class IFileURI(Interface):
464     """I am a URI which represents a filenode."""
465     def get_size():
466@@ -467,21 +467,28 @@
467 
468 class IMutableFileURI(Interface):
469     """I am a URI which represents a mutable filenode."""
470+
471 class IDirectoryURI(Interface):
472     pass
473+
474 class IReadonlyDirectoryURI(Interface):
475     pass
476 
477-class CannotPackUnknownNodeError(Exception):
478-    """UnknownNodes (using filecaps from the future that we don't understand)
479-    cannot yet be copied safely, so I refuse to copy them."""
480-
481-class UnhandledCapTypeError(Exception):
482-    """I recognize the cap/URI, but I cannot create an IFilesystemNode for
483-    it."""
484+class CapConstraintError(Exception):
485+    """A constraint on a cap was violated."""
486 
487-class NotDeepImmutableError(Exception):
488-    """Deep-immutable directories can only contain deep-immutable children"""
489+class MustBeDeepImmutableError(CapConstraintError):
490+    """Mutable children cannot be added to an immutable directory.
491+    Also, caps obtained from an immutable directory can trigger this error
492+    if they are later found to refer to a mutable object and then used."""
493+
494+class MustBeReadonlyError(CapConstraintError):
495+    """Known write caps cannot be specified in a ro_uri field. Also,
496+    caps obtained from a ro_uri field can trigger this error if they
497+    are later found to be write caps and then used."""
498+
499+class MustNotBeUnknownRWError(CapConstraintError):
500+    """Cannot add an unknown child cap specified in a rw_uri field."""
501 
502 # The hierarchy looks like this:
503 #  IFilesystemNode
504@@ -518,9 +525,8 @@
505         """
506 
507     def get_uri():
508-        """
509-        Return the URI string that can be used by others to get access to
510-        this node. If this node is read-only, the URI will only offer
511+        """Return the URI string corresponding to the strongest cap associated
512+        with this node. If this node is read-only, the URI will only offer
513         read-only access. If this node is read-write, the URI will offer
514         read-write access.
515 
516@@ -528,6 +534,11 @@
517         read-only access with others, use get_readonly_uri().
518         """
519 
520+    def get_write_uri(n):
521+        """Return the URI string that can be used by others to get write
522+        access to this node, if it is writeable. If this is a read-only node,
523+        return None."""
524+
525     def get_readonly_uri():
526         """Return the URI string that can be used by others to get read-only
527         access to this node. The result is a read-only URI, regardless of
528@@ -557,6 +568,18 @@
529         file.
530         """
531 
532+    def is_unknown():
533+        """Return True if this is an unknown node."""
534+
535+    def is_allowed_in_immutable_directory():
536+        """Return True if this node is allowed as a child of a deep-immutable
537+        directory. This is true if either the node is of a known-immutable type,
538+        or it is unknown and read-only.
539+        """
540+
541+    def raise_error():
542+        """Raise any error associated with this node."""
543+
544     def get_size():
545         """Return the length (in bytes) of the data this node represents. For
546         directory nodes, I return the size of the backing store. I return
547@@ -902,7 +925,7 @@
548         ctime/mtime semantics of traditional filesystems.
549 
550         If this directory node is read-only, the Deferred will errback with a
551-        NotMutableError."""
552+        NotWriteableError."""
553 
554     def set_children(entries, overwrite=True):
555         """Add multiple children (by writecap+readcap) to a directory node.
556@@ -928,7 +951,7 @@
557         ctime/mtime semantics of traditional filesystems.
558 
559         If this directory node is read-only, the Deferred will errback with a
560-        NotMutableError."""
561+        NotWriteableError."""
562 
563     def set_nodes(entries, overwrite=True):
564         """Add multiple children to a directory node. Takes a dict mapping
565@@ -2074,7 +2097,7 @@
566     Tahoe process will typically have a single NodeMaker, but unit tests may
567     create simplified/mocked forms for testing purposes.
568     """
569-    def create_from_cap(writecap, readcap=None):
570+    def create_from_cap(writecap, readcap=None, **kwargs):
571         """I create an IFilesystemNode from the given writecap/readcap. I can
572         only provide nodes for existing file/directory objects: use my other
573         methods to create new objects. I return synchronously."""
574diff -rN -u old-tahoe/src/allmydata/mutable/common.py new-tahoe/src/allmydata/mutable/common.py
575--- old-tahoe/src/allmydata/mutable/common.py   2010-01-23 12:59:08.999000000 +0000
576+++ new-tahoe/src/allmydata/mutable/common.py   2010-01-23 12:59:11.412000000 +0000
577@@ -8,7 +8,7 @@
578                           # creation
579 MODE_READ = "MODE_READ"
580 
581-class NotMutableError(Exception):
582+class NotWriteableError(Exception):
583     pass
584 
585 class NeedMoreDataError(Exception):
586diff -rN -u old-tahoe/src/allmydata/mutable/filenode.py new-tahoe/src/allmydata/mutable/filenode.py
587--- old-tahoe/src/allmydata/mutable/filenode.py 2010-01-23 12:59:09.004000000 +0000
588+++ new-tahoe/src/allmydata/mutable/filenode.py 2010-01-23 12:59:11.416000000 +0000
589@@ -214,6 +214,12 @@
590 
591     def get_uri(self):
592         return self._uri.to_string()
593+
594+    def get_write_uri(self):
595+        if self.is_readonly():
596+            return None
597+        return self._uri.to_string()
598+
599     def get_readonly_uri(self):
600         return self._uri.get_readonly().to_string()
601 
602@@ -227,9 +233,19 @@
603 
604     def is_mutable(self):
605         return self._uri.is_mutable()
606+
607     def is_readonly(self):
608         return self._uri.is_readonly()
609 
610+    def is_unknown(self):
611+        return False
612+
613+    def is_allowed_in_immutable_directory(self):
614+        return not self._uri.is_mutable()
615+
616+    def raise_error(self):
617+        pass
618+
619     def __hash__(self):
620         return hash((self.__class__, self._uri))
621     def __cmp__(self, them):
622diff -rN -u old-tahoe/src/allmydata/nodemaker.py new-tahoe/src/allmydata/nodemaker.py
623--- old-tahoe/src/allmydata/nodemaker.py        2010-01-23 12:59:09.045000000 +0000
624+++ new-tahoe/src/allmydata/nodemaker.py        2010-01-23 12:59:11.445000000 +0000
625@@ -1,7 +1,7 @@
626 import weakref
627 from zope.interface import implements
628 from allmydata.util.assertutil import precondition
629-from allmydata.interfaces import INodeMaker, NotDeepImmutableError
630+from allmydata.interfaces import INodeMaker, MustBeDeepImmutableError
631 from allmydata.immutable.filenode import ImmutableFileNode, LiteralFileNode
632 from allmydata.immutable.upload import Data
633 from allmydata.mutable.filenode import MutableFileNode
634@@ -44,28 +44,36 @@
635     def _create_dirnode(self, filenode):
636         return DirectoryNode(filenode, self, self.uploader)
637 
638-    def create_from_cap(self, writecap, readcap=None):
639+    def create_from_cap(self, writecap, readcap=None, deep_immutable=False, name=u"<unknown name>"):
640         # this returns synchronously. It starts with a "cap string".
641         assert isinstance(writecap, (str, type(None))), type(writecap)
642         assert isinstance(readcap,  (str, type(None))), type(readcap)
643+        #import traceback
644+        #traceback.print_stack()
645+        #print '%r.create_from_cap(%r, %r, %r)' % (self, writecap, readcap, kwargs)
646+       
647         bigcap = writecap or readcap
648         if not bigcap:
649             # maybe the writecap was hidden because we're in a readonly
650             # directory, and the future cap format doesn't have a readcap, or
651             # something.
652-            return UnknownNode(writecap, readcap)
653-        if bigcap in self._node_cache:
654-            return self._node_cache[bigcap]
655-        cap = uri.from_string(bigcap)
656-        node = self._create_from_cap(cap)
657+            return UnknownNode(None, None)  # deep_immutable and name not needed
658+
659+        # The name doesn't matter for caching since it's only used in the error
660+        # attribute of an UnknownNode, and we don't cache those.
661+        memokey = ("I" if deep_immutable else "M") + bigcap
662+        if memokey in self._node_cache:
663+            return self._node_cache[memokey]
664+        cap = uri.from_string(bigcap, deep_immutable=deep_immutable, name=name)
665+        node = self._create_from_single_cap(cap)
666         if node:
667-            self._node_cache[bigcap] = node  # note: WeakValueDictionary
668+            self._node_cache[memokey] = node  # note: WeakValueDictionary
669         else:
670-            node = UnknownNode(writecap, readcap) # don't cache UnknownNode
671+            # don't cache UnknownNode
672+            node = UnknownNode(writecap, readcap, deep_immutable=deep_immutable, name=name)
673         return node
674 
675-    def _create_from_cap(self, cap):
676-        # This starts with a "cap instance"
677+    def _create_from_single_cap(self, cap):
678         if isinstance(cap, uri.LiteralFileURI):
679             return self._create_lit(cap)
680         if isinstance(cap, uri.CHKFileURI):
681@@ -76,7 +84,7 @@
682                             uri.ReadonlyDirectoryURI,
683                             uri.ImmutableDirectoryURI,
684                             uri.LiteralDirectoryURI)):
685-            filenode = self._create_from_cap(cap.get_filenode_cap())
686+            filenode = self._create_from_single_cap(cap.get_filenode_cap())
687             return self._create_dirnode(filenode)
688         return None
689 
690@@ -89,13 +97,11 @@
691         return d
692 
693     def create_new_mutable_directory(self, initial_children={}):
694-        # initial_children must have metadata (i.e. {} instead of None), and
695-        # should not contain UnknownNodes
696+        # initial_children must have metadata (i.e. {} instead of None)
697         for (name, (node, metadata)) in initial_children.iteritems():
698-            precondition(not isinstance(node, UnknownNode),
699-                         "create_new_mutable_directory does not accept UnknownNode", node)
700             precondition(isinstance(metadata, dict),
701                          "create_new_mutable_directory requires metadata to be a dict, not None", metadata)
702+            node.raise_error()
703         d = self.create_mutable_file(lambda n:
704                                      pack_children(n, initial_children))
705         d.addCallback(self._create_dirnode)
706@@ -105,19 +111,15 @@
707         if convergence is None:
708             convergence = self.secret_holder.get_convergence_secret()
709         for (name, (node, metadata)) in children.iteritems():
710-            precondition(not isinstance(node, UnknownNode),
711-                         "create_immutable_directory does not accept UnknownNode", node)
712             precondition(isinstance(metadata, dict),
713                          "create_immutable_directory requires metadata to be a dict, not None", metadata)
714-            if node.is_mutable():
715-                raise NotDeepImmutableError("%s is not immutable" % (node,))
716+            node.raise_error()
717+            if not node.is_allowed_in_immutable_directory():
718+                raise MustBeDeepImmutableError("%s is not immutable" % (node,), name)
719         n = DummyImmutableFileNode() # writekey=None
720         packed = pack_children(n, children)
721         uploadable = Data(packed, convergence)
722         d = self.uploader.upload(uploadable, history=self.history)
723-        def _uploaded(results):
724-            filecap = self.create_from_cap(results.uri)
725-            return filecap
726-        d.addCallback(_uploaded)
727+        d.addCallback(lambda results: self.create_from_cap(None, results.uri))
728         d.addCallback(self._create_dirnode)
729         return d
730diff -rN -u old-tahoe/src/allmydata/scripts/common.py new-tahoe/src/allmydata/scripts/common.py
731--- old-tahoe/src/allmydata/scripts/common.py   2010-01-23 12:59:09.089000000 +0000
732+++ new-tahoe/src/allmydata/scripts/common.py   2010-01-23 12:59:11.483000000 +0000
733@@ -128,12 +128,14 @@
734     pass
735 
736 def get_alias(aliases, path, default):
737+    from allmydata import uri
738     # transform "work:path/filename" into (aliases["work"], "path/filename").
739     # If default=None, then an empty alias is indicated by returning
740-    # DefaultAliasMarker. We special-case "URI:" to make it easy to access
741-    # specific files/directories by their read-cap.
742+    # DefaultAliasMarker. We special-case strings with a recognized cap URI
743+    # prefix, to make it easy to access specific files/directories by their
744+    # caps.
745     path = path.strip()
746-    if path.startswith("URI:"):
747+    if uri.has_uri_prefix(path):
748         # The only way to get a sub-path is to use URI:blah:./foo, and we
749         # strip out the :./ sequence.
750         sep = path.find(":./")
751diff -rN -u old-tahoe/src/allmydata/scripts/tahoe_cp.py new-tahoe/src/allmydata/scripts/tahoe_cp.py
752--- old-tahoe/src/allmydata/scripts/tahoe_cp.py 2010-01-23 12:59:09.170000000 +0000
753+++ new-tahoe/src/allmydata/scripts/tahoe_cp.py 2010-01-23 12:59:11.536000000 +0000
754@@ -258,8 +258,7 @@
755                 readcap = ascii_or_none(data[1].get("ro_uri"))
756                 self.children[name] = TahoeFileSource(self.nodeurl, mutable,
757                                                       writecap, readcap)
758-            else:
759-                assert data[0] == "dirnode"
760+            elif data[0] == "dirnode":
761                 writecap = ascii_or_none(data[1].get("rw_uri"))
762                 readcap = ascii_or_none(data[1].get("ro_uri"))
763                 if writecap and writecap in self.cache:
764@@ -277,6 +276,11 @@
765                     if recurse:
766                         child.populate(True)
767                 self.children[name] = child
768+            else:
769+                # TODO: there should be an option to skip unknown nodes.
770+                raise TahoeError("Cannot copy unknown nodes (ticket #839). "
771+                                 "You probably need to use a later version of "
772+                                 "Tahoe-LAFS to copy this directory.")
773 
774 class TahoeMissingTarget:
775     def __init__(self, url):
776@@ -353,8 +357,7 @@
777                                                    urllib.quote(name.encode('utf-8'))])
778                 self.children[name] = TahoeFileTarget(self.nodeurl, mutable,
779                                                       writecap, readcap, url)
780-            else:
781-                assert data[0] == "dirnode"
782+            elif data[0] == "dirnode":
783                 writecap = ascii_or_none(data[1].get("rw_uri"))
784                 readcap = ascii_or_none(data[1].get("ro_uri"))
785                 if writecap and writecap in self.cache:
786@@ -372,6 +375,11 @@
787                     if recurse:
788                         child.populate(True)
789                 self.children[name] = child
790+            else:
791+                # TODO: there should be an option to skip unknown nodes.
792+                raise TahoeError("Cannot copy unknown nodes (ticket #839). "
793+                                 "You probably need to use a later version of "
794+                                 "Tahoe-LAFS to copy this directory.")
795 
796     def get_child_target(self, name):
797         # return a new target for a named subdirectory of this dir
798@@ -407,9 +415,11 @@
799         set_data = {}
800         for (name, filecap) in self.new_children.items():
801             # it just so happens that ?t=set_children will accept both file
802-            # read-caps and write-caps as ['rw_uri'], and will handle eithe
803+            # read-caps and write-caps as ['rw_uri'], and will handle either
804             # correctly. So don't bother trying to figure out whether the one
805             # we have is read-only or read-write.
806+            # TODO: think about how this affects forward-compatibility for
807+            # unknown caps
808             set_data[name] = ["filenode", {"rw_uri": filecap}]
809         body = simplejson.dumps(set_data)
810         POST(url, body)
811@@ -770,6 +780,7 @@
812 #  local-file-in-the-way
813 #   touch proposed
814 #   tahoe cp -r my:docs/proposed/denver.txt proposed/denver.txt
815+#  handling of unknown nodes
816 
817 # things that maybe should be errors but aren't
818 #  local-dir-in-the-way
819diff -rN -u old-tahoe/src/allmydata/scripts/tahoe_put.py new-tahoe/src/allmydata/scripts/tahoe_put.py
820--- old-tahoe/src/allmydata/scripts/tahoe_put.py        2010-01-23 12:59:09.198000000 +0000
821+++ new-tahoe/src/allmydata/scripts/tahoe_put.py        2010-01-23 12:59:11.557000000 +0000
822@@ -40,6 +40,7 @@
823         #  DIRCAP:./subdir/foo : DIRCAP/subdir/foo
824         #  MUTABLE-FILE-WRITECAP : filecap
825 
826+        # FIXME: this shouldn't rely on a particular prefix.
827         if to_file.startswith("URI:SSK:"):
828             url = nodeurl + "uri/%s" % urllib.quote(to_file)
829         else:
830diff -rN -u old-tahoe/src/allmydata/test/common.py new-tahoe/src/allmydata/test/common.py
831--- old-tahoe/src/allmydata/test/common.py      2010-01-23 12:59:09.443000000 +0000
832+++ new-tahoe/src/allmydata/test/common.py      2010-01-23 12:59:11.729000000 +0000
833@@ -51,6 +51,8 @@
834 
835     def get_uri(self):
836         return self.my_uri.to_string()
837+    def get_write_uri(self):
838+        return None
839     def get_readonly_uri(self):
840         return self.my_uri.to_string()
841     def get_cap(self):
842@@ -103,6 +105,12 @@
843         return False
844     def is_readonly(self):
845         return True
846+    def is_unknown(self):
847+        return False
848+    def is_allowed_in_immutable_directory(self):
849+        return True
850+    def raise_error(self):
851+        pass
852 
853     def get_size(self):
854         try:
855@@ -190,6 +198,10 @@
856         return self.my_uri.get_readonly()
857     def get_uri(self):
858         return self.my_uri.to_string()
859+    def get_write_uri(self):
860+        if self.is_readonly():
861+            return None
862+        return self.my_uri.to_string()
863     def get_readonly(self):
864         return self.my_uri.get_readonly()
865     def get_readonly_uri(self):
866@@ -200,6 +212,12 @@
867         return self.my_uri.is_readonly()
868     def is_mutable(self):
869         return self.my_uri.is_mutable()
870+    def is_unknown(self):
871+        return False
872+    def is_allowed_in_immutable_directory(self):
873+        return not self.my_uri.is_mutable()
874+    def raise_error(self):
875+        pass
876     def get_writekey(self):
877         return "\x00"*16
878     def get_size(self):
879diff -rN -u old-tahoe/src/allmydata/test/test_client.py new-tahoe/src/allmydata/test/test_client.py
880--- old-tahoe/src/allmydata/test/test_client.py 2010-01-23 12:59:09.713000000 +0000
881+++ new-tahoe/src/allmydata/test/test_client.py 2010-01-23 12:59:11.853000000 +0000
882@@ -288,11 +288,14 @@
883         self.failUnless(n.is_readonly())
884         self.failUnless(n.is_mutable())
885 
886-        future = "x-tahoe-crazy://future_cap_format."
887-        n = c.create_node_from_uri(future)
888+        unknown_rw = "lafs://from_the_future"
889+        unknown_ro = "lafs://readonly_from_the_future"
890+        n = c.create_node_from_uri(unknown_rw, unknown_ro)
891         self.failUnless(IFilesystemNode.providedBy(n))
892         self.failIf(IFileNode.providedBy(n))
893         self.failIf(IImmutableFileNode.providedBy(n))
894         self.failIf(IMutableFileNode.providedBy(n))
895         self.failIf(IDirectoryNode.providedBy(n))
896-        self.failUnlessEqual(n.get_uri(), future)
897+        self.failUnless(n.is_unknown())
898+        self.failUnlessEqual(n.get_uri(), unknown_rw)
899+        self.failUnlessEqual(n.get_readonly_uri(), "ro." + unknown_ro)
900diff -rN -u old-tahoe/src/allmydata/test/test_dirnode.py new-tahoe/src/allmydata/test/test_dirnode.py
901--- old-tahoe/src/allmydata/test/test_dirnode.py        2010-01-23 12:59:09.774000000 +0000
902+++ new-tahoe/src/allmydata/test/test_dirnode.py        2010-01-23 12:59:11.898000000 +0000
903@@ -7,8 +7,8 @@
904 from allmydata.client import Client
905 from allmydata.immutable import upload
906 from allmydata.interfaces import IImmutableFileNode, IMutableFileNode, \
907-     ExistingChildError, NoSuchChildError, NotDeepImmutableError, \
908-     IDeepCheckResults, IDeepCheckAndRepairResults, CannotPackUnknownNodeError
909+     ExistingChildError, NoSuchChildError, MustBeDeepImmutableError, \
910+     IDeepCheckResults, IDeepCheckAndRepairResults, MustNotBeUnknownRWError
911 from allmydata.mutable.filenode import MutableFileNode
912 from allmydata.mutable.common import UncoordinatedWriteError
913 from allmydata.util import hashutil, base32
914@@ -32,6 +32,11 @@
915         d = c.create_dirnode()
916         def _done(res):
917             self.failUnless(isinstance(res, dirnode.DirectoryNode))
918+            self.failUnless(res.is_mutable())
919+            self.failIf(res.is_readonly())
920+            self.failIf(res.is_unknown())
921+            self.failIf(res.is_allowed_in_immutable_directory())
922+            res.raise_error()
923             rep = str(res)
924             self.failUnless("RW-MUT" in rep)
925         d.addCallback(_done)
926@@ -44,36 +49,74 @@
927         nm = c.nodemaker
928         setup_py_uri = "URI:CHK:n7r3m6wmomelk4sep3kw5cvduq:os7ijw5c3maek7pg65e5254k2fzjflavtpejjyhshpsxuqzhcwwq:3:20:14861"
929         one_uri = "URI:LIT:n5xgk" # LIT for "one"
930+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
931+        mut_read_uri = "URI:SSK-RO:jf6wkflosyvntwxqcdo7a54jvm:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
932+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
933+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
934         kids = {u"one": (nm.create_from_cap(one_uri), {}),
935                 u"two": (nm.create_from_cap(setup_py_uri),
936                          {"metakey": "metavalue"}),
937+                u"mut": (nm.create_from_cap(mut_write_uri, mut_read_uri), {}),
938+                u"fut": (nm.create_from_cap(future_write_uri, future_read_uri), {}),
939+                u"fro": (nm.create_from_cap(None, future_read_uri), {}),
940                 }
941         d = c.create_dirnode(kids)
942+       
943         def _created(dn):
944             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
945+            self.failUnless(dn.is_mutable())
946+            self.failIf(dn.is_readonly())
947+            self.failIf(dn.is_unknown())
948+            self.failIf(dn.is_allowed_in_immutable_directory())
949+            dn.raise_error()
950             rep = str(dn)
951             self.failUnless("RW-MUT" in rep)
952             return dn.list()
953         d.addCallback(_created)
954+       
955         def _check_kids(children):
956-            self.failUnlessEqual(sorted(children.keys()), [u"one", u"two"])
957+            self.failUnlessEqual(sorted(children.keys()),
958+                                 [u"fro", u"fut", u"mut", u"one", u"two"])
959             one_node, one_metadata = children[u"one"]
960             two_node, two_metadata = children[u"two"]
961+            mut_node, mut_metadata = children[u"mut"]
962+            fut_node, fut_metadata = children[u"fut"]
963+            fro_node, fro_metadata = children[u"fro"]
964+           
965             self.failUnlessEqual(one_node.get_size(), 3)
966-            self.failUnlessEqual(two_node.get_size(), 14861)
967+            self.failUnlessEqual(one_node.get_uri(), one_uri)
968+            self.failUnlessEqual(one_node.get_readonly_uri(), one_uri)
969             self.failUnless(isinstance(one_metadata, dict), one_metadata)
970+           
971+            self.failUnlessEqual(two_node.get_size(), 14861)
972+            self.failUnlessEqual(two_node.get_uri(), setup_py_uri)
973+            self.failUnlessEqual(two_node.get_readonly_uri(), setup_py_uri)
974             self.failUnlessEqual(two_metadata["metakey"], "metavalue")
975+           
976+            self.failUnlessEqual(mut_node.get_uri(), mut_write_uri)
977+            self.failUnlessEqual(mut_node.get_readonly_uri(), mut_read_uri)
978+            self.failUnless(isinstance(mut_metadata, dict), mut_metadata)
979+           
980+            self.failUnless(fut_node.is_unknown())
981+            self.failUnlessEqual(fut_node.get_uri(), future_write_uri)
982+            self.failUnlessEqual(fut_node.get_readonly_uri(), "ro." + future_read_uri)
983+            self.failUnless(isinstance(fut_metadata, dict), fut_metadata)
984+           
985+            self.failUnless(fro_node.is_unknown())
986+            self.failUnlessEqual(fro_node.get_uri(), "ro." + future_read_uri)
987+            self.failUnlessEqual(fut_node.get_readonly_uri(), "ro." + future_read_uri)
988+            self.failUnless(isinstance(fro_metadata, dict), fro_metadata)
989         d.addCallback(_check_kids)
990+
991         d.addCallback(lambda ign: nm.create_new_mutable_directory(kids))
992         d.addCallback(lambda dn: dn.list())
993         d.addCallback(_check_kids)
994-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
995-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
996-        future_node = UnknownNode(future_writecap, future_readcap)
997-        bad_kids1 = {u"one": (future_node, {})}
998+
999+        bad_future_node = UnknownNode(future_write_uri, None)
1000+        bad_kids1 = {u"one": (bad_future_node, {})}
1001         d.addCallback(lambda ign:
1002-                      self.shouldFail(AssertionError, "bad_kids1",
1003-                                      "does not accept UnknownNode",
1004+                      self.shouldFail(MustNotBeUnknownRWError, "bad_kids1",
1005+                                      "cannot attach unknown",
1006                                       nm.create_new_mutable_directory,
1007                                       bad_kids1))
1008         bad_kids2 = {u"one": (nm.create_from_cap(one_uri), None)}
1009@@ -91,17 +134,24 @@
1010         nm = c.nodemaker
1011         setup_py_uri = "URI:CHK:n7r3m6wmomelk4sep3kw5cvduq:os7ijw5c3maek7pg65e5254k2fzjflavtpejjyhshpsxuqzhcwwq:3:20:14861"
1012         one_uri = "URI:LIT:n5xgk" # LIT for "one"
1013-        mut_readcap = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1014-        mut_writecap = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1015+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1016+        mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1017+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
1018+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
1019         kids = {u"one": (nm.create_from_cap(one_uri), {}),
1020                 u"two": (nm.create_from_cap(setup_py_uri),
1021                          {"metakey": "metavalue"}),
1022+                u"fut": (nm.create_from_cap(None, future_read_uri), {}),
1023                 }
1024         d = c.create_immutable_dirnode(kids)
1025+       
1026         def _created(dn):
1027             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1028             self.failIf(dn.is_mutable())
1029             self.failUnless(dn.is_readonly())
1030+            self.failIf(dn.is_unknown())
1031+            self.failUnless(dn.is_allowed_in_immutable_directory())
1032+            dn.raise_error()
1033             rep = str(dn)
1034             self.failUnless("RO-IMM" in rep)
1035             cap = dn.get_cap()
1036@@ -109,50 +159,73 @@
1037             self.cap = cap
1038             return dn.list()
1039         d.addCallback(_created)
1040+       
1041         def _check_kids(children):
1042-            self.failUnlessEqual(sorted(children.keys()), [u"one", u"two"])
1043+            self.failUnlessEqual(sorted(children.keys()), [u"fut", u"one", u"two"])
1044             one_node, one_metadata = children[u"one"]
1045             two_node, two_metadata = children[u"two"]
1046+            fut_node, fut_metadata = children[u"fut"]
1047+
1048             self.failUnlessEqual(one_node.get_size(), 3)
1049-            self.failUnlessEqual(two_node.get_size(), 14861)
1050+            self.failUnlessEqual(one_node.get_uri(), one_uri)
1051+            self.failUnlessEqual(one_node.get_readonly_uri(), one_uri)
1052             self.failUnless(isinstance(one_metadata, dict), one_metadata)
1053+
1054+            self.failUnlessEqual(two_node.get_size(), 14861)
1055+            self.failUnlessEqual(two_node.get_uri(), setup_py_uri)
1056+            self.failUnlessEqual(two_node.get_readonly_uri(), setup_py_uri)
1057             self.failUnlessEqual(two_metadata["metakey"], "metavalue")
1058+
1059+            self.failUnless(fut_node.is_unknown())
1060+            self.failUnlessEqual(fut_node.get_uri(), "imm." + future_read_uri)
1061+            self.failUnlessEqual(fut_node.get_readonly_uri(), "imm." + future_read_uri)
1062+            self.failUnless(isinstance(fut_metadata, dict), fut_metadata)
1063         d.addCallback(_check_kids)
1064+       
1065         d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string()))
1066         d.addCallback(lambda dn: dn.list())
1067         d.addCallback(_check_kids)
1068-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
1069-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
1070-        future_node = UnknownNode(future_writecap, future_readcap)
1071-        bad_kids1 = {u"one": (future_node, {})}
1072+
1073+        bad_future_node1 = UnknownNode(future_write_uri, None)
1074+        bad_kids1 = {u"one": (bad_future_node1, {})}
1075         d.addCallback(lambda ign:
1076-                      self.shouldFail(AssertionError, "bad_kids1",
1077-                                      "does not accept UnknownNode",
1078+                      self.shouldFail(MustNotBeUnknownRWError, "bad_kids1",
1079+                                      "cannot attach unknown",
1080                                       c.create_immutable_dirnode,
1081                                       bad_kids1))
1082-        bad_kids2 = {u"one": (nm.create_from_cap(one_uri), None)}
1083+        bad_future_node2 = UnknownNode(future_write_uri, future_read_uri)
1084+        bad_kids2 = {u"one": (bad_future_node2, {})}
1085         d.addCallback(lambda ign:
1086-                      self.shouldFail(AssertionError, "bad_kids2",
1087-                                      "requires metadata to be a dict",
1088+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids2",
1089+                                      "is not immutable",
1090                                       c.create_immutable_dirnode,
1091                                       bad_kids2))
1092-        bad_kids3 = {u"one": (nm.create_from_cap(mut_writecap), {})}
1093+        bad_kids3 = {u"one": (nm.create_from_cap(one_uri), None)}
1094         d.addCallback(lambda ign:
1095-                      self.shouldFail(NotDeepImmutableError, "bad_kids3",
1096-                                      "is not immutable",
1097+                      self.shouldFail(AssertionError, "bad_kids3",
1098+                                      "requires metadata to be a dict",
1099                                       c.create_immutable_dirnode,
1100                                       bad_kids3))
1101-        bad_kids4 = {u"one": (nm.create_from_cap(mut_readcap), {})}
1102+        bad_kids4 = {u"one": (nm.create_from_cap(mut_write_uri), {})}
1103         d.addCallback(lambda ign:
1104-                      self.shouldFail(NotDeepImmutableError, "bad_kids4",
1105+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids4",
1106                                       "is not immutable",
1107                                       c.create_immutable_dirnode,
1108                                       bad_kids4))
1109+        bad_kids5 = {u"one": (nm.create_from_cap(mut_read_uri), {})}
1110+        d.addCallback(lambda ign:
1111+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids5",
1112+                                      "is not immutable",
1113+                                      c.create_immutable_dirnode,
1114+                                      bad_kids5))
1115         d.addCallback(lambda ign: c.create_immutable_dirnode({}))
1116         def _created_empty(dn):
1117             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1118             self.failIf(dn.is_mutable())
1119             self.failUnless(dn.is_readonly())
1120+            self.failIf(dn.is_unknown())
1121+            self.failUnless(dn.is_allowed_in_immutable_directory())
1122+            dn.raise_error()
1123             rep = str(dn)
1124             self.failUnless("RO-IMM" in rep)
1125             cap = dn.get_cap()
1126@@ -168,6 +241,9 @@
1127             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1128             self.failIf(dn.is_mutable())
1129             self.failUnless(dn.is_readonly())
1130+            self.failIf(dn.is_unknown())
1131+            self.failUnless(dn.is_allowed_in_immutable_directory())
1132+            dn.raise_error()
1133             rep = str(dn)
1134             self.failUnless("RO-IMM" in rep)
1135             cap = dn.get_cap()
1136@@ -193,9 +269,9 @@
1137             d.addCallback(_check_kids)
1138             d.addCallback(lambda ign: n.get(u"subdir"))
1139             d.addCallback(lambda sd: self.failIf(sd.is_mutable()))
1140-            bad_kids = {u"one": (nm.create_from_cap(mut_writecap), {})}
1141+            bad_kids = {u"one": (nm.create_from_cap(mut_write_uri), {})}
1142             d.addCallback(lambda ign:
1143-                          self.shouldFail(NotDeepImmutableError, "YZ",
1144+                          self.shouldFail(MustBeDeepImmutableError, "YZ",
1145                                           "is not immutable",
1146                                           n.create_subdirectory,
1147                                           u"sub2", bad_kids, mutable=False))
1148@@ -203,7 +279,6 @@
1149         d.addCallback(_made_parent)
1150         return d
1151 
1152-
1153     def test_check(self):
1154         self.basedir = "dirnode/Dirnode/test_check"
1155         self.set_up_grid()
1156@@ -337,24 +412,27 @@
1157             ro_dn = c.create_node_from_uri(ro_uri)
1158             self.failUnless(ro_dn.is_readonly())
1159             self.failUnless(ro_dn.is_mutable())
1160+            self.failIf(ro_dn.is_unknown())
1161+            self.failIf(ro_dn.is_allowed_in_immutable_directory())
1162+            ro_dn.raise_error()
1163 
1164-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1165+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1166                             ro_dn.set_uri, u"newchild", filecap, filecap)
1167-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1168+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1169                             ro_dn.set_node, u"newchild", filenode)
1170-            self.shouldFail(dirnode.NotMutableError, "set_nodes ro", None,
1171+            self.shouldFail(dirnode.NotWriteableError, "set_nodes ro", None,
1172                             ro_dn.set_nodes, { u"newchild": (filenode, None) })
1173-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1174+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1175                             ro_dn.add_file, u"newchild", uploadable)
1176-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1177+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1178                             ro_dn.delete, u"child")
1179-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1180+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1181                             ro_dn.create_subdirectory, u"newchild")
1182-            self.shouldFail(dirnode.NotMutableError, "set_metadata_for ro", None,
1183+            self.shouldFail(dirnode.NotWriteableError, "set_metadata_for ro", None,
1184                             ro_dn.set_metadata_for, u"child", {})
1185-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1186+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1187                             ro_dn.move_child_to, u"child", rw_dn)
1188-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1189+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1190                             rw_dn.move_child_to, u"child", ro_dn)
1191             return ro_dn.list()
1192         d.addCallback(_ready)
1193@@ -901,8 +979,8 @@
1194         nodemaker = NodeMaker(None, None, None,
1195                               None, None, None,
1196                               {"k": 3, "n": 10}, None)
1197-        writecap = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1198-        filenode = nodemaker.create_from_cap(writecap)
1199+        write_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1200+        filenode = nodemaker.create_from_cap(write_uri)
1201         node = dirnode.DirectoryNode(filenode, nodemaker, None)
1202         children = node._unpack_contents(known_tree)
1203         self._check_children(children)
1204@@ -975,23 +1053,23 @@
1205         self.failUnlessIn("lit", packed)
1206 
1207         kids = self._make_kids(nm, ["imm", "lit", "write"])
1208-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1209+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1210                               dirnode.pack_children,
1211                               fn, kids, deep_immutable=True)
1212 
1213         # read-only is not enough: all children must be immutable
1214         kids = self._make_kids(nm, ["imm", "lit", "read"])
1215-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1216+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1217                               dirnode.pack_children,
1218                               fn, kids, deep_immutable=True)
1219 
1220         kids = self._make_kids(nm, ["imm", "lit", "dirwrite"])
1221-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1222+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1223                               dirnode.pack_children,
1224                               fn, kids, deep_immutable=True)
1225 
1226         kids = self._make_kids(nm, ["imm", "lit", "dirread"])
1227-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1228+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1229                               dirnode.pack_children,
1230                               fn, kids, deep_immutable=True)
1231 
1232@@ -1017,16 +1095,31 @@
1233 
1234     def get_cap(self):
1235         return self.uri
1236+
1237     def get_uri(self):
1238         return self.uri.to_string()
1239+
1240+    def get_write_uri(self):
1241+        return self.uri.to_string()
1242+
1243     def download_best_version(self):
1244         return defer.succeed(self.data)
1245+
1246     def get_writekey(self):
1247         return "writekey"
1248+
1249     def is_readonly(self):
1250         return False
1251+
1252     def is_mutable(self):
1253         return True
1254+
1255+    def is_unknown(self):
1256+        return False
1257+
1258+    def is_allowed_in_immutable_directory(self):
1259+        return False
1260+
1261     def modify(self, modifier):
1262         self.data = modifier(self.data, None, True)
1263         return defer.succeed(None)
1264@@ -1050,47 +1143,59 @@
1265 
1266     def test_from_future(self):
1267         # create a dirnode that contains unknown URI types, and make sure we
1268-        # tolerate them properly. Since dirnodes aren't allowed to add
1269-        # unknown node types, we have to be tricky.
1270+        # tolerate them properly.
1271         d = self.nodemaker.create_new_mutable_directory()
1272-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
1273-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
1274-        future_node = UnknownNode(future_writecap, future_readcap)
1275+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
1276+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
1277+        future_node = UnknownNode(future_write_uri, future_read_uri)
1278         def _then(n):
1279             self._node = n
1280             return n.set_node(u"future", future_node)
1281         d.addCallback(_then)
1282 
1283-        # we should be prohibited from adding an unknown URI to a directory,
1284-        # since we don't know how to diminish the cap to a readcap (for the
1285-        # dirnode's rocap slot), and we don't want to accidentally grant
1286-        # write access to a holder of the dirnode's readcap.
1287+        # We should be prohibited from adding an unknown URI to a directory
1288+        # just in the rw_uri slot, since we don't know how to diminish the cap
1289+        # to a readcap (for the ro_uri slot).
1290         d.addCallback(lambda ign:
1291-             self.shouldFail(CannotPackUnknownNodeError,
1292+             self.shouldFail(MustNotBeUnknownRWError,
1293                              "copy unknown",
1294-                             "cannot pack unknown node as child add",
1295+                             "cannot attach unknown rw cap as child",
1296                              self._node.set_uri, u"add",
1297-                             future_writecap, future_readcap))
1298+                             future_write_uri, None))
1299+
1300+        # However, we should be able to add both rw_uri and ro_uri as a pair of
1301+        # unknown URIs.
1302+        d.addCallback(lambda ign: self._node.set_uri(u"add-pair",
1303+                                                     future_write_uri, future_read_uri))
1304+
1305         d.addCallback(lambda ign: self._node.list())
1306         def _check(children):
1307-            self.failUnlessEqual(len(children), 1)
1308+            self.failUnlessEqual(len(children), 2)
1309             (fn, metadata) = children[u"future"]
1310             self.failUnless(isinstance(fn, UnknownNode), fn)
1311-            self.failUnlessEqual(fn.get_uri(), future_writecap)
1312-            self.failUnlessEqual(fn.get_readonly_uri(), future_readcap)
1313-            # but we *should* be allowed to copy this node, because the
1314+            self.failUnlessEqual(fn.get_uri(), future_write_uri)
1315+            self.failUnlessEqual(fn.get_readonly_uri(), "ro." + future_read_uri)
1316+
1317+            (fn2, metadata2) = children[u"add-pair"]
1318+            self.failUnless(isinstance(fn2, UnknownNode), fn2)
1319+            self.failUnlessEqual(fn2.get_uri(), future_write_uri)
1320+            self.failUnlessEqual(fn2.get_readonly_uri(), "ro." + future_read_uri)
1321+
1322+            # we should also be allowed to copy this node, because the
1323             # UnknownNode contains all the information that was in the
1324             # original directory (readcap and writecap), so we're preserving
1325             # everything.
1326             return self._node.set_node(u"copy", fn)
1327         d.addCallback(_check)
1328+
1329         d.addCallback(lambda ign: self._node.list())
1330         def _check2(children):
1331-            self.failUnlessEqual(len(children), 2)
1332+            self.failUnlessEqual(len(children), 3)
1333             (fn, metadata) = children[u"copy"]
1334             self.failUnless(isinstance(fn, UnknownNode), fn)
1335-            self.failUnlessEqual(fn.get_uri(), future_writecap)
1336-            self.failUnlessEqual(fn.get_readonly_uri(), future_readcap)
1337+            self.failUnlessEqual(fn.get_uri(), future_write_uri)
1338+            self.failUnlessEqual(fn.get_readonly_uri(), "ro." + future_read_uri)
1339+        d.addCallback(_check2)
1340         return d
1341 
1342 class DeepStats(unittest.TestCase):
1343diff -rN -u old-tahoe/src/allmydata/test/test_filenode.py new-tahoe/src/allmydata/test/test_filenode.py
1344--- old-tahoe/src/allmydata/test/test_filenode.py       2010-01-23 12:59:09.796000000 +0000
1345+++ new-tahoe/src/allmydata/test/test_filenode.py       2010-01-23 12:59:11.912000000 +0000
1346@@ -41,14 +41,21 @@
1347         self.failUnlessEqual(fn1.get_readcap(), u)
1348         self.failUnlessEqual(fn1.is_readonly(), True)
1349         self.failUnlessEqual(fn1.is_mutable(), False)
1350+        self.failUnlessEqual(fn1.is_unknown(), False)
1351+        self.failUnlessEqual(fn1.is_allowed_in_immutable_directory(), True)
1352+        self.failUnlessEqual(fn1.get_write_uri(), None)
1353         self.failUnlessEqual(fn1.get_readonly_uri(), u.to_string())
1354         self.failUnlessEqual(fn1.get_size(), 1000)
1355         self.failUnlessEqual(fn1.get_storage_index(), u.storage_index)
1356+        fn1.raise_error()
1357+        fn2.raise_error()
1358         d = {}
1359         d[fn1] = 1 # exercise __hash__
1360         v = fn1.get_verify_cap()
1361         self.failUnless(isinstance(v, uri.CHKFileVerifierURI))
1362         self.failUnlessEqual(fn1.get_repair_cap(), v)
1363+        self.failUnlessEqual(v.is_readonly(), True)
1364+        self.failUnlessEqual(v.is_mutable(), False)
1365 
1366 
1367     def test_literal_filenode(self):
1368@@ -64,9 +71,14 @@
1369         self.failUnlessEqual(fn1.get_readcap(), u)
1370         self.failUnlessEqual(fn1.is_readonly(), True)
1371         self.failUnlessEqual(fn1.is_mutable(), False)
1372+        self.failUnlessEqual(fn1.is_unknown(), False)
1373+        self.failUnlessEqual(fn1.is_allowed_in_immutable_directory(), True)
1374+        self.failUnlessEqual(fn1.get_write_uri(), None)
1375         self.failUnlessEqual(fn1.get_readonly_uri(), u.to_string())
1376         self.failUnlessEqual(fn1.get_size(), len(DATA))
1377         self.failUnlessEqual(fn1.get_storage_index(), None)
1378+        fn1.raise_error()
1379+        fn2.raise_error()
1380         d = {}
1381         d[fn1] = 1 # exercise __hash__
1382 
1383@@ -99,24 +111,29 @@
1384         self.failUnlessEqual(n.get_writekey(), wk)
1385         self.failUnlessEqual(n.get_readkey(), rk)
1386         self.failUnlessEqual(n.get_storage_index(), si)
1387-        # these itmes are populated on first read (or create), so until that
1388+        # these items are populated on first read (or create), so until that
1389         # happens they'll be None
1390         self.failUnlessEqual(n.get_privkey(), None)
1391         self.failUnlessEqual(n.get_encprivkey(), None)
1392         self.failUnlessEqual(n.get_pubkey(), None)
1393 
1394         self.failUnlessEqual(n.get_uri(), u.to_string())
1395+        self.failUnlessEqual(n.get_write_uri(), u.to_string())
1396         self.failUnlessEqual(n.get_readonly_uri(), u.get_readonly().to_string())
1397         self.failUnlessEqual(n.get_cap(), u)
1398         self.failUnlessEqual(n.get_readcap(), u.get_readonly())
1399         self.failUnlessEqual(n.is_mutable(), True)
1400         self.failUnlessEqual(n.is_readonly(), False)
1401+        self.failUnlessEqual(n.is_unknown(), False)
1402+        self.failUnlessEqual(n.is_allowed_in_immutable_directory(), False)
1403+        n.raise_error()
1404 
1405         n2 = MutableFileNode(None, None, client.get_encoding_parameters(),
1406                              None).init_from_cap(u)
1407         self.failUnlessEqual(n, n2)
1408         self.failIfEqual(n, "not even the right type")
1409         self.failIfEqual(n, u) # not the right class
1410+        n.raise_error()
1411         d = {n: "can these be used as dictionary keys?"}
1412         d[n2] = "replace the old one"
1413         self.failUnlessEqual(len(d), 1)
1414@@ -127,12 +144,16 @@
1415         self.failUnlessEqual(nro.get_readonly(), nro)
1416         self.failUnlessEqual(nro.get_cap(), u.get_readonly())
1417         self.failUnlessEqual(nro.get_readcap(), u.get_readonly())
1418+        self.failUnlessEqual(nro.is_mutable(), True)
1419+        self.failUnlessEqual(nro.is_readonly(), True)
1420+        self.failUnlessEqual(nro.is_unknown(), False)
1421+        self.failUnlessEqual(nro.is_allowed_in_immutable_directory(), False)
1422         nro_u = nro.get_uri()
1423         self.failUnlessEqual(nro_u, nro.get_readonly_uri())
1424         self.failUnlessEqual(nro_u, u.get_readonly().to_string())
1425-        self.failUnlessEqual(nro.is_mutable(), True)
1426-        self.failUnlessEqual(nro.is_readonly(), True)
1427+        self.failUnlessEqual(nro.get_write_uri(), None)
1428         self.failUnlessEqual(nro.get_repair_cap(), None) # RSAmut needs writecap
1429+        nro.raise_error()
1430 
1431         v = n.get_verify_cap()
1432         self.failUnless(isinstance(v, uri.SSKVerifierURI))
1433diff -rN -u old-tahoe/src/allmydata/test/test_system.py new-tahoe/src/allmydata/test/test_system.py
1434--- old-tahoe/src/allmydata/test/test_system.py 2010-01-23 12:59:10.091000000 +0000
1435+++ new-tahoe/src/allmydata/test/test_system.py 2010-01-23 12:59:12.085000000 +0000
1436@@ -17,7 +17,7 @@
1437 from allmydata.interfaces import IDirectoryNode, IFileNode, \
1438      NoSuchChildError, NoSharesError
1439 from allmydata.monitor import Monitor
1440-from allmydata.mutable.common import NotMutableError
1441+from allmydata.mutable.common import NotWriteableError
1442 from allmydata.mutable import layout as mutable_layout
1443 from foolscap.api import DeadReferenceError
1444 from twisted.python.failure import Failure
1445@@ -890,11 +890,11 @@
1446             d1.addCallback(lambda res: dirnode.list())
1447             d1.addCallback(self.log, "dirnode.list")
1448 
1449-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mkdir(nope)", None, dirnode.create_subdirectory, u"nope"))
1450+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mkdir(nope)", None, dirnode.create_subdirectory, u"nope"))
1451 
1452             d1.addCallback(self.log, "doing add_file(ro)")
1453             ut = upload.Data("I will disappear, unrecorded and unobserved. The tragedy of my demise is made more poignant by its silence, but this beauty is not for you to ever know.", convergence="99i-p1x4-xd4-18yc-ywt-87uu-msu-zo -- completely and totally unguessable string (unless you read this)")
1454-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "add_file(nope)", None, dirnode.add_file, u"hope", ut))
1455+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "add_file(nope)", None, dirnode.add_file, u"hope", ut))
1456 
1457             d1.addCallback(self.log, "doing get(ro)")
1458             d1.addCallback(lambda res: dirnode.get(u"mydata992"))
1459@@ -902,17 +902,17 @@
1460                            self.failUnless(IFileNode.providedBy(filenode)))
1461 
1462             d1.addCallback(self.log, "doing delete(ro)")
1463-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "delete(nope)", None, dirnode.delete, u"mydata992"))
1464+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "delete(nope)", None, dirnode.delete, u"mydata992"))
1465 
1466-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "set_uri(nope)", None, dirnode.set_uri, u"hopeless", self.uri, self.uri))
1467+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "set_uri(nope)", None, dirnode.set_uri, u"hopeless", self.uri, self.uri))
1468 
1469             d1.addCallback(lambda res: self.shouldFail2(NoSuchChildError, "get(missing)", "missing", dirnode.get, u"missing"))
1470 
1471             personal = self._personal_node
1472-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mv from readonly", None, dirnode.move_child_to, u"mydata992", personal, u"nope"))
1473+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mv from readonly", None, dirnode.move_child_to, u"mydata992", personal, u"nope"))
1474 
1475             d1.addCallback(self.log, "doing move_child_to(ro)2")
1476-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mv to readonly", None, personal.move_child_to, u"sekrit data", dirnode, u"nope"))
1477+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mv to readonly", None, personal.move_child_to, u"sekrit data", dirnode, u"nope"))
1478 
1479             d1.addCallback(self.log, "finished with _got_s2ro")
1480             return d1
1481diff -rN -u old-tahoe/src/allmydata/test/test_uri.py new-tahoe/src/allmydata/test/test_uri.py
1482--- old-tahoe/src/allmydata/test/test_uri.py    2010-01-23 12:59:10.134000000 +0000
1483+++ new-tahoe/src/allmydata/test/test_uri.py    2010-01-23 12:59:12.094000000 +0000
1484@@ -3,7 +3,7 @@
1485 from allmydata import uri
1486 from allmydata.util import hashutil, base32
1487 from allmydata.interfaces import IURI, IFileURI, IDirnodeURI, IMutableFileURI, \
1488-    IVerifierURI
1489+    IVerifierURI, CapConstraintError
1490 
1491 class Literal(unittest.TestCase):
1492     def _help_test(self, data):
1493@@ -22,8 +22,16 @@
1494         self.failIf(IDirnodeURI.providedBy(u2))
1495         self.failUnlessEqual(u2.data, data)
1496         self.failUnlessEqual(u2.get_size(), len(data))
1497-        self.failUnless(u.is_readonly())
1498-        self.failIf(u.is_mutable())
1499+        self.failUnless(u2.is_readonly())
1500+        self.failIf(u2.is_mutable())
1501+
1502+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
1503+        self.failUnless(IFileURI.providedBy(u2i))
1504+        self.failIf(IDirnodeURI.providedBy(u2i))
1505+        self.failUnlessEqual(u2i.data, data)
1506+        self.failUnlessEqual(u2i.get_size(), len(data))
1507+        self.failUnless(u2i.is_readonly())
1508+        self.failIf(u2i.is_mutable())
1509 
1510         u3 = u.get_readonly()
1511         self.failUnlessIdentical(u, u3)
1512@@ -51,18 +59,36 @@
1513         fileURI = 'URI:CHK:f5ahxa25t4qkktywz6teyfvcx4:opuioq7tj2y6idzfp6cazehtmgs5fdcebcz3cygrxyydvcozrmeq:3:10:345834'
1514         chk1 = uri.CHKFileURI.init_from_string(fileURI)
1515         chk2 = uri.CHKFileURI.init_from_string(fileURI)
1516+        unk = uri.UnknownURI("lafs://from_the_future")
1517         self.failIfEqual(lit1, chk1)
1518         self.failUnlessEqual(chk1, chk2)
1519         self.failIfEqual(chk1, "not actually a URI")
1520         # these should be hashable too
1521-        s = set([lit1, chk1, chk2])
1522-        self.failUnlessEqual(len(s), 2) # since chk1==chk2
1523+        s = set([lit1, chk1, chk2, unk])
1524+        self.failUnlessEqual(len(s), 3) # since chk1==chk2
1525 
1526     def test_is_uri(self):
1527         lit1 = uri.LiteralFileURI("some data").to_string()
1528         self.failUnless(uri.is_uri(lit1))
1529         self.failIf(uri.is_uri(None))
1530 
1531+    def test_is_literal_file_uri(self):
1532+        lit1 = uri.LiteralFileURI("some data").to_string()
1533+        self.failUnless(uri.is_literal_file_uri(lit1))
1534+        self.failIf(uri.is_literal_file_uri(None))
1535+        self.failIf(uri.is_literal_file_uri("foo"))
1536+        self.failIf(uri.is_literal_file_uri("ro.foo"))
1537+        self.failIf(uri.is_literal_file_uri("URI:LITfoo"))
1538+        self.failUnless(uri.is_literal_file_uri("ro.URI:LIT:foo"))
1539+        self.failUnless(uri.is_literal_file_uri("imm.URI:LIT:foo"))
1540+
1541+    def test_has_uri_prefix(self):
1542+        self.failUnless(uri.has_uri_prefix("URI:foo"))
1543+        self.failUnless(uri.has_uri_prefix("ro.URI:foo"))
1544+        self.failUnless(uri.has_uri_prefix("imm.URI:foo"))
1545+        self.failIf(uri.has_uri_prefix(None))
1546+        self.failIf(uri.has_uri_prefix("foo"))
1547+
1548 class CHKFile(unittest.TestCase):
1549     def test_pack(self):
1550         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
1551@@ -88,8 +114,7 @@
1552         self.failUnless(IFileURI.providedBy(u))
1553         self.failIf(IDirnodeURI.providedBy(u))
1554         self.failUnlessEqual(u.get_size(), 1234)
1555-        self.failUnless(u.is_readonly())
1556-        self.failIf(u.is_mutable())
1557+
1558         u_ro = u.get_readonly()
1559         self.failUnlessIdentical(u, u_ro)
1560         he = u.to_human_encoding()
1561@@ -109,11 +134,19 @@
1562         self.failUnless(IFileURI.providedBy(u2))
1563         self.failIf(IDirnodeURI.providedBy(u2))
1564         self.failUnlessEqual(u2.get_size(), 1234)
1565-        self.failUnless(u2.is_readonly())
1566-        self.failIf(u2.is_mutable())
1567+
1568+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
1569+        self.failUnlessEqual(u.to_string(), u2i.to_string())
1570+        u2ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u.to_string())
1571+        self.failUnlessEqual(u.to_string(), u2ro.to_string())
1572+        u2imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u.to_string())
1573+        self.failUnlessEqual(u.to_string(), u2imm.to_string())
1574 
1575         v = u.get_verify_cap()
1576         self.failUnless(isinstance(v.to_string(), str))
1577+        self.failUnless(v.is_readonly())
1578+        self.failIf(v.is_mutable())
1579+
1580         v2 = uri.from_string(v.to_string())
1581         self.failUnlessEqual(v, v2)
1582         he = v.to_human_encoding()
1583@@ -126,6 +159,8 @@
1584                                     total_shares=10,
1585                                     size=1234)
1586         self.failUnless(isinstance(v3.to_string(), str))
1587+        self.failUnless(v3.is_readonly())
1588+        self.failIf(v3.is_mutable())
1589 
1590     def test_pack_badly(self):
1591         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
1592@@ -179,13 +214,20 @@
1593         self.failUnlessEqual(readable["UEB_hash"],
1594                              base32.b2a(hashutil.uri_extension_hash(ext)))
1595 
1596-class Invalid(unittest.TestCase):
1597+class Unknown(unittest.TestCase):
1598     def test_from_future(self):
1599         # any URI type that we don't recognize should be treated as unknown
1600         future_uri = "I am a URI from the future. Whatever you do, don't "
1601         u = uri.from_string(future_uri)
1602         self.failUnless(isinstance(u, uri.UnknownURI))
1603         self.failUnlessEqual(u.to_string(), future_uri)
1604+        self.failUnless(u.get_readonly() is None)
1605+        self.failUnless(u.get_error() is None)
1606+
1607+        u2 = uri.UnknownURI(future_uri, error=CapConstraintError("..."))
1608+        self.failUnlessEqual(u.to_string(), future_uri)
1609+        self.failUnless(u2.get_readonly() is None)
1610+        self.failUnless(isinstance(u2.get_error(), CapConstraintError))
1611 
1612 class Constraint(unittest.TestCase):
1613     def test_constraint(self):
1614@@ -226,6 +268,13 @@
1615         self.failUnless(IMutableFileURI.providedBy(u2))
1616         self.failIf(IDirnodeURI.providedBy(u2))
1617 
1618+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
1619+        self.failUnless(isinstance(u2i, uri.UnknownURI), u2i)
1620+        u2ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u.to_string())
1621+        self.failUnless(isinstance(u2ro, uri.UnknownURI), u2ro)
1622+        u2imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u.to_string())
1623+        self.failUnless(isinstance(u2imm, uri.UnknownURI), u2imm)
1624+
1625         u3 = u2.get_readonly()
1626         readkey = hashutil.ssk_readkey_hash(writekey)
1627         self.failUnlessEqual(u3.fingerprint, fingerprint)
1628@@ -236,6 +285,13 @@
1629         self.failUnless(IMutableFileURI.providedBy(u3))
1630         self.failIf(IDirnodeURI.providedBy(u3))
1631 
1632+        u3i = uri.from_string(u3.to_string(), deep_immutable=True)
1633+        self.failUnless(isinstance(u3i, uri.UnknownURI), u3i)
1634+        u3ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u3.to_string())
1635+        self.failUnlessEqual(u3.to_string(), u3ro.to_string())
1636+        u3imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u3.to_string())
1637+        self.failUnless(isinstance(u3imm, uri.UnknownURI), u3imm)
1638+
1639         he = u3.to_human_encoding()
1640         u3_h = uri.ReadonlySSKFileURI.init_from_human_encoding(he)
1641         self.failUnlessEqual(u3, u3_h)
1642@@ -249,6 +305,13 @@
1643         self.failUnless(IMutableFileURI.providedBy(u4))
1644         self.failIf(IDirnodeURI.providedBy(u4))
1645 
1646+        u4i = uri.from_string(u4.to_string(), deep_immutable=True)
1647+        self.failUnless(isinstance(u4i, uri.UnknownURI), u4i)
1648+        u4ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u4.to_string())
1649+        self.failUnlessEqual(u4.to_string(), u4ro.to_string())
1650+        u4imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u4.to_string())
1651+        self.failUnless(isinstance(u4imm, uri.UnknownURI), u4imm)
1652+
1653         u4a = uri.from_string(u4.to_string())
1654         self.failUnlessEqual(u4a, u4)
1655         self.failUnless("ReadonlySSKFileURI" in str(u4a))
1656@@ -291,12 +354,19 @@
1657         self.failIf(IFileURI.providedBy(u2))
1658         self.failUnless(IDirnodeURI.providedBy(u2))
1659 
1660+        u2i = uri.from_string(u1.to_string(), deep_immutable=True)
1661+        self.failUnless(isinstance(u2i, uri.UnknownURI))
1662+
1663         u3 = u2.get_readonly()
1664         self.failUnless(u3.is_readonly())
1665         self.failUnless(u3.is_mutable())
1666         self.failUnless(IURI.providedBy(u3))
1667         self.failIf(IFileURI.providedBy(u3))
1668         self.failUnless(IDirnodeURI.providedBy(u3))
1669+
1670+        u3i = uri.from_string(u2.to_string(), deep_immutable=True)
1671+        self.failUnless(isinstance(u3i, uri.UnknownURI))
1672+
1673         u3n = u3._filenode_uri
1674         self.failUnless(u3n.is_readonly())
1675         self.failUnless(u3n.is_mutable())
1676@@ -363,10 +433,16 @@
1677         self.failIf(IFileURI.providedBy(u2))
1678         self.failUnless(IDirnodeURI.providedBy(u2))
1679 
1680+        u2i = uri.from_string(u1.to_string(), deep_immutable=True)
1681+        self.failUnlessEqual(u1.to_string(), u2i.to_string())
1682+
1683         u3 = u2.get_readonly()
1684         self.failUnlessEqual(u3.to_string(), u2.to_string())
1685         self.failUnless(str(u3))
1686 
1687+        u3i = uri.from_string(u2.to_string(), deep_immutable=True)
1688+        self.failUnlessEqual(u2.to_string(), u3i.to_string())
1689+
1690         u2_verifier = u2.get_verify_cap()
1691         self.failUnless(isinstance(u2_verifier,
1692                                    uri.ImmutableDirectoryURIVerifier),
1693diff -rN -u old-tahoe/src/allmydata/test/test_web.py new-tahoe/src/allmydata/test/test_web.py
1694--- old-tahoe/src/allmydata/test/test_web.py    2010-01-23 12:59:10.149000000 +0000
1695+++ new-tahoe/src/allmydata/test/test_web.py    2010-01-23 12:59:12.131000000 +0000
1696@@ -7,7 +7,7 @@
1697 from twisted.web import client, error, http
1698 from twisted.python import failure, log
1699 from nevow import rend
1700-from allmydata import interfaces, uri, webish
1701+from allmydata import interfaces, uri, webish, dirnode
1702 from allmydata.storage.shares import get_share_file
1703 from allmydata.storage_client import StorageFarmBroker
1704 from allmydata.immutable import upload, download
1705@@ -18,6 +18,7 @@
1706 from allmydata.scripts.debug import CorruptShareOptions, corrupt_share
1707 from allmydata.util import fileutil, base32
1708 from allmydata.util.consumer import download_to_data
1709+from allmydata.util.netstring import split_netstring
1710 from allmydata.test.common import FakeCHKFileNode, FakeMutableFileNode, \
1711      create_chk_filenode, WebErrorMixin, ShouldFailMixin, make_mutable_file_uri
1712 from allmydata.interfaces import IMutableFileNode
1713@@ -366,25 +367,101 @@
1714             self.fail("%s was supposed to Error(404), not get '%s'" %
1715                       (which, res))
1716 
1717+    def _dump_res(self, res):
1718+        import traceback
1719+        s = "%r\n" % (res,)
1720+        if hasattr(res, 'tb_frame'):
1721+            s += "Traceback:\n%s\n" % (traceback.format_tb(res),)
1722+        if hasattr(res, 'value'):
1723+            s += "%r\n" % (res.value,)
1724+            if hasattr(res.value, 'tb_frame'):
1725+                s += "Traceback:\n%s\n" % (res, res.value, traceback.format_tb(res))
1726+            if hasattr(res.value, 'response'):
1727+                s += "Response body:\n%s\n" % (res.value.response,)
1728+        return s
1729+
1730+    def shouldSucceedGET(self, urlpath, followRedirect=False,
1731+                         expected_statuscode=http.OK, return_response=False, **kwargs):
1732+        d = self.GET(urlpath, followRedirect=followRedirect, return_response=True, **kwargs)
1733+        def done((res, statuscode, headers)):
1734+            if isinstance(res, failure.Failure):
1735+                self.fail(("'GET %s' with kwargs %r was supposed to succeed with statuscode %s, "
1736+                           "but it failed with statuscode %s instead.\n"
1737+                           "%s\nThe response headers were:\n%s") % (
1738+                               urlpath, kwargs, expected_statuscode, statuscode,
1739+                               self._dump_res(res), headers))
1740+            if str(statuscode) != str(expected_statuscode):
1741+                self.fail(("'GET %s' with kwargs %r was supposed to succeed with statuscode %s, "
1742+                            "but it succeeded with statuscode %s instead.\n"
1743+                            "The response headers were:\n%s\n\n"
1744+                            "The response body was:\n%s") % (
1745+                                urlpath, kwargs, expected_statuscode, statuscode, headers, res))
1746+            if return_response:
1747+                return (res, statuscode, headers)
1748+            else:
1749+                return res
1750+        d.addBoth(done)
1751+        return d
1752+
1753+    def shouldSucceedHEAD(self, urlpath, expected_statuscode=http.OK,
1754+                          return_response=False, **kwargs):
1755+        d = self.HEAD(urlpath, return_response=True, **kwargs)
1756+        def done((res, statuscode, headers)):
1757+            if isinstance(res, failure.Failure):
1758+                self.fail(("'HEAD %s' with kwargs %r was supposed to succeed with statuscode %s, "
1759+                           "but it failed with statuscode %s instead.\n"
1760+                           "%s\nThe response headers were:\n%s") % (
1761+                               urlpath, kwargs, expected_statuscode, statuscode,
1762+                               self._dump_res(res), headers))
1763+            if str(statuscode) != str(expected_statuscode):
1764+                self.fail(("'HEAD %s' with kwargs %r was supposed to succeed with statuscode %s, "
1765+                            "but it succeeded with statuscode %s instead.\n"
1766+                            "The response headers were:\n%s\n\n"
1767+                            "The response body was:\n%s") % (
1768+                                urlpath, kwargs, expected_statuscode, statuscode, headers, res))
1769+            if return_response:
1770+                return (res, statuscode, headers)
1771+            else:
1772+                return res
1773+        d.addBoth(done)
1774+        return d
1775+
1776+    def shouldSucceed(self, which, expected_statuscode, callable, *args, **kwargs):
1777+        d = defer.maybeDeferred(callable, *args, **kwargs)
1778+        def done(res):
1779+            if isinstance(res, failure.Failure):
1780+                self.fail(("%s:\nAn HTTP op with args %r and kwargs %r was supposed to "
1781+                           "succeed with statuscode %s, but it failed:\n%s") % (
1782+                               which, args, kwargs, expected_statuscode,
1783+                               self._dump_res(res)))
1784+            #if str(statuscode) != str(expected_statuscode):
1785+            #    self.fail(("%s:\nAn HTTP op with args %r and kwargs %r was supposed to "
1786+            #               "succeed with statuscode %s, but it succeeded with statuscode %s instead.\n"
1787+            #               "The response body was:\n%s") % (
1788+            #                   which, args, kwargs, expected_statuscode, statuscode, res))
1789+            return res
1790+        d.addBoth(done)
1791+        return d
1792+
1793 
1794 class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
1795     def test_create(self):
1796         pass
1797 
1798     def test_welcome(self):
1799-        d = self.GET("/")
1800+        d = self.shouldSucceedGET("/")
1801         def _check(res):
1802             self.failUnless('Welcome To Tahoe-LAFS' in res, res)
1803 
1804             self.s.basedir = 'web/test_welcome'
1805             fileutil.make_dirs("web/test_welcome")
1806             fileutil.make_dirs("web/test_welcome/private")
1807-            return self.GET("/")
1808+            return self.shouldSucceedGET("/")
1809         d.addCallback(_check)
1810         return d
1811 
1812     def test_provisioning(self):
1813-        d = self.GET("/provisioning/")
1814+        d = self.shouldSucceedGET("/provisioning/")
1815         def _check(res):
1816             self.failUnless('Tahoe Provisioning Tool' in res)
1817             fields = {'filled': True,
1818@@ -400,9 +477,10 @@
1819                       "delete_rate": 10,
1820                       "lease_timer": 7,
1821                       }
1822-            return self.POST("/provisioning/", **fields)
1823-
1824+            return self.shouldSucceed("POST_provisioning-1", http.OK, self.POST,
1825+                                      "/provisioning/", **fields)
1826         d.addCallback(_check)
1827+
1828         def _check2(res):
1829             self.failUnless('Tahoe Provisioning Tool' in res)
1830             self.failUnless("Share space consumed: 167.01TB" in res)
1831@@ -422,13 +500,17 @@
1832                       "delete_rate": 100,
1833                       "lease_timer": 7,
1834                       }
1835-            return self.POST("/provisioning/", **fields)
1836+            return self.shouldSucceed("POST_provisioning-2", http.OK, self.POST,
1837+                                      "/provisioning/", **fields)
1838         d.addCallback(_check2)
1839+
1840         def _check3(res):
1841             self.failUnless("Share space consumed: huge!" in res)
1842             fields = {'filled': True}
1843-            return self.POST("/provisioning/", **fields)
1844+            return self.shouldSucceed("POST_provisioning-3", http.OK, self.POST,
1845+                                      "/provisioning/", **fields)
1846         d.addCallback(_check3)
1847+
1848         def _check4(res):
1849             self.failUnless("Share space consumed:" in res)
1850         d.addCallback(_check4)
1851@@ -442,7 +524,7 @@
1852         except:
1853             raise unittest.SkipTest("reliability tool requires NumPy")
1854 
1855-        d = self.GET("/reliability/")
1856+        d = self.shouldSucceedGET("/reliability/")
1857         def _check(res):
1858             self.failUnless('Tahoe Reliability Tool' in res)
1859             fields = {'drive_lifetime': "8Y",
1860@@ -471,7 +553,7 @@
1861         mu_num = h.list_all_mapupdate_statuses()[0].get_counter()
1862         pub_num = h.list_all_publish_statuses()[0].get_counter()
1863         ret_num = h.list_all_retrieve_statuses()[0].get_counter()
1864-        d = self.GET("/status", followRedirect=True)
1865+        d = self.shouldSucceedGET("/status", followRedirect=True)
1866         def _check(res):
1867             self.failUnless('Upload and Download Status' in res, res)
1868             self.failUnless('"down-%d"' % dl_num in res, res)
1869@@ -480,7 +562,7 @@
1870             self.failUnless('"publish-%d"' % pub_num in res, res)
1871             self.failUnless('"retrieve-%d"' % ret_num in res, res)
1872         d.addCallback(_check)
1873-        d.addCallback(lambda res: self.GET("/status/?t=json"))
1874+        d.addCallback(lambda res: self.shouldSucceedGET("/status/?t=json"))
1875         def _check_json(res):
1876             data = simplejson.loads(res)
1877             self.failUnless(isinstance(data, dict))
1878@@ -489,23 +571,23 @@
1879             # here.
1880         d.addCallback(_check_json)
1881 
1882-        d.addCallback(lambda res: self.GET("/status/down-%d" % dl_num))
1883+        d.addCallback(lambda res: self.shouldSucceedGET("/status/down-%d" % dl_num))
1884         def _check_dl(res):
1885             self.failUnless("File Download Status" in res, res)
1886         d.addCallback(_check_dl)
1887-        d.addCallback(lambda res: self.GET("/status/up-%d" % ul_num))
1888+        d.addCallback(lambda res: self.shouldSucceedGET("/status/up-%d" % ul_num))
1889         def _check_ul(res):
1890             self.failUnless("File Upload Status" in res, res)
1891         d.addCallback(_check_ul)
1892-        d.addCallback(lambda res: self.GET("/status/mapupdate-%d" % mu_num))
1893+        d.addCallback(lambda res: self.shouldSucceedGET("/status/mapupdate-%d" % mu_num))
1894         def _check_mapupdate(res):
1895             self.failUnless("Mutable File Servermap Update Status" in res, res)
1896         d.addCallback(_check_mapupdate)
1897-        d.addCallback(lambda res: self.GET("/status/publish-%d" % pub_num))
1898+        d.addCallback(lambda res: self.shouldSucceedGET("/status/publish-%d" % pub_num))
1899         def _check_publish(res):
1900             self.failUnless("Mutable File Publish Status" in res, res)
1901         d.addCallback(_check_publish)
1902-        d.addCallback(lambda res: self.GET("/status/retrieve-%d" % ret_num))
1903+        d.addCallback(lambda res: self.shouldSucceedGET("/status/retrieve-%d" % ret_num))
1904         def _check_retrieve(res):
1905             self.failUnless("Mutable File Retrieve Status" in res, res)
1906         d.addCallback(_check_retrieve)
1907@@ -536,16 +618,15 @@
1908         self.failUnlessEqual(urrm.render_rate(None, 123), "123Bps")
1909 
1910     def test_GET_FILEURL(self):
1911-        d = self.GET(self.public_url + "/foo/bar.txt")
1912+        d = self.shouldSucceedGET(self.public_url + "/foo/bar.txt")
1913         d.addCallback(self.failUnlessIsBarDotTxt)
1914         return d
1915 
1916     def test_GET_FILEURL_range(self):
1917         headers = {"range": "bytes=1-10"}
1918-        d = self.GET(self.public_url + "/foo/bar.txt", headers=headers,
1919-                     return_response=True)
1920-        def _got((res, status, headers)):
1921-            self.failUnlessEqual(int(status), 206)
1922+        d = self.shouldSucceedGET(self.public_url + "/foo/bar.txt", headers=headers,
1923+                                  expected_statuscode=http.PARTIAL_CONTENT, return_response=True)
1924+        def _got((res, statuscode, headers)):
1925             self.failUnless(headers.has_key("content-range"))
1926             self.failUnlessEqual(headers["content-range"][0],
1927                                  "bytes 1-10/%d" % len(self.BAR_CONTENTS))
1928@@ -556,10 +637,9 @@
1929     def test_GET_FILEURL_partial_range(self):
1930         headers = {"range": "bytes=5-"}
1931         length  = len(self.BAR_CONTENTS)
1932-        d = self.GET(self.public_url + "/foo/bar.txt", headers=headers,
1933-                     return_response=True)
1934-        def _got((res, status, headers)):
1935-            self.failUnlessEqual(int(status), 206)
1936+        d = self.shouldSucceedGET(self.public_url + "/foo/bar.txt", headers=headers,
1937+                                  expected_statuscode=http.PARTIAL_CONTENT, return_response=True)
1938+        def _got((res, statuscode, headers)):
1939             self.failUnless(headers.has_key("content-range"))
1940             self.failUnlessEqual(headers["content-range"][0],
1941                                  "bytes 5-%d/%d" % (length-1, length))
1942@@ -569,11 +649,10 @@
1943 
1944     def test_HEAD_FILEURL_range(self):
1945         headers = {"range": "bytes=1-10"}
1946-        d = self.HEAD(self.public_url + "/foo/bar.txt", headers=headers,
1947-                     return_response=True)
1948-        def _got((res, status, headers)):
1949+        d = self.shouldSucceedHEAD(self.public_url + "/foo/bar.txt", headers=headers,
1950+                                   expected_statuscode=http.PARTIAL_CONTENT, return_response=True)
1951+        def _got((res, statuscode, headers)):
1952             self.failUnlessEqual(res, "")
1953-            self.failUnlessEqual(int(status), 206)
1954             self.failUnless(headers.has_key("content-range"))
1955             self.failUnlessEqual(headers["content-range"][0],
1956                                  "bytes 1-10/%d" % len(self.BAR_CONTENTS))
1957@@ -583,10 +662,9 @@
1958     def test_HEAD_FILEURL_partial_range(self):
1959         headers = {"range": "bytes=5-"}
1960         length  = len(self.BAR_CONTENTS)
1961-        d = self.HEAD(self.public_url + "/foo/bar.txt", headers=headers,
1962-                     return_response=True)
1963-        def _got((res, status, headers)):
1964-            self.failUnlessEqual(int(status), 206)
1965+        d = self.shouldSucceedHEAD(self.public_url + "/foo/bar.txt", headers=headers,
1966+                                   expected_statuscode=http.PARTIAL_CONTENT, return_response=True)
1967+        def _got((res, statuscode, headers)):
1968             self.failUnless(headers.has_key("content-range"))
1969             self.failUnlessEqual(headers["content-range"][0],
1970                                  "bytes 5-%d/%d" % (length-1, length))
1971@@ -595,7 +673,7 @@
1972 
1973     def test_GET_FILEURL_range_bad(self):
1974         headers = {"range": "BOGUS=fizbop-quarnak"}
1975-        d = self.shouldFail2(error.Error, "test_GET_FILEURL_range_bad",
1976+        d = self.shouldFail2(error.Error, "GET_FILEURL_range_bad",
1977                              "400 Bad Request",
1978                              "Syntactically invalid http range header",
1979                              self.GET, self.public_url + "/foo/bar.txt",
1980@@ -603,8 +681,9 @@
1981         return d
1982 
1983     def test_HEAD_FILEURL(self):
1984-        d = self.HEAD(self.public_url + "/foo/bar.txt", return_response=True)
1985-        def _got((res, status, headers)):
1986+        d = self.shouldSucceedHEAD(self.public_url + "/foo/bar.txt",
1987+                                   expected_statuscode=http.OK, return_response=True)
1988+        def _got((res, statuscode, headers)):
1989             self.failUnlessEqual(res, "")
1990             self.failUnlessEqual(headers["content-length"][0],
1991                                  str(len(self.BAR_CONTENTS)))
1992@@ -615,27 +694,27 @@
1993     def test_GET_FILEURL_named(self):
1994         base = "/file/%s" % urllib.quote(self._bar_txt_uri)
1995         base2 = "/named/%s" % urllib.quote(self._bar_txt_uri)
1996-        d = self.GET(base + "/@@name=/blah.txt")
1997+        d = self.shouldSucceedGET(base + "/@@name=/blah.txt")
1998         d.addCallback(self.failUnlessIsBarDotTxt)
1999-        d.addCallback(lambda res: self.GET(base + "/blah.txt"))
2000+        d.addCallback(lambda res: self.shouldSucceedGET(base + "/blah.txt"))
2001         d.addCallback(self.failUnlessIsBarDotTxt)
2002-        d.addCallback(lambda res: self.GET(base + "/ignore/lots/blah.txt"))
2003+        d.addCallback(lambda res: self.shouldSucceedGET(base + "/ignore/lots/blah.txt"))
2004         d.addCallback(self.failUnlessIsBarDotTxt)
2005-        d.addCallback(lambda res: self.GET(base2 + "/@@name=/blah.txt"))
2006+        d.addCallback(lambda res: self.shouldSucceedGET(base2 + "/@@name=/blah.txt"))
2007         d.addCallback(self.failUnlessIsBarDotTxt)
2008         save_url = base + "?save=true&filename=blah.txt"
2009-        d.addCallback(lambda res: self.GET(save_url))
2010+        d.addCallback(lambda res: self.shouldSucceedGET(save_url))
2011         d.addCallback(self.failUnlessIsBarDotTxt) # TODO: check headers
2012         u_filename = u"n\u00e9wer.txt" # n e-acute w e r . t x t
2013         u_fn_e = urllib.quote(u_filename.encode("utf-8"))
2014         u_url = base + "?save=true&filename=" + u_fn_e
2015-        d.addCallback(lambda res: self.GET(u_url))
2016+        d.addCallback(lambda res: self.shouldSucceedGET(u_url))
2017         d.addCallback(self.failUnlessIsBarDotTxt) # TODO: check headers
2018         return d
2019 
2020     def test_PUT_FILEURL_named_bad(self):
2021         base = "/file/%s" % urllib.quote(self._bar_txt_uri)
2022-        d = self.shouldFail2(error.Error, "test_PUT_FILEURL_named_bad",
2023+        d = self.shouldFail2(error.Error, "PUT_FILEURL_named_bad",
2024                              "400 Bad Request",
2025                              "/file can only be used with GET or HEAD",
2026                              self.PUT, base + "/@@name=/blah.txt", "")
2027@@ -643,14 +722,14 @@
2028 
2029     def test_GET_DIRURL_named_bad(self):
2030         base = "/file/%s" % urllib.quote(self._foo_uri)
2031-        d = self.shouldFail2(error.Error, "test_PUT_DIRURL_named_bad",
2032+        d = self.shouldFail2(error.Error, "PUT_DIRURL_named_bad",
2033                              "400 Bad Request",
2034                              "is not a file-cap",
2035                              self.GET, base + "/@@name=/blah.txt")
2036         return d
2037 
2038     def test_GET_slash_file_bad(self):
2039-        d = self.shouldFail2(error.Error, "test_GET_slash_file_bad",
2040+        d = self.shouldFail2(error.Error, "GET_slash_file_bad",
2041                              "404 Not Found",
2042                              "/file must be followed by a file-cap and a name",
2043                              self.GET, "/file")
2044@@ -671,7 +750,7 @@
2045         verifier_cap = n.get_verify_cap().to_string()
2046         base = "/uri/%s" % urllib.quote(verifier_cap)
2047         # client.create_node_from_uri() can't handle verify-caps
2048-        d = self.shouldFail2(error.Error, "test_GET_unhandled_URI",
2049+        d = self.shouldFail2(error.Error, "GET_unhandled_URI",
2050                              "400 Bad Request",
2051                              "GET unknown URI type: can only do t=info",
2052                              self.GET, base)
2053@@ -679,14 +758,14 @@
2054 
2055     def test_GET_FILE_URI(self):
2056         base = "/uri/%s" % urllib.quote(self._bar_txt_uri)
2057-        d = self.GET(base)
2058+        d = self.shouldSucceedGET(base)
2059         d.addCallback(self.failUnlessIsBarDotTxt)
2060         return d
2061 
2062     def test_GET_FILE_URI_badchild(self):
2063         base = "/uri/%s/boguschild" % urllib.quote(self._bar_txt_uri)
2064         errmsg = "Files have no children, certainly not named 'boguschild'"
2065-        d = self.shouldFail2(error.Error, "test_GET_FILE_URI_badchild",
2066+        d = self.shouldFail2(error.Error, "GET_FILE_URI_badchild",
2067                              "400 Bad Request", errmsg,
2068                              self.GET, base)
2069         return d
2070@@ -694,35 +773,42 @@
2071     def test_PUT_FILE_URI_badchild(self):
2072         base = "/uri/%s/boguschild" % urllib.quote(self._bar_txt_uri)
2073         errmsg = "Cannot create directory 'boguschild', because its parent is a file, not a directory"
2074-        d = self.shouldFail2(error.Error, "test_GET_FILE_URI_badchild",
2075+        d = self.shouldFail2(error.Error, "GET_FILE_URI_badchild",
2076                              "400 Bad Request", errmsg,
2077                              self.PUT, base, "")
2078         return d
2079 
2080+    # TODO: version of this with a Unicode filename
2081     def test_GET_FILEURL_save(self):
2082-        d = self.GET(self.public_url + "/foo/bar.txt?filename=bar.txt&save=true")
2083-        # TODO: look at the headers, expect a Content-Disposition: attachment
2084-        # header.
2085-        d.addCallback(self.failUnlessIsBarDotTxt)
2086+        d = self.shouldSucceedGET(self.public_url + "/foo/bar.txt?filename=bar.txt&save=true",
2087+                                  return_response=True)
2088+        def _got((res, statuscode, headers)):
2089+            content_disposition = headers["content-disposition"][0]
2090+            self.failUnless(content_disposition == 'attachment; filename="bar.txt"', content_disposition)
2091+            self.failUnlessIsBarDotTxt(res)
2092+        d.addCallback(_got)
2093         return d
2094 
2095     def test_GET_FILEURL_missing(self):
2096         d = self.GET(self.public_url + "/foo/missing")
2097-        d.addBoth(self.should404, "test_GET_FILEURL_missing")
2098+        d.addBoth(self.should404, "GET_FILEURL_missing")
2099         return d
2100 
2101     def test_PUT_overwrite_only_files(self):
2102         # create a directory, put a file in that directory.
2103         contents, n, filecap = self.makefile(8)
2104-        d = self.PUT(self.public_url + "/foo/dir?t=mkdir", "")
2105+        d = self.shouldSucceed("PUT_overwrite_only_files_1", http.OK, self.PUT,
2106+                               self.public_url + "/foo/dir?t=mkdir", "")
2107         d.addCallback(lambda res:
2108-            self.PUT(self.public_url + "/foo/dir/file1.txt",
2109-                     self.NEWFILE_CONTENTS))
2110+            self.shouldSucceed("PUT_overwrite_only_files_2", http.OK, self.PUT,
2111+                               self.public_url + "/foo/dir/file1.txt",
2112+                               self.NEWFILE_CONTENTS))
2113         # try to overwrite the file with replace=only-files
2114         # (this should work)
2115         d.addCallback(lambda res:
2116-            self.PUT(self.public_url + "/foo/dir/file1.txt?t=uri&replace=only-files",
2117-                     filecap))
2118+            self.shouldSucceed("PUT_overwrite_only_files_3", http.OK, self.PUT,
2119+                               self.public_url + "/foo/dir/file1.txt?t=uri&replace=only-files",
2120+                               filecap))
2121         d.addCallback(lambda res:
2122             self.shouldFail2(error.Error, "PUT_bad_t", "409 Conflict",
2123                  "There was already a child by that name, and you asked me "
2124@@ -732,21 +818,19 @@
2125         return d
2126 
2127     def test_PUT_NEWFILEURL(self):
2128-        d = self.PUT(self.public_url + "/foo/new.txt", self.NEWFILE_CONTENTS)
2129-        # TODO: we lose the response code, so we can't check this
2130-        #self.failUnlessEqual(responsecode, 201)
2131-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2132+        d = self.shouldSucceed("PUT_NEWFILEURL", http.CREATED, self.PUT,
2133+                               self.public_url + "/foo/new.txt", self.NEWFILE_CONTENTS)
2134+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
2135         d.addCallback(lambda res:
2136                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
2137                                                       self.NEWFILE_CONTENTS))
2138         return d
2139 
2140     def test_PUT_NEWFILEURL_not_mutable(self):
2141-        d = self.PUT(self.public_url + "/foo/new.txt?mutable=false",
2142-                     self.NEWFILE_CONTENTS)
2143-        # TODO: we lose the response code, so we can't check this
2144-        #self.failUnlessEqual(responsecode, 201)
2145-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2146+        d = self.shouldSucceed("PUT_NEWFILEURL_not_mutable", http.CREATED, self.PUT,
2147+                               self.public_url + "/foo/new.txt?mutable=false",
2148+                               self.NEWFILE_CONTENTS)
2149+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
2150         d.addCallback(lambda res:
2151                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
2152                                                       self.NEWFILE_CONTENTS))
2153@@ -755,7 +839,7 @@
2154     def test_PUT_NEWFILEURL_range_bad(self):
2155         headers = {"content-range": "bytes 1-10/%d" % len(self.NEWFILE_CONTENTS)}
2156         target = self.public_url + "/foo/new.txt"
2157-        d = self.shouldFail2(error.Error, "test_PUT_NEWFILEURL_range_bad",
2158+        d = self.shouldFail2(error.Error, "PUT_NEWFILEURL_range_bad",
2159                              "501 Not Implemented",
2160                              "Content-Range in PUT not yet supported",
2161                              # (and certainly not for immutable files)
2162@@ -766,17 +850,16 @@
2163         return d
2164 
2165     def test_PUT_NEWFILEURL_mutable(self):
2166-        d = self.PUT(self.public_url + "/foo/new.txt?mutable=true",
2167-                     self.NEWFILE_CONTENTS)
2168-        # TODO: we lose the response code, so we can't check this
2169-        #self.failUnlessEqual(responsecode, 201)
2170+        d = self.shouldSucceed("PUT_NEWFILEURL_mutable", http.CREATED, self.PUT,
2171+                               self.public_url + "/foo/new.txt?mutable=true",
2172+                               self.NEWFILE_CONTENTS)
2173         def _check_uri(res):
2174             u = uri.from_string_mutable_filenode(res)
2175             self.failUnless(u.is_mutable())
2176             self.failIf(u.is_readonly())
2177             return res
2178         d.addCallback(_check_uri)
2179-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2180+        d.addCallback(self.failUnlessURIMatchesRWChild, self._foo_node, u"new.txt")
2181         d.addCallback(lambda res:
2182                       self.failUnlessMutableChildContentsAre(self._foo_node,
2183                                                              u"new.txt",
2184@@ -784,7 +867,7 @@
2185         return d
2186 
2187     def test_PUT_NEWFILEURL_mutable_toobig(self):
2188-        d = self.shouldFail2(error.Error, "test_PUT_NEWFILEURL_mutable_toobig",
2189+        d = self.shouldFail2(error.Error, "PUT_NEWFILEURL_mutable_toobig",
2190                              "413 Request Entity Too Large",
2191                              "SDMF is limited to one segment, and 10001 > 10000",
2192                              self.PUT,
2193@@ -793,10 +876,9 @@
2194         return d
2195 
2196     def test_PUT_NEWFILEURL_replace(self):
2197-        d = self.PUT(self.public_url + "/foo/bar.txt", self.NEWFILE_CONTENTS)
2198-        # TODO: we lose the response code, so we can't check this
2199-        #self.failUnlessEqual(responsecode, 200)
2200-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt")
2201+        d = self.shouldSucceed("PUT_NEWFILEURL_replace", http.OK, self.PUT,
2202+                               self.public_url + "/foo/bar.txt", self.NEWFILE_CONTENTS)
2203+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt")
2204         d.addCallback(lambda res:
2205                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
2206                                                       self.NEWFILE_CONTENTS))
2207@@ -819,9 +901,11 @@
2208         return d
2209 
2210     def test_PUT_NEWFILEURL_mkdirs(self):
2211-        d = self.PUT(self.public_url + "/foo/newdir/new.txt", self.NEWFILE_CONTENTS)
2212+        d = self.shouldSucceed("PUT_NEWFILEURL_mkdirs", http.OK, self.PUT,
2213+                               self.public_url + "/foo/newdir/new.txt",
2214+                               self.NEWFILE_CONTENTS)
2215         fn = self._foo_node
2216-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"newdir/new.txt")
2217+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"newdir/new.txt")
2218         d.addCallback(lambda res: self.failIfNodeHasChild(fn, u"new.txt"))
2219         d.addCallback(lambda res: self.failUnlessNodeHasChild(fn, u"newdir"))
2220         d.addCallback(lambda res:
2221@@ -839,26 +923,27 @@
2222 
2223     def test_PUT_NEWFILEURL_emptyname(self):
2224         # an empty pathname component (i.e. a double-slash) is disallowed
2225-        d = self.shouldFail2(error.Error, "test_PUT_NEWFILEURL_emptyname",
2226+        d = self.shouldFail2(error.Error, "PUT_NEWFILEURL_emptyname",
2227                              "400 Bad Request",
2228                              "The webapi does not allow empty pathname components",
2229                              self.PUT, self.public_url + "/foo//new.txt", "")
2230         return d
2231 
2232     def test_DELETE_FILEURL(self):
2233-        d = self.DELETE(self.public_url + "/foo/bar.txt")
2234+        d = self.shouldSucceed("DELETE_FILEURL", http.OK, self.DELETE,
2235+                               self.public_url + "/foo/bar.txt")
2236         d.addCallback(lambda res:
2237                       self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
2238         return d
2239 
2240     def test_DELETE_FILEURL_missing(self):
2241         d = self.DELETE(self.public_url + "/foo/missing")
2242-        d.addBoth(self.should404, "test_DELETE_FILEURL_missing")
2243+        d.addBoth(self.should404, "DELETE_FILEURL_missing")
2244         return d
2245 
2246     def test_DELETE_FILEURL_missing2(self):
2247         d = self.DELETE(self.public_url + "/missing/missing")
2248-        d.addBoth(self.should404, "test_DELETE_FILEURL_missing2")
2249+        d.addBoth(self.should404, "DELETE_FILEURL_missing2")
2250         return d
2251 
2252     def failUnlessHasBarDotTxtMetadata(self, res):
2253@@ -875,7 +960,7 @@
2254         # I can't do "GET /path?json", I have to do "GET /path/t=json"
2255         # instead. This may make it tricky to emulate the S3 interface
2256         # completely.
2257-        d = self.GET(self.public_url + "/foo/bar.txt?t=json")
2258+        d = self.shouldSucceedGET(self.public_url + "/foo/bar.txt?t=json")
2259         def _check1(data):
2260             self.failUnlessIsBarJSON(data)
2261             self.failUnlessHasBarDotTxtMetadata(data)
2262@@ -885,16 +970,16 @@
2263 
2264     def test_GET_FILEURL_json_missing(self):
2265         d = self.GET(self.public_url + "/foo/missing?json")
2266-        d.addBoth(self.should404, "test_GET_FILEURL_json_missing")
2267+        d.addBoth(self.should404, "GET_FILEURL_json_missing")
2268         return d
2269 
2270     def test_GET_FILEURL_uri(self):
2271-        d = self.GET(self.public_url + "/foo/bar.txt?t=uri")
2272+        d = self.shouldSucceedGET(self.public_url + "/foo/bar.txt?t=uri")
2273         def _check(res):
2274             self.failUnlessEqual(res, self._bar_txt_uri)
2275         d.addCallback(_check)
2276         d.addCallback(lambda res:
2277-                      self.GET(self.public_url + "/foo/bar.txt?t=readonly-uri"))
2278+                      self.shouldSucceedGET(self.public_url + "/foo/bar.txt?t=readonly-uri"))
2279         def _check2(res):
2280             # for now, for files, uris and readonly-uris are the same
2281             self.failUnlessEqual(res, self._bar_txt_uri)
2282@@ -910,14 +995,14 @@
2283 
2284     def test_GET_FILEURL_uri_missing(self):
2285         d = self.GET(self.public_url + "/foo/missing?t=uri")
2286-        d.addBoth(self.should404, "test_GET_FILEURL_uri_missing")
2287+        d.addBoth(self.should404, "GET_FILEURL_uri_missing")
2288         return d
2289 
2290     def test_GET_DIRURL(self):
2291         # the addSlash means we get a redirect here
2292         # from /uri/$URI/foo/ , we need ../../../ to get back to the root
2293         ROOT = "../../.."
2294-        d = self.GET(self.public_url + "/foo", followRedirect=True)
2295+        d = self.shouldSucceedGET(self.public_url + "/foo", followRedirect=True)
2296         def _check(res):
2297             self.failUnless(('<a href="%s">Return to Welcome page' % ROOT)
2298                             in res, res)
2299@@ -954,9 +1039,9 @@
2300             self.failUnless(re.search(get_sub, res), res)
2301         d.addCallback(_check)
2302 
2303-        # look at a directory which is readonly
2304+        # look at a readonly directory
2305         d.addCallback(lambda res:
2306-                      self.GET(self.public_url + "/reedownlee", followRedirect=True))
2307+                      self.shouldSucceedGET(self.public_url + "/reedownlee", followRedirect=True))
2308         def _check2(res):
2309             self.failUnless("(read-only)" in res, res)
2310             self.failIf("Upload a file" in res, res)
2311@@ -964,14 +1049,14 @@
2312 
2313         # and at a directory that contains a readonly directory
2314         d.addCallback(lambda res:
2315-                      self.GET(self.public_url, followRedirect=True))
2316+                      self.shouldSucceedGET(self.public_url, followRedirect=True))
2317         def _check3(res):
2318             self.failUnless(re.search('<td>DIR-RO</td>'
2319                                       r'\s+<td><a href="[\.\/]+/uri/URI%3ADIR2-RO%3A[^"]+">reedownlee</a></td>', res), res)
2320         d.addCallback(_check3)
2321 
2322         # and an empty directory
2323-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/empty/"))
2324+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/empty/"))
2325         def _check4(res):
2326             self.failUnless("directory is empty" in res, res)
2327             MKDIR_BUTTON_RE=re.compile('<input type="hidden" name="t" value="mkdir" />.*<legend class="freeform-form-label">Create a new directory in this directory</legend>.*<input type="submit" value="Create" />', re.I)
2328@@ -981,7 +1066,7 @@
2329         return d
2330 
2331     def test_GET_DIRURL_badtype(self):
2332-        d = self.shouldHTTPError("test_GET_DIRURL_badtype",
2333+        d = self.shouldHTTPError("GET_DIRURL_badtype",
2334                                  400, "Bad Request",
2335                                  "bad t=bogus",
2336                                  self.GET,
2337@@ -989,14 +1074,14 @@
2338         return d
2339 
2340     def test_GET_DIRURL_json(self):
2341-        d = self.GET(self.public_url + "/foo?t=json")
2342+        d = self.shouldSucceedGET(self.public_url + "/foo?t=json")
2343         d.addCallback(self.failUnlessIsFooJSON)
2344         return d
2345 
2346 
2347     def test_POST_DIRURL_manifest_no_ophandle(self):
2348         d = self.shouldFail2(error.Error,
2349-                             "test_POST_DIRURL_manifest_no_ophandle",
2350+                             "POST_DIRURL_manifest_no_ophandle",
2351                              "400 Bad Request",
2352                              "slow operation requires ophandle=",
2353                              self.POST, self.public_url, t="start-manifest")
2354@@ -1005,8 +1090,9 @@
2355     def test_POST_DIRURL_manifest(self):
2356         d = defer.succeed(None)
2357         def getman(ignored, output):
2358-            d = self.POST(self.public_url + "/foo/?t=start-manifest&ophandle=125",
2359-                          followRedirect=True)
2360+            d = self.shouldSucceed("POST_DIRURL_manifest", http.OK, self.POST,
2361+                                   self.public_url + "/foo/?t=start-manifest&ophandle=125",
2362+                                   followRedirect=True)
2363             d.addCallback(self.wait_for_operation, "125")
2364             d.addCallback(self.get_operation_results, "125", output)
2365             return d
2366@@ -1019,7 +1105,7 @@
2367         d.addCallback(_got_html)
2368 
2369         # both t=status and unadorned GET should be identical
2370-        d.addCallback(lambda res: self.GET("/operations/125"))
2371+        d.addCallback(lambda res: self.shouldSucceedGET("/operations/125"))
2372         d.addCallback(_got_html)
2373 
2374         d.addCallback(getman, "html")
2375@@ -1047,15 +1133,16 @@
2376 
2377     def test_POST_DIRURL_deepsize_no_ophandle(self):
2378         d = self.shouldFail2(error.Error,
2379-                             "test_POST_DIRURL_deepsize_no_ophandle",
2380+                             "POST_DIRURL_deepsize_no_ophandle",
2381                              "400 Bad Request",
2382                              "slow operation requires ophandle=",
2383                              self.POST, self.public_url, t="start-deep-size")
2384         return d
2385 
2386     def test_POST_DIRURL_deepsize(self):
2387-        d = self.POST(self.public_url + "/foo/?t=start-deep-size&ophandle=126",
2388-                      followRedirect=True)
2389+        d = self.shouldSucceed("POST_DIRURL_deepsize", http.OK, self.POST,
2390+                               self.public_url + "/foo/?t=start-deep-size&ophandle=126",
2391+                               followRedirect=True)
2392         d.addCallback(self.wait_for_operation, "126")
2393         d.addCallback(self.get_operation_results, "126", "json")
2394         def _got_json(data):
2395@@ -1075,15 +1162,16 @@
2396 
2397     def test_POST_DIRURL_deepstats_no_ophandle(self):
2398         d = self.shouldFail2(error.Error,
2399-                             "test_POST_DIRURL_deepstats_no_ophandle",
2400+                             "POST_DIRURL_deepstats_no_ophandle",
2401                              "400 Bad Request",
2402                              "slow operation requires ophandle=",
2403                              self.POST, self.public_url, t="start-deep-stats")
2404         return d
2405 
2406     def test_POST_DIRURL_deepstats(self):
2407-        d = self.POST(self.public_url + "/foo/?t=start-deep-stats&ophandle=127",
2408-                      followRedirect=True)
2409+        d = self.shouldSucceed("POST_DIRURL_deepstats", http.OK, self.POST,
2410+                               self.public_url + "/foo/?t=start-deep-stats&ophandle=127",
2411+                               followRedirect=True)
2412         d.addCallback(self.wait_for_operation, "127")
2413         d.addCallback(self.get_operation_results, "127", "json")
2414         def _got_json(stats):
2415@@ -1109,7 +1197,8 @@
2416         return d
2417 
2418     def test_POST_DIRURL_stream_manifest(self):
2419-        d = self.POST(self.public_url + "/foo/?t=stream-manifest")
2420+        d = self.shouldSucceed("POST_DIRURL_stream_manifest", http.OK, self.POST,
2421+                               self.public_url + "/foo/?t=stream-manifest")
2422         def _check(res):
2423             self.failUnless(res.endswith("\n"))
2424             units = [simplejson.loads(t) for t in res[:-1].split("\n")]
2425@@ -1129,21 +1218,22 @@
2426         return d
2427 
2428     def test_GET_DIRURL_uri(self):
2429-        d = self.GET(self.public_url + "/foo?t=uri")
2430+        d = self.shouldSucceedGET(self.public_url + "/foo?t=uri")
2431         def _check(res):
2432             self.failUnlessEqual(res, self._foo_uri)
2433         d.addCallback(_check)
2434         return d
2435 
2436     def test_GET_DIRURL_readonly_uri(self):
2437-        d = self.GET(self.public_url + "/foo?t=readonly-uri")
2438+        d = self.shouldSucceedGET(self.public_url + "/foo?t=readonly-uri")
2439         def _check(res):
2440             self.failUnlessEqual(res, self._foo_readonly_uri)
2441         d.addCallback(_check)
2442         return d
2443 
2444     def test_PUT_NEWDIRURL(self):
2445-        d = self.PUT(self.public_url + "/foo/newdir?t=mkdir", "")
2446+        d = self.shouldSucceed("PUT_NEWDIRURL", http.OK, self.PUT,
2447+                               self.public_url + "/foo/newdir?t=mkdir", "")
2448         d.addCallback(lambda res:
2449                       self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
2450         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2451@@ -1151,7 +1241,8 @@
2452         return d
2453 
2454     def test_POST_NEWDIRURL(self):
2455-        d = self.POST2(self.public_url + "/foo/newdir?t=mkdir", "")
2456+        d = self.shouldSucceed("POST_NEWDIRURL", http.OK, self.POST2,
2457+                               self.public_url + "/foo/newdir?t=mkdir", "")
2458         d.addCallback(lambda res:
2459                       self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
2460         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2461@@ -1160,30 +1251,41 @@
2462 
2463     def test_POST_NEWDIRURL_emptyname(self):
2464         # an empty pathname component (i.e. a double-slash) is disallowed
2465-        d = self.shouldFail2(error.Error, "test_POST_NEWDIRURL_emptyname",
2466+        d = self.shouldFail2(error.Error, "POST_NEWDIRURL_emptyname",
2467                              "400 Bad Request",
2468                              "The webapi does not allow empty pathname components, i.e. a double slash",
2469                              self.POST, self.public_url + "//?t=mkdir")
2470         return d
2471 
2472     def test_POST_NEWDIRURL_initial_children(self):
2473-        (newkids, filecap1, filecap2, filecap3,
2474-         dircap) = self._create_initial_children()
2475-        d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-with-children",
2476-                       simplejson.dumps(newkids))
2477+        (newkids, caps) = self._create_initial_children()
2478+        d = self.shouldSucceed("POST_NEWDIRURL_initial_children", http.OK, self.POST2,
2479+                               self.public_url + "/foo/newdir?t=mkdir-with-children",
2480+                               simplejson.dumps(newkids))
2481         def _check(uri):
2482             n = self.s.create_node_from_uri(uri.strip())
2483             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2484             d2.addCallback(lambda ign:
2485-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2486+                           self.failUnlessROChildURIIs(n, u"child-imm",
2487+                                                       caps['filecap1']))
2488+            d2.addCallback(lambda ign:
2489+                           self.failUnlessRWChildURIIs(n, u"child-mutable",
2490+                                                       caps['filecap2']))
2491+            d2.addCallback(lambda ign:
2492+                           self.failUnlessROChildURIIs(n, u"child-mutable-ro",
2493+                                                       caps['filecap3']))
2494             d2.addCallback(lambda ign:
2495-                           self.failUnlessChildURIIs(n, u"child-mutable",
2496-                                                     filecap2))
2497+                           self.failUnlessROChildURIIs(n, u"unknownchild-ro",
2498+                                                       caps['unknown_rocap']))
2499             d2.addCallback(lambda ign:
2500-                           self.failUnlessChildURIIs(n, u"child-mutable-ro",
2501-                                                     filecap3))
2502+                           self.failUnlessRWChildURIIs(n, u"unknownchild-rw",
2503+                                                       caps['unknown_rwcap']))
2504             d2.addCallback(lambda ign:
2505-                           self.failUnlessChildURIIs(n, u"dirchild", dircap))
2506+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2507+                                                       caps['unknown_immcap']))
2508+            d2.addCallback(lambda ign:
2509+                           self.failUnlessRWChildURIIs(n, u"dirchild",
2510+                                                       caps['dircap']))
2511             return d2
2512         d.addCallback(_check)
2513         d.addCallback(lambda res:
2514@@ -1191,21 +1293,26 @@
2515         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2516         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2517         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2518-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2519+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2520         return d
2521 
2522     def test_POST_NEWDIRURL_immutable(self):
2523-        (newkids, filecap1, immdircap) = self._create_immutable_children()
2524-        d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-immutable",
2525-                       simplejson.dumps(newkids))
2526+        (newkids, caps) = self._create_immutable_children()
2527+        d = self.shouldSucceed("POST_NEWDIRURL_immutable", http.OK, self.POST2,
2528+                               self.public_url + "/foo/newdir?t=mkdir-immutable",
2529+                               simplejson.dumps(newkids))
2530         def _check(uri):
2531             n = self.s.create_node_from_uri(uri.strip())
2532             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2533             d2.addCallback(lambda ign:
2534-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2535+                           self.failUnlessROChildURIIs(n, u"child-imm",
2536+                                                       caps['filecap1']))
2537+            d2.addCallback(lambda ign:
2538+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2539+                                                       caps['unknown_immcap']))
2540             d2.addCallback(lambda ign:
2541-                           self.failUnlessChildURIIs(n, u"dirchild-imm",
2542-                                                     immdircap))
2543+                           self.failUnlessROChildURIIs(n, u"dirchild-imm",
2544+                                                       caps['immdircap']))
2545             return d2
2546         d.addCallback(_check)
2547         d.addCallback(lambda res:
2548@@ -1213,25 +1320,27 @@
2549         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2550         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2551         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2552-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2553+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2554+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2555+        d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap'])
2556         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2557-        d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap)
2558+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap'])
2559         d.addErrback(self.explain_web_error)
2560         return d
2561 
2562     def test_POST_NEWDIRURL_immutable_bad(self):
2563-        (newkids, filecap1, filecap2, filecap3,
2564-         dircap) = self._create_initial_children()
2565-        d = self.shouldFail2(error.Error, "test_POST_NEWDIRURL_immutable_bad",
2566+        (newkids, caps) = self._create_initial_children()
2567+        d = self.shouldFail2(error.Error, "POST_NEWDIRURL_immutable_bad",
2568                              "400 Bad Request",
2569-                             "a mkdir-immutable operation was given a child that was not itself immutable",
2570+                             "needed to be immutable but was not",
2571                              self.POST2,
2572                              self.public_url + "/foo/newdir?t=mkdir-immutable",
2573                              simplejson.dumps(newkids))
2574         return d
2575 
2576     def test_PUT_NEWDIRURL_exists(self):
2577-        d = self.PUT(self.public_url + "/foo/sub?t=mkdir", "")
2578+        d = self.shouldSucceed("PUT_NEWDIRURL_exists", http.OK, self.PUT,
2579+                               self.public_url + "/foo/sub?t=mkdir", "")
2580         d.addCallback(lambda res:
2581                       self.failUnlessNodeHasChild(self._foo_node, u"sub"))
2582         d.addCallback(lambda res: self._foo_node.get(u"sub"))
2583@@ -1249,18 +1358,21 @@
2584         d.addCallback(self.failUnlessNodeKeysAre, [u"baz.txt"])
2585         return d
2586 
2587-    def test_PUT_NEWDIRURL_mkdir_p(self):
2588+    def test_POST_NEWDIRURL_mkdir_p(self):
2589         d = defer.succeed(None)
2590-        d.addCallback(lambda res: self.POST(self.public_url + "/foo", t='mkdir', name='mkp'))
2591+        d.addCallback(lambda res: self.shouldSucceed("POST_NEWDIRURL_mkdir_p-1", http.OK, self.POST,
2592+                                                     self.public_url + "/foo", t='mkdir', name='mkp'))
2593         d.addCallback(lambda res: self.failUnlessNodeHasChild(self._foo_node, u"mkp"))
2594         d.addCallback(lambda res: self._foo_node.get(u"mkp"))
2595         def mkdir_p(mkpnode):
2596             url = '/uri/%s?t=mkdir-p&path=/sub1/sub2' % urllib.quote(mkpnode.get_uri())
2597-            d = self.POST(url)
2598+            d = self.shouldSucceed("POST_NEWDIRURL_mkdir_p-2", http.OK, self.POST,
2599+                                   url)
2600             def made_subsub(ssuri):
2601                 d = self._foo_node.get_child_at_path(u"mkp/sub1/sub2")
2602                 d.addCallback(lambda ssnode: self.failUnlessEqual(ssnode.get_uri(), ssuri))
2603-                d = self.POST(url)
2604+                d = self.shouldSucceed("POST_NEWDIRURL_mkdir_p-3", http.OK, self.POST,
2605+                                       url)
2606                 d.addCallback(lambda uri2: self.failUnlessEqual(uri2, ssuri))
2607                 return d
2608             d.addCallback(made_subsub)
2609@@ -1269,7 +1381,8 @@
2610         return d
2611 
2612     def test_PUT_NEWDIRURL_mkdirs(self):
2613-        d = self.PUT(self.public_url + "/foo/subdir/newdir?t=mkdir", "")
2614+        d = self.shouldSucceed("PUT_NEWDIRURL_mkdirs", http.OK, self.PUT,
2615+                               self.public_url + "/foo/subdir/newdir?t=mkdir", "")
2616         d.addCallback(lambda res:
2617                       self.failIfNodeHasChild(self._foo_node, u"newdir"))
2618         d.addCallback(lambda res:
2619@@ -1280,21 +1393,22 @@
2620         return d
2621 
2622     def test_DELETE_DIRURL(self):
2623-        d = self.DELETE(self.public_url + "/foo")
2624+        d = self.shouldSucceed("DELETE_DIRURL", http.OK, self.DELETE,
2625+                               self.public_url + "/foo")
2626         d.addCallback(lambda res:
2627                       self.failIfNodeHasChild(self.public_root, u"foo"))
2628         return d
2629 
2630     def test_DELETE_DIRURL_missing(self):
2631         d = self.DELETE(self.public_url + "/foo/missing")
2632-        d.addBoth(self.should404, "test_DELETE_DIRURL_missing")
2633+        d.addBoth(self.should404, "DELETE_DIRURL_missing")
2634         d.addCallback(lambda res:
2635                       self.failUnlessNodeHasChild(self.public_root, u"foo"))
2636         return d
2637 
2638     def test_DELETE_DIRURL_missing2(self):
2639         d = self.DELETE(self.public_url + "/missing")
2640-        d.addBoth(self.should404, "test_DELETE_DIRURL_missing2")
2641+        d.addBoth(self.should404, "DELETE_DIRURL_missing2")
2642         return d
2643 
2644     def dump_root(self):
2645@@ -1346,18 +1460,44 @@
2646         d.addCallback(_check)
2647         return d
2648 
2649-    def failUnlessChildURIIs(self, node, name, expected_uri):
2650+    def failUnlessRWChildURIIs(self, node, name, expected_uri):
2651+        assert isinstance(name, unicode)
2652+        d = node.get_child_at_path(name)
2653+        def _check(child):
2654+            self.failUnless(child.is_unknown() or not child.is_readonly())
2655+            self.failUnlessEqual(child.get_uri(), expected_uri.strip())
2656+            expected_ro_uri = self._make_readonly(expected_uri)
2657+            if expected_ro_uri:
2658+                self.failUnlessEqual(child.get_readonly_uri(), expected_ro_uri.strip())
2659+        d.addCallback(_check)
2660+        return d
2661+
2662+    def failUnlessROChildURIIs(self, node, name, expected_uri):
2663         assert isinstance(name, unicode)
2664         d = node.get_child_at_path(name)
2665         def _check(child):
2666+            self.failUnless(child.is_unknown() or child.is_readonly())
2667             self.failUnlessEqual(child.get_uri(), expected_uri.strip())
2668         d.addCallback(_check)
2669         return d
2670 
2671-    def failUnlessURIMatchesChild(self, got_uri, node, name):
2672+    def failUnlessURIMatchesRWChild(self, got_uri, node, name):
2673+        assert isinstance(name, unicode)
2674+        d = node.get_child_at_path(name)
2675+        def _check(child):
2676+            self.failUnless(child.is_unknown() or not child.is_readonly())
2677+            self.failUnlessEqual(child.get_uri(), got_uri.strip())
2678+            expected_ro_uri = self._make_readonly(got_uri)
2679+            if expected_ro_uri:
2680+                self.failUnlessEqual(child.get_readonly_uri(), expected_ro_uri.strip())
2681+        d.addCallback(_check)
2682+        return d
2683+
2684+    def failUnlessURIMatchesROChild(self, got_uri, node, name):
2685         assert isinstance(name, unicode)
2686         d = node.get_child_at_path(name)
2687         def _check(child):
2688+            self.failUnless(child.is_unknown() or child.is_readonly())
2689             self.failUnlessEqual(got_uri.strip(), child.get_uri())
2690         d.addCallback(_check)
2691         return d
2692@@ -1366,10 +1506,11 @@
2693         self.failUnless(FakeCHKFileNode.all_contents[got_uri] == contents)
2694 
2695     def test_POST_upload(self):
2696-        d = self.POST(self.public_url + "/foo", t="upload",
2697-                      file=("new.txt", self.NEWFILE_CONTENTS))
2698+        d = self.shouldSucceed("POST_upload", http.OK, self.POST,
2699+                               self.public_url + "/foo", t="upload",
2700+                               file=("new.txt", self.NEWFILE_CONTENTS))
2701         fn = self._foo_node
2702-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2703+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt")
2704         d.addCallback(lambda res:
2705                       self.failUnlessChildContentsAre(fn, u"new.txt",
2706                                                       self.NEWFILE_CONTENTS))
2707@@ -1377,15 +1518,16 @@
2708 
2709     def test_POST_upload_unicode(self):
2710         filename = u"n\u00e9wer.txt" # n e-acute w e r . t x t
2711-        d = self.POST(self.public_url + "/foo", t="upload",
2712-                      file=(filename, self.NEWFILE_CONTENTS))
2713+        d = self.shouldSucceed("POST_upload_unicode", http.OK, self.POST,
2714+                               self.public_url + "/foo", t="upload",
2715+                               file=(filename, self.NEWFILE_CONTENTS))
2716         fn = self._foo_node
2717-        d.addCallback(self.failUnlessURIMatchesChild, fn, filename)
2718+        d.addCallback(self.failUnlessURIMatchesROChild, fn, filename)
2719         d.addCallback(lambda res:
2720                       self.failUnlessChildContentsAre(fn, filename,
2721                                                       self.NEWFILE_CONTENTS))
2722         target_url = self.public_url + "/foo/" + filename.encode("utf-8")
2723-        d.addCallback(lambda res: self.GET(target_url))
2724+        d.addCallback(lambda res: self.shouldSucceedGET(target_url))
2725         d.addCallback(lambda contents: self.failUnlessEqual(contents,
2726                                                             self.NEWFILE_CONTENTS,
2727                                                             contents))
2728@@ -1393,24 +1535,26 @@
2729 
2730     def test_POST_upload_unicode_named(self):
2731         filename = u"n\u00e9wer.txt" # n e-acute w e r . t x t
2732-        d = self.POST(self.public_url + "/foo", t="upload",
2733-                      name=filename,
2734-                      file=("overridden", self.NEWFILE_CONTENTS))
2735+        d = self.shouldSucceed("POST_upload_unicode_named", http.OK, self.POST,
2736+                               self.public_url + "/foo", t="upload",
2737+                               name=filename,
2738+                               file=("overridden", self.NEWFILE_CONTENTS))
2739         fn = self._foo_node
2740-        d.addCallback(self.failUnlessURIMatchesChild, fn, filename)
2741+        d.addCallback(self.failUnlessURIMatchesROChild, fn, filename)
2742         d.addCallback(lambda res:
2743                       self.failUnlessChildContentsAre(fn, filename,
2744                                                       self.NEWFILE_CONTENTS))
2745         target_url = self.public_url + "/foo/" + filename.encode("utf-8")
2746-        d.addCallback(lambda res: self.GET(target_url))
2747+        d.addCallback(lambda res: self.shouldSucceedGET(target_url))
2748         d.addCallback(lambda contents: self.failUnlessEqual(contents,
2749                                                             self.NEWFILE_CONTENTS,
2750                                                             contents))
2751         return d
2752 
2753     def test_POST_upload_no_link(self):
2754-        d = self.POST("/uri", t="upload",
2755-                      file=("new.txt", self.NEWFILE_CONTENTS))
2756+        d = self.shouldSucceed("POST_upload_no_link", http.OK, self.POST,
2757+                               "/uri", t="upload",
2758+                               file=("new.txt", self.NEWFILE_CONTENTS))
2759         def _check_upload_results(page):
2760             # this should be a page which describes the results of the upload
2761             # that just finished.
2762@@ -1449,7 +1593,7 @@
2763             self.failUnlessEqual(statuscode, str(http.FOUND))
2764             self.failUnless(target.startswith(self.webish_url), target)
2765             return client.getPage(target, method="GET")
2766-        d = self.shouldRedirect2("test_POST_upload_no_link_whendone_results",
2767+        d = self.shouldRedirect2("POST_upload_no_link_whendone_results",
2768                                  check,
2769                                  self.POST, "/uri", t="upload",
2770                                  when_done="/uri/%(uri)s",
2771@@ -1459,8 +1603,9 @@
2772         return d
2773 
2774     def test_POST_upload_no_link_mutable(self):
2775-        d = self.POST("/uri", t="upload", mutable="true",
2776-                      file=("new.txt", self.NEWFILE_CONTENTS))
2777+        d = self.shouldSucceed("POST_upload_no_link_mutable", http.OK, self.POST,
2778+                               "/uri", t="upload", mutable="true",
2779+                               file=("new.txt", self.NEWFILE_CONTENTS))
2780         def _check(filecap):
2781             filecap = filecap.strip()
2782             self.failUnless(filecap.startswith("URI:SSK:"), filecap)
2783@@ -1472,11 +1617,11 @@
2784         d.addCallback(_check)
2785         def _check2(data):
2786             self.failUnlessEqual(data, self.NEWFILE_CONTENTS)
2787-            return self.GET("/uri/%s" % urllib.quote(self.filecap))
2788+            return self.shouldSucceedGET("/uri/%s" % urllib.quote(self.filecap))
2789         d.addCallback(_check2)
2790         def _check3(data):
2791             self.failUnlessEqual(data, self.NEWFILE_CONTENTS)
2792-            return self.GET("/file/%s" % urllib.quote(self.filecap))
2793+            return self.shouldSucceedGET("/file/%s" % urllib.quote(self.filecap))
2794         d.addCallback(_check3)
2795         def _check4(data):
2796             self.failUnlessEqual(data, self.NEWFILE_CONTENTS)
2797@@ -1485,7 +1630,7 @@
2798 
2799     def test_POST_upload_no_link_mutable_toobig(self):
2800         d = self.shouldFail2(error.Error,
2801-                             "test_POST_upload_no_link_mutable_toobig",
2802+                             "POST_upload_no_link_mutable_toobig",
2803                              "413 Request Entity Too Large",
2804                              "SDMF is limited to one segment, and 10001 > 10000",
2805                              self.POST,
2806@@ -1496,10 +1641,11 @@
2807 
2808     def test_POST_upload_mutable(self):
2809         # this creates a mutable file
2810-        d = self.POST(self.public_url + "/foo", t="upload", mutable="true",
2811-                      file=("new.txt", self.NEWFILE_CONTENTS))
2812+        d = self.shouldSucceed("POST_upload_mutable", http.OK, self.POST,
2813+                               self.public_url + "/foo", t="upload", mutable="true",
2814+                               file=("new.txt", self.NEWFILE_CONTENTS))
2815         fn = self._foo_node
2816-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2817+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2818         d.addCallback(lambda res:
2819                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2820                                                              self.NEWFILE_CONTENTS))
2821@@ -1515,10 +1661,11 @@
2822         # now upload it again and make sure that the URI doesn't change
2823         NEWER_CONTENTS = self.NEWFILE_CONTENTS + "newer\n"
2824         d.addCallback(lambda res:
2825-                      self.POST(self.public_url + "/foo", t="upload",
2826-                                mutable="true",
2827-                                file=("new.txt", NEWER_CONTENTS)))
2828-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2829+                      self.shouldSucceed("POST_upload_mutable-again", http.OK, self.POST,
2830+                                         self.public_url + "/foo", t="upload",
2831+                                         mutable="true",
2832+                                         file=("new.txt", NEWER_CONTENTS)))
2833+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2834         d.addCallback(lambda res:
2835                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2836                                                              NEWER_CONTENTS))
2837@@ -1533,8 +1680,9 @@
2838         # upload a second time, using PUT instead of POST
2839         NEW2_CONTENTS = NEWER_CONTENTS + "overwrite with PUT\n"
2840         d.addCallback(lambda res:
2841-                      self.PUT(self.public_url + "/foo/new.txt", NEW2_CONTENTS))
2842-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2843+                      self.shouldSucceed("POST_upload_mutable-again-with-PUT", http.OK, self.PUT,
2844+                                         self.public_url + "/foo/new.txt", NEW2_CONTENTS))
2845+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2846         d.addCallback(lambda res:
2847                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2848                                                              NEW2_CONTENTS))
2849@@ -1543,8 +1691,8 @@
2850         # slightly differently
2851 
2852         d.addCallback(lambda res:
2853-                      self.GET(self.public_url + "/foo/",
2854-                               followRedirect=True))
2855+                      self.shouldSucceedGET(self.public_url + "/foo/",
2856+                                            followRedirect=True))
2857         def _check_page(res):
2858             # TODO: assert more about the contents
2859             self.failUnless("SSK" in res)
2860@@ -1561,8 +1709,8 @@
2861 
2862         # look at the JSON form of the enclosing directory
2863         d.addCallback(lambda res:
2864-                      self.GET(self.public_url + "/foo/?t=json",
2865-                               followRedirect=True))
2866+                      self.shouldSucceedGET(self.public_url + "/foo/?t=json",
2867+                                            followRedirect=True))
2868         def _check_page_json(res):
2869             parsed = simplejson.loads(res)
2870             self.failUnlessEqual(parsed[0], "dirnode")
2871@@ -1580,7 +1728,7 @@
2872 
2873         # and the JSON form of the file
2874         d.addCallback(lambda res:
2875-                      self.GET(self.public_url + "/foo/new.txt?t=json"))
2876+                      self.shouldSucceedGET(self.public_url + "/foo/new.txt?t=json"))
2877         def _check_file_json(res):
2878             parsed = simplejson.loads(res)
2879             self.failUnlessEqual(parsed[0], "filenode")
2880@@ -1592,10 +1740,10 @@
2881 
2882         # and look at t=uri and t=readonly-uri
2883         d.addCallback(lambda res:
2884-                      self.GET(self.public_url + "/foo/new.txt?t=uri"))
2885+                      self.shouldSucceedGET(self.public_url + "/foo/new.txt?t=uri"))
2886         d.addCallback(lambda res: self.failUnlessEqual(res, self._mutable_uri))
2887         d.addCallback(lambda res:
2888-                      self.GET(self.public_url + "/foo/new.txt?t=readonly-uri"))
2889+                      self.shouldSucceedGET(self.public_url + "/foo/new.txt?t=readonly-uri"))
2890         def _check_ro_uri(res):
2891             ro_uri = unicode(self._mutable_node.get_readonly().to_string())
2892             self.failUnlessEqual(res, ro_uri)
2893@@ -1603,15 +1751,15 @@
2894 
2895         # make sure we can get to it from /uri/URI
2896         d.addCallback(lambda res:
2897-                      self.GET("/uri/%s" % urllib.quote(self._mutable_uri)))
2898+                      self.shouldSucceedGET("/uri/%s" % urllib.quote(self._mutable_uri)))
2899         d.addCallback(lambda res:
2900                       self.failUnlessEqual(res, NEW2_CONTENTS))
2901 
2902         # and that HEAD computes the size correctly
2903         d.addCallback(lambda res:
2904-                      self.HEAD(self.public_url + "/foo/new.txt",
2905-                                return_response=True))
2906-        def _got_headers((res, status, headers)):
2907+                      self.shouldSucceedHEAD(self.public_url + "/foo/new.txt",
2908+                                             return_response=True))
2909+        def _got_headers((res, statuscode, headers)):
2910             self.failUnlessEqual(res, "")
2911             self.failUnlessEqual(headers["content-length"][0],
2912                                  str(len(NEW2_CONTENTS)))
2913@@ -1621,7 +1769,7 @@
2914         # make sure that size errors are displayed correctly for overwrite
2915         d.addCallback(lambda res:
2916                       self.shouldFail2(error.Error,
2917-                                       "test_POST_upload_mutable-toobig",
2918+                                       "POST_upload_mutable-toobig",
2919                                        "413 Request Entity Too Large",
2920                                        "SDMF is limited to one segment, and 10001 > 10000",
2921                                        self.POST,
2922@@ -1636,7 +1784,7 @@
2923 
2924     def test_POST_upload_mutable_toobig(self):
2925         d = self.shouldFail2(error.Error,
2926-                             "test_POST_upload_mutable_toobig",
2927+                             "POST_upload_mutable_toobig",
2928                              "413 Request Entity Too Large",
2929                              "SDMF is limited to one segment, and 10001 > 10000",
2930                              self.POST,
2931@@ -1660,19 +1808,21 @@
2932         return f
2933 
2934     def test_POST_upload_replace(self):
2935-        d = self.POST(self.public_url + "/foo", t="upload",
2936-                      file=("bar.txt", self.NEWFILE_CONTENTS))
2937+        d = self.shouldSucceed("POST_upload_replace", http.OK, self.POST,
2938+                               self.public_url + "/foo", t="upload",
2939+                               file=("bar.txt", self.NEWFILE_CONTENTS))
2940         fn = self._foo_node
2941-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"bar.txt")
2942+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"bar.txt")
2943         d.addCallback(lambda res:
2944                       self.failUnlessChildContentsAre(fn, u"bar.txt",
2945                                                       self.NEWFILE_CONTENTS))
2946         return d
2947 
2948     def test_POST_upload_no_replace_ok(self):
2949-        d = self.POST(self.public_url + "/foo?replace=false", t="upload",
2950-                      file=("new.txt", self.NEWFILE_CONTENTS))
2951-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/new.txt"))
2952+        d = self.shouldSucceed("POST_upload_no_replace_ok", http.OK, self.POST,
2953+                               self.public_url + "/foo?replace=false", t="upload",
2954+                               file=("new.txt", self.NEWFILE_CONTENTS))
2955+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/new.txt"))
2956         d.addCallback(lambda res: self.failUnlessEqual(res,
2957                                                        self.NEWFILE_CONTENTS))
2958         return d
2959@@ -1685,7 +1835,7 @@
2960                   "409 Conflict",
2961                   "There was already a child by that name, and you asked me "
2962                   "to not replace it")
2963-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
2964+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/bar.txt"))
2965         d.addCallback(self.failUnlessIsBarDotTxt)
2966         return d
2967 
2968@@ -1696,7 +1846,7 @@
2969                   "409 Conflict",
2970                   "There was already a child by that name, and you asked me "
2971                   "to not replace it")
2972-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
2973+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/bar.txt"))
2974         d.addCallback(self.failUnlessIsBarDotTxt)
2975         return d
2976 
2977@@ -1712,9 +1862,10 @@
2978 
2979     def test_POST_upload_named(self):
2980         fn = self._foo_node
2981-        d = self.POST(self.public_url + "/foo", t="upload",
2982-                      name="new.txt", file=self.NEWFILE_CONTENTS)
2983-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2984+        d = self.shouldSucceed("POST_upload_named", http.OK, self.POST,
2985+                               self.public_url + "/foo", t="upload",
2986+                               name="new.txt", file=self.NEWFILE_CONTENTS)
2987+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt")
2988         d.addCallback(lambda res:
2989                       self.failUnlessChildContentsAre(fn, u"new.txt",
2990                                                       self.NEWFILE_CONTENTS))
2991@@ -1724,7 +1875,7 @@
2992         d = self.POST(self.public_url + "/foo", t="upload",
2993                       name="slashes/are/bad.txt", file=self.NEWFILE_CONTENTS)
2994         d.addBoth(self.shouldFail, error.Error,
2995-                  "test_POST_upload_named_badfilename",
2996+                  "POST_upload_named_badfilename",
2997                   "400 Bad Request",
2998                   "name= may not contain a slash",
2999                   )
3000@@ -1738,7 +1889,8 @@
3001 
3002     def test_POST_FILEURL_check(self):
3003         bar_url = self.public_url + "/foo/bar.txt"
3004-        d = self.POST(bar_url, t="check")
3005+        d = self.shouldSucceed("POST_FILEURL_check-1", http.OK, self.POST,
3006+                               bar_url, t="check")
3007         def _check(res):
3008             self.failUnless("Healthy :" in res)
3009         d.addCallback(_check)
3010@@ -1747,13 +1899,14 @@
3011             self.failUnlessEqual(statuscode, str(http.FOUND))
3012             self.failUnlessEqual(target, redir_url)
3013         d.addCallback(lambda res:
3014-                      self.shouldRedirect2("test_POST_FILEURL_check",
3015+                      self.shouldRedirect2("POST_FILEURL_check-2",
3016                                            _check2,
3017                                            self.POST, bar_url,
3018                                            t="check",
3019                                            when_done=redir_url))
3020         d.addCallback(lambda res:
3021-                      self.POST(bar_url, t="check", return_to=redir_url))
3022+                      self.shouldSucceed("POST_FILEURL_check-3", http.OK, self.POST,
3023+                                         bar_url, t="check", return_to=redir_url))
3024         def _check3(res):
3025             self.failUnless("Healthy :" in res)
3026             self.failUnless("Return to file" in res)
3027@@ -1761,7 +1914,8 @@
3028         d.addCallback(_check3)
3029 
3030         d.addCallback(lambda res:
3031-                      self.POST(bar_url, t="check", output="JSON"))
3032+                      self.shouldSucceed("POST_FILEURL_check-4", http.OK, self.POST,
3033+                                         bar_url, t="check", output="JSON"))
3034         def _check_json(res):
3035             data = simplejson.loads(res)
3036             self.failUnless("storage-index" in data)
3037@@ -1772,7 +1926,8 @@
3038 
3039     def test_POST_FILEURL_check_and_repair(self):
3040         bar_url = self.public_url + "/foo/bar.txt"
3041-        d = self.POST(bar_url, t="check", repair="true")
3042+        d = self.shouldSucceed("POST_FILEURL_check_and_repair-1", http.OK, self.POST,
3043+                               bar_url, t="check", repair="true")
3044         def _check(res):
3045             self.failUnless("Healthy :" in res)
3046         d.addCallback(_check)
3047@@ -1781,13 +1936,14 @@
3048             self.failUnlessEqual(statuscode, str(http.FOUND))
3049             self.failUnlessEqual(target, redir_url)
3050         d.addCallback(lambda res:
3051-                      self.shouldRedirect2("test_POST_FILEURL_check_and_repair",
3052+                      self.shouldRedirect2("POST_FILEURL_check_and_repair-2",
3053                                            _check2,
3054                                            self.POST, bar_url,
3055                                            t="check", repair="true",
3056                                            when_done=redir_url))
3057         d.addCallback(lambda res:
3058-                      self.POST(bar_url, t="check", return_to=redir_url))
3059+                      self.shouldSucceed("POST_FILEURL_check_and_repair-3", http.OK, self.POST,
3060+                                         bar_url, t="check", return_to=redir_url))
3061         def _check3(res):
3062             self.failUnless("Healthy :" in res)
3063             self.failUnless("Return to file" in res)
3064@@ -1797,7 +1953,8 @@
3065 
3066     def test_POST_DIRURL_check(self):
3067         foo_url = self.public_url + "/foo/"
3068-        d = self.POST(foo_url, t="check")
3069+        d = self.shouldSucceed("POST_DIRURL_check-1", http.OK, self.POST,
3070+                               foo_url, t="check")
3071         def _check(res):
3072             self.failUnless("Healthy :" in res, res)
3073         d.addCallback(_check)
3074@@ -1806,13 +1963,14 @@
3075             self.failUnlessEqual(statuscode, str(http.FOUND))
3076             self.failUnlessEqual(target, redir_url)
3077         d.addCallback(lambda res:
3078-                      self.shouldRedirect2("test_POST_DIRURL_check",
3079+                      self.shouldRedirect2("POST_DIRURL_check-2",
3080                                            _check2,
3081                                            self.POST, foo_url,
3082                                            t="check",
3083                                            when_done=redir_url))
3084         d.addCallback(lambda res:
3085-                      self.POST(foo_url, t="check", return_to=redir_url))
3086+                      self.shouldSucceed("POST_DIRURL_check-3", http.OK, self.POST,
3087+                                         foo_url, t="check", return_to=redir_url))
3088         def _check3(res):
3089             self.failUnless("Healthy :" in res, res)
3090             self.failUnless("Return to file/directory" in res)
3091@@ -1820,7 +1978,8 @@
3092         d.addCallback(_check3)
3093 
3094         d.addCallback(lambda res:
3095-                      self.POST(foo_url, t="check", output="JSON"))
3096+                      self.shouldSucceed("POST_DIRURL_check-4", http.OK, self.POST,
3097+                                         foo_url, t="check", output="JSON"))
3098         def _check_json(res):
3099             data = simplejson.loads(res)
3100             self.failUnless("storage-index" in data)
3101@@ -1831,7 +1990,8 @@
3102 
3103     def test_POST_DIRURL_check_and_repair(self):
3104         foo_url = self.public_url + "/foo/"
3105-        d = self.POST(foo_url, t="check", repair="true")
3106+        d = self.shouldSucceed("POST_DIRURL_check_and_repair-1", http.OK, self.POST,
3107+                               foo_url, t="check", repair="true")
3108         def _check(res):
3109             self.failUnless("Healthy :" in res, res)
3110         d.addCallback(_check)
3111@@ -1840,13 +2000,14 @@
3112             self.failUnlessEqual(statuscode, str(http.FOUND))
3113             self.failUnlessEqual(target, redir_url)
3114         d.addCallback(lambda res:
3115-                      self.shouldRedirect2("test_POST_DIRURL_check_and_repair",
3116+                      self.shouldRedirect2("POST_DIRURL_check_and_repair-2",
3117                                            _check2,
3118                                            self.POST, foo_url,
3119                                            t="check", repair="true",
3120                                            when_done=redir_url))
3121         d.addCallback(lambda res:
3122-                      self.POST(foo_url, t="check", return_to=redir_url))
3123+                      self.shouldSucceed("POST_DIRURL_check_and_repair-3", http.OK, self.POST,
3124+                                         foo_url, t="check", return_to=redir_url))
3125         def _check3(res):
3126             self.failUnless("Healthy :" in res)
3127             self.failUnless("Return to file/directory" in res)
3128@@ -1857,7 +2018,7 @@
3129     def wait_for_operation(self, ignored, ophandle):
3130         url = "/operations/" + ophandle
3131         url += "?t=status&output=JSON"
3132-        d = self.GET(url)
3133+        d = self.shouldSucceedGET(url)
3134         def _got(res):
3135             data = simplejson.loads(res)
3136             if not data["finished"]:
3137@@ -1873,7 +2034,7 @@
3138         url += "?t=status"
3139         if output:
3140             url += "&output=" + output
3141-        d = self.GET(url)
3142+        d = self.shouldSucceedGET(url)
3143         def _got(res):
3144             if output and output.lower() == "json":
3145                 return simplejson.loads(res)
3146@@ -1883,7 +2044,7 @@
3147 
3148     def test_POST_DIRURL_deepcheck_no_ophandle(self):
3149         d = self.shouldFail2(error.Error,
3150-                             "test_POST_DIRURL_deepcheck_no_ophandle",
3151+                             "POST_DIRURL_deepcheck_no_ophandle",
3152                              "400 Bad Request",
3153                              "slow operation requires ophandle=",
3154                              self.POST, self.public_url, t="start-deep-check")
3155@@ -1893,7 +2054,7 @@
3156         def _check_redirect(statuscode, target):
3157             self.failUnlessEqual(statuscode, str(http.FOUND))
3158             self.failUnless(target.endswith("/operations/123"))
3159-        d = self.shouldRedirect2("test_POST_DIRURL_deepcheck", _check_redirect,
3160+        d = self.shouldRedirect2("POST_DIRURL_deepcheck", _check_redirect,
3161                                  self.POST, self.public_url,
3162                                  t="start-deep-check", ophandle="123")
3163         d.addCallback(self.wait_for_operation, "123")
3164@@ -1909,7 +2070,7 @@
3165         d.addCallback(_check_html)
3166 
3167         d.addCallback(lambda res:
3168-                      self.GET("/operations/123/"))
3169+                      self.shouldSucceedGET("/operations/123/"))
3170         d.addCallback(_check_html) # should be the same as without the slash
3171 
3172         d.addCallback(lambda res:
3173@@ -1920,7 +2081,7 @@
3174         foo_si = self._foo_node.get_storage_index()
3175         foo_si_s = base32.b2a(foo_si)
3176         d.addCallback(lambda res:
3177-                      self.GET("/operations/123/%s?output=JSON" % foo_si_s))
3178+                      self.shouldSucceedGET("/operations/123/%s?output=JSON" % foo_si_s))
3179         def _check_foo_json(res):
3180             data = simplejson.loads(res)
3181             self.failUnlessEqual(data["storage-index"], foo_si_s)
3182@@ -1929,8 +2090,9 @@
3183         return d
3184 
3185     def test_POST_DIRURL_deepcheck_and_repair(self):
3186-        d = self.POST(self.public_url, t="start-deep-check", repair="true",
3187-                      ophandle="124", output="json", followRedirect=True)
3188+        d = self.shouldSucceed("POST_DIRURL_deepcheck_and_repair", http.OK, self.POST,
3189+                               self.public_url, t="start-deep-check", repair="true",
3190+                               ophandle="124", output="json", followRedirect=True)
3191         d.addCallback(self.wait_for_operation, "124")
3192         def _check_json(data):
3193             self.failUnlessEqual(data["finished"], True)
3194@@ -1971,45 +2133,47 @@
3195         return d
3196 
3197     def test_POST_mkdir(self): # return value?
3198-        d = self.POST(self.public_url + "/foo", t="mkdir", name="newdir")
3199+        d = self.shouldSucceed("POST_mkdir", http.OK, self.POST,
3200+                               self.public_url + "/foo", t="mkdir", name="newdir")
3201         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
3202         d.addCallback(self.failUnlessNodeKeysAre, [])
3203         return d
3204 
3205     def test_POST_mkdir_initial_children(self):
3206-        newkids, filecap1, ign, ign, ign = self._create_initial_children()
3207-        d = self.POST2(self.public_url +
3208-                       "/foo?t=mkdir-with-children&name=newdir",
3209-                       simplejson.dumps(newkids))
3210+        (newkids, caps) = self._create_initial_children()
3211+        d = self.shouldSucceed("POST_mkdir_initial_children", http.OK, self.POST2,
3212+                               self.public_url + "/foo?t=mkdir-with-children&name=newdir",
3213+                               simplejson.dumps(newkids))
3214         d.addCallback(lambda res:
3215                       self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
3216         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
3217         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
3218         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
3219-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
3220+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
3221         return d
3222 
3223     def test_POST_mkdir_immutable(self):
3224-        (newkids, filecap1, immdircap) = self._create_immutable_children()
3225-        d = self.POST2(self.public_url +
3226-                       "/foo?t=mkdir-immutable&name=newdir",
3227-                       simplejson.dumps(newkids))
3228+        (newkids, caps) = self._create_immutable_children()
3229+        d = self.shouldSucceed("POST_mkdir_immutable", http.OK, self.POST2,
3230+                               self.public_url + "/foo?t=mkdir-immutable&name=newdir",
3231+                               simplejson.dumps(newkids))
3232         d.addCallback(lambda res:
3233                       self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
3234         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
3235         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
3236         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
3237-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
3238+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
3239+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
3240+        d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap'])
3241         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
3242-        d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap)
3243+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap'])
3244         return d
3245 
3246     def test_POST_mkdir_immutable_bad(self):
3247-        (newkids, filecap1, filecap2, filecap3,
3248-         dircap) = self._create_initial_children()
3249-        d = self.shouldFail2(error.Error, "test_POST_mkdir_immutable_bad",
3250+        (newkids, caps) = self._create_initial_children()
3251+        d = self.shouldFail2(error.Error, "POST_mkdir_immutable_bad",
3252                              "400 Bad Request",
3253-                             "a mkdir-immutable operation was given a child that was not itself immutable",
3254+                             "needed to be immutable but was not",
3255                              self.POST2,
3256                              self.public_url +
3257                              "/foo?t=mkdir-immutable&name=newdir",
3258@@ -2017,7 +2181,8 @@
3259         return d
3260 
3261     def test_POST_mkdir_2(self):
3262-        d = self.POST(self.public_url + "/foo/newdir?t=mkdir", "")
3263+        d = self.shouldSucceed("POST_mkdir_2", http.OK, self.POST,
3264+                               self.public_url + "/foo/newdir?t=mkdir", "")
3265         d.addCallback(lambda res:
3266                       self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
3267         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
3268@@ -2025,7 +2190,8 @@
3269         return d
3270 
3271     def test_POST_mkdirs_2(self):
3272-        d = self.POST(self.public_url + "/foo/bardir/newdir?t=mkdir", "")
3273+        d = self.shouldSucceed("POST_mkdirs_2", http.OK, self.POST,
3274+                               self.public_url + "/foo/bardir/newdir?t=mkdir", "")
3275         d.addCallback(lambda res:
3276                       self.failUnlessNodeHasChild(self._foo_node, u"bardir"))
3277         d.addCallback(lambda res: self._foo_node.get(u"bardir"))
3278@@ -2034,7 +2200,8 @@
3279         return d
3280 
3281     def test_POST_mkdir_no_parentdir_noredirect(self):
3282-        d = self.POST("/uri?t=mkdir")
3283+        d = self.shouldSucceed("POST_mkdir_no_parentdir_noredirect", http.OK, self.POST,
3284+                               "/uri?t=mkdir")
3285         def _after_mkdir(res):
3286             uri.DirectoryURI.init_from_string(res)
3287         d.addCallback(_after_mkdir)
3288@@ -2049,21 +2216,43 @@
3289         d.addCallback(_check_target)
3290         return d
3291 
3292+    def _make_readonly(self, u):
3293+        ro_uri = uri.from_string(u).get_readonly()
3294+        if ro_uri is None:
3295+            return None
3296+        return ro_uri.to_string()
3297+
3298     def _create_initial_children(self):
3299         contents, n, filecap1 = self.makefile(12)
3300         md1 = {"metakey1": "metavalue1"}
3301         filecap2 = make_mutable_file_uri()
3302         node3 = self.s.create_node_from_uri(make_mutable_file_uri())
3303         filecap3 = node3.get_readonly_uri()
3304+        unknown_rwcap = "lafs://from_the_future"
3305+        unknown_rocap = "ro.lafs://readonly_from_the_future"
3306+        unknown_immcap = "imm.lafs://immutable_from_the_future"
3307         node4 = self.s.create_node_from_uri(make_mutable_file_uri())
3308         dircap = DirectoryNode(node4, None, None).get_uri()
3309-        newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1,
3310-                                               "metadata": md1, }],
3311-                   u"child-mutable": ["filenode", {"rw_uri": filecap2}],
3312+        newkids = {u"child-imm":        ["filenode", {"rw_uri": filecap1,
3313+                                                      "ro_uri": self._make_readonly(filecap1),
3314+                                                      "metadata": md1, }],
3315+                   u"child-mutable":    ["filenode", {"rw_uri": filecap2,
3316+                                                      "ro_uri": self._make_readonly(filecap2)}],
3317                    u"child-mutable-ro": ["filenode", {"ro_uri": filecap3}],
3318-                   u"dirchild": ["dirnode", {"rw_uri": dircap}],
3319+                   u"unknownchild-rw":  ["unknown",  {"rw_uri": unknown_rwcap,
3320+                                                      "ro_uri": unknown_rocap}],
3321+                   u"unknownchild-ro":  ["unknown",  {"ro_uri": unknown_rocap}],
3322+                   u"unknownchild-imm": ["unknown",  {"ro_uri": unknown_immcap}],
3323+                   u"dirchild":         ["dirnode",  {"rw_uri": dircap,
3324+                                                      "ro_uri": self._make_readonly(dircap)}],
3325                    }
3326-        return newkids, filecap1, filecap2, filecap3, dircap
3327+        return newkids, {'filecap1': filecap1,
3328+                         'filecap2': filecap2,
3329+                         'filecap3': filecap3,
3330+                         'unknown_rwcap': unknown_rwcap,
3331+                         'unknown_rocap': unknown_rocap,
3332+                         'unknown_immcap': unknown_immcap,
3333+                         'dircap': dircap}
3334 
3335     def _create_immutable_children(self):
3336         contents, n, filecap1 = self.makefile(12)
3337@@ -2071,31 +2260,46 @@
3338         tnode = create_chk_filenode("immutable directory contents\n"*10)
3339         dnode = DirectoryNode(tnode, None, None)
3340         assert not dnode.is_mutable()
3341+        unknown_immcap = "imm.lafs://immutable_from_the_future"
3342         immdircap = dnode.get_uri()
3343-        newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1,
3344-                                               "metadata": md1, }],
3345-                   u"dirchild-imm": ["dirnode", {"ro_uri": immdircap}],
3346+        newkids = {u"child-imm":        ["filenode", {"ro_uri": filecap1,
3347+                                                      "metadata": md1, }],
3348+                   u"unknownchild-imm": ["unknown",  {"ro_uri": unknown_immcap}],
3349+                   u"dirchild-imm":     ["dirnode",  {"ro_uri": immdircap}],
3350                    }
3351-        return newkids, filecap1, immdircap
3352+        return newkids, {'filecap1': filecap1,
3353+                         'unknown_immcap': unknown_immcap,
3354+                         'immdircap': immdircap}
3355 
3356     def test_POST_mkdir_no_parentdir_initial_children(self):
3357-        (newkids, filecap1, filecap2, filecap3,
3358-         dircap) = self._create_initial_children()
3359-        d = self.POST2("/uri?t=mkdir-with-children", simplejson.dumps(newkids))
3360+        (newkids, caps) = self._create_initial_children()
3361+        d = self.shouldSucceed("POST_mkdir_no_parentdir_initial_children", http.OK, self.POST2,
3362+                               "/uri?t=mkdir-with-children", simplejson.dumps(newkids))
3363         def _after_mkdir(res):
3364             self.failUnless(res.startswith("URI:DIR"), res)
3365             n = self.s.create_node_from_uri(res)
3366             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
3367             d2.addCallback(lambda ign:
3368-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
3369+                           self.failUnlessROChildURIIs(n, u"child-imm",
3370+                                                       caps['filecap1']))
3371+            d2.addCallback(lambda ign:
3372+                           self.failUnlessRWChildURIIs(n, u"child-mutable",
3373+                                                       caps['filecap2']))
3374+            d2.addCallback(lambda ign:
3375+                           self.failUnlessROChildURIIs(n, u"child-mutable-ro",
3376+                                                       caps['filecap3']))
3377             d2.addCallback(lambda ign:
3378-                           self.failUnlessChildURIIs(n, u"child-mutable",
3379-                                                     filecap2))
3380+                           self.failUnlessRWChildURIIs(n, u"unknownchild-rw",
3381+                                                       caps['unknown_rwcap']))
3382             d2.addCallback(lambda ign:
3383-                           self.failUnlessChildURIIs(n, u"child-mutable-ro",
3384-                                                     filecap3))
3385+                           self.failUnlessROChildURIIs(n, u"unknownchild-ro",
3386+                                                       caps['unknown_rocap']))
3387             d2.addCallback(lambda ign:
3388-                           self.failUnlessChildURIIs(n, u"dirchild", dircap))
3389+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
3390+                                                       caps['unknown_immcap']))
3391+            d2.addCallback(lambda ign:
3392+                           self.failUnlessRWChildURIIs(n, u"dirchild",
3393+                                                       caps['dircap']))
3394             return d2
3395         d.addCallback(_after_mkdir)
3396         return d
3397@@ -2103,8 +2307,7 @@
3398     def test_POST_mkdir_no_parentdir_unexpected_children(self):
3399         # the regular /uri?t=mkdir operation is specified to ignore its body.
3400         # Only t=mkdir-with-children pays attention to it.
3401-        (newkids, filecap1, filecap2, filecap3,
3402-         dircap) = self._create_initial_children()
3403+        (newkids, caps) = self._create_initial_children()
3404         d = self.shouldHTTPError("POST t=mkdir unexpected children",
3405                                  400, "Bad Request",
3406                                  "t=mkdir does not accept children=, "
3407@@ -2121,28 +2324,32 @@
3408         return d
3409 
3410     def test_POST_mkdir_no_parentdir_immutable(self):
3411-        (newkids, filecap1, immdircap) = self._create_immutable_children()
3412-        d = self.POST2("/uri?t=mkdir-immutable", simplejson.dumps(newkids))
3413+        (newkids, caps) = self._create_immutable_children()
3414+        d = self.shouldSucceed("POST_mkdir_no_parentdir_immutable", http.OK, self.POST2,
3415+                               "/uri?t=mkdir-immutable", simplejson.dumps(newkids))
3416         def _after_mkdir(res):
3417             self.failUnless(res.startswith("URI:DIR"), res)
3418             n = self.s.create_node_from_uri(res)
3419             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
3420             d2.addCallback(lambda ign:
3421-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
3422+                           self.failUnlessROChildURIIs(n, u"child-imm",
3423+                                                          caps['filecap1']))
3424+            d2.addCallback(lambda ign:
3425+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
3426+                                                          caps['unknown_immcap']))
3427             d2.addCallback(lambda ign:
3428-                           self.failUnlessChildURIIs(n, u"dirchild-imm",
3429-                                                     immdircap))
3430+                           self.failUnlessROChildURIIs(n, u"dirchild-imm",
3431+                                                          caps['immdircap']))
3432             return d2
3433         d.addCallback(_after_mkdir)
3434         return d
3435 
3436     def test_POST_mkdir_no_parentdir_immutable_bad(self):
3437-        (newkids, filecap1, filecap2, filecap3,
3438-         dircap) = self._create_initial_children()
3439+        (newkids, caps) = self._create_initial_children()
3440         d = self.shouldFail2(error.Error,
3441-                             "test_POST_mkdir_no_parentdir_immutable_bad",
3442+                             "POST_mkdir_no_parentdir_immutable_bad",
3443                              "400 Bad Request",
3444-                             "a mkdir-immutable operation was given a child that was not itself immutable",
3445+                             "needed to be immutable but was not",
3446                              self.POST2,
3447                              "/uri?t=mkdir-immutable",
3448                              simplejson.dumps(newkids))
3449@@ -2150,9 +2357,14 @@
3450 
3451     def test_welcome_page_mkdir_button(self):
3452         # Fetch the welcome page.
3453-        d = self.GET("/")
3454+        d = self.shouldSucceedGET("/")
3455         def _after_get_welcome_page(res):
3456-            MKDIR_BUTTON_RE=re.compile('<form action="([^"]*)" method="post".*?<input type="hidden" name="t" value="([^"]*)" /><input type="hidden" name="([^"]*)" value="([^"]*)" /><input type="submit" value="Create a directory" />', re.I)
3457+            MKDIR_BUTTON_RE = re.compile(
3458+                '<form action="([^"]*)" method="post".*?'
3459+                '<input type="hidden" name="t" value="([^"]*)" />'
3460+                '<input type="hidden" name="([^"]*)" value="([^"]*)" />'
3461+                '<input type="submit" value="Create a directory" />',
3462+                re.I)
3463             mo = MKDIR_BUTTON_RE.search(res)
3464             formaction = mo.group(1)
3465             formt = mo.group(2)
3466@@ -2168,7 +2380,8 @@
3467         return d
3468 
3469     def test_POST_mkdir_replace(self): # return value?
3470-        d = self.POST(self.public_url + "/foo", t="mkdir", name="sub")
3471+        d = self.shouldSucceed("POST_mkdir_replace", http.OK, self.POST,
3472+                               self.public_url + "/foo", t="mkdir", name="sub")
3473         d.addCallback(lambda res: self._foo_node.get(u"sub"))
3474         d.addCallback(self.failUnlessNodeKeysAre, [])
3475         return d
3476@@ -2250,9 +2463,9 @@
3477 
3478         d = client.getPage(url, method="POST", postdata=reqbody)
3479         def _then(res):
3480-            self.failUnlessURIMatchesChild(newuri9, self._foo_node, u"atomic_added_1")
3481-            self.failUnlessURIMatchesChild(newuri10, self._foo_node, u"atomic_added_2")
3482-            self.failUnlessURIMatchesChild(newuri11, self._foo_node, u"atomic_added_3")
3483+            self.failUnlessURIMatchesROChild(newuri9, self._foo_node, u"atomic_added_1")
3484+            self.failUnlessURIMatchesROChild(newuri10, self._foo_node, u"atomic_added_2")
3485+            self.failUnlessURIMatchesROChild(newuri11, self._foo_node, u"atomic_added_3")
3486 
3487         d.addCallback(_then)
3488         d.addErrback(self.dump_error)
3489@@ -2260,8 +2473,9 @@
3490 
3491     def test_POST_put_uri(self):
3492         contents, n, newuri = self.makefile(8)
3493-        d = self.POST(self.public_url + "/foo", t="uri", name="new.txt", uri=newuri)
3494-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
3495+        d = self.shouldSucceed("POST_put_uri", http.OK, self.POST,
3496+                               self.public_url + "/foo", t="uri", name="new.txt", uri=newuri)
3497+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
3498         d.addCallback(lambda res:
3499                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
3500                                                       contents))
3501@@ -2269,8 +2483,9 @@
3502 
3503     def test_POST_put_uri_replace(self):
3504         contents, n, newuri = self.makefile(8)
3505-        d = self.POST(self.public_url + "/foo", t="uri", name="bar.txt", uri=newuri)
3506-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt")
3507+        d = self.shouldSucceed("POST_put_uri_replace", http.OK, self.POST,
3508+                               self.public_url + "/foo", t="uri", name="bar.txt", uri=newuri)
3509+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt")
3510         d.addCallback(lambda res:
3511                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
3512                                                       contents))
3513@@ -2285,7 +2500,7 @@
3514                   "409 Conflict",
3515                   "There was already a child by that name, and you asked me "
3516                   "to not replace it")
3517-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
3518+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/bar.txt"))
3519         d.addCallback(self.failUnlessIsBarDotTxt)
3520         return d
3521 
3522@@ -2298,12 +2513,13 @@
3523                   "409 Conflict",
3524                   "There was already a child by that name, and you asked me "
3525                   "to not replace it")
3526-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
3527+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/bar.txt"))
3528         d.addCallback(self.failUnlessIsBarDotTxt)
3529         return d
3530 
3531     def test_POST_delete(self):
3532-        d = self.POST(self.public_url + "/foo", t="delete", name="bar.txt")
3533+        d = self.shouldSucceed("POST_delete", http.OK, self.POST,
3534+                               self.public_url + "/foo", t="delete", name="bar.txt")
3535         d.addCallback(lambda res: self._foo_node.list())
3536         def _check(children):
3537             self.failIf(u"bar.txt" in children)
3538@@ -2311,40 +2527,43 @@
3539         return d
3540 
3541     def test_POST_rename_file(self):
3542-        d = self.POST(self.public_url + "/foo", t="rename",
3543-                      from_name="bar.txt", to_name='wibble.txt')
3544+        d = self.shouldSucceed("POST_rename_file", http.OK, self.POST,
3545+                               self.public_url + "/foo", t="rename",
3546+                               from_name="bar.txt", to_name='wibble.txt')
3547         d.addCallback(lambda res:
3548                       self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
3549         d.addCallback(lambda res:
3550                       self.failUnlessNodeHasChild(self._foo_node, u"wibble.txt"))
3551-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/wibble.txt"))
3552+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/wibble.txt"))
3553         d.addCallback(self.failUnlessIsBarDotTxt)
3554-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/wibble.txt?t=json"))
3555+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/wibble.txt?t=json"))
3556         d.addCallback(self.failUnlessIsBarJSON)
3557         return d
3558 
3559     def test_POST_rename_file_redundant(self):
3560-        d = self.POST(self.public_url + "/foo", t="rename",
3561-                      from_name="bar.txt", to_name='bar.txt')
3562+        d = self.shouldSucceed("POST_rename_file_redundant", http.OK, self.POST,
3563+                               self.public_url + "/foo", t="rename",
3564+                               from_name="bar.txt", to_name='bar.txt')
3565         d.addCallback(lambda res:
3566                       self.failUnlessNodeHasChild(self._foo_node, u"bar.txt"))
3567-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt"))
3568+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/bar.txt"))
3569         d.addCallback(self.failUnlessIsBarDotTxt)
3570-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/bar.txt?t=json"))
3571+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/bar.txt?t=json"))
3572         d.addCallback(self.failUnlessIsBarJSON)
3573         return d
3574 
3575     def test_POST_rename_file_replace(self):
3576         # rename a file and replace a directory with it
3577-        d = self.POST(self.public_url + "/foo", t="rename",
3578-                      from_name="bar.txt", to_name='empty')
3579+        d = self.shouldSucceed("POST_rename_file_replace", http.OK, self.POST,
3580+                               self.public_url + "/foo", t="rename",
3581+                               from_name="bar.txt", to_name='empty')
3582         d.addCallback(lambda res:
3583                       self.failIfNodeHasChild(self._foo_node, u"bar.txt"))
3584         d.addCallback(lambda res:
3585                       self.failUnlessNodeHasChild(self._foo_node, u"empty"))
3586-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/empty"))
3587+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/empty"))
3588         d.addCallback(self.failUnlessIsBarDotTxt)
3589-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/empty?t=json"))
3590+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/empty?t=json"))
3591         d.addCallback(self.failUnlessIsBarJSON)
3592         return d
3593 
3594@@ -2357,7 +2576,7 @@
3595                   "409 Conflict",
3596                   "There was already a child by that name, and you asked me "
3597                   "to not replace it")
3598-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/empty?t=json"))
3599+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/empty?t=json"))
3600         d.addCallback(self.failUnlessIsEmptyJSON)
3601         return d
3602 
3603@@ -2370,7 +2589,7 @@
3604                   "409 Conflict",
3605                   "There was already a child by that name, and you asked me "
3606                   "to not replace it")
3607-        d.addCallback(lambda res: self.GET(self.public_url + "/foo/empty?t=json"))
3608+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/foo/empty?t=json"))
3609         d.addCallback(self.failUnlessIsEmptyJSON)
3610         return d
3611 
3612@@ -2383,7 +2602,7 @@
3613         d = self.POST(self.public_url + "/foo", t="rename",
3614                       from_name="bar.txt", to_name='kirk/spock.txt')
3615         d.addBoth(self.shouldFail, error.Error,
3616-                  "test_POST_rename_file_slash_fail",
3617+                  "POST_rename_file_slash_fail",
3618                   "400 Bad Request",
3619                   "to_name= may not contain a slash",
3620                   )
3621@@ -2392,13 +2611,14 @@
3622         return d
3623 
3624     def test_POST_rename_dir(self):
3625-        d = self.POST(self.public_url, t="rename",
3626-                      from_name="foo", to_name='plunk')
3627+        d = self.shouldSucceed("POST_rename_dir", http.OK, self.POST,
3628+                               self.public_url, t="rename",
3629+                               from_name="foo", to_name='plunk')
3630         d.addCallback(lambda res:
3631                       self.failIfNodeHasChild(self.public_root, u"foo"))
3632         d.addCallback(lambda res:
3633                       self.failUnlessNodeHasChild(self.public_root, u"plunk"))
3634-        d.addCallback(lambda res: self.GET(self.public_url + "/plunk?t=json"))
3635+        d.addCallback(lambda res: self.shouldSucceedGET(self.public_url + "/plunk?t=json"))
3636         d.addCallback(self.failUnlessIsFooJSON)
3637         return d
3638 
3639@@ -2433,24 +2653,24 @@
3640         d.addCallback(lambda res: self.GET(base+"&t=json"))
3641         d.addBoth(self.shouldRedirect, targetbase+"?t=json")
3642         d.addCallback(self.log, "about to get file by uri")
3643-        d.addCallback(lambda res: self.GET(base, followRedirect=True))
3644+        d.addCallback(lambda res: self.shouldSucceedGET(base, followRedirect=True))
3645         d.addCallback(self.failUnlessIsBarDotTxt)
3646         d.addCallback(self.log, "got file by uri, about to get dir by uri")
3647-        d.addCallback(lambda res: self.GET("/uri?uri=%s&t=json" % self._foo_uri,
3648-                                           followRedirect=True))
3649+        d.addCallback(lambda res: self.shouldSucceedGET("/uri?uri=%s&t=json" % self._foo_uri,
3650+                                                        followRedirect=True))
3651         d.addCallback(self.failUnlessIsFooJSON)
3652         d.addCallback(self.log, "got dir by uri")
3653 
3654         return d
3655 
3656     def test_GET_URI_form_bad(self):
3657-        d = self.shouldFail2(error.Error, "test_GET_URI_form_bad",
3658+        d = self.shouldFail2(error.Error, "GET_URI_form_bad",
3659                              "400 Bad Request", "GET /uri requires uri=",
3660                              self.GET, "/uri")
3661         return d
3662 
3663     def test_GET_rename_form(self):
3664-        d = self.GET(self.public_url + "/foo?t=rename-form&name=bar.txt",
3665+        d = self.shouldSucceedGET(self.public_url + "/foo?t=rename-form&name=bar.txt",
3666                      followRedirect=True)
3667         def _check(res):
3668             self.failUnless('name="when_done" value="."' in res, res)
3669@@ -2465,23 +2685,23 @@
3670 
3671     def test_GET_URI_URL(self):
3672         base = "/uri/%s" % self._bar_txt_uri
3673-        d = self.GET(base)
3674+        d = self.shouldSucceedGET(base)
3675         d.addCallback(self.failUnlessIsBarDotTxt)
3676-        d.addCallback(lambda res: self.GET(base+"?filename=bar.txt"))
3677+        d.addCallback(lambda res: self.shouldSucceedGET(base+"?filename=bar.txt"))
3678         d.addCallback(self.failUnlessIsBarDotTxt)
3679-        d.addCallback(lambda res: self.GET(base+"?filename=bar.txt&save=true"))
3680+        d.addCallback(lambda res: self.shouldSucceedGET(base+"?filename=bar.txt&save=true"))
3681         d.addCallback(self.failUnlessIsBarDotTxt)
3682         return d
3683 
3684     def test_GET_URI_URL_dir(self):
3685         base = "/uri/%s?t=json" % self._foo_uri
3686-        d = self.GET(base)
3687+        d = self.shouldSucceedGET(base)
3688         d.addCallback(self.failUnlessIsFooJSON)
3689         return d
3690 
3691     def test_GET_URI_URL_missing(self):
3692         base = "/uri/%s" % self._bad_file_uri
3693-        d = self.shouldHTTPError("test_GET_URI_URL_missing",
3694+        d = self.shouldHTTPError("GET_URI_URL_missing",
3695                                  http.GONE, None, "NotEnoughSharesError",
3696                                  self.GET, base)
3697         # TODO: how can we exercise both sides of WebDownloadTarget.fail
3698@@ -2499,9 +2719,9 @@
3699             d.addCallback(lambda res:
3700                           self.failUnlessEqual(res.strip(), new_uri))
3701             d.addCallback(lambda res:
3702-                          self.failUnlessChildURIIs(self.public_root,
3703-                                                    u"foo",
3704-                                                    new_uri))
3705+                          self.failUnlessRWChildURIIs(self.public_root,
3706+                                                      u"foo",
3707+                                                      new_uri))
3708             return d
3709         d.addCallback(_made_dir)
3710         return d
3711@@ -2512,32 +2732,33 @@
3712             new_uri = dn.get_uri()
3713             # replace /foo with a new (empty) directory, but ask that
3714             # replace=false, so it should fail
3715-            d = self.shouldFail2(error.Error, "test_PUT_DIRURL_uri_noreplace",
3716+            d = self.shouldFail2(error.Error, "PUT_DIRURL_uri_noreplace",
3717                                  "409 Conflict", "There was already a child by that name, and you asked me to not replace it",
3718                                  self.PUT,
3719                                  self.public_url + "/foo?t=uri&replace=false",
3720                                  new_uri)
3721             d.addCallback(lambda res:
3722-                          self.failUnlessChildURIIs(self.public_root,
3723-                                                    u"foo",
3724-                                                    self._foo_uri))
3725+                          self.failUnlessRWChildURIIs(self.public_root,
3726+                                                      u"foo",
3727+                                                      self._foo_uri))
3728             return d
3729         d.addCallback(_made_dir)
3730         return d
3731 
3732     def test_PUT_DIRURL_bad_t(self):
3733-        d = self.shouldFail2(error.Error, "test_PUT_DIRURL_bad_t",
3734+        d = self.shouldFail2(error.Error, "PUT_DIRURL_bad_t",
3735                                  "400 Bad Request", "PUT to a directory",
3736                                  self.PUT, self.public_url + "/foo?t=BOGUS", "")
3737         d.addCallback(lambda res:
3738-                      self.failUnlessChildURIIs(self.public_root,
3739-                                                u"foo",
3740-                                                self._foo_uri))
3741+                      self.failUnlessRWChildURIIs(self.public_root,
3742+                                                  u"foo",
3743+                                                  self._foo_uri))
3744         return d
3745 
3746     def test_PUT_NEWFILEURL_uri(self):
3747         contents, n, new_uri = self.makefile(8)
3748-        d = self.PUT(self.public_url + "/foo/new.txt?t=uri", new_uri)
3749+        d = self.shouldSucceed("PUT_NEWFILEURL_uri", http.OK, self.PUT,
3750+                               self.public_url + "/foo/new.txt?t=uri", new_uri)
3751         d.addCallback(lambda res: self.failUnlessEqual(res.strip(), new_uri))
3752         d.addCallback(lambda res:
3753                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
3754@@ -2564,13 +2785,14 @@
3755 
3756     def test_PUT_NEWFILE_URI(self):
3757         file_contents = "New file contents here\n"
3758-        d = self.PUT("/uri", file_contents)
3759+        d = self.shouldSucceed("PUT_NEWFILE_URI", http.OK, self.PUT,
3760+                               "/uri", file_contents)
3761         def _check(uri):
3762             assert isinstance(uri, str), uri
3763             self.failUnless(uri in FakeCHKFileNode.all_contents)
3764             self.failUnlessEqual(FakeCHKFileNode.all_contents[uri],
3765                                  file_contents)
3766-            return self.GET("/uri/%s" % uri)
3767+            return self.shouldSucceedGET("/uri/%s" % uri)
3768         d.addCallback(_check)
3769         def _check2(res):
3770             self.failUnlessEqual(res, file_contents)
3771@@ -2579,13 +2801,14 @@
3772 
3773     def test_PUT_NEWFILE_URI_not_mutable(self):
3774         file_contents = "New file contents here\n"
3775-        d = self.PUT("/uri?mutable=false", file_contents)
3776+        d = self.shouldSucceed("PUT_NEWFILE_URI_not_mutable", http.OK, self.PUT,
3777+                               "/uri?mutable=false", file_contents)
3778         def _check(uri):
3779             assert isinstance(uri, str), uri
3780             self.failUnless(uri in FakeCHKFileNode.all_contents)
3781             self.failUnlessEqual(FakeCHKFileNode.all_contents[uri],
3782                                  file_contents)
3783-            return self.GET("/uri/%s" % uri)
3784+            return self.shouldSucceedGET("/uri/%s" % uri)
3785         d.addCallback(_check)
3786         def _check2(res):
3787             self.failUnlessEqual(res, file_contents)
3788@@ -2602,7 +2825,8 @@
3789 
3790     def test_PUT_NEWFILE_URI_mutable(self):
3791         file_contents = "New file contents here\n"
3792-        d = self.PUT("/uri?mutable=true", file_contents)
3793+        d = self.shouldSucceed("PUT_NEWFILE_URI_mutable", http.OK, self.PUT,
3794+                               "/uri?mutable=true", file_contents)
3795         def _check1(filecap):
3796             filecap = filecap.strip()
3797             self.failUnless(filecap.startswith("URI:SSK:"), filecap)
3798@@ -2614,7 +2838,7 @@
3799         d.addCallback(_check1)
3800         def _check2(data):
3801             self.failUnlessEqual(data, file_contents)
3802-            return self.GET("/uri/%s" % urllib.quote(self.filecap))
3803+            return self.shouldSucceedGET("/uri/%s" % urllib.quote(self.filecap))
3804         d.addCallback(_check2)
3805         def _check3(res):
3806             self.failUnlessEqual(res, file_contents)
3807@@ -2622,19 +2846,21 @@
3808         return d
3809 
3810     def test_PUT_mkdir(self):
3811-        d = self.PUT("/uri?t=mkdir", "")
3812+        d = self.shouldSucceed("PUT_mkdir", http.OK, self.PUT,
3813+                               "/uri?t=mkdir", "")
3814         def _check(uri):
3815             n = self.s.create_node_from_uri(uri.strip())
3816             d2 = self.failUnlessNodeKeysAre(n, [])
3817             d2.addCallback(lambda res:
3818-                           self.GET("/uri/%s?t=json" % uri))
3819+                           self.shouldSucceedGET("/uri/%s?t=json" % uri))
3820             return d2
3821         d.addCallback(_check)
3822         d.addCallback(self.failUnlessIsEmptyJSON)
3823         return d
3824 
3825     def test_POST_check(self):
3826-        d = self.POST(self.public_url + "/foo", t="check", name="bar.txt")
3827+        d = self.shouldSucceed("POST_check", http.OK, self.POST,
3828+                               self.public_url + "/foo", t="check", name="bar.txt")
3829         def _done(res):
3830             # this returns a string form of the results, which are probably
3831             # None since we're using fake filenodes.
3832@@ -2647,7 +2873,7 @@
3833 
3834     def test_bad_method(self):
3835         url = self.webish_url + self.public_url + "/foo/bar.txt"
3836-        d = self.shouldHTTPError("test_bad_method",
3837+        d = self.shouldHTTPError("bad_method",
3838                                  501, "Not Implemented",
3839                                  "I don't know how to treat a BOGUS request.",
3840                                  client.getPage, url, method="BOGUS")
3841@@ -2655,28 +2881,30 @@
3842 
3843     def test_short_url(self):
3844         url = self.webish_url + "/uri"
3845-        d = self.shouldHTTPError("test_short_url", 501, "Not Implemented",
3846+        d = self.shouldHTTPError("short_url", 501, "Not Implemented",
3847                                  "I don't know how to treat a DELETE request.",
3848                                  client.getPage, url, method="DELETE")
3849         return d
3850 
3851     def test_ophandle_bad(self):
3852         url = self.webish_url + "/operations/bogus?t=status"
3853-        d = self.shouldHTTPError("test_ophandle_bad", 404, "404 Not Found",
3854+        d = self.shouldHTTPError("ophandle_bad", 404, "404 Not Found",
3855                                  "unknown/expired handle 'bogus'",
3856                                  client.getPage, url)
3857         return d
3858 
3859     def test_ophandle_cancel(self):
3860-        d = self.POST(self.public_url + "/foo/?t=start-manifest&ophandle=128",
3861-                      followRedirect=True)
3862+        d = self.shouldSucceed("ophandle_cancel-1", http.OK, self.POST,
3863+                               self.public_url + "/foo/?t=start-manifest&ophandle=128",
3864+                               followRedirect=True)
3865         d.addCallback(lambda ignored:
3866-                      self.GET("/operations/128?t=status&output=JSON"))
3867+                      self.shouldSucceedGET("/operations/128?t=status&output=JSON"))
3868         def _check1(res):
3869             data = simplejson.loads(res)
3870             self.failUnless("finished" in data, res)
3871             monitor = self.ws.root.child_operations.handles["128"][0]
3872-            d = self.POST("/operations/128?t=cancel&output=JSON")
3873+            d = self.shouldSucceed("ophandle_cancel-2", http.OK, self.POST,
3874+                                   "/operations/128?t=cancel&output=JSON")
3875             def _check2(res):
3876                 data = simplejson.loads(res)
3877                 self.failUnless("finished" in data, res)
3878@@ -2686,7 +2914,7 @@
3879             return d
3880         d.addCallback(_check1)
3881         d.addCallback(lambda ignored:
3882-                      self.shouldHTTPError("test_ophandle_cancel",
3883+                      self.shouldHTTPError("ophandle_cancel",
3884                                            404, "404 Not Found",
3885                                            "unknown/expired handle '128'",
3886                                            self.GET,
3887@@ -2697,7 +2925,7 @@
3888         d = self.POST(self.public_url + "/foo/?t=start-manifest&ophandle=129&retain-for=60",
3889                       followRedirect=True)
3890         d.addCallback(lambda ignored:
3891-                      self.GET("/operations/129?t=status&output=JSON&retain-for=0"))
3892+                      self.shouldSucceedGET("/operations/129?t=status&output=JSON&retain-for=0"))
3893         def _check1(res):
3894             data = simplejson.loads(res)
3895             self.failUnless("finished" in data, res)
3896@@ -2705,7 +2933,7 @@
3897         # the retain-for=0 will cause the handle to be expired very soon
3898         d.addCallback(self.stall, 2.0)
3899         d.addCallback(lambda ignored:
3900-                      self.shouldHTTPError("test_ophandle_retainfor",
3901+                      self.shouldHTTPError("ophandle_retainfor",
3902                                            404, "404 Not Found",
3903                                            "unknown/expired handle '129'",
3904                                            self.GET,
3905@@ -2713,14 +2941,15 @@
3906         return d
3907 
3908     def test_ophandle_release_after_complete(self):
3909-        d = self.POST(self.public_url + "/foo/?t=start-manifest&ophandle=130",
3910-                      followRedirect=True)
3911+        d = self.shouldSucceed("ophandle_release_after_complete", http.OK, self.POST,
3912+                               self.public_url + "/foo/?t=start-manifest&ophandle=130",
3913+                               followRedirect=True)
3914         d.addCallback(self.wait_for_operation, "130")
3915         d.addCallback(lambda ignored:
3916-                      self.GET("/operations/130?t=status&output=JSON&release-after-complete=true"))
3917+                      self.shouldSucceedGET("/operations/130?t=status&output=JSON&release-after-complete=true"))
3918         # the release-after-complete=true will cause the handle to be expired
3919         d.addCallback(lambda ignored:
3920-                      self.shouldHTTPError("test_ophandle_release_after_complete",
3921+                      self.shouldHTTPError("ophandle_release_after_complete",
3922                                            404, "404 Not Found",
3923                                            "unknown/expired handle '130'",
3924                                            self.GET,
3925@@ -2728,7 +2957,8 @@
3926         return d
3927 
3928     def test_incident(self):
3929-        d = self.POST("/report_incident", details="eek")
3930+        d = self.shouldSucceed("incident", http.OK, self.POST,
3931+                               "/report_incident", details="eek")
3932         def _done(res):
3933             self.failUnless("Thank you for your report!" in res, res)
3934         d.addCallback(_done)
3935@@ -2741,7 +2971,7 @@
3936         f.write("hello")
3937         f.close()
3938 
3939-        d = self.GET("/static/subdir/hello.txt")
3940+        d = self.shouldSucceedGET("/static/subdir/hello.txt")
3941         def _check(res):
3942             self.failUnlessEqual(res, "hello")
3943         d.addCallback(_check)
3944@@ -2754,7 +2984,7 @@
3945         self.failUnlessEqual(common.parse_replace_arg("false"), False)
3946         self.failUnlessEqual(common.parse_replace_arg("only-files"),
3947                              "only-files")
3948-        self.shouldFail(AssertionError, "test_parse_replace_arg", "",
3949+        self.shouldFail(AssertionError, "parse_replace_arg", "",
3950                         common.parse_replace_arg, "only_fles")
3951 
3952     def test_abbreviate_time(self):
3953@@ -3059,71 +3289,225 @@
3954         d.addErrback(self.explain_web_error)
3955         return d
3956 
3957-    def test_unknown(self):
3958+    def test_unknown(self, immutable=False):
3959         self.basedir = "web/Grid/unknown"
3960+        if immutable:
3961+            self.basedir = "web/Grid/unknown-immutable"
3962+
3963         self.set_up_grid()
3964         c0 = self.g.clients[0]
3965         self.uris = {}
3966         self.fileurls = {}
3967 
3968-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
3969-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
3970+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
3971+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
3972         # the future cap format may contain slashes, which must be tolerated
3973-        expected_info_url = "uri/%s?t=info" % urllib.quote(future_writecap,
3974+        expected_info_url = "uri/%s?t=info" % urllib.quote(future_write_uri,
3975                                                            safe="")
3976-        future_node = UnknownNode(future_writecap, future_readcap)
3977 
3978-        d = c0.create_dirnode()
3979+        if immutable:
3980+            name = u"future-imm"
3981+            future_node = UnknownNode(None, future_read_uri, deep_immutable=True)
3982+            d = c0.create_immutable_dirnode({name: (future_node, {})})
3983+        else:
3984+            name = u"future"
3985+            future_node = UnknownNode(future_write_uri, future_read_uri)
3986+            d = c0.create_dirnode()
3987+
3988         def _stash_root_and_create_file(n):
3989             self.rootnode = n
3990             self.rooturl = "uri/" + urllib.quote(n.get_uri()) + "/"
3991             self.rourl = "uri/" + urllib.quote(n.get_readonly_uri()) + "/"
3992-            return self.rootnode.set_node(u"future", future_node)
3993+            if not immutable:
3994+                return self.rootnode.set_node(name, future_node)
3995         d.addCallback(_stash_root_and_create_file)
3996+
3997         # make sure directory listing tolerates unknown nodes
3998         d.addCallback(lambda ign: self.GET(self.rooturl))
3999         def _check_html(res):
4000-            self.failUnlessIn("<td>future</td>", res)
4001-            # find the More Info link for "future", should be relative
4002+            self.failUnlessIn("<td>%s</td>" % (str(name),), res)
4003+            # find the More Info link for name, should be relative
4004             mo = re.search(r'<a href="([^"]+)">More Info</a>', res)
4005             info_url = mo.group(1)
4006-            self.failUnlessEqual(info_url, "future?t=info")
4007+            self.failUnlessEqual(info_url, "%s?t=info" % (str(name),))
4008 
4009         d.addCallback(_check_html)
4010         d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json"))
4011-        def _check_json(res, expect_writecap):
4012+        def _check_json(res, expect_rw_uri):
4013             data = simplejson.loads(res)
4014             self.failUnlessEqual(data[0], "dirnode")
4015-            f = data[1]["children"]["future"]
4016+            f = data[1]["children"][name]
4017             self.failUnlessEqual(f[0], "unknown")
4018-            if expect_writecap:
4019-                self.failUnlessEqual(f[1]["rw_uri"], future_writecap)
4020+            if expect_rw_uri:
4021+                self.failUnlessEqual(f[1]["rw_uri"], future_write_uri)
4022             else:
4023                 self.failIfIn("rw_uri", f[1])
4024-            self.failUnlessEqual(f[1]["ro_uri"], future_readcap)
4025+            self.failUnlessEqual(f[1]["ro_uri"],
4026+                                 ("imm." if immutable else "ro.") + future_read_uri)
4027             self.failUnless("metadata" in f[1])
4028-        d.addCallback(_check_json, expect_writecap=True)
4029-        d.addCallback(lambda ign: self.GET(expected_info_url))
4030-        def _check_info(res, expect_readcap):
4031+        d.addCallback(_check_json, expect_rw_uri=not immutable)
4032+
4033+        def _check_info(res, expect_rw_uri, expect_ro_uri):
4034             self.failUnlessIn("Object Type: <span>unknown</span>", res)
4035-            self.failUnlessIn(future_writecap, res)
4036-            if expect_readcap:
4037-                self.failUnlessIn(future_readcap, res)
4038+            if expect_rw_uri:
4039+                self.failUnlessIn(future_write_uri, res)
4040+            if expect_ro_uri:
4041+                self.failUnlessIn(future_read_uri, res)
4042+            else:
4043+                self.failIfIn(future_read_uri, res)
4044             self.failIfIn("Raw data as", res)
4045             self.failIfIn("Directory writecap", res)
4046             self.failIfIn("Checker Operations", res)
4047             self.failIfIn("Mutable File Operations", res)
4048             self.failIfIn("Directory Operations", res)
4049-        d.addCallback(_check_info, expect_readcap=False)
4050-        d.addCallback(lambda ign: self.GET(self.rooturl+"future?t=info"))
4051-        d.addCallback(_check_info, expect_readcap=True)
4052+
4053+        # Known bug: these should have expect_rw_uri=not immutable, but the
4054+        # info pages are currently broken. Related to ticket #922.
4055+
4056+        d.addCallback(lambda ign: self.GET(expected_info_url))
4057+        d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=False)
4058+        d.addCallback(lambda ign: self.GET("%s%s?t=info" % (self.rooturl, str(name))))
4059+        d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=True)
4060 
4061         # and make sure that a read-only version of the directory can be
4062-        # rendered too. This version will not have future_writecap
4063+        # rendered too. This version will not have future_write_uri, whether
4064+        # or not future_node was immutable.
4065         d.addCallback(lambda ign: self.GET(self.rourl))
4066         d.addCallback(_check_html)
4067         d.addCallback(lambda ign: self.GET(self.rourl+"?t=json"))
4068-        d.addCallback(_check_json, expect_writecap=False)
4069+        d.addCallback(_check_json, expect_rw_uri=False)
4070+        return d
4071+
4072+    def test_immutable_unknown(self):
4073+        return self.test_unknown(immutable=True)
4074+
4075+    def test_mutant_dirnodes_are_omitted(self):
4076+        self.basedir = "web/Grid/mutant_dirnodes_are_omitted"
4077+
4078+        self.set_up_grid()
4079+        c = self.g.clients[0]
4080+        nm = c.nodemaker
4081+        self.uris = {}
4082+        self.fileurls = {}
4083+
4084+        lonely_uri = "URI:LIT:n5xgk" # LIT for "one"
4085+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
4086+        mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
4087+       
4088+        # This method tests mainly dirnode, but we'd have to duplicate code in order to
4089+        # test the dirnode and web layers separately.
4090+       
4091+        # 'lonely' is a valid LIT child, 'ro' is a mutant child with an SSK-RO readcap,
4092+        # and 'write-in-ro' is a mutant child with an SSK writecap in the ro_uri field.
4093+        # When the directory is read, the mutants should be silently disposed of, leaving
4094+        # their lonely sibling.
4095+        # We don't test the case of a retrieving a cap from the encrypted rw_uri field,
4096+        # because immutable directories don't have a writecap and therefore that field
4097+        # isn't (and can't be) decrypted.
4098+        # TODO: The field still exists in the netstring. Technically we should check what
4099+        # happens if something is put there (it should be ignored), but that can wait.
4100+
4101+        lonely_child = nm.create_from_cap(lonely_uri)
4102+        mutant_ro_child = nm.create_from_cap(mut_read_uri)
4103+        mutant_write_in_ro_child = nm.create_from_cap(mut_write_uri)
4104+
4105+        def _by_hook_or_by_crook():
4106+            return True
4107+        for n in [mutant_ro_child, mutant_write_in_ro_child]:
4108+            n.is_allowed_in_immutable_directory = _by_hook_or_by_crook
4109+
4110+        mutant_write_in_ro_child.get_write_uri    = lambda: None
4111+        mutant_write_in_ro_child.get_readonly_uri = lambda: mut_write_uri
4112+
4113+        kids = {u"lonely":      (lonely_child, {}),
4114+                u"ro":          (mutant_ro_child, {}),
4115+                u"write-in-ro": (mutant_write_in_ro_child, {}),
4116+                }
4117+        d = c.create_immutable_dirnode(kids)
4118+       
4119+        def _created(dn):
4120+            self.failUnless(isinstance(dn, dirnode.DirectoryNode))
4121+            self.failIf(dn.is_mutable())
4122+            self.failUnless(dn.is_readonly())
4123+            # This checks that if we somehow ended up calling dn._decrypt_rwcapdata, it would fail.
4124+            self.failIf(hasattr(dn._node, 'get_writekey'))
4125+            rep = str(dn)
4126+            self.failUnless("RO-IMM" in rep)
4127+            cap = dn.get_cap()
4128+            self.failUnlessIn("CHK", cap.to_string())
4129+            self.cap = cap
4130+            self.rootnode = dn
4131+            self.rooturl = "uri/" + urllib.quote(dn.get_uri()) + "/"
4132+            return download_to_data(dn._node)
4133+        d.addCallback(_created)
4134+
4135+        def _check_data(data):
4136+            # Decode the netstring representation of the directory to check that all children
4137+            # are present. This is a bit of an abstraction violation, but there's not really
4138+            # any other way to do it given that the real DirectoryNode._unpack_contents would
4139+            # strip the mutant children out (which is what we're trying to test, later).
4140+            position = 0
4141+            numkids = 0
4142+            while position < len(data):
4143+                entries, position = split_netstring(data, 1, position)
4144+                entry = entries[0]
4145+                (name, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
4146+                name = name.decode("utf-8")
4147+                self.failUnless(rwcapdata == "")
4148+                ro_uri = ro_uri.strip()
4149+                if name in kids:
4150+                    self.failIfEqual(ro_uri, "")
4151+                    (expected_child, ign) = kids[name]
4152+                    self.failUnlessEqual(ro_uri, expected_child.get_readonly_uri())
4153+                    numkids += 1
4154+
4155+            self.failUnlessEqual(numkids, 3)
4156+            return self.rootnode.list()
4157+        d.addCallback(_check_data)
4158+       
4159+        # Now when we use the real directory listing code, the mutants should be absent.
4160+        def _check_kids(children):
4161+            self.failUnlessEqual(sorted(children.keys()), [u"lonely"])
4162+            lonely_node, lonely_metadata = children[u"lonely"]
4163+
4164+            self.failUnlessEqual(lonely_node.get_write_uri(), None)
4165+            self.failUnlessEqual(lonely_node.get_readonly_uri(), lonely_uri)
4166+        d.addCallback(_check_kids)
4167+
4168+        d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string()))
4169+        d.addCallback(lambda n: n.list())
4170+        d.addCallback(_check_kids)  # again with dirnode recreated from cap
4171+
4172+        # Make sure the lonely child can be listed in HTML...
4173+        d.addCallback(lambda ign: self.GET(self.rooturl))
4174+        def _check_html(res):
4175+            self.failIfIn("URI:SSK", res)
4176+            get_lonely = "".join([r'<td>FILE</td>',
4177+                                  r'\s+<td>',
4178+                                  r'<a href="[^"]+%s[^"]+">lonely</a>' % (urllib.quote(lonely_uri),),
4179+                                  r'</td>',
4180+                                  r'\s+<td>%d</td>' % len("one"),
4181+                                  ])
4182+            self.failUnless(re.search(get_lonely, res), res)
4183+
4184+            # find the More Info link for name, should be relative
4185+            mo = re.search(r'<a href="([^"]+)">More Info</a>', res)
4186+            info_url = mo.group(1)
4187+            self.failUnless(info_url.endswith(urllib.quote(lonely_uri) + "?t=info"), info_url)
4188+        d.addCallback(_check_html)
4189+
4190+        # ... and in JSON.
4191+        d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json"))
4192+        def _check_json(res):
4193+            data = simplejson.loads(res)
4194+            self.failUnlessEqual(data[0], "dirnode")
4195+            listed_children = data[1]["children"]
4196+            self.failUnlessEqual(sorted(listed_children.keys()), [u"lonely"])
4197+            ll_type, ll_data = listed_children[u"lonely"]
4198+            self.failUnlessEqual(ll_type, "filenode")
4199+            self.failIf("rw_uri" in ll_data)
4200+            self.failUnlessEqual(ll_data["ro_uri"], lonely_uri)
4201+        d.addCallback(_check_json)
4202         return d
4203 
4204     def test_deep_check(self):
4205@@ -3156,10 +3540,10 @@
4206 
4207         # this tests that deep-check and stream-manifest will ignore
4208         # UnknownNode instances. Hopefully this will also cover deep-stats.
4209-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
4210-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
4211-        future_node = UnknownNode(future_writecap, future_readcap)
4212-        d.addCallback(lambda ign: self.rootnode.set_node(u"future",future_node))
4213+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
4214+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
4215+        future_node = UnknownNode(future_write_uri, future_read_uri)
4216+        d.addCallback(lambda ign: self.rootnode.set_node(u"future", future_node))
4217 
4218         def _clobber_shares(ignored):
4219             self.delete_shares_numbered(self.uris["sick"], [0,1])
4220diff -rN -u old-tahoe/src/allmydata/unknown.py new-tahoe/src/allmydata/unknown.py
4221--- old-tahoe/src/allmydata/unknown.py  2010-01-23 12:59:10.164000000 +0000
4222+++ new-tahoe/src/allmydata/unknown.py  2010-01-23 12:59:12.153000000 +0000
4223@@ -1,29 +1,146 @@
4224+
4225 from zope.interface import implements
4226 from twisted.internet import defer
4227-from allmydata.interfaces import IFilesystemNode
4228+from allmydata.interfaces import IFilesystemNode, MustNotBeUnknownRWError
4229+from allmydata import uri
4230+from allmydata.uri import ALLEGED_READONLY_PREFIX, ALLEGED_IMMUTABLE_PREFIX
4231+
4232+
4233+# See ticket #833 for design rationale of UnknownNodes.
4234+
4235+"""Strip prefixes when storing an URI in a ro_uri field."""
4236+def strip_prefix_for_ro(ro_uri, deep_immutable):
4237+    # It is possible for an alleged-immutable URI to be put into a
4238+    # mutable directory. In that case the ALLEGED_IMMUTABLE_PREFIX
4239+    # should not be stripped. In other cases, the prefix can safely
4240+    # be stripped because it is implied by the context.
4241+
4242+    if ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX):
4243+        if not deep_immutable:
4244+            return ro_uri
4245+        return ro_uri[len(ALLEGED_IMMUTABLE_PREFIX):]
4246+    elif ro_uri.startswith(ALLEGED_READONLY_PREFIX):
4247+        return ro_uri[len(ALLEGED_READONLY_PREFIX):]
4248+    else:
4249+        return ro_uri
4250 
4251 class UnknownNode:
4252     implements(IFilesystemNode)
4253-    def __init__(self, writecap, readcap):
4254-        assert writecap is None or isinstance(writecap, str)
4255-        self.writecap = writecap
4256-        assert readcap is None or isinstance(readcap, str)
4257-        self.readcap = readcap
4258+
4259+    def __init__(self, rw_uri, ro_uri, deep_immutable=False,
4260+                 name=u"<unknown name>"):
4261+        #traceback.print_stack()
4262+        #print '%r.__init__(%r, %r, deep_immutable=%r, name=%r)' % (self, rw_uri, ro_uri, deep_immutable, name)
4263+        assert rw_uri is None or isinstance(rw_uri, str)
4264+        assert ro_uri is None or isinstance(ro_uri, str)
4265+
4266+        # We don't raise errors when creating an UnknownNode; we instead create an
4267+        # opaque node that records the error. This avoids breaking operations that
4268+        # never store the opaque node.
4269+        # Note that this means that if a stored dirnode has only a rw_uri, it
4270+        # might be dropped. Any future "write-only" cap formats should have a dummy
4271+        # unusable read cap to stop that from happening.
4272+
4273+        self.error = None
4274+        self.rw_uri = self.ro_uri = None
4275+        if rw_uri is not None:
4276+            if deep_immutable:
4277+                self.error = MustNotBeUnknownRWError("cannot attach unknown rw cap as immutable child",
4278+                                                     name, True)
4279+                return
4280+            elif ro_uri is None:
4281+                # If we have a single unknown cap (specified as a single cap
4282+                # argument, or from a rw_uri slot when ro_uri has been omitted),
4283+                # then we cannot tell whether it is a rw_uri, and we cannot
4284+                # diminish it to a ro_uri. Prefixing it with ALLEGED_READONLY_PREFIX
4285+                # would not be sufficient because we have no reason to believe
4286+                # that it is a ro_uri, so that might grant excess authority.
4287+                self.error = MustNotBeUnknownRWError("cannot attach unknown rw cap as child",
4288+                                                     name, False)
4289+                return
4290+
4291+        # If ro_uri definitely fails the constraint, it should be treated as opaque.
4292+        if ro_uri is not None:
4293+            read_cap = uri.from_string(ro_uri, deep_immutable=deep_immutable, name=name)
4294+            if isinstance(read_cap, uri.UnknownURI):
4295+                self.error = read_cap.get_error()
4296+                if self.error:
4297+                    return
4298+
4299+        if deep_immutable:
4300+            # strengthen ro_uri to have ALLEGED_IMMUTABLE_PREFIX
4301+            if ro_uri is not None:
4302+                if ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX):
4303+                    self.ro_uri = ro_uri
4304+                elif ro_uri.startswith(ALLEGED_READONLY_PREFIX):
4305+                    self.ro_uri = ALLEGED_IMMUTABLE_PREFIX + ro_uri[len(ALLEGED_READONLY_PREFIX):]
4306+                else:
4307+                    self.ro_uri = ALLEGED_IMMUTABLE_PREFIX + ro_uri
4308+        else:
4309+            self.rw_uri = rw_uri
4310+            # strengthen ro_uri to have ALLEGED_READONLY_PREFIX
4311+            if ro_uri is not None:
4312+                if (ro_uri.startswith(ALLEGED_READONLY_PREFIX) or
4313+                    ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX)):
4314+                    self.ro_uri = ro_uri
4315+                else:
4316+                    self.ro_uri = ALLEGED_READONLY_PREFIX + ro_uri
4317+
4318+        #print 'self.(error, rw_uri, ro_uri) = (%r, %r, %r)' % (self.error, self.rw_uri, self.ro_uri)
4319+
4320+    def get_cap(self):
4321+        return uri.UnknownURI(self.rw_uri or self.ro_uri)
4322+
4323+    def get_readcap(self):
4324+        return uri.UnknownURI(self.ro_uri)
4325+
4326+    def is_readonly(self):
4327+        raise AssertionError("an UnknownNode might be either read-only or "
4328+                             "read/write, so we shouldn't be calling is_readonly")
4329+
4330+    def is_mutable(self):
4331+        raise AssertionError("an UnknownNode might be either mutable or immutable, "
4332+                             "so we shouldn't be calling is_mutable")
4333+
4334+    def is_unknown(self):
4335+        return True
4336+
4337+    def is_allowed_in_immutable_directory(self):
4338+        # An UnknownNode consisting only of a ro_uri is allowed in an
4339+        # immutable directory, even though we do not know that it is
4340+        # immutable (or even read-only), provided that no error was detected.
4341+        return not self.error and not self.rw_uri
4342+
4343+    def raise_error(self):
4344+        if self.error is not None:
4345+            raise self.error
4346+
4347     def get_uri(self):
4348-        return self.writecap
4349+        return self.rw_uri or self.ro_uri
4350+
4351+    def get_write_uri(self):
4352+        return self.rw_uri
4353+
4354     def get_readonly_uri(self):
4355-        return self.readcap
4356+        return self.ro_uri
4357+
4358     def get_storage_index(self):
4359         return None
4360+
4361     def get_verify_cap(self):
4362         return None
4363+
4364     def get_repair_cap(self):
4365         return None
4366+
4367     def get_size(self):
4368         return None
4369+
4370     def get_current_size(self):
4371         return defer.succeed(None)
4372+
4373     def check(self, monitor, verify, add_lease):
4374         return defer.succeed(None)
4375+
4376     def check_and_repair(self, monitor, verify, add_lease):
4377         return defer.succeed(None)
4378diff -rN -u old-tahoe/src/allmydata/uri.py new-tahoe/src/allmydata/uri.py
4379--- old-tahoe/src/allmydata/uri.py      2010-01-23 12:59:10.175000000 +0000
4380+++ new-tahoe/src/allmydata/uri.py      2010-01-23 12:59:12.157000000 +0000
4381@@ -5,14 +5,16 @@
4382 from allmydata.storage.server import si_a2b, si_b2a
4383 from allmydata.util import base32, hashutil
4384 from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IImmutableFileURI, \
4385-    IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI
4386+    IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI, \
4387+    MustBeDeepImmutableError, MustBeReadonlyError
4388 
4389 class BadURIError(Exception):
4390     pass
4391 
4392-# the URI shall be an ascii representation of the file. It shall contain
4393-# enough information to retrieve and validate the contents. It shall be
4394-# expressed in a limited character set (namely [TODO]).
4395+# The URI shall be an ASCII representation of a reference to the file/directory.
4396+# It shall contain enough information to retrieve and validate the contents.
4397+# It shall be expressed in a limited character set (currently base32 plus ':' and
4398+# capital letters, but future URIs might use a larger charset).
4399 
4400 BASE32STR_128bits = '(%s{25}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_3bits)
4401 BASE32STR_256bits = '(%s{51}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_1bits)
4402@@ -39,6 +41,10 @@
4403             return self.to_string() != them.to_string()
4404         else:
4405             return True
4406+
4407+    def is_unknown(self):
4408+        return False
4409+
4410     def to_human_encoding(self):
4411         return 'http://127.0.0.1:3456/uri/'+self.to_string()
4412 
4413@@ -97,8 +103,10 @@
4414 
4415     def is_readonly(self):
4416         return True
4417+
4418     def is_mutable(self):
4419         return False
4420+
4421     def get_readonly(self):
4422         return self
4423 
4424@@ -157,6 +165,18 @@
4425                  self.total_shares,
4426                  self.size))
4427 
4428+    def is_readonly(self):
4429+        return True
4430+
4431+    def is_mutable(self):
4432+        return False
4433+
4434+    def get_readonly(self):
4435+        return self
4436+
4437+    def get_verify_cap(self):
4438+        return self
4439+
4440 
4441 class LiteralFileURI(_BaseURI):
4442     implements(IURI, IImmutableFileURI)
4443@@ -297,10 +317,13 @@
4444 
4445     def is_readonly(self):
4446         return True
4447+
4448     def is_mutable(self):
4449         return True
4450+
4451     def get_readonly(self):
4452         return self
4453+
4454     def get_verify_cap(self):
4455         return SSKVerifierURI(self.storage_index, self.fingerprint)
4456 
4457@@ -334,6 +357,15 @@
4458         return 'URI:SSK-Verifier:%s:%s' % (si_b2a(self.storage_index),
4459                                            base32.b2a(self.fingerprint))
4460 
4461+    def is_readonly(self):
4462+        return True
4463+    def is_mutable(self):
4464+        return False
4465+    def get_readonly(self):
4466+        return self
4467+    def get_verify_cap(self):
4468+        return self
4469+
4470 class _DirectoryBaseURI(_BaseURI):
4471     implements(IURI, IDirnodeURI)
4472     def __init__(self, filenode_uri=None):
4473@@ -376,12 +408,12 @@
4474     def abbrev_si(self):
4475         return base32.b2a(self._filenode_uri.storage_index)[:5]
4476 
4477-    def get_filenode_cap(self):
4478-        return self._filenode_uri
4479-
4480     def is_mutable(self):
4481         return True
4482 
4483+    def get_filenode_cap(self):
4484+        return self._filenode_uri
4485+
4486     def get_verify_cap(self):
4487         return DirectoryURIVerifier(self._filenode_uri.get_verify_cap())
4488 
4489@@ -432,12 +464,12 @@
4490             assert isinstance(filenode_uri, self.INNER_URI_CLASS), filenode_uri
4491         _DirectoryBaseURI.__init__(self, filenode_uri)
4492 
4493-    def is_mutable(self):
4494-        return False
4495-
4496     def is_readonly(self):
4497         return True
4498 
4499+    def is_mutable(self):
4500+        return False
4501+
4502     def get_readonly(self):
4503         return self
4504 
4505@@ -460,6 +492,7 @@
4506         # LIT caps have no verifier, since they aren't distributed
4507         return None
4508 
4509+
4510 def wrap_dirnode_cap(filecap):
4511     if isinstance(filecap, WriteableSSKFileURI):
4512         return DirectoryURI(filecap)
4513@@ -469,7 +502,8 @@
4514         return ImmutableDirectoryURI(filecap)
4515     if isinstance(filecap, LiteralFileURI):
4516         return LiteralDirectoryURI(filecap)
4517-    assert False, "cannot wrap a dirnode around %s" % filecap.__class__
4518+    assert False, "cannot interpret as a directory cap: %s" % filecap.__class__
4519+
4520 
4521 class DirectoryURIVerifier(_DirectoryBaseURI):
4522     implements(IVerifierURI)
4523@@ -487,6 +521,10 @@
4524     def get_filenode_cap(self):
4525         return self._filenode_uri
4526 
4527+    def is_mutable(self):
4528+        return False
4529+
4530+
4531 class ImmutableDirectoryURIVerifier(DirectoryURIVerifier):
4532     implements(IVerifierURI)
4533     BASE_STRING='URI:DIR2-CHK-Verifier:'
4534@@ -494,68 +532,133 @@
4535     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK-VERIFIER'+SEP)
4536     INNER_URI_CLASS=CHKFileVerifierURI
4537 
4538+
4539 class UnknownURI:
4540-    def __init__(self, uri):
4541+    def __init__(self, uri, error=None):
4542         self._uri = uri
4543+        self._error = error
4544+
4545     def to_string(self):
4546         return self._uri
4547 
4548-def from_string(s):
4549-    if not isinstance(s, str):
4550-        raise TypeError("unknown URI type: %s.." % str(s)[:100])
4551-    elif s.startswith('URI:CHK:'):
4552+    def get_readonly(self):
4553+        return None
4554+
4555+    def get_error(self):
4556+        return self._error
4557+
4558+
4559+ALLEGED_READONLY_PREFIX = 'ro.'
4560+ALLEGED_IMMUTABLE_PREFIX = 'imm.'
4561+
4562+def from_string(u, deep_immutable=False, name=u"<unknown name>"):
4563+    if not isinstance(u, str):
4564+        raise TypeError("unknown URI type: %s.." % str(u)[:100])
4565+
4566+    # We allow and check ALLEGED_READONLY_PREFIX or ALLEGED_IMMUTABLE_PREFIX
4567+    # on all URIs, even though we would only strictly need to do so for caps of
4568+    # new formats (post Tahoe-LAFS 1.6). URIs that are not consistent with their
4569+    # prefix are treated as unknown. This should be revisited when we add the
4570+    # new cap formats. See <http://allmydata.org/trac/tahoe/ticket/833#comment:31>.
4571+    s = u
4572+    can_be_mutable = can_be_writeable = not deep_immutable
4573+    if s.startswith(ALLEGED_IMMUTABLE_PREFIX):
4574+        can_be_mutable = can_be_writeable = False
4575+        s = s[len(ALLEGED_IMMUTABLE_PREFIX):]
4576+    elif s.startswith(ALLEGED_READONLY_PREFIX):
4577+        can_be_writeable = False
4578+        s = s[len(ALLEGED_READONLY_PREFIX):]
4579+
4580+    error = None
4581+    if s.startswith('URI:CHK:'):
4582         return CHKFileURI.init_from_string(s)
4583     elif s.startswith('URI:CHK-Verifier:'):
4584         return CHKFileVerifierURI.init_from_string(s)
4585     elif s.startswith('URI:LIT:'):
4586         return LiteralFileURI.init_from_string(s)
4587     elif s.startswith('URI:SSK:'):
4588-        return WriteableSSKFileURI.init_from_string(s)
4589+        if can_be_writeable:
4590+            return WriteableSSKFileURI.init_from_string(s)
4591+        error = MustBeReadonlyError("URI:SSK file writecap used in a read-only context",
4592+                                    name)
4593     elif s.startswith('URI:SSK-RO:'):
4594-        return ReadonlySSKFileURI.init_from_string(s)
4595+        if can_be_mutable:
4596+            return ReadonlySSKFileURI.init_from_string(s)
4597+        error = MustBeDeepImmutableError("URI:SSK-RO readcap to a mutable file used in an immutable context",
4598+                                      name)
4599     elif s.startswith('URI:SSK-Verifier:'):
4600         return SSKVerifierURI.init_from_string(s)
4601     elif s.startswith('URI:DIR2:'):
4602-        return DirectoryURI.init_from_string(s)
4603+        if can_be_writeable:
4604+            return DirectoryURI.init_from_string(s)
4605+        error = MustBeReadonlyError("URI:DIR2 directory writecap used in a read-only context",
4606+                                    name)
4607     elif s.startswith('URI:DIR2-RO:'):
4608-        return ReadonlyDirectoryURI.init_from_string(s)
4609+        if can_be_mutable:
4610+            return ReadonlyDirectoryURI.init_from_string(s)
4611+        error = MustBeDeepImmutableError("URI:DIR2-RO readcap to a mutable directory used in an immutable context",
4612+                                         name)
4613     elif s.startswith('URI:DIR2-Verifier:'):
4614         return DirectoryURIVerifier.init_from_string(s)
4615     elif s.startswith('URI:DIR2-CHK:'):
4616         return ImmutableDirectoryURI.init_from_string(s)
4617     elif s.startswith('URI:DIR2-LIT:'):
4618         return LiteralDirectoryURI.init_from_string(s)
4619-    return UnknownURI(s)
4620+    elif s.startswith('x-tahoe-future-test-writeable:') and not can_be_writeable:
4621+        # For testing how future writeable caps would behave in read-only contexts.
4622+        error = MustBeReadonlyError("x-tahoe-future-test-writeable: testing cap used in a read-only context",
4623+                                    name)
4624+    elif s.startswith('x-tahoe-future-test-mutable:') and not can_be_mutable:
4625+        # For testing how future mutable readcaps would behave in immutable contexts.
4626+        error = MustBeDeepImmutableError("x-tahoe-future-test-mutable: testing cap used in an immutable context",
4627+                                      name)
4628+
4629+    #if error: print error
4630+    return UnknownURI(u, error=error)
4631 
4632 def is_uri(s):
4633     try:
4634-        from_string(s)
4635+        from_string(s, deep_immutable=False)
4636         return True
4637     except (TypeError, AssertionError):
4638         return False
4639 
4640-def from_string_dirnode(s):
4641-    u = from_string(s)
4642+def is_literal_file_uri(s):
4643+    if not isinstance(s, str):
4644+        return False
4645+    return (s.startswith('URI:LIT:') or
4646+            s.startswith(ALLEGED_READONLY_PREFIX + 'URI:LIT:') or
4647+            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:'))
4648+
4649+def has_uri_prefix(s):
4650+    if not isinstance(s, str):
4651+        return False
4652+    return (s.startswith("URI:") or
4653+            s.startswith(ALLEGED_READONLY_PREFIX + 'URI:') or
4654+            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:'))
4655+
4656+def from_string_dirnode(s, **kwargs):
4657+    u = from_string(s, **kwargs)
4658     assert IDirnodeURI.providedBy(u)
4659     return u
4660 
4661 registerAdapter(from_string_dirnode, str, IDirnodeURI)
4662 
4663-def from_string_filenode(s):
4664-    u = from_string(s)
4665+def from_string_filenode(s, **kwargs):
4666+    u = from_string(s, **kwargs)
4667     assert IFileURI.providedBy(u)
4668     return u
4669 
4670 registerAdapter(from_string_filenode, str, IFileURI)
4671 
4672-def from_string_mutable_filenode(s):
4673-    u = from_string(s)
4674+def from_string_mutable_filenode(s, **kwargs):
4675+    u = from_string(s, **kwargs)
4676     assert IMutableFileURI.providedBy(u)
4677     return u
4678 registerAdapter(from_string_mutable_filenode, str, IMutableFileURI)
4679 
4680-def from_string_verifier(s):
4681-    u = from_string(s)
4682+def from_string_verifier(s, **kwargs):
4683+    u = from_string(s, **kwargs)
4684     assert IVerifierURI.providedBy(u)
4685     return u
4686 registerAdapter(from_string_verifier, str, IVerifierURI)
4687diff -rN -u old-tahoe/src/allmydata/web/common.py new-tahoe/src/allmydata/web/common.py
4688--- old-tahoe/src/allmydata/web/common.py       2010-01-23 12:59:10.472000000 +0000
4689+++ new-tahoe/src/allmydata/web/common.py       2010-01-23 12:59:12.357000000 +0000
4690@@ -8,7 +8,8 @@
4691 from nevow.util import resource_filename
4692 from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
4693      FileTooLargeError, NotEnoughSharesError, NoSharesError, \
4694-     NotDeepImmutableError, EmptyPathnameComponentError
4695+     EmptyPathnameComponentError, MustBeDeepImmutableError, \
4696+     MustBeReadonlyError, MustNotBeUnknownRWError
4697 from allmydata.mutable.common import UnrecoverableFileError
4698 from allmydata.util import abbreviate # TODO: consolidate
4699 
4700@@ -181,9 +182,42 @@
4701              "failure, or disk corruption. You should perform a filecheck on "
4702              "this object to learn more.")
4703         return (t, http.GONE)
4704-    if f.check(NotDeepImmutableError):
4705-        t = ("NotDeepImmutableError: a mkdir-immutable operation was given "
4706-             "a child that was not itself immutable: %s" % (f.value,))
4707+    if f.check(MustNotBeUnknownRWError):
4708+        name = f.value.args[1]
4709+        immutable = f.value.args[2]
4710+        if immutable:
4711+            t = ("MustNotBeUnknownRWError: an operation to add a child named "
4712+                 "'%s' to a directory was given an unknown cap in a write slot.\n"
4713+                 "If the cap is actually an immutable readcap, then using a "
4714+                 "webapi server that supports a later version of Tahoe may help.\n\n"
4715+                 "If you are using the webapi directly, then specifying an immutable "
4716+                 "readcap in the read slot (ro_uri) of the JSON PROPDICT, and "
4717+                 "omitting the write slot (rw_uri), would also work in this "
4718+                 "case.") % name.encode("utf-8")
4719+        else:
4720+            t = ("MustNotBeUnknownRWError: an operation to add a child named "
4721+                 "'%s' to a directory was given an unknown cap in a write slot.\n"
4722+                 "Using a webapi server that supports a later version of Tahoe "
4723+                 "may help.\n\n"
4724+                 "If you are using the webapi directly, specifying a readcap in "
4725+                 "the read slot (ro_uri) of the JSON PROPDICT, as well as a "
4726+                 "writecap in the write slot if desired, would also work in this "
4727+                 "case.") % name.encode("utf-8")
4728+        return (t, http.BAD_REQUEST)
4729+    if f.check(MustBeDeepImmutableError):
4730+        name = f.value.args[1]
4731+        t = ("MustBeDeepImmutableError: a cap passed to this operation for "
4732+             "the child named '%s', needed to be immutable but was not. Either "
4733+             "the cap is being added to an immutable directory, or it was "
4734+             "originally retrieved from an immutable directory as an unknown "
4735+             "cap." % name.encode("utf-8"))
4736+        return (t, http.BAD_REQUEST)
4737+    if f.check(MustBeReadonlyError):
4738+        name = f.value.args[1]
4739+        t = ("MustBeReadonlyError: a cap passed to this operation for "
4740+             "the child named '%s', needed to be read-only but was not. "
4741+             "The cap is being passed in a read slot (ro_uri), or was retrieved "
4742+             "from a read slot as an unknown cap." % name.encode("utf-8"))
4743         return (t, http.BAD_REQUEST)
4744     if f.check(WebError):
4745         return (f.value.text, f.value.code)
4746diff -rN -u old-tahoe/src/allmydata/web/directory.py new-tahoe/src/allmydata/web/directory.py
4747--- old-tahoe/src/allmydata/web/directory.py    2010-01-23 12:59:10.503000000 +0000
4748+++ new-tahoe/src/allmydata/web/directory.py    2010-01-23 12:59:12.384000000 +0000
4749@@ -351,7 +351,12 @@
4750         charset = get_arg(req, "_charset", "utf-8")
4751         name = name.decode(charset)
4752         replace = boolean_of_arg(get_arg(req, "replace", "true"))
4753-        d = self.node.set_uri(name, childcap, childcap, overwrite=replace)
4754+       
4755+        # We mustn't pass childcap for the readcap argument because we don't
4756+        # know whether it is a read cap. Passing a read cap as the writecap
4757+        # argument will work (it ends up calling NodeMaker.create_from_cap,
4758+        # which derives a readcap if necessary and possible).
4759+        d = self.node.set_uri(name, childcap, None, overwrite=replace)
4760         d.addCallback(lambda res: childcap)
4761         return d
4762 
4763@@ -362,9 +367,9 @@
4764             # won't show up in the resulting encoded form.. the 'name'
4765             # field is completely missing. So to allow deletion of an
4766             # empty file, we have to pretend that None means ''. The only
4767-            # downide of this is a slightly confusing error message if
4768+            # downside of this is a slightly confusing error message if
4769             # someone does a POST without a name= field. For our own HTML
4770-            # thisn't a big deal, because we create the 'delete' POST
4771+            # this isn't a big deal, because we create the 'delete' POST
4772             # buttons ourselves.
4773             name = ''
4774         charset = get_arg(req, "_charset", "utf-8")
4775@@ -584,7 +589,11 @@
4776     def render_title(self, ctx, data):
4777         si_s = abbreviated_dirnode(self.node)
4778         header = ["Tahoe-LAFS - Directory SI=%s" % si_s]
4779-        if self.node.is_readonly():
4780+        if self.node.is_unknown():
4781+            header.append(" (unknown)")
4782+        elif not self.node.is_mutable():
4783+            header.append(" (immutable)")
4784+        elif self.node.is_readonly():
4785             header.append(" (read-only)")
4786         else:
4787             header.append(" (modifiable)")
4788@@ -593,7 +602,11 @@
4789     def render_header(self, ctx, data):
4790         si_s = abbreviated_dirnode(self.node)
4791         header = ["Tahoe-LAFS Directory SI=", T.span(class_="data-chars")[si_s]]
4792-        if self.node.is_readonly():
4793+        if self.node.is_unknown():
4794+            header.append(" (unknown)")
4795+        elif not self.node.is_mutable():
4796+            header.append(" (immutable)")
4797+        elif self.node.is_readonly():
4798             header.append(" (read-only)")
4799         return ctx.tag[header]
4800 
4801@@ -602,7 +615,7 @@
4802         return T.div[T.a(href=link)["Return to Welcome page"]]
4803 
4804     def render_show_readonly(self, ctx, data):
4805-        if self.node.is_readonly():
4806+        if self.node.is_unknown() or self.node.is_readonly():
4807             return ""
4808         rocap = self.node.get_readonly_uri()
4809         root = get_root(ctx)
4810@@ -629,7 +642,7 @@
4811 
4812         root = get_root(ctx)
4813         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
4814-        if self.node.is_readonly():
4815+        if self.node.is_unknown() or self.node.is_readonly():
4816             delete = "-"
4817             rename = "-"
4818         else:
4819@@ -677,8 +690,8 @@
4820         ctx.fillSlots("times", times)
4821 
4822         assert IFilesystemNode.providedBy(target), target
4823-        writecap = target.get_uri() or ""
4824-        quoted_uri = urllib.quote(writecap, safe="") # escape slashes too
4825+        target_uri = target.get_uri() or ""
4826+        quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too
4827 
4828         if IMutableFileNode.providedBy(target):
4829             # to prevent javascript in displayed .html files from stealing a
4830@@ -707,7 +720,7 @@
4831 
4832         elif IDirectoryNode.providedBy(target):
4833             # directory
4834-            uri_link = "%s/uri/%s/" % (root, urllib.quote(writecap))
4835+            uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
4836             ctx.fillSlots("filename",
4837                           T.a(href=uri_link)[html.escape(name)])
4838             if not target.is_mutable():
4839@@ -794,35 +807,30 @@
4840         kids = {}
4841         for name, (childnode, metadata) in children.iteritems():
4842             assert IFilesystemNode.providedBy(childnode), childnode
4843-            rw_uri = childnode.get_uri()
4844+            rw_uri = childnode.get_write_uri()
4845             ro_uri = childnode.get_readonly_uri()
4846             if IFileNode.providedBy(childnode):
4847-                if childnode.is_readonly():
4848-                    rw_uri = None
4849                 kiddata = ("filenode", {'size': childnode.get_size(),
4850                                         'mutable': childnode.is_mutable(),
4851                                         })
4852             elif IDirectoryNode.providedBy(childnode):
4853-                if childnode.is_readonly():
4854-                    rw_uri = None
4855                 kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
4856             else:
4857                 kiddata = ("unknown", {})
4858+
4859             kiddata[1]["metadata"] = metadata
4860-            if ro_uri:
4861-                kiddata[1]["ro_uri"] = ro_uri
4862             if rw_uri:
4863                 kiddata[1]["rw_uri"] = rw_uri
4864+            if ro_uri:
4865+                kiddata[1]["ro_uri"] = ro_uri
4866             verifycap = childnode.get_verify_cap()
4867             if verifycap:
4868                 kiddata[1]['verify_uri'] = verifycap.to_string()
4869+
4870             kids[name] = kiddata
4871-        if dirnode.is_readonly():
4872-            drw_uri = None
4873-            dro_uri = dirnode.get_uri()
4874-        else:
4875-            drw_uri = dirnode.get_uri()
4876-            dro_uri = dirnode.get_readonly_uri()
4877+
4878+        drw_uri = dirnode.get_write_uri()
4879+        dro_uri = dirnode.get_readonly_uri()
4880         contents = { 'children': kids }
4881         if dro_uri:
4882             contents['ro_uri'] = dro_uri
4883@@ -833,13 +841,14 @@
4884             contents['verify_uri'] = verifycap.to_string()
4885         contents['mutable'] = dirnode.is_mutable()
4886         data = ("dirnode", contents)
4887-        return simplejson.dumps(data, indent=1) + "\n"
4888+        json = simplejson.dumps(data, indent=1) + "\n"
4889+        #print json
4890+        return json
4891     d.addCallback(_got)
4892     d.addCallback(text_plain, ctx)
4893     return d
4894 
4895 
4896-
4897 def DirectoryURI(ctx, dirnode):
4898     return text_plain(dirnode.get_uri(), ctx)
4899 
4900diff -rN -u old-tahoe/src/allmydata/web/filenode.py new-tahoe/src/allmydata/web/filenode.py
4901--- old-tahoe/src/allmydata/web/filenode.py     2010-01-23 12:59:10.572000000 +0000
4902+++ new-tahoe/src/allmydata/web/filenode.py     2010-01-23 12:59:12.403000000 +0000
4903@@ -6,10 +6,9 @@
4904 from nevow import url, rend
4905 from nevow.inevow import IRequest
4906 
4907-from allmydata.interfaces import ExistingChildError, CannotPackUnknownNodeError
4908+from allmydata.interfaces import ExistingChildError
4909 from allmydata.monitor import Monitor
4910 from allmydata.immutable.upload import FileHandle
4911-from allmydata.unknown import UnknownNode
4912 from allmydata.util import log, base32
4913 
4914 from allmydata.web.common import text_plain, WebError, RenderMixin, \
4915@@ -20,7 +19,6 @@
4916 from allmydata.web.info import MoreInfo
4917 
4918 class ReplaceMeMixin:
4919-
4920     def replace_me_with_a_child(self, req, client, replace):
4921         # a new file is being uploaded in our place.
4922         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
4923@@ -55,14 +53,7 @@
4924     def replace_me_with_a_childcap(self, req, client, replace):
4925         req.content.seek(0)
4926         childcap = req.content.read()
4927-        childnode = client.create_node_from_uri(childcap, childcap+"readonly")
4928-        if isinstance(childnode, UnknownNode):
4929-            # don't be willing to pack unknown nodes: we might accidentally
4930-            # put some write-authority into the rocap slot because we don't
4931-            # know how to diminish the URI they gave us. We don't even know
4932-            # if they gave us a readcap or a writecap.
4933-            msg = "cannot attach unknown node as child %s" % str(self.name)
4934-            raise CannotPackUnknownNodeError(msg)
4935+        childnode = client.create_node_from_uri(childcap, None, name=self.name)
4936         d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
4937         d.addCallback(lambda res: childnode.get_uri())
4938         return d
4939@@ -426,12 +417,8 @@
4940 
4941 
4942 def FileJSONMetadata(ctx, filenode, edge_metadata):
4943-    if filenode.is_readonly():
4944-        rw_uri = None
4945-        ro_uri = filenode.get_uri()
4946-    else:
4947-        rw_uri = filenode.get_uri()
4948-        ro_uri = filenode.get_readonly_uri()
4949+    rw_uri = filenode.get_write_uri()
4950+    ro_uri = filenode.get_readonly_uri()
4951     data = ("filenode", {})
4952     data[1]['size'] = filenode.get_size()
4953     if ro_uri:
4954diff -rN -u old-tahoe/src/allmydata/web/info.py new-tahoe/src/allmydata/web/info.py
4955--- old-tahoe/src/allmydata/web/info.py 2010-01-23 12:59:10.609000000 +0000
4956+++ new-tahoe/src/allmydata/web/info.py 2010-01-23 12:59:12.419000000 +0000
4957@@ -21,6 +21,8 @@
4958     def get_type(self):
4959         node = self.original
4960         if IDirectoryNode.providedBy(node):
4961+            if not node.is_mutable():
4962+                return "immutable directory"
4963             return "directory"
4964         if IFileNode.providedBy(node):
4965             si = node.get_storage_index()
4966@@ -28,7 +30,7 @@
4967                 if node.is_mutable():
4968                     return "mutable file"
4969                 return "immutable file"
4970-            return "LIT file"
4971+            return "immutable LIT file"
4972         return "unknown"
4973 
4974     def render_title(self, ctx, data):
4975@@ -68,10 +70,10 @@
4976 
4977     def render_directory_writecap(self, ctx, data):
4978         node = self.original
4979-        if node.is_readonly():
4980-            return ""
4981         if not IDirectoryNode.providedBy(node):
4982             return ""
4983+        if node.is_readonly():
4984+            return ""
4985         return ctx.tag[node.get_uri()]
4986 
4987     def render_directory_readcap(self, ctx, data):
4988@@ -86,27 +88,24 @@
4989             return ""
4990         return ctx.tag[node.get_verify_cap().to_string()]
4991 
4992-
4993     def render_file_writecap(self, ctx, data):
4994         node = self.original
4995         if IDirectoryNode.providedBy(node):
4996             node = node._node
4997-        if ((IDirectoryNode.providedBy(node) or IFileNode.providedBy(node))
4998-            and node.is_readonly()):
4999-            return ""
5000-        writecap = node.get_uri()
5001-        if not writecap:
5002+        write_uri = node.get_write_uri()
5003+        #print "write_uri = %r, node = %r" % (write_uri, node)
5004+        if not write_uri:
5005             return ""
5006-        return ctx.tag[writecap]
5007+        return ctx.tag[write_uri]
5008 
5009     def render_file_readcap(self, ctx, data):
5010         node = self.original
5011         if IDirectoryNode.providedBy(node):
5012             node = node._node
5013-        readcap = node.get_readonly_uri()
5014-        if not readcap:
5015+        read_uri = node.get_readonly_uri()
5016+        if not read_uri:
5017             return ""
5018-        return ctx.tag[readcap]
5019+        return ctx.tag[read_uri]
5020 
5021     def render_file_verifycap(self, ctx, data):
5022         node = self.original
5023diff -rN -u old-tahoe/src/allmydata/web/root.py new-tahoe/src/allmydata/web/root.py
5024--- old-tahoe/src/allmydata/web/root.py 2010-01-23 12:59:10.718000000 +0000
5025+++ new-tahoe/src/allmydata/web/root.py 2010-01-23 12:59:12.488000000 +0000
5026@@ -12,7 +12,7 @@
5027 from allmydata import get_package_versions_string
5028 from allmydata import provisioning
5029 from allmydata.util import idlib, log
5030-from allmydata.interfaces import IFileNode, UnhandledCapTypeError
5031+from allmydata.interfaces import IFileNode
5032 from allmydata.web import filenode, directory, unlinked, status, operations
5033 from allmydata.web import reliability, storage
5034 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
5035@@ -85,7 +85,7 @@
5036         try:
5037             node = self.client.create_node_from_uri(name)
5038             return directory.make_handler_for(node, self.client)
5039-        except (TypeError, UnhandledCapTypeError, AssertionError):
5040+        except (TypeError, AssertionError):
5041             raise WebError("'%s' is not a valid file- or directory- cap"
5042                            % name)
5043 
5044@@ -104,7 +104,7 @@
5045         # 'name' must be a file URI
5046         try:
5047             node = self.client.create_node_from_uri(name)
5048-        except (TypeError, UnhandledCapTypeError, AssertionError):
5049+        except (TypeError, AssertionError):
5050             # I think this can no longer be reached
5051             raise WebError("'%s' is not a valid file- or directory- cap"
5052                            % name)
5053
5054diff -rN -u old-tahoe/contrib/fuse/impl_c/blackmatch.py new-tahoe/contrib/fuse/impl_c/blackmatch.py
5055--- old-tahoe/contrib/fuse/impl_c/blackmatch.py 2010-01-23 12:59:10.975000000 +0000
5056+++ new-tahoe/contrib/fuse/impl_c/blackmatch.py 2010-01-23 12:59:12.773000000 +0000
5057@@ -1,7 +1,7 @@
5058 #!/usr/bin/env python
5059 
5060 #-----------------------------------------------------------------------------------------------
5061-from allmydata.uri import CHKFileURI, DirectoryURI, LiteralFileURI
5062+from allmydata.uri import CHKFileURI, DirectoryURI, LiteralFileURI, is_literal_file_uri
5063 from allmydata.scripts.common_http import do_http as do_http_req
5064 from allmydata.util.hashutil import tagged_hash
5065 from allmydata.util.assertutil import precondition
5066@@ -335,7 +335,7 @@
5067                 self.fname = self.tfs.cache.tmp_file(os.urandom(20))
5068                 if self.fnode is None:
5069                     log('TFF: [%s] open() for write: no file node, creating new File %s' % (self.name, self.fname, ))
5070-                    self.fnode = File(0, 'URI:LIT:')
5071+                    self.fnode = File(0, LiteralFileURI.BASE_STRING)
5072                     self.fnode.tmp_fname = self.fname # XXX kill this
5073                     self.parent.add_child(self.name, self.fnode, {})
5074                 elif hasattr(self.fnode, 'tmp_fname'):
5075@@ -362,7 +362,7 @@
5076                     self.fname = self.fnode.tmp_fname
5077                     log('TFF: reopening(%s) for reading' % self.fname)
5078                 else:
5079-                    if uri.startswith("URI:LIT") or not self.tfs.async:
5080+                    if is_literal_file_uri(uri) or not self.tfs.async:
5081                         log('TFF: synchronously fetching file from cache for reading')
5082                         self.fname = self.tfs.cache.get_file(uri)
5083                     else:
5084@@ -906,7 +906,7 @@
5085 
5086 class TStat(fuse.Stat):
5087     # in fuse 0.2, these are set by fuse.Stat.__init__
5088-    # in fuse 0.2-pre3 (hardy) they are not. badness unsues if they're missing
5089+    # in fuse 0.2-pre3 (hardy) they are not. badness ensues if they're missing
5090     st_mode  = None
5091     st_ino   = 0
5092     st_dev   = 0
5093@@ -1237,7 +1237,7 @@
5094 
5095     def get_file(self, uri):
5096         self.log('get_file(%s)' % (uri,))
5097-        if uri.startswith("URI:LIT"):
5098+        if is_literal_file_uri(uri):
5099             return self.get_literal(uri)
5100         else:
5101             return self.get_chk(uri, async=False)