Ticket #833: mutable-in-immutable-darcspatch.txt

File mutable-in-immutable-darcspatch.txt, 236.9 KB (added by davidsarah, at 2010-01-27T07:11:54Z)

Prevent mutable objects from being retrieved from an immutable directory, and associated forward-compatibility improvements

Line 
1Wed Jan 27 06:44:30 GMT Standard Time 2010  david-sarah@jacaranda.org
2  * Prevent mutable objects from being retrieved from an immutable directory, and associated forward-compatibility improvements.
3
4New patches:
5
6[Prevent mutable objects from being retrieved from an immutable directory, and associated forward-compatibility improvements.
7david-sarah@jacaranda.org**20100127064430
8 Ignore-this: 5ef6a3554cf6bef0bf0712cc7d6c0252
9] {
10hunk ./contrib/fuse/impl_c/blackmatch.py 4
11 #!/usr/bin/env python
12 
13 #-----------------------------------------------------------------------------------------------
14-from allmydata.uri import CHKFileURI, DirectoryURI, LiteralFileURI
15+from allmydata.uri import CHKFileURI, DirectoryURI, LiteralFileURI, is_literal_file_uri
16 from allmydata.scripts.common_http import do_http as do_http_req
17 from allmydata.util.hashutil import tagged_hash
18 from allmydata.util.assertutil import precondition
19hunk ./contrib/fuse/impl_c/blackmatch.py 338
20                 self.fname = self.tfs.cache.tmp_file(os.urandom(20))
21                 if self.fnode is None:
22                     log('TFF: [%s] open() for write: no file node, creating new File %s' % (self.name, self.fname, ))
23-                    self.fnode = File(0, 'URI:LIT:')
24+                    self.fnode = File(0, LiteralFileURI.BASE_STRING)
25                     self.fnode.tmp_fname = self.fname # XXX kill this
26                     self.parent.add_child(self.name, self.fnode, {})
27                 elif hasattr(self.fnode, 'tmp_fname'):
28hunk ./contrib/fuse/impl_c/blackmatch.py 365
29                     self.fname = self.fnode.tmp_fname
30                     log('TFF: reopening(%s) for reading' % self.fname)
31                 else:
32-                    if uri.startswith("URI:LIT") or not self.tfs.async:
33+                    if is_literal_file_uri(uri) or not self.tfs.async:
34                         log('TFF: synchronously fetching file from cache for reading')
35                         self.fname = self.tfs.cache.get_file(uri)
36                     else:
37hunk ./contrib/fuse/impl_c/blackmatch.py 1240
38 
39     def get_file(self, uri):
40         self.log('get_file(%s)' % (uri,))
41-        if uri.startswith("URI:LIT"):
42+        if is_literal_file_uri(uri):
43             return self.get_literal(uri)
44         else:
45             return self.get_chk(uri, async=False)
46hunk ./docs/frontends/webapi.txt 153
47 
48 === Child Lookup ===
49 
50-Tahoe directories contain named children, just like directories in a regular
51-local filesystem. These children can be either files or subdirectories.
52+Tahoe directories contain named child entries, just like directories in a regular
53+local filesystem. These child entries, called "dirnodes", consist of a name,
54+metadata, a write slot, and a read slot. The write and read slots normally contain
55+a write-cap and read-cap referring to the same object, which can be either a file
56+or a subdirectory. The write slot may be empty (actually, both may be empty,
57+but that is unusual).
58 
59 If you have a Tahoe URL that refers to a directory, and want to reference a
60 named child inside it, just append the child name to the URL. For example, if
61hunk ./docs/frontends/webapi.txt 397
62           } } } ]
63  }
64 
65+ For forward-compatibility, a mutable directory can also contain caps in
66+ a format that is unknown to the webapi server. When such caps are retrieved
67+ from a mutable directory in a "ro_uri" field, they will be prefixed with
68+ the string "ro.", indicating that they must not be decoded without
69+ checking that they are read-only. The "ro." prefix must not be stripped
70+ off without performing this check. (Future versions of the webapi server
71+ will perform it where necessary.)
72+
73+ If both the "rw_uri" and "ro_uri" fields are present in a given PROPDICT,
74+ and the webapi server recognizes the rw_uri as a write cap, then it will
75+ reset the ro_uri to the corresponding read cap and discard the original
76+ contents of ro_uri (in order to ensure that the two caps correspond to the
77+ same object and that the ro_uri is in fact read-only). However this may not
78+ happen for caps in a format unknown to the webapi server. Therefore, when
79+ writing a directory the webapi client should ensure that the contents
80+ of "rw_uri" and "ro_uri" for a given PROPDICT are a consistent
81+ (write cap, read cap) pair if possible. If the webapi client only has
82+ one cap and does not know whether it is a write cap or read cap, then
83+ it is acceptable to set "rw_uri" to that cap and omit "ro_uri". The
84+ client must not put a write cap into a "ro_uri" field.
85+
86  Note that the webapi-using client application must not provide the
87  "Content-Type: multipart/form-data" header that usually accompanies HTML
88  form submissions, since the body is not formatted this way. Doing so will
89hunk ./docs/frontends/webapi.txt 432
90 
91  Like t=mkdir-with-children above, but the new directory will be
92  deep-immutable. This means that the directory itself is immutable, and that
93- it can only contain deep-immutable objects, like immutable files, literal
94- files, and deep-immutable directories. A non-empty request body is
95- mandatory, since after the directory is created, it will not be possible to
96- add more children to it.
97+ it can only contain objects that are treated as being deep-immutable, like
98+ immutable files, literal files, and deep-immutable directories.
99+
100+ For forward-compatibility, a deep-immutable directory can also contain caps
101+ in a format that is unknown to the webapi server. When such caps are retrieved
102+ from a deep-immutable directory in a "ro_uri" field, they will be prefixed
103+ with the string "imm.", indicating that they must not be decoded without
104+ checking that they are immutable. The "imm." prefix must not be stripped
105+ off without performing this check. (Future versions of the webapi server
106+ will perform it where necessary.)
107+
108+ The cap for each child may be given either in the "rw_uri" or "ro_uri"
109+ field of the PROPDICT (not both). If a cap is given in the "rw_uri" field,
110+ then the webapi server will check that it is an immutable read-cap of a
111+ *known* format, and give an error if it is not. If a cap is given in the
112+ "ro_uri" field, then the webapi server will still check whether known
113+ caps are immutable, but for unknown caps it will simply assume that the
114+ cap can be stored, as described above. Note that an attacker would be
115+ able to store any cap in an immutable directory, so this check when
116+ creating the directory is only to help non-malicious clients to avoid
117+ accidentally giving away more authority than intended.
118+
119+ A non-empty request body is mandatory, since after the directory is created,
120+ it will not be possible to add more children to it.
121 
122 POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir
123 PUT /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir
124hunk ./docs/frontends/webapi.txt 462
125 
126  Create new directories as necessary to make sure that the named target
127  ($DIRCAP/SUBDIRS../SUBDIR) is a directory. This will create additional
128- intermediate directories as necessary. If the named target directory already
129- exists, this will make no changes to it.
130+ intermediate mutable directories as necessary. If the named target directory
131+ already exists, this will make no changes to it.
132 
133  If the final directory is created, it will be empty.
134 
135hunk ./docs/frontends/webapi.txt 467
136- This will return an error if a blocking file is present at any of the parent
137- names, preventing the server from creating the necessary parent directory.
138+ This operation will return an error if a blocking file is present at any of
139+ the parent names, preventing the server from creating the necessary parent
140+ directory; or if it would require changing an immutable directory.
141 
142  The write-cap of the new directory will be returned as the HTTP response
143  body.
144hunk ./docs/frontends/webapi.txt 476
145 
146 POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir-with-children
147 
148- Like above, but if the final directory is created, it will be populated with
149- initial children from the POST request body, as described above in the
150- /uri?t=mkdir-with-children operation.
151+ Like /uri?t=mkdir-with-children, but the final directory is created as a
152+ child of an existing mutable directory. This will create additional
153+ intermediate mutable directories as necessary. If the final directory is
154+ created, it will be populated with initial children from the POST request
155+ body, as described above.
156+
157+ This operation will return an error if a blocking file is present at any of
158+ the parent names, preventing the server from creating the necessary parent
159+ directory; or if it would require changing an immutable directory; or if
160+ the immediate parent directory already has a a child named SUBDIR.
161 
162 POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir-immutable
163 
164hunk ./docs/frontends/webapi.txt 489
165- Like above, but the final directory will be deep-immutable, with the
166- children specified as a JSON dictionary in the POST request body.
167+ Like /uri?t=mkdir-immutable, but the final directory is created as a child
168+ of an existing mutable directory. The final directory will be deep-immutable,
169+ and will be populated with the children specified as a JSON dictionary in
170+ the POST request body.
171+
172+ In Tahoe 1.6 this operation creates intermediate mutable directories if
173+ necessary, but that behaviour should not be relied on; see ticket #920.
174+
175+ This operation will return an error if the parent directory is immutable,
176+ or already has a child named SUBDIR.
177 
178 POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME
179 
180hunk ./docs/frontends/webapi.txt 502
181- Create a new empty directory and attach it to the given existing directory.
182- This will create additional intermediate directories as necessary.
183+ Create a new empty mutable directory and attach it to the given existing
184+ directory. This will create additional intermediate directories as necessary.
185 
186hunk ./docs/frontends/webapi.txt 505
187- The URL of this form points to the parent of the bottom-most new directory,
188- whereas the previous form has a URL that points directly to the bottom-most
189- new directory.
190+ This operation will return an error if a blocking file is present at any of
191+ the parent names, preventing the server from creating the necessary parent
192+ directory, or if it would require changing any immutable directory.
193+
194+ The URL of this operation points to the parent of the bottommost new directory,
195+ whereas the /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir operation above has a URL
196+ that points directly to the bottommost new directory.
197 
198 POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-with-children&name=NAME
199 
200hunk ./docs/frontends/webapi.txt 515
201- As above, but the new directory will be populated with initial children via
202- the POST request body, as described in /uri?t=mkdir-with-children above.
203+ Like /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME, but the new directory will
204+ be populated with initial children via the POST request body. This command
205+ will create additional intermediate mutable directories as necessary.
206+
207+ This operation will return an error if a blocking file is present at any of
208+ the parent names, preventing the server from creating the necessary parent
209+ directory; or if it would require changing an immutable directory; or if
210+ the immediate parent directory already has a a child named NAME.
211+
212  Note that the name= argument must be passed as a queryarg, because the POST
213  request body is used for the initial children JSON.
214 
215hunk ./docs/frontends/webapi.txt 529
216 POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-immutable&name=NAME
217 
218- As above, but the new directory will be deep-immutable, with the children
219- specified as a JSON dictionary in the POST request body. Again, the name=
220- argument must be passed as a queryarg.
221+ Like /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-with-children&name=NAME, but the
222+ final directory will be deep-immutable. The children are specified as a
223+ JSON dictionary in the POST request body. Again, the name= argument must be
224+ passed as a queryarg.
225+
226+ In Tahoe 1.6 this operation creates intermediate mutable directories if
227+ necessary, but that behaviour should not be relied on; see ticket #920.
228+
229+ This operation will return an error if the parent directory is immutable,
230+ or already has a child named NAME.
231 
232 === Get Information About A File Or Directory (as JSON) ===
233 
234hunk ./docs/frontends/webapi.txt 761
235   "childinfo" is a dictionary that contains "rw_uri", "ro_uri", and
236   "metadata" keys. You can take the output of "GET /uri/$DIRCAP1?t=json" and
237   use it as the input to "POST /uri/$DIRCAP2?t=set_children" to make DIR2
238-  look very much like DIR1.
239+  look very much like DIR1 (except for any existing children of DIR2 that
240+  were not overwritten, and any existing "tahoe" metadata keys as described
241+  below).
242 
243   When the set_children request contains a child name that already exists in
244   the target directory, this command defaults to overwriting that child with
245hunk ./docs/frontends/webapi.txt 964
246 
247 POST /uri/$DIRCAP/[SUBDIRS../]?t=upload
248 
249- This uploads a file, and attaches it as a new child of the given directory.
250- The file must be provided as the "file" field of an HTML encoded form body,
251- produced in response to an HTML form like this:
252+ This uploads a file, and attaches it as a new child of the given directory,
253+ which must be mutable. The file must be provided as the "file" field of an
254+ HTML-encoded form body, produced in response to an HTML form like this:
255   <form action="." method="POST" enctype="multipart/form-data">
256    <input type="hidden" name="t" value="upload" />
257    <input type="file" name="file" />
258hunk ./docs/frontends/webapi.txt 1009
259 POST /uri/$DIRCAP/[SUBDIRS../]FILENAME?t=upload
260 
261  This also uploads a file and attaches it as a new child of the given
262- directory. It is a slight variant of the previous operation, as the URL
263- refers to the target file rather than the parent directory. It is otherwise
264- identical: this accepts mutable= and when_done= arguments too.
265+ directory, which must be mutable. It is a slight variant of the previous
266+ operation, as the URL refers to the target file rather than the parent
267+ directory. It is otherwise identical: this accepts mutable= and when_done=
268+ arguments too.
269 
270 POST /uri/$FILECAP?t=upload
271 
272hunk ./docs/frontends/webapi.txt 1040
273 
274 POST /uri/$DIRCAP/[SUBDIRS../]?t=delete&name=CHILDNAME
275 
276- This instructs the node to delete a child object (file or subdirectory) from
277- the given directory. Note that the entire subtree is removed. This is
278- somewhat like "rm -rf" (from the point of view of the parent), but other
279- references into the subtree will see that the child subdirectories are not
280- modified by this operation. Only the link from the given directory to its
281- child is severed.
282+ This instructs the node to remove a child object (file or subdirectory) from
283+ the given directory, which must be mutable. Note that the entire subtree is
284+ unlinked from the parent. Unlike deleting a subdirectory in a UNIX local
285+ filesystem, the subtree need not be empty; if it isn't, then other references
286+ into the subtree will see that the child subdirectories are not modified by
287+ this operation. Only the link from the given directory to its child is severed.
288 
289 === Renaming A Child ===
290 
291hunk ./docs/frontends/webapi.txt 1051
292 POST /uri/$DIRCAP/[SUBDIRS../]?t=rename&from_name=OLD&to_name=NEW
293 
294- This instructs the node to rename a child of the given directory. This is
295- exactly the same as removing the child, then adding the same child-cap under
296- the new name. This operation cannot move the child to a different directory.
297+ This instructs the node to rename a child of the given directory, which must
298+ be mutable. This has a similar effect to removing the child, then adding the
299+ same child-cap under the new name, except that it preserves metadata. This
300+ operation cannot move the child to a different directory.
301 
302  This operation will replace any existing child of the new name, making it
303  behave like the UNIX "mv -f" command.
304hunk ./docs/frontends/webapi.txt 1250
305 
306   "path": a list of strings, with the path that is traversed to reach the
307           object
308-  "cap": a writecap for the file or directory, if available, else a readcap
309-  "verifycap": a verifycap for the file or directory
310-  "repaircap": the weakest cap which can still be used to repair the object
311+  "cap": a write-cap URI for the file or directory, if available, else a
312+         read-cap URI
313+  "verifycap": a verify-cap URI for the file or directory
314+  "repaircap": an URI for the weakest cap that can still be used to repair
315+               the object
316   "storage-index": a base32 storage index for the object
317   "check-results": a copy of the dictionary which would be returned by
318                    t=check&output=json, with three top-level keys:
319hunk ./docs/frontends/webapi.txt 1496
320 
321   "path": a list of strings, with the path that is traversed to reach the
322           object
323-  "cap": a writecap for the file or directory, if available, else a readcap
324-  "verifycap": a verifycap for the file or directory
325-  "repaircap": the weakest cap which can still be used to repair the object
326+  "cap": a write-cap URI for the file or directory, if available, else a
327+         read-cap URI
328+  "verifycap": a verify-cap URI for the file or directory
329+  "repaircap": an URI for the weakest cap that can still be used to repair
330+               the object
331   "storage-index": a base32 storage index for the object
332 
333  Note that non-distributed files (i.e. LIT files) will have values of None
334hunk ./docs/frontends/webapi.txt 1731
335 child's name and the child's URI are included in the results of listing the
336 parent directory, so it isn't any harder to use the URI for this purpose.
337 
338+The read and write caps in a given directory node are separate URIs, and
339+can't be assumed to point to the same object even if they were retrieved in
340+the same operation (although the webapi server attempts to ensure this
341+in most cases). If you need to rely on that property, you should explicitly
342+verify it. More generally, you should not make assumptions about the
343+internal consistency of the contents of mutable directories. As a result
344+of the signatures on mutable object versions, it is guaranteed that a given
345+version was written in a single update, but -- as in the case of a file --
346+the contents may have been chosen by a malicious writer in a way that is
347+designed to confuse applications that rely on their consistency.
348+
349 In general, use names if you want "whatever object (whether file or
350 directory) is found by following this name (or sequence of names) when my
351 request reaches the server". Use URIs if you want "this particular object".
352hunk ./src/allmydata/client.py 474
353     # dirnodes. The first takes a URI and produces a filenode or (new-style)
354     # dirnode. The other three create brand-new filenodes/dirnodes.
355 
356-    def create_node_from_uri(self, writecap, readcap=None):
357-        # this returns synchronously.
358-        return self.nodemaker.create_from_cap(writecap, readcap)
359+    def create_node_from_uri(self, write_uri, read_uri=None, deep_immutable=False, name="<unknown name>"):
360+        # This returns synchronously.
361+        # Note that it does *not* validate the write_uri and read_uri; instead we
362+        # may get an opaque node if there were any problems.
363+        return self.nodemaker.create_from_cap(write_uri, read_uri, deep_immutable=deep_immutable, name=name)
364 
365     def create_dirnode(self, initial_children={}):
366         d = self.nodemaker.create_new_mutable_directory(initial_children)
367hunk ./src/allmydata/client.py 483
368         return d
369+
370     def create_immutable_dirnode(self, children, convergence=None):
371         return self.nodemaker.create_immutable_directory(children, convergence)
372 
373hunk ./src/allmydata/control.py 8
374 from twisted.internet import defer
375 from twisted.internet.interfaces import IConsumer
376 from foolscap.api import Referenceable
377-from allmydata.interfaces import RIControlClient
378+from allmydata.interfaces import RIControlClient, IFileNode
379 from allmydata.util import fileutil, mathutil
380 from allmydata.immutable import upload
381 from twisted.python import log
382hunk ./src/allmydata/control.py 70
383         return d
384 
385     def remote_download_from_uri_to_file(self, uri, filename):
386-        filenode = self.parent.create_node_from_uri(uri)
387+        filenode = self.parent.create_node_from_uri(uri, name=filename)
388+        if not IFileNode.providedBy(filenode):
389+            raise AssertionError("The URI does not reference a file.")
390         c = FileWritingConsumer(filename)
391         d = filenode.read(c)
392         d.addCallback(lambda res: filename)
393hunk ./src/allmydata/control.py 204
394             if i >= self.count:
395                 return
396             n = self.parent.create_node_from_uri(self.uris[i])
397+            if not IFileNode.providedBy(n):
398+                raise AssertionError("The URI does not reference a file.")
399             if n.is_mutable():
400                 d1 = n.download_best_version()
401             else:
402hunk ./src/allmydata/dirnode.py 8
403 from twisted.internet import defer
404 from foolscap.api import fireEventually
405 import simplejson
406-from allmydata.mutable.common import NotMutableError
407+from allmydata.mutable.common import NotWriteableError
408 from allmydata.mutable.filenode import MutableFileNode
409hunk ./src/allmydata/dirnode.py 10
410-from allmydata.unknown import UnknownNode
411+from allmydata.unknown import UnknownNode, strip_prefix_for_ro
412 from allmydata.interfaces import IFilesystemNode, IDirectoryNode, IFileNode, \
413      IImmutableFileNode, IMutableFileNode, \
414      ExistingChildError, NoSuchChildError, ICheckable, IDeepCheckable, \
415hunk ./src/allmydata/dirnode.py 14
416-     CannotPackUnknownNodeError
417+     MustBeDeepImmutableError, CapConstraintError
418 from allmydata.check_results import DeepCheckResults, \
419      DeepCheckAndRepairResults
420 from allmydata.monitor import Monitor
421hunk ./src/allmydata/dirnode.py 43
422         new_contents = self.node._pack_contents(children)
423         return new_contents
424 
425+
426 class MetadataSetter:
427     def __init__(self, node, name, metadata):
428         self.node = node
429hunk ./src/allmydata/dirnode.py 79
430         for (name, (child, new_metadata)) in self.entries.iteritems():
431             precondition(isinstance(name, unicode), name)
432             precondition(IFilesystemNode.providedBy(child), child)
433+
434+            # Strictly speaking this is redundant because we would raise the
435+            # error again in pack_children.
436+            child.raise_error()
437+
438             if name in children:
439                 if not self.overwrite:
440                     raise ExistingChildError("child '%s' already exists" % name)
441hunk ./src/allmydata/dirnode.py 132
442         new_contents = self.node._pack_contents(children)
443         return new_contents
444 
445-def _encrypt_rwcap(filenode, rwcap):
446-    assert isinstance(rwcap, str)
447+def _encrypt_rw_uri(filenode, rw_uri):
448+    assert isinstance(rw_uri, str)
449     writekey = filenode.get_writekey()
450     if not writekey:
451         return ""
452hunk ./src/allmydata/dirnode.py 137
453-    salt = hashutil.mutable_rwcap_salt_hash(rwcap)
454+    salt = hashutil.mutable_rwcap_salt_hash(rw_uri)
455     key = hashutil.mutable_rwcap_key_hash(salt, writekey)
456     cryptor = AES(key)
457hunk ./src/allmydata/dirnode.py 140
458-    crypttext = cryptor.process(rwcap)
459+    crypttext = cryptor.process(rw_uri)
460     mac = hashutil.hmac(key, salt + crypttext)
461     assert len(mac) == 32
462     return salt + crypttext + mac
463hunk ./src/allmydata/dirnode.py 147
464     # The MAC is not checked by readers in Tahoe >= 1.3.0, but we still
465     # produce it for the sake of older readers.
466 
467-class MustBeDeepImmutable(Exception):
468-    """You tried to add a non-deep-immutable node to a deep-immutable
469-    directory."""
470-
471 def pack_children(filenode, children, deep_immutable=False):
472     """Take a dict that maps:
473          children[unicode_name] = (IFileSystemNode, metadata_dict)
474hunk ./src/allmydata/dirnode.py 157
475     time.
476 
477     If deep_immutable is True, I will require that all my children are deeply
478-    immutable, and will raise a MustBeDeepImmutable exception if not.
479+    immutable, and will raise a MustBeDeepImmutableError if not.
480     """
481 
482     has_aux = isinstance(children, AuxValueDict)
483hunk ./src/allmydata/dirnode.py 166
484         assert isinstance(name, unicode)
485         entry = None
486         (child, metadata) = children[name]
487-        if deep_immutable and child.is_mutable():
488-            # TODO: consider adding IFileSystemNode.is_deep_immutable()
489-            raise MustBeDeepImmutable("child '%s' is mutable" % (name,))
490+        child.raise_error()
491+        if deep_immutable and not child.is_allowed_in_immutable_directory():
492+            raise MustBeDeepImmutableError("child '%s' is not allowed in an immutable directory" % (name,), name)
493         if has_aux:
494             entry = children.get_aux(name)
495         if not entry:
496hunk ./src/allmydata/dirnode.py 174
497             assert IFilesystemNode.providedBy(child), (name,child)
498             assert isinstance(metadata, dict)
499-            rwcap = child.get_uri() # might be RO if the child is not writeable
500-            if rwcap is None:
501-                rwcap = ""
502-            assert isinstance(rwcap, str), rwcap
503-            rocap = child.get_readonly_uri()
504-            if rocap is None:
505-                rocap = ""
506-            assert isinstance(rocap, str), rocap
507+            rw_uri = child.get_write_uri()
508+            if rw_uri is None:
509+                rw_uri = ""
510+            assert isinstance(rw_uri, str), rw_uri
511+           
512+            # should be prevented by MustBeDeepImmutableError check above
513+            assert not (rw_uri and deep_immutable)
514+
515+            ro_uri = child.get_readonly_uri()
516+            if ro_uri is None:
517+                ro_uri = ""
518+            assert isinstance(ro_uri, str), ro_uri
519             entry = "".join([netstring(name.encode("utf-8")),
520hunk ./src/allmydata/dirnode.py 187
521-                             netstring(rocap),
522-                             netstring(_encrypt_rwcap(filenode, rwcap)),
523+                             netstring(strip_prefix_for_ro(ro_uri, deep_immutable)),
524+                             netstring(_encrypt_rw_uri(filenode, rw_uri)),
525                              netstring(simplejson.dumps(metadata))])
526         entries.append(netstring(entry))
527     return "".join(entries)
528hunk ./src/allmydata/dirnode.py 239
529         plaintext = cryptor.process(crypttext)
530         return plaintext
531 
532-    def _create_node(self, rwcap, rocap):
533-        return self._nodemaker.create_from_cap(rwcap, rocap)
534+    def _create_and_validate_node(self, rw_uri, ro_uri, name):
535+        node = self._nodemaker.create_from_cap(rw_uri, ro_uri,
536+                                               deep_immutable=not self.is_mutable(),
537+                                               name=name)
538+        node.raise_error()
539+        return node
540 
541     def _unpack_contents(self, data):
542         # the directory is serialized as a list of netstrings, one per child.
543hunk ./src/allmydata/dirnode.py 248
544-        # Each child is serialized as a list of four netstrings: (name,
545-        # rocap, rwcap, metadata), in which the name,rocap,metadata are in
546-        # cleartext. The 'name' is UTF-8 encoded. The rwcap is formatted as:
547-        # pack("16ss32s", iv, AES(H(writekey+iv), plaintextrwcap), mac)
548+        # Each child is serialized as a list of four netstrings: (name, ro_uri,
549+        # rwcapdata, metadata), in which the name, ro_uri, metadata are in
550+        # cleartext. The 'name' is UTF-8 encoded. The rwcapdata is formatted as:
551+        # pack("16ss32s", iv, AES(H(writekey+iv), plaintext_rw_uri), mac)
552         assert isinstance(data, str), (repr(data), type(data))
553         # an empty directory is serialized as an empty string
554         if data == "":
555hunk ./src/allmydata/dirnode.py 257
556             return AuxValueDict()
557         writeable = not self.is_readonly()
558+        mutable = self.is_mutable()
559         children = AuxValueDict()
560         position = 0
561         while position < len(data):
562hunk ./src/allmydata/dirnode.py 263
563             entries, position = split_netstring(data, 1, position)
564             entry = entries[0]
565-            (name, rocap, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
566+            (name, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
567+            if not mutable and len(rwcapdata) > 0:
568+                raise ValueError("the rwcapdata field of a dirnode in an immutable directory was not empty")
569             name = name.decode("utf-8")
570hunk ./src/allmydata/dirnode.py 267
571-            rwcap = None
572+            rw_uri = ""
573             if writeable:
574hunk ./src/allmydata/dirnode.py 269
575-                rwcap = self._decrypt_rwcapdata(rwcapdata)
576-            if not rwcap:
577-                rwcap = None # rwcap is None or a non-empty string
578-            if not rocap:
579-                rocap = None # rocap is None or a non-empty string
580-            child = self._create_node(rwcap, rocap)
581-            metadata = simplejson.loads(metadata_s)
582-            assert isinstance(metadata, dict)
583-            children.set_with_aux(name, (child, metadata), auxilliary=entry)
584+                rw_uri = self._decrypt_rwcapdata(rwcapdata)
585+
586+            # Since the encryption uses CTR mode, it currently leaks the length of the
587+            # plaintext rw_uri -- and therefore whether it is present, i.e. whether the
588+            # dirnode is writeable (ticket #925). By stripping spaces in Tahoe >= 1.6.0,
589+            # we may make it easier for future versions to plug this leak.
590+            # ro_uri is treated in the same way for consistency.
591+            # rw_uri and ro_uri will be either None or a non-empty string.
592+
593+            rw_uri = rw_uri.strip(' ') or None
594+            ro_uri = ro_uri.strip(' ') or None
595+
596+            try:
597+                child = self._create_and_validate_node(rw_uri, ro_uri, name)
598+                if mutable or child.is_allowed_in_immutable_directory():
599+                    metadata = simplejson.loads(metadata_s)
600+                    assert isinstance(metadata, dict)
601+                    children[name] = (child, metadata)
602+                    children.set_with_aux(name, (child, metadata), auxilliary=entry)
603+                else:
604+                    log.msg(format="mutable cap for child '%(name)s' unpacked from an immutable directory",
605+                                   name=name.encode("utf-8"),
606+                                   facility="tahoe.webish", level=log.UNUSUAL)
607+            except CapConstraintError, e:
608+                log.msg(format="unmet constraint on cap for child '%(name)s' unpacked from a directory:\n"
609+                               "%(message)s", message=e.args[0], name=name.encode("utf-8"),
610+                               facility="tahoe.webish", level=log.UNUSUAL)
611+
612         return children
613 
614     def _pack_contents(self, children):
615hunk ./src/allmydata/dirnode.py 305
616 
617     def is_readonly(self):
618         return self._node.is_readonly()
619+
620     def is_mutable(self):
621         return self._node.is_mutable()
622 
623hunk ./src/allmydata/dirnode.py 309
624+    def is_unknown(self):
625+        return False
626+
627+    def is_allowed_in_immutable_directory(self):
628+        return not self._node.is_mutable()
629+
630+    def raise_error(self):
631+        pass
632+
633     def get_uri(self):
634         return self._uri.to_string()
635 
636hunk ./src/allmydata/dirnode.py 321
637+    def get_write_uri(self):
638+        if self.is_readonly():
639+            return None
640+        return self._uri.to_string()
641+
642     def get_readonly_uri(self):
643         return self._uri.get_readonly().to_string()
644 
645hunk ./src/allmydata/dirnode.py 331
646     def get_cap(self):
647         return self._uri
648+
649     def get_readcap(self):
650         return self._uri.get_readonly()
651hunk ./src/allmydata/dirnode.py 334
652+
653     def get_verify_cap(self):
654         return self._uri.get_verify_cap()
655hunk ./src/allmydata/dirnode.py 337
656+
657     def get_repair_cap(self):
658         if self._node.is_readonly():
659             return None # readonly (mutable) dirnodes are not yet repairable
660hunk ./src/allmydata/dirnode.py 403
661     def set_metadata_for(self, name, metadata):
662         assert isinstance(name, unicode)
663         if self.is_readonly():
664-            return defer.fail(NotMutableError())
665+            return defer.fail(NotWriteableError())
666         assert isinstance(metadata, dict)
667         s = MetadataSetter(self, name, metadata)
668         d = self._node.modify(s.modify)
669hunk ./src/allmydata/dirnode.py 451
670         precondition(isinstance(name, unicode), name)
671         precondition(isinstance(writecap, (str,type(None))), writecap)
672         precondition(isinstance(readcap, (str,type(None))), readcap)
673-        child_node = self._create_node(writecap, readcap)
674-        if isinstance(child_node, UnknownNode):
675-            # don't be willing to pack unknown nodes: we might accidentally
676-            # put some write-authority into the rocap slot because we don't
677-            # know how to diminish the URI they gave us. We don't even know
678-            # if they gave us a readcap or a writecap.
679-            msg = "cannot pack unknown node as child %s" % str(name)
680-            raise CannotPackUnknownNodeError(msg)
681+           
682+        # We now allow packing unknown nodes, provided they are valid
683+        # for this type of directory.
684+        child_node = self._create_and_validate_node(writecap, readcap, name)
685         d = self.set_node(name, child_node, metadata, overwrite)
686         d.addCallback(lambda res: child_node)
687         return d
688hunk ./src/allmydata/dirnode.py 472
689                 writecap, readcap, metadata = e
690             precondition(isinstance(writecap, (str,type(None))), writecap)
691             precondition(isinstance(readcap, (str,type(None))), readcap)
692-            child_node = self._create_node(writecap, readcap)
693-            if isinstance(child_node, UnknownNode):
694-                msg = "cannot pack unknown node as child %s" % str(name)
695-                raise CannotPackUnknownNodeError(msg)
696+           
697+            # We now allow packing unknown nodes, provided they are valid
698+            # for this type of directory.
699+            child_node = self._create_and_validate_node(writecap, readcap, name)
700             a.set_node(name, child_node, metadata)
701         d = self._node.modify(a.modify)
702         d.addCallback(lambda ign: self)
703hunk ./src/allmydata/dirnode.py 488
704         same name.
705 
706         If this directory node is read-only, the Deferred will errback with a
707-        NotMutableError."""
708+        NotWriteableError."""
709 
710         precondition(IFilesystemNode.providedBy(child), child)
711 
712hunk ./src/allmydata/dirnode.py 493
713         if self.is_readonly():
714-            return defer.fail(NotMutableError())
715+            return defer.fail(NotWriteableError())
716         assert isinstance(name, unicode)
717         assert IFilesystemNode.providedBy(child), child
718         a = Adder(self, overwrite=overwrite)
719hunk ./src/allmydata/dirnode.py 505
720     def set_nodes(self, entries, overwrite=True):
721         precondition(isinstance(entries, dict), entries)
722         if self.is_readonly():
723-            return defer.fail(NotMutableError())
724+            return defer.fail(NotWriteableError())
725         a = Adder(self, entries, overwrite=overwrite)
726         d = self._node.modify(a.modify)
727         d.addCallback(lambda res: self)
728hunk ./src/allmydata/dirnode.py 519
729         the operation completes."""
730         assert isinstance(name, unicode)
731         if self.is_readonly():
732-            return defer.fail(NotMutableError())
733+            return defer.fail(NotWriteableError())
734         d = self._uploader.upload(uploadable)
735hunk ./src/allmydata/dirnode.py 521
736-        d.addCallback(lambda results: results.uri)
737-        d.addCallback(self._nodemaker.create_from_cap)
738+        d.addCallback(lambda results:
739+                      self._create_and_validate_node(results.uri, None, name))
740         d.addCallback(lambda node:
741                       self.set_node(name, node, metadata, overwrite))
742         return d
743hunk ./src/allmydata/dirnode.py 532
744         fires (with the node just removed) when the operation finishes."""
745         assert isinstance(name, unicode)
746         if self.is_readonly():
747-            return defer.fail(NotMutableError())
748+            return defer.fail(NotWriteableError())
749         deleter = Deleter(self, name)
750         d = self._node.modify(deleter.modify)
751         d.addCallback(lambda res: deleter.old_child)
752hunk ./src/allmydata/dirnode.py 542
753                             mutable=True):
754         assert isinstance(name, unicode)
755         if self.is_readonly():
756-            return defer.fail(NotMutableError())
757+            return defer.fail(NotWriteableError())
758         if mutable:
759             d = self._nodemaker.create_new_mutable_directory(initial_children)
760         else:
761hunk ./src/allmydata/dirnode.py 564
762         Deferred that fires when the operation finishes."""
763         assert isinstance(current_child_name, unicode)
764         if self.is_readonly() or new_parent.is_readonly():
765-            return defer.fail(NotMutableError())
766+            return defer.fail(NotWriteableError())
767         if new_child_name is None:
768             new_child_name = current_child_name
769         assert isinstance(new_child_name, unicode)
770hunk ./src/allmydata/immutable/filenode.py 20
771 class _ImmutableFileNodeBase(object):
772     implements(IImmutableFileNode, ICheckable)
773 
774+    def get_write_uri(self):
775+        return None
776+
777     def get_readonly_uri(self):
778         return self.get_uri()
779 
780hunk ./src/allmydata/immutable/filenode.py 32
781     def is_readonly(self):
782         return True
783 
784+    def is_unknown(self):
785+        return False
786+
787+    def is_allowed_in_immutable_directory(self):
788+        return True
789+
790+    def raise_error(self):
791+        pass
792+
793     def __hash__(self):
794         return self.u.__hash__()
795     def __eq__(self, other):
796hunk ./src/allmydata/interfaces.py 459
797 class IDirnodeURI(Interface):
798     """I am a URI which represents a dirnode."""
799 
800-
801 class IFileURI(Interface):
802     """I am a URI which represents a filenode."""
803     def get_size():
804hunk ./src/allmydata/interfaces.py 469
805 
806 class IMutableFileURI(Interface):
807     """I am a URI which represents a mutable filenode."""
808+
809 class IDirectoryURI(Interface):
810     pass
811hunk ./src/allmydata/interfaces.py 472
812+
813 class IReadonlyDirectoryURI(Interface):
814     pass
815 
816hunk ./src/allmydata/interfaces.py 476
817-class CannotPackUnknownNodeError(Exception):
818-    """UnknownNodes (using filecaps from the future that we don't understand)
819-    cannot yet be copied safely, so I refuse to copy them."""
820+class CapConstraintError(Exception):
821+    """A constraint on a cap was violated."""
822 
823hunk ./src/allmydata/interfaces.py 479
824-class UnhandledCapTypeError(Exception):
825-    """I recognize the cap/URI, but I cannot create an IFilesystemNode for
826-    it."""
827+class MustBeDeepImmutableError(CapConstraintError):
828+    """Mutable children cannot be added to an immutable directory.
829+    Also, caps obtained from an immutable directory can trigger this error
830+    if they are later found to refer to a mutable object and then used."""
831 
832hunk ./src/allmydata/interfaces.py 484
833-class NotDeepImmutableError(Exception):
834-    """Deep-immutable directories can only contain deep-immutable children"""
835+class MustBeReadonlyError(CapConstraintError):
836+    """Known write caps cannot be specified in a ro_uri field. Also,
837+    caps obtained from a ro_uri field can trigger this error if they
838+    are later found to be write caps and then used."""
839+
840+class MustNotBeUnknownRWError(CapConstraintError):
841+    """Cannot add an unknown child cap specified in a rw_uri field."""
842 
843 # The hierarchy looks like this:
844 #  IFilesystemNode
845hunk ./src/allmydata/interfaces.py 527
846         """
847 
848     def get_uri():
849-        """
850-        Return the URI string that can be used by others to get access to
851-        this node. If this node is read-only, the URI will only offer
852+        """Return the URI string corresponding to the strongest cap associated
853+        with this node. If this node is read-only, the URI will only offer
854         read-only access. If this node is read-write, the URI will offer
855         read-write access.
856 
857hunk ./src/allmydata/interfaces.py 536
858         read-only access with others, use get_readonly_uri().
859         """
860 
861+    def get_write_uri(n):
862+        """Return the URI string that can be used by others to get write
863+        access to this node, if it is writeable. If this is a read-only node,
864+        return None."""
865+
866     def get_readonly_uri():
867         """Return the URI string that can be used by others to get read-only
868         access to this node. The result is a read-only URI, regardless of
869hunk ./src/allmydata/interfaces.py 570
870         file.
871         """
872 
873+    def is_unknown():
874+        """Return True if this is an unknown node."""
875+
876+    def is_allowed_in_immutable_directory():
877+        """Return True if this node is allowed as a child of a deep-immutable
878+        directory. This is true if either the node is of a known-immutable type,
879+        or it is unknown and read-only.
880+        """
881+
882+    def raise_error():
883+        """Raise any error associated with this node."""
884+
885     def get_size():
886         """Return the length (in bytes) of the data this node represents. For
887         directory nodes, I return the size of the backing store. I return
888hunk ./src/allmydata/interfaces.py 927
889         ctime/mtime semantics of traditional filesystems.
890 
891         If this directory node is read-only, the Deferred will errback with a
892-        NotMutableError."""
893+        NotWriteableError."""
894 
895     def set_children(entries, overwrite=True):
896         """Add multiple children (by writecap+readcap) to a directory node.
897hunk ./src/allmydata/interfaces.py 953
898         ctime/mtime semantics of traditional filesystems.
899 
900         If this directory node is read-only, the Deferred will errback with a
901-        NotMutableError."""
902+        NotWriteableError."""
903 
904     def set_nodes(entries, overwrite=True):
905         """Add multiple children to a directory node. Takes a dict mapping
906hunk ./src/allmydata/interfaces.py 2099
907     Tahoe process will typically have a single NodeMaker, but unit tests may
908     create simplified/mocked forms for testing purposes.
909     """
910-    def create_from_cap(writecap, readcap=None):
911+    def create_from_cap(writecap, readcap=None, **kwargs):
912         """I create an IFilesystemNode from the given writecap/readcap. I can
913         only provide nodes for existing file/directory objects: use my other
914         methods to create new objects. I return synchronously."""
915hunk ./src/allmydata/mutable/common.py 11
916                           # creation
917 MODE_READ = "MODE_READ"
918 
919-class NotMutableError(Exception):
920+class NotWriteableError(Exception):
921     pass
922 
923 class NeedMoreDataError(Exception):
924hunk ./src/allmydata/mutable/filenode.py 217
925 
926     def get_uri(self):
927         return self._uri.to_string()
928+
929+    def get_write_uri(self):
930+        if self.is_readonly():
931+            return None
932+        return self._uri.to_string()
933+
934     def get_readonly_uri(self):
935         return self._uri.get_readonly().to_string()
936 
937hunk ./src/allmydata/mutable/filenode.py 236
938 
939     def is_mutable(self):
940         return self._uri.is_mutable()
941+
942     def is_readonly(self):
943         return self._uri.is_readonly()
944 
945hunk ./src/allmydata/mutable/filenode.py 240
946+    def is_unknown(self):
947+        return False
948+
949+    def is_allowed_in_immutable_directory(self):
950+        return not self._uri.is_mutable()
951+
952+    def raise_error(self):
953+        pass
954+
955     def __hash__(self):
956         return hash((self.__class__, self._uri))
957     def __cmp__(self, them):
958hunk ./src/allmydata/nodemaker.py 4
959 import weakref
960 from zope.interface import implements
961 from allmydata.util.assertutil import precondition
962-from allmydata.interfaces import INodeMaker, NotDeepImmutableError
963+from allmydata.interfaces import INodeMaker, MustBeDeepImmutableError
964 from allmydata.immutable.filenode import ImmutableFileNode, LiteralFileNode
965 from allmydata.immutable.upload import Data
966 from allmydata.mutable.filenode import MutableFileNode
967hunk ./src/allmydata/nodemaker.py 47
968     def _create_dirnode(self, filenode):
969         return DirectoryNode(filenode, self, self.uploader)
970 
971-    def create_from_cap(self, writecap, readcap=None):
972+    def create_from_cap(self, writecap, readcap=None, deep_immutable=False, name=u"<unknown name>"):
973         # this returns synchronously. It starts with a "cap string".
974         assert isinstance(writecap, (str, type(None))), type(writecap)
975         assert isinstance(readcap,  (str, type(None))), type(readcap)
976hunk ./src/allmydata/nodemaker.py 51
977+       
978         bigcap = writecap or readcap
979         if not bigcap:
980             # maybe the writecap was hidden because we're in a readonly
981hunk ./src/allmydata/nodemaker.py 57
982             # directory, and the future cap format doesn't have a readcap, or
983             # something.
984-            return UnknownNode(writecap, readcap)
985-        if bigcap in self._node_cache:
986-            return self._node_cache[bigcap]
987-        cap = uri.from_string(bigcap)
988-        node = self._create_from_cap(cap)
989+            return UnknownNode(None, None)  # deep_immutable and name not needed
990+
991+        # The name doesn't matter for caching since it's only used in the error
992+        # attribute of an UnknownNode, and we don't cache those.
993+        memokey = ("I" if deep_immutable else "M") + bigcap
994+        if memokey in self._node_cache:
995+            return self._node_cache[memokey]
996+        cap = uri.from_string(bigcap, deep_immutable=deep_immutable, name=name)
997+        node = self._create_from_single_cap(cap)
998         if node:
999hunk ./src/allmydata/nodemaker.py 67
1000-            self._node_cache[bigcap] = node  # note: WeakValueDictionary
1001+            self._node_cache[memokey] = node  # note: WeakValueDictionary
1002         else:
1003hunk ./src/allmydata/nodemaker.py 69
1004-            node = UnknownNode(writecap, readcap) # don't cache UnknownNode
1005+            # don't cache UnknownNode
1006+            node = UnknownNode(writecap, readcap, deep_immutable=deep_immutable, name=name)
1007         return node
1008 
1009hunk ./src/allmydata/nodemaker.py 73
1010-    def _create_from_cap(self, cap):
1011-        # This starts with a "cap instance"
1012+    def _create_from_single_cap(self, cap):
1013         if isinstance(cap, uri.LiteralFileURI):
1014             return self._create_lit(cap)
1015         if isinstance(cap, uri.CHKFileURI):
1016hunk ./src/allmydata/nodemaker.py 84
1017                             uri.ReadonlyDirectoryURI,
1018                             uri.ImmutableDirectoryURI,
1019                             uri.LiteralDirectoryURI)):
1020-            filenode = self._create_from_cap(cap.get_filenode_cap())
1021+            filenode = self._create_from_single_cap(cap.get_filenode_cap())
1022             return self._create_dirnode(filenode)
1023         return None
1024 
1025hunk ./src/allmydata/nodemaker.py 97
1026         return d
1027 
1028     def create_new_mutable_directory(self, initial_children={}):
1029-        # initial_children must have metadata (i.e. {} instead of None), and
1030-        # should not contain UnknownNodes
1031+        # initial_children must have metadata (i.e. {} instead of None)
1032         for (name, (node, metadata)) in initial_children.iteritems():
1033hunk ./src/allmydata/nodemaker.py 99
1034-            precondition(not isinstance(node, UnknownNode),
1035-                         "create_new_mutable_directory does not accept UnknownNode", node)
1036             precondition(isinstance(metadata, dict),
1037                          "create_new_mutable_directory requires metadata to be a dict, not None", metadata)
1038hunk ./src/allmydata/nodemaker.py 101
1039+            node.raise_error()
1040         d = self.create_mutable_file(lambda n:
1041                                      pack_children(n, initial_children))
1042         d.addCallback(self._create_dirnode)
1043hunk ./src/allmydata/nodemaker.py 111
1044         if convergence is None:
1045             convergence = self.secret_holder.get_convergence_secret()
1046         for (name, (node, metadata)) in children.iteritems():
1047-            precondition(not isinstance(node, UnknownNode),
1048-                         "create_immutable_directory does not accept UnknownNode", node)
1049             precondition(isinstance(metadata, dict),
1050                          "create_immutable_directory requires metadata to be a dict, not None", metadata)
1051hunk ./src/allmydata/nodemaker.py 113
1052-            if node.is_mutable():
1053-                raise NotDeepImmutableError("%s is not immutable" % (node,))
1054+            node.raise_error()
1055+            if not node.is_allowed_in_immutable_directory():
1056+                raise MustBeDeepImmutableError("%s is not immutable" % (node,), name)
1057         n = DummyImmutableFileNode() # writekey=None
1058         packed = pack_children(n, children)
1059         uploadable = Data(packed, convergence)
1060hunk ./src/allmydata/nodemaker.py 120
1061         d = self.uploader.upload(uploadable, history=self.history)
1062-        def _uploaded(results):
1063-            filecap = self.create_from_cap(results.uri)
1064-            return filecap
1065-        d.addCallback(_uploaded)
1066+        d.addCallback(lambda results: self.create_from_cap(None, results.uri))
1067         d.addCallback(self._create_dirnode)
1068         return d
1069hunk ./src/allmydata/scripts/common.py 131
1070     pass
1071 
1072 def get_alias(aliases, path, default):
1073+    from allmydata import uri
1074     # transform "work:path/filename" into (aliases["work"], "path/filename").
1075     # If default=None, then an empty alias is indicated by returning
1076hunk ./src/allmydata/scripts/common.py 134
1077-    # DefaultAliasMarker. We special-case "URI:" to make it easy to access
1078-    # specific files/directories by their read-cap.
1079+    # DefaultAliasMarker. We special-case strings with a recognized cap URI
1080+    # prefix, to make it easy to access specific files/directories by their
1081+    # caps.
1082     path = path.strip()
1083hunk ./src/allmydata/scripts/common.py 138
1084-    if path.startswith("URI:"):
1085+    if uri.has_uri_prefix(path):
1086         # The only way to get a sub-path is to use URI:blah:./foo, and we
1087         # strip out the :./ sequence.
1088         sep = path.find(":./")
1089hunk ./src/allmydata/scripts/tahoe_cp.py 261
1090                 readcap = ascii_or_none(data[1].get("ro_uri"))
1091                 self.children[name] = TahoeFileSource(self.nodeurl, mutable,
1092                                                       writecap, readcap)
1093-            else:
1094-                assert data[0] == "dirnode"
1095+            elif data[0] == "dirnode":
1096                 writecap = ascii_or_none(data[1].get("rw_uri"))
1097                 readcap = ascii_or_none(data[1].get("ro_uri"))
1098                 if writecap and writecap in self.cache:
1099hunk ./src/allmydata/scripts/tahoe_cp.py 279
1100                     if recurse:
1101                         child.populate(True)
1102                 self.children[name] = child
1103+            else:
1104+                # TODO: there should be an option to skip unknown nodes.
1105+                raise TahoeError("Cannot copy unknown nodes (ticket #839). "
1106+                                 "You probably need to use a later version of "
1107+                                 "Tahoe-LAFS to copy this directory.")
1108 
1109 class TahoeMissingTarget:
1110     def __init__(self, url):
1111hunk ./src/allmydata/scripts/tahoe_cp.py 360
1112                                                    urllib.quote(name.encode('utf-8'))])
1113                 self.children[name] = TahoeFileTarget(self.nodeurl, mutable,
1114                                                       writecap, readcap, url)
1115-            else:
1116-                assert data[0] == "dirnode"
1117+            elif data[0] == "dirnode":
1118                 writecap = ascii_or_none(data[1].get("rw_uri"))
1119                 readcap = ascii_or_none(data[1].get("ro_uri"))
1120                 if writecap and writecap in self.cache:
1121hunk ./src/allmydata/scripts/tahoe_cp.py 378
1122                     if recurse:
1123                         child.populate(True)
1124                 self.children[name] = child
1125+            else:
1126+                # TODO: there should be an option to skip unknown nodes.
1127+                raise TahoeError("Cannot copy unknown nodes (ticket #839). "
1128+                                 "You probably need to use a later version of "
1129+                                 "Tahoe-LAFS to copy this directory.")
1130 
1131     def get_child_target(self, name):
1132         # return a new target for a named subdirectory of this dir
1133hunk ./src/allmydata/scripts/tahoe_cp.py 418
1134         set_data = {}
1135         for (name, filecap) in self.new_children.items():
1136             # it just so happens that ?t=set_children will accept both file
1137-            # read-caps and write-caps as ['rw_uri'], and will handle eithe
1138+            # read-caps and write-caps as ['rw_uri'], and will handle either
1139             # correctly. So don't bother trying to figure out whether the one
1140             # we have is read-only or read-write.
1141hunk ./src/allmydata/scripts/tahoe_cp.py 421
1142+            # TODO: think about how this affects forward-compatibility for
1143+            # unknown caps
1144             set_data[name] = ["filenode", {"rw_uri": filecap}]
1145         body = simplejson.dumps(set_data)
1146         POST(url, body)
1147hunk ./src/allmydata/scripts/tahoe_cp.py 783
1148 #  local-file-in-the-way
1149 #   touch proposed
1150 #   tahoe cp -r my:docs/proposed/denver.txt proposed/denver.txt
1151+#  handling of unknown nodes
1152 
1153 # things that maybe should be errors but aren't
1154 #  local-dir-in-the-way
1155hunk ./src/allmydata/scripts/tahoe_put.py 43
1156         #  DIRCAP:./subdir/foo : DIRCAP/subdir/foo
1157         #  MUTABLE-FILE-WRITECAP : filecap
1158 
1159+        # FIXME: this shouldn't rely on a particular prefix.
1160         if to_file.startswith("URI:SSK:"):
1161             url = nodeurl + "uri/%s" % urllib.quote(to_file)
1162         else:
1163hunk ./src/allmydata/test/common.py 54
1164 
1165     def get_uri(self):
1166         return self.my_uri.to_string()
1167+    def get_write_uri(self):
1168+        return None
1169     def get_readonly_uri(self):
1170         return self.my_uri.to_string()
1171     def get_cap(self):
1172hunk ./src/allmydata/test/common.py 108
1173         return False
1174     def is_readonly(self):
1175         return True
1176+    def is_unknown(self):
1177+        return False
1178+    def is_allowed_in_immutable_directory(self):
1179+        return True
1180+    def raise_error(self):
1181+        pass
1182 
1183     def get_size(self):
1184         try:
1185hunk ./src/allmydata/test/common.py 201
1186         return self.my_uri.get_readonly()
1187     def get_uri(self):
1188         return self.my_uri.to_string()
1189+    def get_write_uri(self):
1190+        if self.is_readonly():
1191+            return None
1192+        return self.my_uri.to_string()
1193     def get_readonly(self):
1194         return self.my_uri.get_readonly()
1195     def get_readonly_uri(self):
1196hunk ./src/allmydata/test/common.py 215
1197         return self.my_uri.is_readonly()
1198     def is_mutable(self):
1199         return self.my_uri.is_mutable()
1200+    def is_unknown(self):
1201+        return False
1202+    def is_allowed_in_immutable_directory(self):
1203+        return not self.my_uri.is_mutable()
1204+    def raise_error(self):
1205+        pass
1206     def get_writekey(self):
1207         return "\x00"*16
1208     def get_size(self):
1209hunk ./src/allmydata/test/test_client.py 291
1210         self.failUnless(n.is_readonly())
1211         self.failUnless(n.is_mutable())
1212 
1213-        future = "x-tahoe-crazy://future_cap_format."
1214-        n = c.create_node_from_uri(future)
1215+        unknown_rw = "lafs://from_the_future"
1216+        unknown_ro = "lafs://readonly_from_the_future"
1217+        n = c.create_node_from_uri(unknown_rw, unknown_ro)
1218         self.failUnless(IFilesystemNode.providedBy(n))
1219         self.failIf(IFileNode.providedBy(n))
1220         self.failIf(IImmutableFileNode.providedBy(n))
1221hunk ./src/allmydata/test/test_client.py 299
1222         self.failIf(IMutableFileNode.providedBy(n))
1223         self.failIf(IDirectoryNode.providedBy(n))
1224-        self.failUnlessEqual(n.get_uri(), future)
1225+        self.failUnless(n.is_unknown())
1226+        self.failUnlessEqual(n.get_uri(), unknown_rw)
1227+        self.failUnlessEqual(n.get_write_uri(), unknown_rw)
1228+        self.failUnlessEqual(n.get_readonly_uri(), "ro." + unknown_ro)
1229hunk ./src/allmydata/test/test_dirnode.py 10
1230 from allmydata.client import Client
1231 from allmydata.immutable import upload
1232 from allmydata.interfaces import IImmutableFileNode, IMutableFileNode, \
1233-     ExistingChildError, NoSuchChildError, NotDeepImmutableError, \
1234-     IDeepCheckResults, IDeepCheckAndRepairResults, CannotPackUnknownNodeError
1235+     ExistingChildError, NoSuchChildError, MustNotBeUnknownRWError, \
1236+     MustBeDeepImmutableError, MustBeReadonlyError, \
1237+     IDeepCheckResults, IDeepCheckAndRepairResults
1238 from allmydata.mutable.filenode import MutableFileNode
1239 from allmydata.mutable.common import UncoordinatedWriteError
1240 from allmydata.util import hashutil, base32
1241hunk ./src/allmydata/test/test_dirnode.py 20
1242 from allmydata.test.common import make_chk_file_uri, make_mutable_file_uri, \
1243      ErrorMixin
1244 from allmydata.test.no_network import GridTestMixin
1245-from allmydata.unknown import UnknownNode
1246+from allmydata.unknown import UnknownNode, strip_prefix_for_ro
1247 from allmydata.nodemaker import NodeMaker
1248 from base64 import b32decode
1249 import common_util as testutil
1250hunk ./src/allmydata/test/test_dirnode.py 36
1251         d = c.create_dirnode()
1252         def _done(res):
1253             self.failUnless(isinstance(res, dirnode.DirectoryNode))
1254+            self.failUnless(res.is_mutable())
1255+            self.failIf(res.is_readonly())
1256+            self.failIf(res.is_unknown())
1257+            self.failIf(res.is_allowed_in_immutable_directory())
1258+            res.raise_error()
1259             rep = str(res)
1260             self.failUnless("RW-MUT" in rep)
1261         d.addCallback(_done)
1262hunk ./src/allmydata/test/test_dirnode.py 53
1263         nm = c.nodemaker
1264         setup_py_uri = "URI:CHK:n7r3m6wmomelk4sep3kw5cvduq:os7ijw5c3maek7pg65e5254k2fzjflavtpejjyhshpsxuqzhcwwq:3:20:14861"
1265         one_uri = "URI:LIT:n5xgk" # LIT for "one"
1266+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1267+        mut_read_uri = "URI:SSK-RO:jf6wkflosyvntwxqcdo7a54jvm:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1268+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
1269+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
1270         kids = {u"one": (nm.create_from_cap(one_uri), {}),
1271                 u"two": (nm.create_from_cap(setup_py_uri),
1272                          {"metakey": "metavalue"}),
1273hunk ./src/allmydata/test/test_dirnode.py 60
1274+                u"mut": (nm.create_from_cap(mut_write_uri, mut_read_uri), {}),
1275+                u"fut": (nm.create_from_cap(future_write_uri, future_read_uri), {}),
1276+                u"fro": (nm.create_from_cap(None, future_read_uri), {}),
1277                 }
1278         d = c.create_dirnode(kids)
1279hunk ./src/allmydata/test/test_dirnode.py 65
1280+       
1281         def _created(dn):
1282             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1283hunk ./src/allmydata/test/test_dirnode.py 68
1284+            self.failUnless(dn.is_mutable())
1285+            self.failIf(dn.is_readonly())
1286+            self.failIf(dn.is_unknown())
1287+            self.failIf(dn.is_allowed_in_immutable_directory())
1288+            dn.raise_error()
1289             rep = str(dn)
1290             self.failUnless("RW-MUT" in rep)
1291             return dn.list()
1292hunk ./src/allmydata/test/test_dirnode.py 77
1293         d.addCallback(_created)
1294+       
1295         def _check_kids(children):
1296hunk ./src/allmydata/test/test_dirnode.py 79
1297-            self.failUnlessEqual(sorted(children.keys()), [u"one", u"two"])
1298+            self.failUnlessEqual(sorted(children.keys()),
1299+                                 [u"fro", u"fut", u"mut", u"one", u"two"])
1300             one_node, one_metadata = children[u"one"]
1301             two_node, two_metadata = children[u"two"]
1302hunk ./src/allmydata/test/test_dirnode.py 83
1303+            mut_node, mut_metadata = children[u"mut"]
1304+            fut_node, fut_metadata = children[u"fut"]
1305+            fro_node, fro_metadata = children[u"fro"]
1306+           
1307             self.failUnlessEqual(one_node.get_size(), 3)
1308hunk ./src/allmydata/test/test_dirnode.py 88
1309-            self.failUnlessEqual(two_node.get_size(), 14861)
1310+            self.failUnlessEqual(one_node.get_uri(), one_uri)
1311+            self.failUnlessEqual(one_node.get_readonly_uri(), one_uri)
1312             self.failUnless(isinstance(one_metadata, dict), one_metadata)
1313hunk ./src/allmydata/test/test_dirnode.py 91
1314+           
1315+            self.failUnlessEqual(two_node.get_size(), 14861)
1316+            self.failUnlessEqual(two_node.get_uri(), setup_py_uri)
1317+            self.failUnlessEqual(two_node.get_readonly_uri(), setup_py_uri)
1318             self.failUnlessEqual(two_metadata["metakey"], "metavalue")
1319hunk ./src/allmydata/test/test_dirnode.py 96
1320+           
1321+            self.failUnlessEqual(mut_node.get_uri(), mut_write_uri)
1322+            self.failUnlessEqual(mut_node.get_readonly_uri(), mut_read_uri)
1323+            self.failUnless(isinstance(mut_metadata, dict), mut_metadata)
1324+           
1325+            self.failUnless(fut_node.is_unknown())
1326+            self.failUnlessEqual(fut_node.get_uri(), future_write_uri)
1327+            self.failUnlessEqual(fut_node.get_readonly_uri(), "ro." + future_read_uri)
1328+            self.failUnless(isinstance(fut_metadata, dict), fut_metadata)
1329+           
1330+            self.failUnless(fro_node.is_unknown())
1331+            self.failUnlessEqual(fro_node.get_uri(), "ro." + future_read_uri)
1332+            self.failUnlessEqual(fut_node.get_readonly_uri(), "ro." + future_read_uri)
1333+            self.failUnless(isinstance(fro_metadata, dict), fro_metadata)
1334         d.addCallback(_check_kids)
1335hunk ./src/allmydata/test/test_dirnode.py 111
1336+
1337         d.addCallback(lambda ign: nm.create_new_mutable_directory(kids))
1338         d.addCallback(lambda dn: dn.list())
1339         d.addCallback(_check_kids)
1340hunk ./src/allmydata/test/test_dirnode.py 115
1341-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
1342-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
1343-        future_node = UnknownNode(future_writecap, future_readcap)
1344-        bad_kids1 = {u"one": (future_node, {})}
1345+
1346+        bad_future_node = UnknownNode(future_write_uri, None)
1347+        bad_kids1 = {u"one": (bad_future_node, {})}
1348         d.addCallback(lambda ign:
1349hunk ./src/allmydata/test/test_dirnode.py 119
1350-                      self.shouldFail(AssertionError, "bad_kids1",
1351-                                      "does not accept UnknownNode",
1352+                      self.shouldFail(MustNotBeUnknownRWError, "bad_kids1",
1353+                                      "cannot attach unknown",
1354                                       nm.create_new_mutable_directory,
1355                                       bad_kids1))
1356         bad_kids2 = {u"one": (nm.create_from_cap(one_uri), None)}
1357hunk ./src/allmydata/test/test_dirnode.py 138
1358         nm = c.nodemaker
1359         setup_py_uri = "URI:CHK:n7r3m6wmomelk4sep3kw5cvduq:os7ijw5c3maek7pg65e5254k2fzjflavtpejjyhshpsxuqzhcwwq:3:20:14861"
1360         one_uri = "URI:LIT:n5xgk" # LIT for "one"
1361-        mut_readcap = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1362-        mut_writecap = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1363+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1364+        mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1365+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
1366+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
1367         kids = {u"one": (nm.create_from_cap(one_uri), {}),
1368                 u"two": (nm.create_from_cap(setup_py_uri),
1369                          {"metakey": "metavalue"}),
1370hunk ./src/allmydata/test/test_dirnode.py 145
1371+                u"fut": (nm.create_from_cap(None, future_read_uri), {}),
1372                 }
1373         d = c.create_immutable_dirnode(kids)
1374hunk ./src/allmydata/test/test_dirnode.py 148
1375+       
1376         def _created(dn):
1377             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1378             self.failIf(dn.is_mutable())
1379hunk ./src/allmydata/test/test_dirnode.py 153
1380             self.failUnless(dn.is_readonly())
1381+            self.failIf(dn.is_unknown())
1382+            self.failUnless(dn.is_allowed_in_immutable_directory())
1383+            dn.raise_error()
1384             rep = str(dn)
1385             self.failUnless("RO-IMM" in rep)
1386             cap = dn.get_cap()
1387hunk ./src/allmydata/test/test_dirnode.py 163
1388             self.cap = cap
1389             return dn.list()
1390         d.addCallback(_created)
1391+       
1392         def _check_kids(children):
1393hunk ./src/allmydata/test/test_dirnode.py 165
1394-            self.failUnlessEqual(sorted(children.keys()), [u"one", u"two"])
1395+            self.failUnlessEqual(sorted(children.keys()), [u"fut", u"one", u"two"])
1396             one_node, one_metadata = children[u"one"]
1397             two_node, two_metadata = children[u"two"]
1398hunk ./src/allmydata/test/test_dirnode.py 168
1399+            fut_node, fut_metadata = children[u"fut"]
1400+
1401             self.failUnlessEqual(one_node.get_size(), 3)
1402hunk ./src/allmydata/test/test_dirnode.py 171
1403-            self.failUnlessEqual(two_node.get_size(), 14861)
1404+            self.failUnlessEqual(one_node.get_uri(), one_uri)
1405+            self.failUnlessEqual(one_node.get_readonly_uri(), one_uri)
1406             self.failUnless(isinstance(one_metadata, dict), one_metadata)
1407hunk ./src/allmydata/test/test_dirnode.py 174
1408+
1409+            self.failUnlessEqual(two_node.get_size(), 14861)
1410+            self.failUnlessEqual(two_node.get_uri(), setup_py_uri)
1411+            self.failUnlessEqual(two_node.get_readonly_uri(), setup_py_uri)
1412             self.failUnlessEqual(two_metadata["metakey"], "metavalue")
1413hunk ./src/allmydata/test/test_dirnode.py 179
1414+
1415+            self.failUnless(fut_node.is_unknown())
1416+            self.failUnlessEqual(fut_node.get_uri(), "imm." + future_read_uri)
1417+            self.failUnlessEqual(fut_node.get_readonly_uri(), "imm." + future_read_uri)
1418+            self.failUnless(isinstance(fut_metadata, dict), fut_metadata)
1419         d.addCallback(_check_kids)
1420hunk ./src/allmydata/test/test_dirnode.py 185
1421+       
1422         d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string()))
1423         d.addCallback(lambda dn: dn.list())
1424         d.addCallback(_check_kids)
1425hunk ./src/allmydata/test/test_dirnode.py 189
1426-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
1427-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
1428-        future_node = UnknownNode(future_writecap, future_readcap)
1429-        bad_kids1 = {u"one": (future_node, {})}
1430+
1431+        bad_future_node1 = UnknownNode(future_write_uri, None)
1432+        bad_kids1 = {u"one": (bad_future_node1, {})}
1433         d.addCallback(lambda ign:
1434hunk ./src/allmydata/test/test_dirnode.py 193
1435-                      self.shouldFail(AssertionError, "bad_kids1",
1436-                                      "does not accept UnknownNode",
1437+                      self.shouldFail(MustNotBeUnknownRWError, "bad_kids1",
1438+                                      "cannot attach unknown",
1439                                       c.create_immutable_dirnode,
1440                                       bad_kids1))
1441hunk ./src/allmydata/test/test_dirnode.py 197
1442-        bad_kids2 = {u"one": (nm.create_from_cap(one_uri), None)}
1443+        bad_future_node2 = UnknownNode(future_write_uri, future_read_uri)
1444+        bad_kids2 = {u"one": (bad_future_node2, {})}
1445         d.addCallback(lambda ign:
1446hunk ./src/allmydata/test/test_dirnode.py 200
1447-                      self.shouldFail(AssertionError, "bad_kids2",
1448-                                      "requires metadata to be a dict",
1449+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids2",
1450+                                      "is not immutable",
1451                                       c.create_immutable_dirnode,
1452                                       bad_kids2))
1453hunk ./src/allmydata/test/test_dirnode.py 204
1454-        bad_kids3 = {u"one": (nm.create_from_cap(mut_writecap), {})}
1455+        bad_kids3 = {u"one": (nm.create_from_cap(one_uri), None)}
1456         d.addCallback(lambda ign:
1457hunk ./src/allmydata/test/test_dirnode.py 206
1458-                      self.shouldFail(NotDeepImmutableError, "bad_kids3",
1459-                                      "is not immutable",
1460+                      self.shouldFail(AssertionError, "bad_kids3",
1461+                                      "requires metadata to be a dict",
1462                                       c.create_immutable_dirnode,
1463                                       bad_kids3))
1464hunk ./src/allmydata/test/test_dirnode.py 210
1465-        bad_kids4 = {u"one": (nm.create_from_cap(mut_readcap), {})}
1466+        bad_kids4 = {u"one": (nm.create_from_cap(mut_write_uri), {})}
1467         d.addCallback(lambda ign:
1468hunk ./src/allmydata/test/test_dirnode.py 212
1469-                      self.shouldFail(NotDeepImmutableError, "bad_kids4",
1470+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids4",
1471                                       "is not immutable",
1472                                       c.create_immutable_dirnode,
1473                                       bad_kids4))
1474hunk ./src/allmydata/test/test_dirnode.py 216
1475+        bad_kids5 = {u"one": (nm.create_from_cap(mut_read_uri), {})}
1476+        d.addCallback(lambda ign:
1477+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids5",
1478+                                      "is not immutable",
1479+                                      c.create_immutable_dirnode,
1480+                                      bad_kids5))
1481         d.addCallback(lambda ign: c.create_immutable_dirnode({}))
1482         def _created_empty(dn):
1483             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1484hunk ./src/allmydata/test/test_dirnode.py 227
1485             self.failIf(dn.is_mutable())
1486             self.failUnless(dn.is_readonly())
1487+            self.failIf(dn.is_unknown())
1488+            self.failUnless(dn.is_allowed_in_immutable_directory())
1489+            dn.raise_error()
1490             rep = str(dn)
1491             self.failUnless("RO-IMM" in rep)
1492             cap = dn.get_cap()
1493hunk ./src/allmydata/test/test_dirnode.py 245
1494             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1495             self.failIf(dn.is_mutable())
1496             self.failUnless(dn.is_readonly())
1497+            self.failIf(dn.is_unknown())
1498+            self.failUnless(dn.is_allowed_in_immutable_directory())
1499+            dn.raise_error()
1500             rep = str(dn)
1501             self.failUnless("RO-IMM" in rep)
1502             cap = dn.get_cap()
1503hunk ./src/allmydata/test/test_dirnode.py 273
1504             d.addCallback(_check_kids)
1505             d.addCallback(lambda ign: n.get(u"subdir"))
1506             d.addCallback(lambda sd: self.failIf(sd.is_mutable()))
1507-            bad_kids = {u"one": (nm.create_from_cap(mut_writecap), {})}
1508+            bad_kids = {u"one": (nm.create_from_cap(mut_write_uri), {})}
1509             d.addCallback(lambda ign:
1510hunk ./src/allmydata/test/test_dirnode.py 275
1511-                          self.shouldFail(NotDeepImmutableError, "YZ",
1512+                          self.shouldFail(MustBeDeepImmutableError, "YZ",
1513                                           "is not immutable",
1514                                           n.create_subdirectory,
1515                                           u"sub2", bad_kids, mutable=False))
1516hunk ./src/allmydata/test/test_dirnode.py 283
1517         d.addCallback(_made_parent)
1518         return d
1519 
1520-
1521     def test_check(self):
1522         self.basedir = "dirnode/Dirnode/test_check"
1523         self.set_up_grid()
1524hunk ./src/allmydata/test/test_dirnode.py 416
1525             ro_dn = c.create_node_from_uri(ro_uri)
1526             self.failUnless(ro_dn.is_readonly())
1527             self.failUnless(ro_dn.is_mutable())
1528+            self.failIf(ro_dn.is_unknown())
1529+            self.failIf(ro_dn.is_allowed_in_immutable_directory())
1530+            ro_dn.raise_error()
1531 
1532hunk ./src/allmydata/test/test_dirnode.py 420
1533-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1534+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1535                             ro_dn.set_uri, u"newchild", filecap, filecap)
1536hunk ./src/allmydata/test/test_dirnode.py 422
1537-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1538+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1539                             ro_dn.set_node, u"newchild", filenode)
1540hunk ./src/allmydata/test/test_dirnode.py 424
1541-            self.shouldFail(dirnode.NotMutableError, "set_nodes ro", None,
1542+            self.shouldFail(dirnode.NotWriteableError, "set_nodes ro", None,
1543                             ro_dn.set_nodes, { u"newchild": (filenode, None) })
1544hunk ./src/allmydata/test/test_dirnode.py 426
1545-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1546+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1547                             ro_dn.add_file, u"newchild", uploadable)
1548hunk ./src/allmydata/test/test_dirnode.py 428
1549-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1550+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1551                             ro_dn.delete, u"child")
1552hunk ./src/allmydata/test/test_dirnode.py 430
1553-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1554+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1555                             ro_dn.create_subdirectory, u"newchild")
1556hunk ./src/allmydata/test/test_dirnode.py 432
1557-            self.shouldFail(dirnode.NotMutableError, "set_metadata_for ro", None,
1558+            self.shouldFail(dirnode.NotWriteableError, "set_metadata_for ro", None,
1559                             ro_dn.set_metadata_for, u"child", {})
1560hunk ./src/allmydata/test/test_dirnode.py 434
1561-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1562+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1563                             ro_dn.move_child_to, u"child", rw_dn)
1564hunk ./src/allmydata/test/test_dirnode.py 436
1565-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1566+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1567                             rw_dn.move_child_to, u"child", ro_dn)
1568             return ro_dn.list()
1569         d.addCallback(_ready)
1570hunk ./src/allmydata/test/test_dirnode.py 983
1571         nodemaker = NodeMaker(None, None, None,
1572                               None, None, None,
1573                               {"k": 3, "n": 10}, None)
1574-        writecap = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1575-        filenode = nodemaker.create_from_cap(writecap)
1576+        write_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1577+        filenode = nodemaker.create_from_cap(write_uri)
1578         node = dirnode.DirectoryNode(filenode, nodemaker, None)
1579         children = node._unpack_contents(known_tree)
1580         self._check_children(children)
1581hunk ./src/allmydata/test/test_dirnode.py 1057
1582         self.failUnlessIn("lit", packed)
1583 
1584         kids = self._make_kids(nm, ["imm", "lit", "write"])
1585-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1586+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1587                               dirnode.pack_children,
1588                               fn, kids, deep_immutable=True)
1589 
1590hunk ./src/allmydata/test/test_dirnode.py 1063
1591         # read-only is not enough: all children must be immutable
1592         kids = self._make_kids(nm, ["imm", "lit", "read"])
1593-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1594+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1595                               dirnode.pack_children,
1596                               fn, kids, deep_immutable=True)
1597 
1598hunk ./src/allmydata/test/test_dirnode.py 1068
1599         kids = self._make_kids(nm, ["imm", "lit", "dirwrite"])
1600-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1601+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1602                               dirnode.pack_children,
1603                               fn, kids, deep_immutable=True)
1604 
1605hunk ./src/allmydata/test/test_dirnode.py 1073
1606         kids = self._make_kids(nm, ["imm", "lit", "dirread"])
1607-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1608+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1609                               dirnode.pack_children,
1610                               fn, kids, deep_immutable=True)
1611 
1612hunk ./src/allmydata/test/test_dirnode.py 1099
1613 
1614     def get_cap(self):
1615         return self.uri
1616+
1617     def get_uri(self):
1618         return self.uri.to_string()
1619hunk ./src/allmydata/test/test_dirnode.py 1102
1620+
1621+    def get_write_uri(self):
1622+        return self.uri.to_string()
1623+
1624     def download_best_version(self):
1625         return defer.succeed(self.data)
1626hunk ./src/allmydata/test/test_dirnode.py 1108
1627+
1628     def get_writekey(self):
1629         return "writekey"
1630hunk ./src/allmydata/test/test_dirnode.py 1111
1631+
1632     def is_readonly(self):
1633         return False
1634hunk ./src/allmydata/test/test_dirnode.py 1114
1635+
1636     def is_mutable(self):
1637         return True
1638hunk ./src/allmydata/test/test_dirnode.py 1117
1639+
1640+    def is_unknown(self):
1641+        return False
1642+
1643+    def is_allowed_in_immutable_directory(self):
1644+        return False
1645+
1646     def modify(self, modifier):
1647         self.data = modifier(self.data, None, True)
1648         return defer.succeed(None)
1649hunk ./src/allmydata/test/test_dirnode.py 1146
1650         self.nodemaker = client.nodemaker
1651 
1652     def test_from_future(self):
1653-        # create a dirnode that contains unknown URI types, and make sure we
1654-        # tolerate them properly. Since dirnodes aren't allowed to add
1655-        # unknown node types, we have to be tricky.
1656+        # Create a mutable directory that contains unknown URI types, and make sure
1657+        # we tolerate them properly.
1658         d = self.nodemaker.create_new_mutable_directory()
1659hunk ./src/allmydata/test/test_dirnode.py 1149
1660-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
1661-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
1662-        future_node = UnknownNode(future_writecap, future_readcap)
1663+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
1664+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
1665+        future_imm_uri = "x-tahoe-crazy-immutable://I_am_from_the_future."
1666+        future_node = UnknownNode(future_write_uri, future_read_uri)
1667         def _then(n):
1668             self._node = n
1669             return n.set_node(u"future", future_node)
1670hunk ./src/allmydata/test/test_dirnode.py 1158
1671         d.addCallback(_then)
1672 
1673-        # we should be prohibited from adding an unknown URI to a directory,
1674-        # since we don't know how to diminish the cap to a readcap (for the
1675-        # dirnode's rocap slot), and we don't want to accidentally grant
1676-        # write access to a holder of the dirnode's readcap.
1677+        # We should be prohibited from adding an unknown URI to a directory
1678+        # just in the rw_uri slot, since we don't know how to diminish the cap
1679+        # to a readcap (for the ro_uri slot).
1680         d.addCallback(lambda ign:
1681hunk ./src/allmydata/test/test_dirnode.py 1162
1682-             self.shouldFail(CannotPackUnknownNodeError,
1683+             self.shouldFail(MustNotBeUnknownRWError,
1684                              "copy unknown",
1685hunk ./src/allmydata/test/test_dirnode.py 1164
1686-                             "cannot pack unknown node as child add",
1687+                             "cannot attach unknown rw cap as child",
1688                              self._node.set_uri, u"add",
1689hunk ./src/allmydata/test/test_dirnode.py 1166
1690-                             future_writecap, future_readcap))
1691+                             future_write_uri, None))
1692+
1693+        # However, we should be able to add both rw_uri and ro_uri as a pair of
1694+        # unknown URIs.
1695+        d.addCallback(lambda ign: self._node.set_uri(u"add-pair",
1696+                                                     future_write_uri, future_read_uri))
1697+
1698+        # and to add an URI prefixed with "ro." or "imm." when it is given in a
1699+        # write slot (or URL parameter).
1700+        d.addCallback(lambda ign: self._node.set_uri(u"add-ro",
1701+                                                     "ro." + future_read_uri, None))
1702+        d.addCallback(lambda ign: self._node.set_uri(u"add-imm",
1703+                                                     "imm." + future_imm_uri, None))
1704+
1705         d.addCallback(lambda ign: self._node.list())
1706         def _check(children):
1707hunk ./src/allmydata/test/test_dirnode.py 1182
1708-            self.failUnlessEqual(len(children), 1)
1709+            self.failUnlessEqual(len(children), 4)
1710             (fn, metadata) = children[u"future"]
1711             self.failUnless(isinstance(fn, UnknownNode), fn)
1712hunk ./src/allmydata/test/test_dirnode.py 1185
1713-            self.failUnlessEqual(fn.get_uri(), future_writecap)
1714-            self.failUnlessEqual(fn.get_readonly_uri(), future_readcap)
1715-            # but we *should* be allowed to copy this node, because the
1716-            # UnknownNode contains all the information that was in the
1717-            # original directory (readcap and writecap), so we're preserving
1718-            # everything.
1719+            self.failUnlessEqual(fn.get_uri(), future_write_uri)
1720+            self.failUnlessEqual(fn.get_write_uri(), future_write_uri)
1721+            self.failUnlessEqual(fn.get_readonly_uri(), "ro." + future_read_uri)
1722+
1723+            (fn2, metadata2) = children[u"add-pair"]
1724+            self.failUnless(isinstance(fn2, UnknownNode), fn2)
1725+            self.failUnlessEqual(fn2.get_uri(), future_write_uri)
1726+            self.failUnlessEqual(fn2.get_write_uri(), future_write_uri)
1727+            self.failUnlessEqual(fn2.get_readonly_uri(), "ro." + future_read_uri)
1728+
1729+            (fn3, metadata3) = children[u"add-ro"]
1730+            self.failUnless(isinstance(fn3, UnknownNode), fn3)
1731+            self.failUnlessEqual(fn3.get_uri(), "ro." + future_read_uri)
1732+            self.failUnlessEqual(fn3.get_write_uri(), None)
1733+            self.failUnlessEqual(fn3.get_readonly_uri(), "ro." + future_read_uri)
1734+
1735+            (fn4, metadata4) = children[u"add-imm"]
1736+            self.failUnless(isinstance(fn4, UnknownNode), fn4)
1737+            self.failUnlessEqual(fn4.get_uri(), "imm." + future_imm_uri)
1738+            self.failUnlessEqual(fn4.get_write_uri(), None)
1739+            self.failUnlessEqual(fn4.get_readonly_uri(), "imm." + future_imm_uri)
1740+
1741+            # We should also be allowed to copy the "future" UnknownNode, because
1742+            # it contains all the information that was in the original directory
1743+            # (readcap and writecap), so we're preserving everything.
1744             return self._node.set_node(u"copy", fn)
1745         d.addCallback(_check)
1746hunk ./src/allmydata/test/test_dirnode.py 1212
1747+
1748         d.addCallback(lambda ign: self._node.list())
1749         def _check2(children):
1750hunk ./src/allmydata/test/test_dirnode.py 1215
1751-            self.failUnlessEqual(len(children), 2)
1752+            self.failUnlessEqual(len(children), 5)
1753             (fn, metadata) = children[u"copy"]
1754             self.failUnless(isinstance(fn, UnknownNode), fn)
1755hunk ./src/allmydata/test/test_dirnode.py 1218
1756-            self.failUnlessEqual(fn.get_uri(), future_writecap)
1757-            self.failUnlessEqual(fn.get_readonly_uri(), future_readcap)
1758+            self.failUnlessEqual(fn.get_uri(), future_write_uri)
1759+            self.failUnlessEqual(fn.get_write_uri(), future_write_uri)
1760+            self.failUnlessEqual(fn.get_readonly_uri(), "ro." + future_read_uri)
1761+        d.addCallback(_check2)
1762         return d
1763 
1764hunk ./src/allmydata/test/test_dirnode.py 1224
1765+    def test_unknown_strip_prefix_for_ro(self):
1766+        self.failUnlessEqual(strip_prefix_for_ro("foo",     False), "foo")
1767+        self.failUnlessEqual(strip_prefix_for_ro("ro.foo",  False), "foo")
1768+        self.failUnlessEqual(strip_prefix_for_ro("imm.foo", False), "imm.foo")
1769+        self.failUnlessEqual(strip_prefix_for_ro("foo",     True),  "foo")
1770+        self.failUnlessEqual(strip_prefix_for_ro("ro.foo",  True),  "foo")
1771+        self.failUnlessEqual(strip_prefix_for_ro("imm.foo", True),  "foo")
1772+
1773+    def test_unknownnode(self):
1774+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1775+        mut_read_uri = "URI:SSK-RO:jf6wkflosyvntwxqcdo7a54jvm:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1776+        lit_uri = "URI:LIT:n5xgk"
1777+
1778+        # This does not attempt to be exhaustive.
1779+        no_no        = [# Opaque node, but not an error.
1780+                        ( 0, UnknownNode(None, None)),
1781+                        ( 1, UnknownNode(None, None, deep_immutable=True)),
1782+                       ]
1783+        unknown_rw   = [# These are errors because we're only given a rw_uri, and we can't
1784+                        # diminish it.
1785+                        ( 2, UnknownNode("foo", None)),
1786+                        ( 3, UnknownNode("foo", None, deep_immutable=True)),
1787+                        ( 4, UnknownNode("ro.foo", None, deep_immutable=True)),
1788+                        ( 5, UnknownNode("ro." + mut_read_uri, None, deep_immutable=True)),
1789+                        ( 6, UnknownNode("URI:SSK-RO:foo", None, deep_immutable=True)),
1790+                        ( 7, UnknownNode("URI:SSK:foo", None)),
1791+                       ]
1792+        must_be_ro   = [# These are errors because a readonly constraint is not met.
1793+                        ( 8, UnknownNode("ro." + mut_write_uri, None)),
1794+                        ( 9, UnknownNode(None, "ro." + mut_write_uri)),
1795+                       ]
1796+        must_be_imm  = [# These are errors because an immutable constraint is not met.
1797+                        (10, UnknownNode(None, "ro.URI:SSK-RO:foo", deep_immutable=True)),
1798+                        (11, UnknownNode(None, "imm.URI:SSK:foo")),
1799+                        (12, UnknownNode(None, "imm.URI:SSK-RO:foo")),
1800+                        (13, UnknownNode("bar", "ro.foo", deep_immutable=True)),
1801+                        (14, UnknownNode("bar", "imm.foo", deep_immutable=True)),
1802+                        (15, UnknownNode("bar", "imm." + lit_uri, deep_immutable=True)),
1803+                        (16, UnknownNode("imm." + mut_write_uri, None)),
1804+                        (17, UnknownNode("imm." + mut_read_uri, None)),
1805+                        (18, UnknownNode("bar", "imm.foo")),
1806+                       ]
1807+        bad_uri      = [# These are errors because the URI is bad once we've stripped the prefix.
1808+                        (19, UnknownNode("ro.URI:SSK-RO:foo", None)),
1809+                        (20, UnknownNode("imm.URI:CHK:foo", None, deep_immutable=True)),
1810+                       ]
1811+        ro_prefixed  = [# These are valid, and the readcap should end up with a ro. prefix.
1812+                        (21, UnknownNode(None, "foo")),
1813+                        (22, UnknownNode(None, "ro.foo")),
1814+                        (32, UnknownNode(None, "ro." + lit_uri)),
1815+                        (23, UnknownNode("bar", "foo")),
1816+                        (24, UnknownNode("bar", "ro.foo")),
1817+                        (32, UnknownNode("bar", "ro." + lit_uri)),
1818+                        (25, UnknownNode("ro.foo", None)),
1819+                        (30, UnknownNode("ro." + lit_uri, None)),
1820+                       ]
1821+        imm_prefixed = [# These are valid, and the readcap should end up with an imm. prefix.
1822+                        (26, UnknownNode(None, "foo", deep_immutable=True)),
1823+                        (27, UnknownNode(None, "ro.foo", deep_immutable=True)),
1824+                        (28, UnknownNode(None, "imm.foo")),
1825+                        (29, UnknownNode(None, "imm.foo", deep_immutable=True)),
1826+                        (31, UnknownNode("imm." + lit_uri, None)),
1827+                        (31, UnknownNode("imm." + lit_uri, None, deep_immutable=True)),
1828+                        (33, UnknownNode(None, "imm." + lit_uri)),
1829+                        (33, UnknownNode(None, "imm." + lit_uri, deep_immutable=True)),
1830+                       ]
1831+        error = unknown_rw + must_be_ro + must_be_imm + bad_uri
1832+        ok = ro_prefixed + imm_prefixed
1833+
1834+        for (i, n) in no_no + error + ok:
1835+            self.failUnless(n.is_unknown(), i)
1836+
1837+        for (i, n) in no_no + error:
1838+            self.failUnless(n.get_uri() is None, i)
1839+            self.failUnless(n.get_write_uri() is None, i)
1840+            self.failUnless(n.get_readonly_uri() is None, i)
1841+
1842+        for (i, n) in no_no + ok:
1843+            n.raise_error()
1844+
1845+        for (i, n) in unknown_rw:
1846+            self.failUnlessRaises(MustNotBeUnknownRWError, lambda: n.raise_error())
1847+
1848+        for (i, n) in must_be_ro:
1849+            self.failUnlessRaises(MustBeReadonlyError, lambda: n.raise_error())
1850+
1851+        for (i, n) in must_be_imm:
1852+            self.failUnlessRaises(MustBeDeepImmutableError, lambda: n.raise_error())
1853+
1854+        for (i, n) in bad_uri:
1855+            self.failUnlessRaises(uri.BadURIError, lambda: n.raise_error())
1856+
1857+        for (i, n) in ok:
1858+            self.failIf(n.get_readonly_uri() is None, i)
1859+
1860+        for (i, n) in ro_prefixed:
1861+            self.failUnless(n.get_readonly_uri().startswith("ro."), i)
1862+
1863+        for (i, n) in imm_prefixed:
1864+            self.failUnless(n.get_readonly_uri().startswith("imm."), i)
1865+
1866+
1867 class DeepStats(unittest.TestCase):
1868     timeout = 240 # It takes longer than 120 seconds on Francois's arm box.
1869     def test_stats(self):
1870hunk ./src/allmydata/test/test_filenode.py 44
1871         self.failUnlessEqual(fn1.get_readcap(), u)
1872         self.failUnlessEqual(fn1.is_readonly(), True)
1873         self.failUnlessEqual(fn1.is_mutable(), False)
1874+        self.failUnlessEqual(fn1.is_unknown(), False)
1875+        self.failUnlessEqual(fn1.is_allowed_in_immutable_directory(), True)
1876+        self.failUnlessEqual(fn1.get_write_uri(), None)
1877         self.failUnlessEqual(fn1.get_readonly_uri(), u.to_string())
1878         self.failUnlessEqual(fn1.get_size(), 1000)
1879         self.failUnlessEqual(fn1.get_storage_index(), u.storage_index)
1880hunk ./src/allmydata/test/test_filenode.py 50
1881+        fn1.raise_error()
1882+        fn2.raise_error()
1883         d = {}
1884         d[fn1] = 1 # exercise __hash__
1885         v = fn1.get_verify_cap()
1886hunk ./src/allmydata/test/test_filenode.py 57
1887         self.failUnless(isinstance(v, uri.CHKFileVerifierURI))
1888         self.failUnlessEqual(fn1.get_repair_cap(), v)
1889+        self.failUnlessEqual(v.is_readonly(), True)
1890+        self.failUnlessEqual(v.is_mutable(), False)
1891 
1892 
1893     def test_literal_filenode(self):
1894hunk ./src/allmydata/test/test_filenode.py 74
1895         self.failUnlessEqual(fn1.get_readcap(), u)
1896         self.failUnlessEqual(fn1.is_readonly(), True)
1897         self.failUnlessEqual(fn1.is_mutable(), False)
1898+        self.failUnlessEqual(fn1.is_unknown(), False)
1899+        self.failUnlessEqual(fn1.is_allowed_in_immutable_directory(), True)
1900+        self.failUnlessEqual(fn1.get_write_uri(), None)
1901         self.failUnlessEqual(fn1.get_readonly_uri(), u.to_string())
1902         self.failUnlessEqual(fn1.get_size(), len(DATA))
1903         self.failUnlessEqual(fn1.get_storage_index(), None)
1904hunk ./src/allmydata/test/test_filenode.py 80
1905+        fn1.raise_error()
1906+        fn2.raise_error()
1907         d = {}
1908         d[fn1] = 1 # exercise __hash__
1909 
1910hunk ./src/allmydata/test/test_filenode.py 114
1911         self.failUnlessEqual(n.get_writekey(), wk)
1912         self.failUnlessEqual(n.get_readkey(), rk)
1913         self.failUnlessEqual(n.get_storage_index(), si)
1914-        # these itmes are populated on first read (or create), so until that
1915+        # these items are populated on first read (or create), so until that
1916         # happens they'll be None
1917         self.failUnlessEqual(n.get_privkey(), None)
1918         self.failUnlessEqual(n.get_encprivkey(), None)
1919hunk ./src/allmydata/test/test_filenode.py 121
1920         self.failUnlessEqual(n.get_pubkey(), None)
1921 
1922         self.failUnlessEqual(n.get_uri(), u.to_string())
1923+        self.failUnlessEqual(n.get_write_uri(), u.to_string())
1924         self.failUnlessEqual(n.get_readonly_uri(), u.get_readonly().to_string())
1925         self.failUnlessEqual(n.get_cap(), u)
1926         self.failUnlessEqual(n.get_readcap(), u.get_readonly())
1927hunk ./src/allmydata/test/test_filenode.py 127
1928         self.failUnlessEqual(n.is_mutable(), True)
1929         self.failUnlessEqual(n.is_readonly(), False)
1930+        self.failUnlessEqual(n.is_unknown(), False)
1931+        self.failUnlessEqual(n.is_allowed_in_immutable_directory(), False)
1932+        n.raise_error()
1933 
1934         n2 = MutableFileNode(None, None, client.get_encoding_parameters(),
1935                              None).init_from_cap(u)
1936hunk ./src/allmydata/test/test_filenode.py 136
1937         self.failUnlessEqual(n, n2)
1938         self.failIfEqual(n, "not even the right type")
1939         self.failIfEqual(n, u) # not the right class
1940+        n.raise_error()
1941         d = {n: "can these be used as dictionary keys?"}
1942         d[n2] = "replace the old one"
1943         self.failUnlessEqual(len(d), 1)
1944hunk ./src/allmydata/test/test_filenode.py 147
1945         self.failUnlessEqual(nro.get_readonly(), nro)
1946         self.failUnlessEqual(nro.get_cap(), u.get_readonly())
1947         self.failUnlessEqual(nro.get_readcap(), u.get_readonly())
1948+        self.failUnlessEqual(nro.is_mutable(), True)
1949+        self.failUnlessEqual(nro.is_readonly(), True)
1950+        self.failUnlessEqual(nro.is_unknown(), False)
1951+        self.failUnlessEqual(nro.is_allowed_in_immutable_directory(), False)
1952         nro_u = nro.get_uri()
1953         self.failUnlessEqual(nro_u, nro.get_readonly_uri())
1954         self.failUnlessEqual(nro_u, u.get_readonly().to_string())
1955hunk ./src/allmydata/test/test_filenode.py 154
1956-        self.failUnlessEqual(nro.is_mutable(), True)
1957-        self.failUnlessEqual(nro.is_readonly(), True)
1958+        self.failUnlessEqual(nro.get_write_uri(), None)
1959         self.failUnlessEqual(nro.get_repair_cap(), None) # RSAmut needs writecap
1960hunk ./src/allmydata/test/test_filenode.py 156
1961+        nro.raise_error()
1962 
1963         v = n.get_verify_cap()
1964         self.failUnless(isinstance(v, uri.SSKVerifierURI))
1965hunk ./src/allmydata/test/test_system.py 20
1966 from allmydata.interfaces import IDirectoryNode, IFileNode, \
1967      NoSuchChildError, NoSharesError
1968 from allmydata.monitor import Monitor
1969-from allmydata.mutable.common import NotMutableError
1970+from allmydata.mutable.common import NotWriteableError
1971 from allmydata.mutable import layout as mutable_layout
1972 from foolscap.api import DeadReferenceError
1973 from twisted.python.failure import Failure
1974hunk ./src/allmydata/test/test_system.py 893
1975             d1.addCallback(lambda res: dirnode.list())
1976             d1.addCallback(self.log, "dirnode.list")
1977 
1978-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mkdir(nope)", None, dirnode.create_subdirectory, u"nope"))
1979+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mkdir(nope)", None, dirnode.create_subdirectory, u"nope"))
1980 
1981             d1.addCallback(self.log, "doing add_file(ro)")
1982             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)")
1983hunk ./src/allmydata/test/test_system.py 897
1984-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "add_file(nope)", None, dirnode.add_file, u"hope", ut))
1985+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "add_file(nope)", None, dirnode.add_file, u"hope", ut))
1986 
1987             d1.addCallback(self.log, "doing get(ro)")
1988             d1.addCallback(lambda res: dirnode.get(u"mydata992"))
1989hunk ./src/allmydata/test/test_system.py 905
1990                            self.failUnless(IFileNode.providedBy(filenode)))
1991 
1992             d1.addCallback(self.log, "doing delete(ro)")
1993-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "delete(nope)", None, dirnode.delete, u"mydata992"))
1994+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "delete(nope)", None, dirnode.delete, u"mydata992"))
1995 
1996hunk ./src/allmydata/test/test_system.py 907
1997-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "set_uri(nope)", None, dirnode.set_uri, u"hopeless", self.uri, self.uri))
1998+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "set_uri(nope)", None, dirnode.set_uri, u"hopeless", self.uri, self.uri))
1999 
2000             d1.addCallback(lambda res: self.shouldFail2(NoSuchChildError, "get(missing)", "missing", dirnode.get, u"missing"))
2001 
2002hunk ./src/allmydata/test/test_system.py 912
2003             personal = self._personal_node
2004-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mv from readonly", None, dirnode.move_child_to, u"mydata992", personal, u"nope"))
2005+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mv from readonly", None, dirnode.move_child_to, u"mydata992", personal, u"nope"))
2006 
2007             d1.addCallback(self.log, "doing move_child_to(ro)2")
2008hunk ./src/allmydata/test/test_system.py 915
2009-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mv to readonly", None, personal.move_child_to, u"sekrit data", dirnode, u"nope"))
2010+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mv to readonly", None, personal.move_child_to, u"sekrit data", dirnode, u"nope"))
2011 
2012             d1.addCallback(self.log, "finished with _got_s2ro")
2013             return d1
2014hunk ./src/allmydata/test/test_uri.py 6
2015 from allmydata import uri
2016 from allmydata.util import hashutil, base32
2017 from allmydata.interfaces import IURI, IFileURI, IDirnodeURI, IMutableFileURI, \
2018-    IVerifierURI
2019+    IVerifierURI, CapConstraintError
2020 
2021 class Literal(unittest.TestCase):
2022     def _help_test(self, data):
2023hunk ./src/allmydata/test/test_uri.py 25
2024         self.failIf(IDirnodeURI.providedBy(u2))
2025         self.failUnlessEqual(u2.data, data)
2026         self.failUnlessEqual(u2.get_size(), len(data))
2027-        self.failUnless(u.is_readonly())
2028-        self.failIf(u.is_mutable())
2029+        self.failUnless(u2.is_readonly())
2030+        self.failIf(u2.is_mutable())
2031+
2032+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
2033+        self.failUnless(IFileURI.providedBy(u2i))
2034+        self.failIf(IDirnodeURI.providedBy(u2i))
2035+        self.failUnlessEqual(u2i.data, data)
2036+        self.failUnlessEqual(u2i.get_size(), len(data))
2037+        self.failUnless(u2i.is_readonly())
2038+        self.failIf(u2i.is_mutable())
2039 
2040         u3 = u.get_readonly()
2041         self.failUnlessIdentical(u, u3)
2042hunk ./src/allmydata/test/test_uri.py 62
2043         fileURI = 'URI:CHK:f5ahxa25t4qkktywz6teyfvcx4:opuioq7tj2y6idzfp6cazehtmgs5fdcebcz3cygrxyydvcozrmeq:3:10:345834'
2044         chk1 = uri.CHKFileURI.init_from_string(fileURI)
2045         chk2 = uri.CHKFileURI.init_from_string(fileURI)
2046+        unk = uri.UnknownURI("lafs://from_the_future")
2047         self.failIfEqual(lit1, chk1)
2048         self.failUnlessEqual(chk1, chk2)
2049         self.failIfEqual(chk1, "not actually a URI")
2050hunk ./src/allmydata/test/test_uri.py 67
2051         # these should be hashable too
2052-        s = set([lit1, chk1, chk2])
2053-        self.failUnlessEqual(len(s), 2) # since chk1==chk2
2054+        s = set([lit1, chk1, chk2, unk])
2055+        self.failUnlessEqual(len(s), 3) # since chk1==chk2
2056 
2057     def test_is_uri(self):
2058         lit1 = uri.LiteralFileURI("some data").to_string()
2059hunk ./src/allmydata/test/test_uri.py 75
2060         self.failUnless(uri.is_uri(lit1))
2061         self.failIf(uri.is_uri(None))
2062 
2063+    def test_is_literal_file_uri(self):
2064+        lit1 = uri.LiteralFileURI("some data").to_string()
2065+        self.failUnless(uri.is_literal_file_uri(lit1))
2066+        self.failIf(uri.is_literal_file_uri(None))
2067+        self.failIf(uri.is_literal_file_uri("foo"))
2068+        self.failIf(uri.is_literal_file_uri("ro.foo"))
2069+        self.failIf(uri.is_literal_file_uri("URI:LITfoo"))
2070+        self.failUnless(uri.is_literal_file_uri("ro.URI:LIT:foo"))
2071+        self.failUnless(uri.is_literal_file_uri("imm.URI:LIT:foo"))
2072+
2073+    def test_has_uri_prefix(self):
2074+        self.failUnless(uri.has_uri_prefix("URI:foo"))
2075+        self.failUnless(uri.has_uri_prefix("ro.URI:foo"))
2076+        self.failUnless(uri.has_uri_prefix("imm.URI:foo"))
2077+        self.failIf(uri.has_uri_prefix(None))
2078+        self.failIf(uri.has_uri_prefix("foo"))
2079+
2080 class CHKFile(unittest.TestCase):
2081     def test_pack(self):
2082         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
2083hunk ./src/allmydata/test/test_uri.py 117
2084         self.failUnless(IFileURI.providedBy(u))
2085         self.failIf(IDirnodeURI.providedBy(u))
2086         self.failUnlessEqual(u.get_size(), 1234)
2087-        self.failUnless(u.is_readonly())
2088-        self.failIf(u.is_mutable())
2089+
2090         u_ro = u.get_readonly()
2091         self.failUnlessIdentical(u, u_ro)
2092         he = u.to_human_encoding()
2093hunk ./src/allmydata/test/test_uri.py 137
2094         self.failUnless(IFileURI.providedBy(u2))
2095         self.failIf(IDirnodeURI.providedBy(u2))
2096         self.failUnlessEqual(u2.get_size(), 1234)
2097-        self.failUnless(u2.is_readonly())
2098-        self.failIf(u2.is_mutable())
2099+
2100+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
2101+        self.failUnlessEqual(u.to_string(), u2i.to_string())
2102+        u2ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u.to_string())
2103+        self.failUnlessEqual(u.to_string(), u2ro.to_string())
2104+        u2imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u.to_string())
2105+        self.failUnlessEqual(u.to_string(), u2imm.to_string())
2106 
2107         v = u.get_verify_cap()
2108         self.failUnless(isinstance(v.to_string(), str))
2109hunk ./src/allmydata/test/test_uri.py 147
2110+        self.failUnless(v.is_readonly())
2111+        self.failIf(v.is_mutable())
2112+
2113         v2 = uri.from_string(v.to_string())
2114         self.failUnlessEqual(v, v2)
2115         he = v.to_human_encoding()
2116hunk ./src/allmydata/test/test_uri.py 162
2117                                     total_shares=10,
2118                                     size=1234)
2119         self.failUnless(isinstance(v3.to_string(), str))
2120+        self.failUnless(v3.is_readonly())
2121+        self.failIf(v3.is_mutable())
2122 
2123     def test_pack_badly(self):
2124         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
2125hunk ./src/allmydata/test/test_uri.py 217
2126         self.failUnlessEqual(readable["UEB_hash"],
2127                              base32.b2a(hashutil.uri_extension_hash(ext)))
2128 
2129-class Invalid(unittest.TestCase):
2130+class Unknown(unittest.TestCase):
2131     def test_from_future(self):
2132         # any URI type that we don't recognize should be treated as unknown
2133         future_uri = "I am a URI from the future. Whatever you do, don't "
2134hunk ./src/allmydata/test/test_uri.py 224
2135         u = uri.from_string(future_uri)
2136         self.failUnless(isinstance(u, uri.UnknownURI))
2137         self.failUnlessEqual(u.to_string(), future_uri)
2138+        self.failUnless(u.get_readonly() is None)
2139+        self.failUnless(u.get_error() is None)
2140+
2141+        u2 = uri.UnknownURI(future_uri, error=CapConstraintError("..."))
2142+        self.failUnlessEqual(u.to_string(), future_uri)
2143+        self.failUnless(u2.get_readonly() is None)
2144+        self.failUnless(isinstance(u2.get_error(), CapConstraintError))
2145 
2146 class Constraint(unittest.TestCase):
2147     def test_constraint(self):
2148hunk ./src/allmydata/test/test_uri.py 271
2149         self.failUnless(IMutableFileURI.providedBy(u2))
2150         self.failIf(IDirnodeURI.providedBy(u2))
2151 
2152+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
2153+        self.failUnless(isinstance(u2i, uri.UnknownURI), u2i)
2154+        u2ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u.to_string())
2155+        self.failUnless(isinstance(u2ro, uri.UnknownURI), u2ro)
2156+        u2imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u.to_string())
2157+        self.failUnless(isinstance(u2imm, uri.UnknownURI), u2imm)
2158+
2159         u3 = u2.get_readonly()
2160         readkey = hashutil.ssk_readkey_hash(writekey)
2161         self.failUnlessEqual(u3.fingerprint, fingerprint)
2162hunk ./src/allmydata/test/test_uri.py 288
2163         self.failUnless(IMutableFileURI.providedBy(u3))
2164         self.failIf(IDirnodeURI.providedBy(u3))
2165 
2166+        u3i = uri.from_string(u3.to_string(), deep_immutable=True)
2167+        self.failUnless(isinstance(u3i, uri.UnknownURI), u3i)
2168+        u3ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u3.to_string())
2169+        self.failUnlessEqual(u3.to_string(), u3ro.to_string())
2170+        u3imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u3.to_string())
2171+        self.failUnless(isinstance(u3imm, uri.UnknownURI), u3imm)
2172+
2173         he = u3.to_human_encoding()
2174         u3_h = uri.ReadonlySSKFileURI.init_from_human_encoding(he)
2175         self.failUnlessEqual(u3, u3_h)
2176hunk ./src/allmydata/test/test_uri.py 308
2177         self.failUnless(IMutableFileURI.providedBy(u4))
2178         self.failIf(IDirnodeURI.providedBy(u4))
2179 
2180+        u4i = uri.from_string(u4.to_string(), deep_immutable=True)
2181+        self.failUnless(isinstance(u4i, uri.UnknownURI), u4i)
2182+        u4ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u4.to_string())
2183+        self.failUnlessEqual(u4.to_string(), u4ro.to_string())
2184+        u4imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u4.to_string())
2185+        self.failUnless(isinstance(u4imm, uri.UnknownURI), u4imm)
2186+
2187         u4a = uri.from_string(u4.to_string())
2188         self.failUnlessEqual(u4a, u4)
2189         self.failUnless("ReadonlySSKFileURI" in str(u4a))
2190hunk ./src/allmydata/test/test_uri.py 357
2191         self.failIf(IFileURI.providedBy(u2))
2192         self.failUnless(IDirnodeURI.providedBy(u2))
2193 
2194+        u2i = uri.from_string(u1.to_string(), deep_immutable=True)
2195+        self.failUnless(isinstance(u2i, uri.UnknownURI))
2196+
2197         u3 = u2.get_readonly()
2198         self.failUnless(u3.is_readonly())
2199         self.failUnless(u3.is_mutable())
2200hunk ./src/allmydata/test/test_uri.py 366
2201         self.failUnless(IURI.providedBy(u3))
2202         self.failIf(IFileURI.providedBy(u3))
2203         self.failUnless(IDirnodeURI.providedBy(u3))
2204+
2205+        u3i = uri.from_string(u2.to_string(), deep_immutable=True)
2206+        self.failUnless(isinstance(u3i, uri.UnknownURI))
2207+
2208         u3n = u3._filenode_uri
2209         self.failUnless(u3n.is_readonly())
2210         self.failUnless(u3n.is_mutable())
2211hunk ./src/allmydata/test/test_uri.py 436
2212         self.failIf(IFileURI.providedBy(u2))
2213         self.failUnless(IDirnodeURI.providedBy(u2))
2214 
2215+        u2i = uri.from_string(u1.to_string(), deep_immutable=True)
2216+        self.failUnlessEqual(u1.to_string(), u2i.to_string())
2217+
2218         u3 = u2.get_readonly()
2219         self.failUnlessEqual(u3.to_string(), u2.to_string())
2220         self.failUnless(str(u3))
2221hunk ./src/allmydata/test/test_uri.py 443
2222 
2223+        u3i = uri.from_string(u2.to_string(), deep_immutable=True)
2224+        self.failUnlessEqual(u2.to_string(), u3i.to_string())
2225+
2226         u2_verifier = u2.get_verify_cap()
2227         self.failUnless(isinstance(u2_verifier,
2228                                    uri.ImmutableDirectoryURIVerifier),
2229hunk ./src/allmydata/test/test_web.py 10
2230 from twisted.web import client, error, http
2231 from twisted.python import failure, log
2232 from nevow import rend
2233-from allmydata import interfaces, uri, webish
2234+from allmydata import interfaces, uri, webish, dirnode
2235 from allmydata.storage.shares import get_share_file
2236 from allmydata.storage_client import StorageFarmBroker
2237 from allmydata.immutable import upload, download
2238hunk ./src/allmydata/test/test_web.py 21
2239 from allmydata.scripts.debug import CorruptShareOptions, corrupt_share
2240 from allmydata.util import fileutil, base32
2241 from allmydata.util.consumer import download_to_data
2242+from allmydata.util.netstring import split_netstring
2243 from allmydata.test.common import FakeCHKFileNode, FakeMutableFileNode, \
2244      create_chk_filenode, WebErrorMixin, ShouldFailMixin, make_mutable_file_uri
2245 from allmydata.interfaces import IMutableFileNode
2246hunk ./src/allmydata/test/test_web.py 739
2247         d = self.PUT(self.public_url + "/foo/new.txt", self.NEWFILE_CONTENTS)
2248         # TODO: we lose the response code, so we can't check this
2249         #self.failUnlessEqual(responsecode, 201)
2250-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2251+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
2252         d.addCallback(lambda res:
2253                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
2254                                                       self.NEWFILE_CONTENTS))
2255hunk ./src/allmydata/test/test_web.py 750
2256                      self.NEWFILE_CONTENTS)
2257         # TODO: we lose the response code, so we can't check this
2258         #self.failUnlessEqual(responsecode, 201)
2259-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2260+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
2261         d.addCallback(lambda res:
2262                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
2263                                                       self.NEWFILE_CONTENTS))
2264hunk ./src/allmydata/test/test_web.py 780
2265             self.failIf(u.is_readonly())
2266             return res
2267         d.addCallback(_check_uri)
2268-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2269+        d.addCallback(self.failUnlessURIMatchesRWChild, self._foo_node, u"new.txt")
2270         d.addCallback(lambda res:
2271                       self.failUnlessMutableChildContentsAre(self._foo_node,
2272                                                              u"new.txt",
2273hunk ./src/allmydata/test/test_web.py 800
2274         d = self.PUT(self.public_url + "/foo/bar.txt", self.NEWFILE_CONTENTS)
2275         # TODO: we lose the response code, so we can't check this
2276         #self.failUnlessEqual(responsecode, 200)
2277-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt")
2278+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt")
2279         d.addCallback(lambda res:
2280                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
2281                                                       self.NEWFILE_CONTENTS))
2282hunk ./src/allmydata/test/test_web.py 825
2283     def test_PUT_NEWFILEURL_mkdirs(self):
2284         d = self.PUT(self.public_url + "/foo/newdir/new.txt", self.NEWFILE_CONTENTS)
2285         fn = self._foo_node
2286-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"newdir/new.txt")
2287+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"newdir/new.txt")
2288         d.addCallback(lambda res: self.failIfNodeHasChild(fn, u"new.txt"))
2289         d.addCallback(lambda res: self.failUnlessNodeHasChild(fn, u"newdir"))
2290         d.addCallback(lambda res:
2291hunk ./src/allmydata/test/test_web.py 958
2292             self.failUnless(re.search(get_sub, res), res)
2293         d.addCallback(_check)
2294 
2295-        # look at a directory which is readonly
2296+        # look at a readonly directory
2297         d.addCallback(lambda res:
2298                       self.GET(self.public_url + "/reedownlee", followRedirect=True))
2299         def _check2(res):
2300hunk ./src/allmydata/test/test_web.py 1171
2301         return d
2302 
2303     def test_POST_NEWDIRURL_initial_children(self):
2304-        (newkids, filecap1, filecap2, filecap3,
2305-         dircap) = self._create_initial_children()
2306+        (newkids, caps) = self._create_initial_children()
2307         d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-with-children",
2308                        simplejson.dumps(newkids))
2309         def _check(uri):
2310hunk ./src/allmydata/test/test_web.py 1178
2311             n = self.s.create_node_from_uri(uri.strip())
2312             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2313             d2.addCallback(lambda ign:
2314-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2315+                           self.failUnlessROChildURIIs(n, u"child-imm",
2316+                                                       caps['filecap1']))
2317+            d2.addCallback(lambda ign:
2318+                           self.failUnlessRWChildURIIs(n, u"child-mutable",
2319+                                                       caps['filecap2']))
2320+            d2.addCallback(lambda ign:
2321+                           self.failUnlessROChildURIIs(n, u"child-mutable-ro",
2322+                                                       caps['filecap3']))
2323             d2.addCallback(lambda ign:
2324hunk ./src/allmydata/test/test_web.py 1187
2325-                           self.failUnlessChildURIIs(n, u"child-mutable",
2326-                                                     filecap2))
2327+                           self.failUnlessROChildURIIs(n, u"unknownchild-ro",
2328+                                                       caps['unknown_rocap']))
2329             d2.addCallback(lambda ign:
2330hunk ./src/allmydata/test/test_web.py 1190
2331-                           self.failUnlessChildURIIs(n, u"child-mutable-ro",
2332-                                                     filecap3))
2333+                           self.failUnlessRWChildURIIs(n, u"unknownchild-rw",
2334+                                                       caps['unknown_rwcap']))
2335             d2.addCallback(lambda ign:
2336hunk ./src/allmydata/test/test_web.py 1193
2337-                           self.failUnlessChildURIIs(n, u"dirchild", dircap))
2338+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2339+                                                       caps['unknown_immcap']))
2340+            d2.addCallback(lambda ign:
2341+                           self.failUnlessRWChildURIIs(n, u"dirchild",
2342+                                                       caps['dircap']))
2343             return d2
2344         d.addCallback(_check)
2345         d.addCallback(lambda res:
2346hunk ./src/allmydata/test/test_web.py 1205
2347         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2348         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2349         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2350-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2351+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2352         return d
2353 
2354     def test_POST_NEWDIRURL_immutable(self):
2355hunk ./src/allmydata/test/test_web.py 1209
2356-        (newkids, filecap1, immdircap) = self._create_immutable_children()
2357+        (newkids, caps) = self._create_immutable_children()
2358         d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-immutable",
2359                        simplejson.dumps(newkids))
2360         def _check(uri):
2361hunk ./src/allmydata/test/test_web.py 1216
2362             n = self.s.create_node_from_uri(uri.strip())
2363             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2364             d2.addCallback(lambda ign:
2365-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2366+                           self.failUnlessROChildURIIs(n, u"child-imm",
2367+                                                       caps['filecap1']))
2368+            d2.addCallback(lambda ign:
2369+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2370+                                                       caps['unknown_immcap']))
2371             d2.addCallback(lambda ign:
2372hunk ./src/allmydata/test/test_web.py 1222
2373-                           self.failUnlessChildURIIs(n, u"dirchild-imm",
2374-                                                     immdircap))
2375+                           self.failUnlessROChildURIIs(n, u"dirchild-imm",
2376+                                                       caps['immdircap']))
2377             return d2
2378         d.addCallback(_check)
2379         d.addCallback(lambda res:
2380hunk ./src/allmydata/test/test_web.py 1231
2381         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2382         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2383         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2384-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2385+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2386+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2387+        d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap'])
2388         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2389hunk ./src/allmydata/test/test_web.py 1235
2390-        d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap)
2391+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap'])
2392         d.addErrback(self.explain_web_error)
2393         return d
2394 
2395hunk ./src/allmydata/test/test_web.py 1240
2396     def test_POST_NEWDIRURL_immutable_bad(self):
2397-        (newkids, filecap1, filecap2, filecap3,
2398-         dircap) = self._create_initial_children()
2399+        (newkids, caps) = self._create_initial_children()
2400         d = self.shouldFail2(error.Error, "test_POST_NEWDIRURL_immutable_bad",
2401                              "400 Bad Request",
2402hunk ./src/allmydata/test/test_web.py 1243
2403-                             "a mkdir-immutable operation was given a child that was not itself immutable",
2404+                             "needed to be immutable but was not",
2405                              self.POST2,
2406                              self.public_url + "/foo/newdir?t=mkdir-immutable",
2407                              simplejson.dumps(newkids))
2408hunk ./src/allmydata/test/test_web.py 1365
2409         d.addCallback(_check)
2410         return d
2411 
2412-    def failUnlessChildURIIs(self, node, name, expected_uri):
2413+    def failUnlessRWChildURIIs(self, node, name, expected_uri):
2414         assert isinstance(name, unicode)
2415         d = node.get_child_at_path(name)
2416         def _check(child):
2417hunk ./src/allmydata/test/test_web.py 1369
2418+            self.failUnless(child.is_unknown() or not child.is_readonly())
2419             self.failUnlessEqual(child.get_uri(), expected_uri.strip())
2420hunk ./src/allmydata/test/test_web.py 1371
2421+            self.failUnlessEqual(child.get_write_uri(), expected_uri.strip())
2422+            expected_ro_uri = self._make_readonly(expected_uri)
2423+            if expected_ro_uri:
2424+                self.failUnlessEqual(child.get_readonly_uri(), expected_ro_uri.strip())
2425+        d.addCallback(_check)
2426+        return d
2427+
2428+    def failUnlessROChildURIIs(self, node, name, expected_uri):
2429+        assert isinstance(name, unicode)
2430+        d = node.get_child_at_path(name)
2431+        def _check(child):
2432+            self.failUnless(child.is_unknown() or child.is_readonly())
2433+            self.failUnlessEqual(child.get_write_uri(), None)
2434+            self.failUnlessEqual(child.get_uri(), expected_uri.strip())
2435+            self.failUnlessEqual(child.get_readonly_uri(), expected_uri.strip())
2436+        d.addCallback(_check)
2437+        return d
2438+
2439+    def failUnlessURIMatchesRWChild(self, got_uri, node, name):
2440+        assert isinstance(name, unicode)
2441+        d = node.get_child_at_path(name)
2442+        def _check(child):
2443+            self.failUnless(child.is_unknown() or not child.is_readonly())
2444+            self.failUnlessEqual(child.get_uri(), got_uri.strip())
2445+            self.failUnlessEqual(child.get_write_uri(), got_uri.strip())
2446+            expected_ro_uri = self._make_readonly(got_uri)
2447+            if expected_ro_uri:
2448+                self.failUnlessEqual(child.get_readonly_uri(), expected_ro_uri.strip())
2449         d.addCallback(_check)
2450         return d
2451 
2452hunk ./src/allmydata/test/test_web.py 1402
2453-    def failUnlessURIMatchesChild(self, got_uri, node, name):
2454+    def failUnlessURIMatchesROChild(self, got_uri, node, name):
2455         assert isinstance(name, unicode)
2456         d = node.get_child_at_path(name)
2457         def _check(child):
2458hunk ./src/allmydata/test/test_web.py 1406
2459+            self.failUnless(child.is_unknown() or child.is_readonly())
2460+            self.failUnlessEqual(child.get_write_uri(), None)
2461             self.failUnlessEqual(got_uri.strip(), child.get_uri())
2462hunk ./src/allmydata/test/test_web.py 1409
2463+            self.failUnlessEqual(got_uri.strip(), child.get_readonly_uri())
2464         d.addCallback(_check)
2465         return d
2466 
2467hunk ./src/allmydata/test/test_web.py 1420
2468         d = self.POST(self.public_url + "/foo", t="upload",
2469                       file=("new.txt", self.NEWFILE_CONTENTS))
2470         fn = self._foo_node
2471-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2472+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt")
2473         d.addCallback(lambda res:
2474                       self.failUnlessChildContentsAre(fn, u"new.txt",
2475                                                       self.NEWFILE_CONTENTS))
2476hunk ./src/allmydata/test/test_web.py 1431
2477         d = self.POST(self.public_url + "/foo", t="upload",
2478                       file=(filename, self.NEWFILE_CONTENTS))
2479         fn = self._foo_node
2480-        d.addCallback(self.failUnlessURIMatchesChild, fn, filename)
2481+        d.addCallback(self.failUnlessURIMatchesROChild, fn, filename)
2482         d.addCallback(lambda res:
2483                       self.failUnlessChildContentsAre(fn, filename,
2484                                                       self.NEWFILE_CONTENTS))
2485hunk ./src/allmydata/test/test_web.py 1448
2486                       name=filename,
2487                       file=("overridden", self.NEWFILE_CONTENTS))
2488         fn = self._foo_node
2489-        d.addCallback(self.failUnlessURIMatchesChild, fn, filename)
2490+        d.addCallback(self.failUnlessURIMatchesROChild, fn, filename)
2491         d.addCallback(lambda res:
2492                       self.failUnlessChildContentsAre(fn, filename,
2493                                                       self.NEWFILE_CONTENTS))
2494hunk ./src/allmydata/test/test_web.py 1550
2495         d = self.POST(self.public_url + "/foo", t="upload", mutable="true",
2496                       file=("new.txt", self.NEWFILE_CONTENTS))
2497         fn = self._foo_node
2498-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2499+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2500         d.addCallback(lambda res:
2501                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2502                                                              self.NEWFILE_CONTENTS))
2503hunk ./src/allmydata/test/test_web.py 1569
2504                       self.POST(self.public_url + "/foo", t="upload",
2505                                 mutable="true",
2506                                 file=("new.txt", NEWER_CONTENTS)))
2507-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2508+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2509         d.addCallback(lambda res:
2510                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2511                                                              NEWER_CONTENTS))
2512hunk ./src/allmydata/test/test_web.py 1585
2513         NEW2_CONTENTS = NEWER_CONTENTS + "overwrite with PUT\n"
2514         d.addCallback(lambda res:
2515                       self.PUT(self.public_url + "/foo/new.txt", NEW2_CONTENTS))
2516-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2517+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2518         d.addCallback(lambda res:
2519                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2520                                                              NEW2_CONTENTS))
2521hunk ./src/allmydata/test/test_web.py 1714
2522         d = self.POST(self.public_url + "/foo", t="upload",
2523                       file=("bar.txt", self.NEWFILE_CONTENTS))
2524         fn = self._foo_node
2525-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"bar.txt")
2526+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"bar.txt")
2527         d.addCallback(lambda res:
2528                       self.failUnlessChildContentsAre(fn, u"bar.txt",
2529                                                       self.NEWFILE_CONTENTS))
2530hunk ./src/allmydata/test/test_web.py 1765
2531         fn = self._foo_node
2532         d = self.POST(self.public_url + "/foo", t="upload",
2533                       name="new.txt", file=self.NEWFILE_CONTENTS)
2534-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2535+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt")
2536         d.addCallback(lambda res:
2537                       self.failUnlessChildContentsAre(fn, u"new.txt",
2538                                                       self.NEWFILE_CONTENTS))
2539hunk ./src/allmydata/test/test_web.py 2028
2540         return d
2541 
2542     def test_POST_mkdir_initial_children(self):
2543-        newkids, filecap1, ign, ign, ign = self._create_initial_children()
2544+        (newkids, caps) = self._create_initial_children()
2545         d = self.POST2(self.public_url +
2546                        "/foo?t=mkdir-with-children&name=newdir",
2547                        simplejson.dumps(newkids))
2548hunk ./src/allmydata/test/test_web.py 2037
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         return d
2555 
2556     def test_POST_mkdir_immutable(self):
2557hunk ./src/allmydata/test/test_web.py 2041
2558-        (newkids, filecap1, immdircap) = self._create_immutable_children()
2559+        (newkids, caps) = self._create_immutable_children()
2560         d = self.POST2(self.public_url +
2561                        "/foo?t=mkdir-immutable&name=newdir",
2562                        simplejson.dumps(newkids))
2563hunk ./src/allmydata/test/test_web.py 2050
2564         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2565         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2566         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2567-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2568+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2569+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2570+        d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap'])
2571         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2572hunk ./src/allmydata/test/test_web.py 2054
2573-        d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap)
2574+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap'])
2575         return d
2576 
2577     def test_POST_mkdir_immutable_bad(self):
2578hunk ./src/allmydata/test/test_web.py 2058
2579-        (newkids, filecap1, filecap2, filecap3,
2580-         dircap) = self._create_initial_children()
2581+        (newkids, caps) = self._create_initial_children()
2582         d = self.shouldFail2(error.Error, "test_POST_mkdir_immutable_bad",
2583                              "400 Bad Request",
2584hunk ./src/allmydata/test/test_web.py 2061
2585-                             "a mkdir-immutable operation was given a child that was not itself immutable",
2586+                             "needed to be immutable but was not",
2587                              self.POST2,
2588                              self.public_url +
2589                              "/foo?t=mkdir-immutable&name=newdir",
2590hunk ./src/allmydata/test/test_web.py 2120
2591         d.addErrback(self.explain_web_error)
2592         return d
2593 
2594+    def _make_readonly(self, u):
2595+        ro_uri = uri.from_string(u).get_readonly()
2596+        if ro_uri is None:
2597+            return None
2598+        return ro_uri.to_string()
2599+
2600     def _create_initial_children(self):
2601         contents, n, filecap1 = self.makefile(12)
2602         md1 = {"metakey1": "metavalue1"}
2603hunk ./src/allmydata/test/test_web.py 2132
2604         filecap2 = make_mutable_file_uri()
2605         node3 = self.s.create_node_from_uri(make_mutable_file_uri())
2606         filecap3 = node3.get_readonly_uri()
2607+        unknown_rwcap = "lafs://from_the_future"
2608+        unknown_rocap = "ro.lafs://readonly_from_the_future"
2609+        unknown_immcap = "imm.lafs://immutable_from_the_future"
2610         node4 = self.s.create_node_from_uri(make_mutable_file_uri())
2611         dircap = DirectoryNode(node4, None, None).get_uri()
2612hunk ./src/allmydata/test/test_web.py 2137
2613-        newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1,
2614-                                               "metadata": md1, }],
2615-                   u"child-mutable": ["filenode", {"rw_uri": filecap2}],
2616+        newkids = {u"child-imm":        ["filenode", {"rw_uri": filecap1,
2617+                                                      "ro_uri": self._make_readonly(filecap1),
2618+                                                      "metadata": md1, }],
2619+                   u"child-mutable":    ["filenode", {"rw_uri": filecap2,
2620+                                                      "ro_uri": self._make_readonly(filecap2)}],
2621                    u"child-mutable-ro": ["filenode", {"ro_uri": filecap3}],
2622hunk ./src/allmydata/test/test_web.py 2143
2623-                   u"dirchild": ["dirnode", {"rw_uri": dircap}],
2624+                   u"unknownchild-rw":  ["unknown",  {"rw_uri": unknown_rwcap,
2625+                                                      "ro_uri": unknown_rocap}],
2626+                   u"unknownchild-ro":  ["unknown",  {"ro_uri": unknown_rocap}],
2627+                   u"unknownchild-imm": ["unknown",  {"ro_uri": unknown_immcap}],
2628+                   u"dirchild":         ["dirnode",  {"rw_uri": dircap,
2629+                                                      "ro_uri": self._make_readonly(dircap)}],
2630                    }
2631hunk ./src/allmydata/test/test_web.py 2150
2632-        return newkids, filecap1, filecap2, filecap3, dircap
2633+        return newkids, {'filecap1': filecap1,
2634+                         'filecap2': filecap2,
2635+                         'filecap3': filecap3,
2636+                         'unknown_rwcap': unknown_rwcap,
2637+                         'unknown_rocap': unknown_rocap,
2638+                         'unknown_immcap': unknown_immcap,
2639+                         'dircap': dircap}
2640 
2641     def _create_immutable_children(self):
2642         contents, n, filecap1 = self.makefile(12)
2643hunk ./src/allmydata/test/test_web.py 2164
2644         tnode = create_chk_filenode("immutable directory contents\n"*10)
2645         dnode = DirectoryNode(tnode, None, None)
2646         assert not dnode.is_mutable()
2647+        unknown_immcap = "imm.lafs://immutable_from_the_future"
2648         immdircap = dnode.get_uri()
2649hunk ./src/allmydata/test/test_web.py 2166
2650-        newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1,
2651-                                               "metadata": md1, }],
2652-                   u"dirchild-imm": ["dirnode", {"ro_uri": immdircap}],
2653+        newkids = {u"child-imm":        ["filenode", {"ro_uri": filecap1,
2654+                                                      "metadata": md1, }],
2655+                   u"unknownchild-imm": ["unknown",  {"ro_uri": unknown_immcap}],
2656+                   u"dirchild-imm":     ["dirnode",  {"ro_uri": immdircap}],
2657                    }
2658hunk ./src/allmydata/test/test_web.py 2171
2659-        return newkids, filecap1, immdircap
2660+        return newkids, {'filecap1': filecap1,
2661+                         'unknown_immcap': unknown_immcap,
2662+                         'immdircap': immdircap}
2663 
2664     def test_POST_mkdir_no_parentdir_initial_children(self):
2665hunk ./src/allmydata/test/test_web.py 2176
2666-        (newkids, filecap1, filecap2, filecap3,
2667-         dircap) = self._create_initial_children()
2668+        (newkids, caps) = self._create_initial_children()
2669         d = self.POST2("/uri?t=mkdir-with-children", simplejson.dumps(newkids))
2670         def _after_mkdir(res):
2671             self.failUnless(res.startswith("URI:DIR"), res)
2672hunk ./src/allmydata/test/test_web.py 2183
2673             n = self.s.create_node_from_uri(res)
2674             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2675             d2.addCallback(lambda ign:
2676-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2677+                           self.failUnlessROChildURIIs(n, u"child-imm",
2678+                                                       caps['filecap1']))
2679+            d2.addCallback(lambda ign:
2680+                           self.failUnlessRWChildURIIs(n, u"child-mutable",
2681+                                                       caps['filecap2']))
2682             d2.addCallback(lambda ign:
2683hunk ./src/allmydata/test/test_web.py 2189
2684-                           self.failUnlessChildURIIs(n, u"child-mutable",
2685-                                                     filecap2))
2686+                           self.failUnlessROChildURIIs(n, u"child-mutable-ro",
2687+                                                       caps['filecap3']))
2688             d2.addCallback(lambda ign:
2689hunk ./src/allmydata/test/test_web.py 2192
2690-                           self.failUnlessChildURIIs(n, u"child-mutable-ro",
2691-                                                     filecap3))
2692+                           self.failUnlessRWChildURIIs(n, u"unknownchild-rw",
2693+                                                       caps['unknown_rwcap']))
2694             d2.addCallback(lambda ign:
2695hunk ./src/allmydata/test/test_web.py 2195
2696-                           self.failUnlessChildURIIs(n, u"dirchild", dircap))
2697+                           self.failUnlessROChildURIIs(n, u"unknownchild-ro",
2698+                                                       caps['unknown_rocap']))
2699+            d2.addCallback(lambda ign:
2700+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2701+                                                       caps['unknown_immcap']))
2702+            d2.addCallback(lambda ign:
2703+                           self.failUnlessRWChildURIIs(n, u"dirchild",
2704+                                                       caps['dircap']))
2705             return d2
2706         d.addCallback(_after_mkdir)
2707         return d
2708hunk ./src/allmydata/test/test_web.py 2210
2709     def test_POST_mkdir_no_parentdir_unexpected_children(self):
2710         # the regular /uri?t=mkdir operation is specified to ignore its body.
2711         # Only t=mkdir-with-children pays attention to it.
2712-        (newkids, filecap1, filecap2, filecap3,
2713-         dircap) = self._create_initial_children()
2714+        (newkids, caps) = self._create_initial_children()
2715         d = self.shouldHTTPError("POST t=mkdir unexpected children",
2716                                  400, "Bad Request",
2717                                  "t=mkdir does not accept children=, "
2718hunk ./src/allmydata/test/test_web.py 2227
2719         return d
2720 
2721     def test_POST_mkdir_no_parentdir_immutable(self):
2722-        (newkids, filecap1, immdircap) = self._create_immutable_children()
2723+        (newkids, caps) = self._create_immutable_children()
2724         d = self.POST2("/uri?t=mkdir-immutable", simplejson.dumps(newkids))
2725         def _after_mkdir(res):
2726             self.failUnless(res.startswith("URI:DIR"), res)
2727hunk ./src/allmydata/test/test_web.py 2234
2728             n = self.s.create_node_from_uri(res)
2729             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2730             d2.addCallback(lambda ign:
2731-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2732+                           self.failUnlessROChildURIIs(n, u"child-imm",
2733+                                                          caps['filecap1']))
2734+            d2.addCallback(lambda ign:
2735+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2736+                                                          caps['unknown_immcap']))
2737             d2.addCallback(lambda ign:
2738hunk ./src/allmydata/test/test_web.py 2240
2739-                           self.failUnlessChildURIIs(n, u"dirchild-imm",
2740-                                                     immdircap))
2741+                           self.failUnlessROChildURIIs(n, u"dirchild-imm",
2742+                                                          caps['immdircap']))
2743             return d2
2744         d.addCallback(_after_mkdir)
2745         return d
2746hunk ./src/allmydata/test/test_web.py 2247
2747 
2748     def test_POST_mkdir_no_parentdir_immutable_bad(self):
2749-        (newkids, filecap1, filecap2, filecap3,
2750-         dircap) = self._create_initial_children()
2751+        (newkids, caps) = self._create_initial_children()
2752         d = self.shouldFail2(error.Error,
2753                              "test_POST_mkdir_no_parentdir_immutable_bad",
2754                              "400 Bad Request",
2755hunk ./src/allmydata/test/test_web.py 2251
2756-                             "a mkdir-immutable operation was given a child that was not itself immutable",
2757+                             "needed to be immutable but was not",
2758                              self.POST2,
2759                              "/uri?t=mkdir-immutable",
2760                              simplejson.dumps(newkids))
2761hunk ./src/allmydata/test/test_web.py 2359
2762 
2763         d = client.getPage(url, method="POST", postdata=reqbody)
2764         def _then(res):
2765-            self.failUnlessURIMatchesChild(newuri9, self._foo_node, u"atomic_added_1")
2766-            self.failUnlessURIMatchesChild(newuri10, self._foo_node, u"atomic_added_2")
2767-            self.failUnlessURIMatchesChild(newuri11, self._foo_node, u"atomic_added_3")
2768+            self.failUnlessURIMatchesROChild(newuri9, self._foo_node, u"atomic_added_1")
2769+            self.failUnlessURIMatchesROChild(newuri10, self._foo_node, u"atomic_added_2")
2770+            self.failUnlessURIMatchesROChild(newuri11, self._foo_node, u"atomic_added_3")
2771 
2772         d.addCallback(_then)
2773         d.addErrback(self.dump_error)
2774hunk ./src/allmydata/test/test_web.py 2373
2775     def test_POST_put_uri(self):
2776         contents, n, newuri = self.makefile(8)
2777         d = self.POST(self.public_url + "/foo", t="uri", name="new.txt", uri=newuri)
2778-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2779+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
2780         d.addCallback(lambda res:
2781                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
2782                                                       contents))
2783hunk ./src/allmydata/test/test_web.py 2382
2784     def test_POST_put_uri_replace(self):
2785         contents, n, newuri = self.makefile(8)
2786         d = self.POST(self.public_url + "/foo", t="uri", name="bar.txt", uri=newuri)
2787-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt")
2788+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt")
2789         d.addCallback(lambda res:
2790                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
2791                                                       contents))
2792hunk ./src/allmydata/test/test_web.py 2611
2793             d.addCallback(lambda res:
2794                           self.failUnlessEqual(res.strip(), new_uri))
2795             d.addCallback(lambda res:
2796-                          self.failUnlessChildURIIs(self.public_root,
2797-                                                    u"foo",
2798-                                                    new_uri))
2799+                          self.failUnlessRWChildURIIs(self.public_root,
2800+                                                      u"foo",
2801+                                                      new_uri))
2802             return d
2803         d.addCallback(_made_dir)
2804         return d
2805hunk ./src/allmydata/test/test_web.py 2630
2806                                  self.public_url + "/foo?t=uri&replace=false",
2807                                  new_uri)
2808             d.addCallback(lambda res:
2809-                          self.failUnlessChildURIIs(self.public_root,
2810-                                                    u"foo",
2811-                                                    self._foo_uri))
2812+                          self.failUnlessRWChildURIIs(self.public_root,
2813+                                                      u"foo",
2814+                                                      self._foo_uri))
2815             return d
2816         d.addCallback(_made_dir)
2817         return d
2818hunk ./src/allmydata/test/test_web.py 2642
2819                                  "400 Bad Request", "PUT to a directory",
2820                                  self.PUT, self.public_url + "/foo?t=BOGUS", "")
2821         d.addCallback(lambda res:
2822-                      self.failUnlessChildURIIs(self.public_root,
2823-                                                u"foo",
2824-                                                self._foo_uri))
2825+                      self.failUnlessRWChildURIIs(self.public_root,
2826+                                                  u"foo",
2827+                                                  self._foo_uri))
2828         return d
2829 
2830     def test_PUT_NEWFILEURL_uri(self):
2831hunk ./src/allmydata/test/test_web.py 3171
2832         d.addErrback(self.explain_web_error)
2833         return d
2834 
2835-    def test_unknown(self):
2836+    def test_unknown(self, immutable=False):
2837         self.basedir = "web/Grid/unknown"
2838hunk ./src/allmydata/test/test_web.py 3173
2839+        if immutable:
2840+            self.basedir = "web/Grid/unknown-immutable"
2841+
2842         self.set_up_grid()
2843         c0 = self.g.clients[0]
2844         self.uris = {}
2845hunk ./src/allmydata/test/test_web.py 3181
2846         self.fileurls = {}
2847 
2848-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
2849-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
2850+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
2851+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
2852         # the future cap format may contain slashes, which must be tolerated
2853hunk ./src/allmydata/test/test_web.py 3184
2854-        expected_info_url = "uri/%s?t=info" % urllib.quote(future_writecap,
2855+        expected_info_url = "uri/%s?t=info" % urllib.quote(future_write_uri,
2856                                                            safe="")
2857hunk ./src/allmydata/test/test_web.py 3186
2858-        future_node = UnknownNode(future_writecap, future_readcap)
2859+
2860+        if immutable:
2861+            name = u"future-imm"
2862+            future_node = UnknownNode(None, future_read_uri, deep_immutable=True)
2863+            d = c0.create_immutable_dirnode({name: (future_node, {})})
2864+        else:
2865+            name = u"future"
2866+            future_node = UnknownNode(future_write_uri, future_read_uri)
2867+            d = c0.create_dirnode()
2868 
2869hunk ./src/allmydata/test/test_web.py 3196
2870-        d = c0.create_dirnode()
2871         def _stash_root_and_create_file(n):
2872             self.rootnode = n
2873             self.rooturl = "uri/" + urllib.quote(n.get_uri()) + "/"
2874hunk ./src/allmydata/test/test_web.py 3200
2875             self.rourl = "uri/" + urllib.quote(n.get_readonly_uri()) + "/"
2876-            return self.rootnode.set_node(u"future", future_node)
2877+            if not immutable:
2878+                return self.rootnode.set_node(name, future_node)
2879         d.addCallback(_stash_root_and_create_file)
2880hunk ./src/allmydata/test/test_web.py 3203
2881+
2882         # make sure directory listing tolerates unknown nodes
2883         d.addCallback(lambda ign: self.GET(self.rooturl))
2884hunk ./src/allmydata/test/test_web.py 3206
2885-        def _check_html(res):
2886-            self.failUnlessIn("<td>future</td>", res)
2887-            # find the More Info link for "future", should be relative
2888+        def _check_directory_html(res):
2889+            self.failUnlessIn("<td>%s</td>" % (str(name),), res)
2890+            # find the More Info link for name, should be relative
2891             mo = re.search(r'<a href="([^"]+)">More Info</a>', res)
2892             info_url = mo.group(1)
2893hunk ./src/allmydata/test/test_web.py 3211
2894-            self.failUnlessEqual(info_url, "future?t=info")
2895+            self.failUnlessEqual(info_url, "%s?t=info" % (str(name),))
2896+        d.addCallback(_check_directory_html)
2897 
2898hunk ./src/allmydata/test/test_web.py 3214
2899-        d.addCallback(_check_html)
2900         d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json"))
2901hunk ./src/allmydata/test/test_web.py 3215
2902-        def _check_json(res, expect_writecap):
2903+        def _check_directory_json(res, expect_rw_uri):
2904             data = simplejson.loads(res)
2905             self.failUnlessEqual(data[0], "dirnode")
2906hunk ./src/allmydata/test/test_web.py 3218
2907-            f = data[1]["children"]["future"]
2908+            f = data[1]["children"][name]
2909             self.failUnlessEqual(f[0], "unknown")
2910hunk ./src/allmydata/test/test_web.py 3220
2911-            if expect_writecap:
2912-                self.failUnlessEqual(f[1]["rw_uri"], future_writecap)
2913+            if expect_rw_uri:
2914+                self.failUnlessEqual(f[1]["rw_uri"], future_write_uri)
2915             else:
2916                 self.failIfIn("rw_uri", f[1])
2917hunk ./src/allmydata/test/test_web.py 3224
2918-            self.failUnlessEqual(f[1]["ro_uri"], future_readcap)
2919+            self.failUnlessEqual(f[1]["ro_uri"],
2920+                                 ("imm." if immutable else "ro.") + future_read_uri)
2921             self.failUnless("metadata" in f[1])
2922hunk ./src/allmydata/test/test_web.py 3227
2923-        d.addCallback(_check_json, expect_writecap=True)
2924-        d.addCallback(lambda ign: self.GET(expected_info_url))
2925-        def _check_info(res, expect_readcap):
2926+        d.addCallback(_check_directory_json, expect_rw_uri=not immutable)
2927+
2928+        def _check_info(res, expect_rw_uri, expect_ro_uri):
2929             self.failUnlessIn("Object Type: <span>unknown</span>", res)
2930hunk ./src/allmydata/test/test_web.py 3231
2931-            self.failUnlessIn(future_writecap, res)
2932-            if expect_readcap:
2933-                self.failUnlessIn(future_readcap, res)
2934+            if expect_rw_uri:
2935+                self.failUnlessIn(future_write_uri, res)
2936+            if expect_ro_uri:
2937+                self.failUnlessIn(future_read_uri, res)
2938+            else:
2939+                self.failIfIn(future_read_uri, res)
2940             self.failIfIn("Raw data as", res)
2941             self.failIfIn("Directory writecap", res)
2942             self.failIfIn("Checker Operations", res)
2943hunk ./src/allmydata/test/test_web.py 3242
2944             self.failIfIn("Mutable File Operations", res)
2945             self.failIfIn("Directory Operations", res)
2946-        d.addCallback(_check_info, expect_readcap=False)
2947-        d.addCallback(lambda ign: self.GET(self.rooturl+"future?t=info"))
2948-        d.addCallback(_check_info, expect_readcap=True)
2949+
2950+        # FIXME: these should have expect_rw_uri=not immutable; I don't know
2951+        # why they fail. Possibly related to ticket #922.
2952+
2953+        d.addCallback(lambda ign: self.GET(expected_info_url))
2954+        d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=False)
2955+        d.addCallback(lambda ign: self.GET("%s%s?t=info" % (self.rooturl, str(name))))
2956+        d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=True)
2957+
2958+        def _check_json(res, expect_rw_uri):
2959+            data = simplejson.loads(res)
2960+            self.failUnlessEqual(data[0], "unknown")
2961+            if expect_rw_uri:
2962+                self.failUnlessEqual(data[1]["rw_uri"], future_write_uri)
2963+            else:
2964+                self.failIfIn("rw_uri", data[1])
2965+            self.failUnlessEqual(data[1]["ro_uri"],
2966+                                 ("imm." if immutable else "ro.") + future_read_uri)
2967+            # TODO: check metadata contents
2968+            self.failUnless("metadata" in data[1])
2969+
2970+        d.addCallback(lambda ign: self.GET("%s%s?t=json" % (self.rooturl, str(name))))
2971+        d.addCallback(_check_json, expect_rw_uri=not immutable)
2972 
2973         # and make sure that a read-only version of the directory can be
2974hunk ./src/allmydata/test/test_web.py 3267
2975-        # rendered too. This version will not have future_writecap
2976+        # rendered too. This version will not have future_write_uri, whether
2977+        # or not future_node was immutable.
2978         d.addCallback(lambda ign: self.GET(self.rourl))
2979hunk ./src/allmydata/test/test_web.py 3270
2980-        d.addCallback(_check_html)
2981+        d.addCallback(_check_directory_html)
2982         d.addCallback(lambda ign: self.GET(self.rourl+"?t=json"))
2983hunk ./src/allmydata/test/test_web.py 3272
2984-        d.addCallback(_check_json, expect_writecap=False)
2985+        d.addCallback(_check_directory_json, expect_rw_uri=False)
2986+
2987+        d.addCallback(lambda ign: self.GET("%s%s?t=json" % (self.rourl, str(name))))
2988+        d.addCallback(_check_json, expect_rw_uri=False)
2989+       
2990+        # TODO: check that getting t=info from the Info link in the ro directory
2991+        # works, and does not include the writecap URI.
2992+        return d
2993+
2994+    def test_immutable_unknown(self):
2995+        return self.test_unknown(immutable=True)
2996+
2997+    def test_mutant_dirnodes_are_omitted(self):
2998+        self.basedir = "web/Grid/mutant_dirnodes_are_omitted"
2999+
3000+        self.set_up_grid()
3001+        c = self.g.clients[0]
3002+        nm = c.nodemaker
3003+        self.uris = {}
3004+        self.fileurls = {}
3005+
3006+        lonely_uri = "URI:LIT:n5xgk" # LIT for "one"
3007+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
3008+        mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
3009+       
3010+        # This method tests mainly dirnode, but we'd have to duplicate code in order to
3011+        # test the dirnode and web layers separately.
3012+       
3013+        # 'lonely' is a valid LIT child, 'ro' is a mutant child with an SSK-RO readcap,
3014+        # and 'write-in-ro' is a mutant child with an SSK writecap in the ro_uri field.
3015+        # When the directory is read, the mutants should be silently disposed of, leaving
3016+        # their lonely sibling.
3017+        # We don't test the case of a retrieving a cap from the encrypted rw_uri field,
3018+        # because immutable directories don't have a writecap and therefore that field
3019+        # isn't (and can't be) decrypted.
3020+        # TODO: The field still exists in the netstring. Technically we should check what
3021+        # happens if something is put there (it should be ignored), but that can wait.
3022+
3023+        lonely_child = nm.create_from_cap(lonely_uri)
3024+        mutant_ro_child = nm.create_from_cap(mut_read_uri)
3025+        mutant_write_in_ro_child = nm.create_from_cap(mut_write_uri)
3026+
3027+        def _by_hook_or_by_crook():
3028+            return True
3029+        for n in [mutant_ro_child, mutant_write_in_ro_child]:
3030+            n.is_allowed_in_immutable_directory = _by_hook_or_by_crook
3031+
3032+        mutant_write_in_ro_child.get_write_uri    = lambda: None
3033+        mutant_write_in_ro_child.get_readonly_uri = lambda: mut_write_uri
3034+
3035+        kids = {u"lonely":      (lonely_child, {}),
3036+                u"ro":          (mutant_ro_child, {}),
3037+                u"write-in-ro": (mutant_write_in_ro_child, {}),
3038+                }
3039+        d = c.create_immutable_dirnode(kids)
3040+       
3041+        def _created(dn):
3042+            self.failUnless(isinstance(dn, dirnode.DirectoryNode))
3043+            self.failIf(dn.is_mutable())
3044+            self.failUnless(dn.is_readonly())
3045+            # This checks that if we somehow ended up calling dn._decrypt_rwcapdata, it would fail.
3046+            self.failIf(hasattr(dn._node, 'get_writekey'))
3047+            rep = str(dn)
3048+            self.failUnless("RO-IMM" in rep)
3049+            cap = dn.get_cap()
3050+            self.failUnlessIn("CHK", cap.to_string())
3051+            self.cap = cap
3052+            self.rootnode = dn
3053+            self.rooturl = "uri/" + urllib.quote(dn.get_uri()) + "/"
3054+            return download_to_data(dn._node)
3055+        d.addCallback(_created)
3056+
3057+        def _check_data(data):
3058+            # Decode the netstring representation of the directory to check that all children
3059+            # are present. This is a bit of an abstraction violation, but there's not really
3060+            # any other way to do it given that the real DirectoryNode._unpack_contents would
3061+            # strip the mutant children out (which is what we're trying to test, later).
3062+            position = 0
3063+            numkids = 0
3064+            while position < len(data):
3065+                entries, position = split_netstring(data, 1, position)
3066+                entry = entries[0]
3067+                (name, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
3068+                name = name.decode("utf-8")
3069+                self.failUnless(rwcapdata == "")
3070+                ro_uri = ro_uri.strip()
3071+                if name in kids:
3072+                    self.failIfEqual(ro_uri, "")
3073+                    (expected_child, ign) = kids[name]
3074+                    self.failUnlessEqual(ro_uri, expected_child.get_readonly_uri())
3075+                    numkids += 1
3076+
3077+            self.failUnlessEqual(numkids, 3)
3078+            return self.rootnode.list()
3079+        d.addCallback(_check_data)
3080+       
3081+        # Now when we use the real directory listing code, the mutants should be absent.
3082+        def _check_kids(children):
3083+            self.failUnlessEqual(sorted(children.keys()), [u"lonely"])
3084+            lonely_node, lonely_metadata = children[u"lonely"]
3085+
3086+            self.failUnlessEqual(lonely_node.get_write_uri(), None)
3087+            self.failUnlessEqual(lonely_node.get_readonly_uri(), lonely_uri)
3088+        d.addCallback(_check_kids)
3089+
3090+        d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string()))
3091+        d.addCallback(lambda n: n.list())
3092+        d.addCallback(_check_kids)  # again with dirnode recreated from cap
3093+
3094+        # Make sure the lonely child can be listed in HTML...
3095+        d.addCallback(lambda ign: self.GET(self.rooturl))
3096+        def _check_html(res):
3097+            self.failIfIn("URI:SSK", res)
3098+            get_lonely = "".join([r'<td>FILE</td>',
3099+                                  r'\s+<td>',
3100+                                  r'<a href="[^"]+%s[^"]+">lonely</a>' % (urllib.quote(lonely_uri),),
3101+                                  r'</td>',
3102+                                  r'\s+<td>%d</td>' % len("one"),
3103+                                  ])
3104+            self.failUnless(re.search(get_lonely, res), res)
3105+
3106+            # find the More Info link for name, should be relative
3107+            mo = re.search(r'<a href="([^"]+)">More Info</a>', res)
3108+            info_url = mo.group(1)
3109+            self.failUnless(info_url.endswith(urllib.quote(lonely_uri) + "?t=info"), info_url)
3110+        d.addCallback(_check_html)
3111+
3112+        # ... and in JSON.
3113+        d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json"))
3114+        def _check_json(res):
3115+            data = simplejson.loads(res)
3116+            self.failUnlessEqual(data[0], "dirnode")
3117+            listed_children = data[1]["children"]
3118+            self.failUnlessEqual(sorted(listed_children.keys()), [u"lonely"])
3119+            ll_type, ll_data = listed_children[u"lonely"]
3120+            self.failUnlessEqual(ll_type, "filenode")
3121+            self.failIf("rw_uri" in ll_data)
3122+            self.failUnlessEqual(ll_data["ro_uri"], lonely_uri)
3123+        d.addCallback(_check_json)
3124         return d
3125 
3126     def test_deep_check(self):
3127hunk ./src/allmydata/test/test_web.py 3443
3128 
3129         # this tests that deep-check and stream-manifest will ignore
3130         # UnknownNode instances. Hopefully this will also cover deep-stats.
3131-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
3132-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
3133-        future_node = UnknownNode(future_writecap, future_readcap)
3134-        d.addCallback(lambda ign: self.rootnode.set_node(u"future",future_node))
3135+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
3136+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
3137+        future_node = UnknownNode(future_write_uri, future_read_uri)
3138+        d.addCallback(lambda ign: self.rootnode.set_node(u"future", future_node))
3139 
3140         def _clobber_shares(ignored):
3141             self.delete_shares_numbered(self.uris["sick"], [0,1])
3142hunk ./src/allmydata/unknown.py 1
3143+
3144 from zope.interface import implements
3145 from twisted.internet import defer
3146hunk ./src/allmydata/unknown.py 4
3147-from allmydata.interfaces import IFilesystemNode
3148+from allmydata.interfaces import IFilesystemNode, MustNotBeUnknownRWError, \
3149+    MustBeDeepImmutableError
3150+from allmydata import uri
3151+from allmydata.uri import ALLEGED_READONLY_PREFIX, ALLEGED_IMMUTABLE_PREFIX
3152+
3153+
3154+# See ticket #833 for design rationale of UnknownNodes.
3155+
3156+def strip_prefix_for_ro(ro_uri, deep_immutable):
3157+    """Strip prefixes when storing an URI in a ro_uri slot."""
3158+
3159+    # It is possible for an alleged-immutable URI to be put into a
3160+    # mutable directory. In that case the ALLEGED_IMMUTABLE_PREFIX
3161+    # should not be stripped. In other cases, the prefix can safely
3162+    # be stripped because it is implied by the context.
3163+
3164+    if ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX):
3165+        if not deep_immutable:
3166+            return ro_uri
3167+        return ro_uri[len(ALLEGED_IMMUTABLE_PREFIX):]
3168+    elif ro_uri.startswith(ALLEGED_READONLY_PREFIX):
3169+        return ro_uri[len(ALLEGED_READONLY_PREFIX):]
3170+    else:
3171+        return ro_uri
3172 
3173 class UnknownNode:
3174     implements(IFilesystemNode)
3175hunk ./src/allmydata/unknown.py 31
3176-    def __init__(self, writecap, readcap):
3177-        assert writecap is None or isinstance(writecap, str)
3178-        self.writecap = writecap
3179-        assert readcap is None or isinstance(readcap, str)
3180-        self.readcap = readcap
3181+
3182+    def __init__(self, given_rw_uri, given_ro_uri, deep_immutable=False,
3183+                 name=u"<unknown name>"):
3184+        assert given_rw_uri is None or isinstance(given_rw_uri, str)
3185+        assert given_ro_uri is None or isinstance(given_ro_uri, str)
3186+        given_rw_uri = given_rw_uri or None
3187+        given_ro_uri = given_ro_uri or None
3188+
3189+        # We don't raise errors when creating an UnknownNode; we instead create an
3190+        # opaque node (with rw_uri and ro_uri both None) that records the error.
3191+        # This avoids breaking operations that never store the opaque node.
3192+        # Note that this means that if a stored dirnode has only a rw_uri, it
3193+        # might be dropped. Any future "write-only" cap formats should have a dummy
3194+        # unusable readcap to stop that from happening.
3195+
3196+        self.error = None
3197+        self.rw_uri = self.ro_uri = None
3198+        if given_rw_uri:
3199+            if deep_immutable:
3200+                if given_rw_uri.startswith(ALLEGED_IMMUTABLE_PREFIX) and not given_ro_uri:
3201+                    # We needed an immutable cap, and were given one. It was given in the
3202+                    # rw_uri slot, but that's fine; we'll move it to ro_uri below.
3203+                    pass
3204+                elif not given_ro_uri:
3205+                    self.error = MustNotBeUnknownRWError("cannot attach unknown rw cap as immutable child",
3206+                                                         name, True)
3207+                    return  # node will be opaque
3208+                else:
3209+                    # We could report either error, but this probably makes more sense.
3210+                    self.error = MustBeDeepImmutableError("cannot attach unknown rw cap as immutable child",
3211+                                                         name)
3212+                    return  # node will be opaque
3213+
3214+            if not given_ro_uri:
3215+                # We were given a single cap argument, or a rw_uri with no ro_uri.
3216+
3217+                if not (given_rw_uri.startswith(ALLEGED_READONLY_PREFIX)
3218+                        or given_rw_uri.startswith(ALLEGED_IMMUTABLE_PREFIX)):
3219+                    # If the single cap is unprefixed, then we cannot tell whether it is a
3220+                    # writecap, and we don't know how to diminish it to a readcap if it is one.
3221+                    # If it didn't *already* have at least an ALLEGED_READONLY_PREFIX, then
3222+                    # prefixing it would be a bad idea because we have been given no reason
3223+                    # to believe that it is a readcap, so we might be letting a client
3224+                    # inadvertently grant excess write authority.
3225+                    self.error = MustNotBeUnknownRWError("cannot attach unknown rw cap as child",
3226+                                                         name, False)
3227+                    return  # node will be opaque
3228+
3229+                # OTOH, if the single cap already had a prefix (which is of the required
3230+                # strength otherwise an error would have been thrown above), then treat it
3231+                # as though it had been given in the ro_uri slot. This has a similar effect
3232+                # to the use for known caps of 'bigcap = writecap or readcap' in
3233+                # nodemaker.py: create_from_cap. It enables copying of unknown readcaps to
3234+                # work in as many cases as we can securely allow.
3235+                given_ro_uri = given_rw_uri
3236+                given_rw_uri = None
3237+            elif given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX):
3238+                # Strange corner case: we were given a cap in both slots, with the ro_uri
3239+                # alleged to be immutable. A real immutable object wouldn't have a writecap.
3240+                self.error = MustBeDeepImmutableError("cannot accept a child entry that specifies "
3241+                                                      "both rw_uri, and ro_uri with an imm. prefix",
3242+                                                      name)
3243+                return  # node will be opaque
3244+
3245+        # If the ro_uri definitely fails the constraint, it should be treated as opaque and
3246+        # the error recorded.
3247+        if given_ro_uri:
3248+            read_cap = uri.from_string(given_ro_uri, deep_immutable=deep_immutable, name=name)
3249+            if isinstance(read_cap, uri.UnknownURI):
3250+                self.error = read_cap.get_error()
3251+                if self.error:
3252+                    assert self.rw_uri is None and self.ro_uri is None
3253+                    return
3254+
3255+        if deep_immutable:
3256+            assert self.rw_uri is None
3257+            # strengthen the constraint on ro_uri to ALLEGED_IMMUTABLE_PREFIX
3258+            if given_ro_uri:
3259+                if given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX):
3260+                    self.ro_uri = given_ro_uri
3261+                elif given_ro_uri.startswith(ALLEGED_READONLY_PREFIX):
3262+                    self.ro_uri = ALLEGED_IMMUTABLE_PREFIX + given_ro_uri[len(ALLEGED_READONLY_PREFIX):]
3263+                else:
3264+                    self.ro_uri = ALLEGED_IMMUTABLE_PREFIX + given_ro_uri
3265+        else:
3266+            # not immutable, so a writecap is allowed
3267+            self.rw_uri = given_rw_uri
3268+            # strengthen the constraint on ro_uri to ALLEGED_READONLY_PREFIX
3269+            if given_ro_uri:
3270+                if (given_ro_uri.startswith(ALLEGED_READONLY_PREFIX) or
3271+                    given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX)):
3272+                    self.ro_uri = given_ro_uri
3273+                else:
3274+                    self.ro_uri = ALLEGED_READONLY_PREFIX + given_ro_uri
3275+
3276+    def get_cap(self):
3277+        return uri.UnknownURI(self.rw_uri or self.ro_uri)
3278+
3279+    def get_readcap(self):
3280+        return uri.UnknownURI(self.ro_uri)
3281+
3282+    def is_readonly(self):
3283+        raise AssertionError("an UnknownNode might be either read-only or "
3284+                             "read/write, so we shouldn't be calling is_readonly")
3285+
3286+    def is_mutable(self):
3287+        raise AssertionError("an UnknownNode might be either mutable or immutable, "
3288+                             "so we shouldn't be calling is_mutable")
3289+
3290+    def is_unknown(self):
3291+        return True
3292+
3293+    def is_allowed_in_immutable_directory(self):
3294+        # An UnknownNode consisting only of a ro_uri is allowed in an
3295+        # immutable directory, even though we do not know that it is
3296+        # immutable (or even read-only), provided that no error was detected.
3297+        return not self.error and not self.rw_uri
3298+
3299+    def raise_error(self):
3300+        if self.error is not None:
3301+            raise self.error
3302+
3303     def get_uri(self):
3304hunk ./src/allmydata/unknown.py 154
3305-        return self.writecap
3306+        return self.rw_uri or self.ro_uri
3307+
3308+    def get_write_uri(self):
3309+        return self.rw_uri
3310+
3311     def get_readonly_uri(self):
3312hunk ./src/allmydata/unknown.py 160
3313-        return self.readcap
3314+        return self.ro_uri
3315+
3316     def get_storage_index(self):
3317         return None
3318hunk ./src/allmydata/unknown.py 164
3319+
3320     def get_verify_cap(self):
3321         return None
3322hunk ./src/allmydata/unknown.py 167
3323+
3324     def get_repair_cap(self):
3325         return None
3326hunk ./src/allmydata/unknown.py 170
3327+
3328     def get_size(self):
3329         return None
3330hunk ./src/allmydata/unknown.py 173
3331+
3332     def get_current_size(self):
3333         return defer.succeed(None)
3334hunk ./src/allmydata/unknown.py 176
3335+
3336     def check(self, monitor, verify, add_lease):
3337         return defer.succeed(None)
3338hunk ./src/allmydata/unknown.py 179
3339+
3340     def check_and_repair(self, monitor, verify, add_lease):
3341         return defer.succeed(None)
3342hunk ./src/allmydata/uri.py 8
3343 from allmydata.storage.server import si_a2b, si_b2a
3344 from allmydata.util import base32, hashutil
3345 from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IImmutableFileURI, \
3346-    IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI
3347+    IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI, \
3348+    MustBeDeepImmutableError, MustBeReadonlyError, CapConstraintError
3349 
3350hunk ./src/allmydata/uri.py 11
3351-class BadURIError(Exception):
3352+class BadURIError(CapConstraintError):
3353     pass
3354 
3355 # the URI shall be an ascii representation of the file. It shall contain
3356hunk ./src/allmydata/uri.py 75
3357     def init_from_human_encoding(cls, uri):
3358         mo = cls.HUMAN_RE.search(uri)
3359         if not mo:
3360-            raise BadURIError("%s doesn't look like a cap" % (uri,))
3361+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3362         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
3363                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
3364 
3365hunk ./src/allmydata/uri.py 83
3366     def init_from_string(cls, uri):
3367         mo = cls.STRING_RE.search(uri)
3368         if not mo:
3369-            raise BadURIError("%s doesn't look like a cap" % (uri,))
3370+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3371         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
3372                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
3373 
3374hunk ./src/allmydata/uri.py 101
3375 
3376     def is_readonly(self):
3377         return True
3378+
3379     def is_mutable(self):
3380         return False
3381hunk ./src/allmydata/uri.py 104
3382+
3383     def get_readonly(self):
3384         return self
3385 
3386hunk ./src/allmydata/uri.py 140
3387     @classmethod
3388     def init_from_human_encoding(cls, uri):
3389         mo = cls.HUMAN_RE.search(uri)
3390-        assert mo, uri
3391+        if not mo:
3392+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3393         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
3394                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
3395 
3396hunk ./src/allmydata/uri.py 148
3397     @classmethod
3398     def init_from_string(cls, uri):
3399         mo = cls.STRING_RE.search(uri)
3400-        assert mo, (uri, cls, cls.STRING_RE)
3401+        if not mo:
3402+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3403         return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)),
3404                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
3405 
3406hunk ./src/allmydata/uri.py 165
3407                  self.total_shares,
3408                  self.size))
3409 
3410+    def is_readonly(self):
3411+        return True
3412+
3413+    def is_mutable(self):
3414+        return False
3415+
3416+    def get_readonly(self):
3417+        return self
3418+
3419+    def get_verify_cap(self):
3420+        return self
3421+
3422 
3423 class LiteralFileURI(_BaseURI):
3424     implements(IURI, IImmutableFileURI)
3425hunk ./src/allmydata/uri.py 193
3426     @classmethod
3427     def init_from_human_encoding(cls, uri):
3428         mo = cls.HUMAN_RE.search(uri)
3429-        assert mo, uri
3430+        if not mo:
3431+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3432         return cls(base32.a2b(mo.group(1)))
3433 
3434     @classmethod
3435hunk ./src/allmydata/uri.py 200
3436     def init_from_string(cls, uri):
3437         mo = cls.STRING_RE.search(uri)
3438-        assert mo, uri
3439+        if not mo:
3440+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3441         return cls(base32.a2b(mo.group(1)))
3442 
3443     def to_string(self):
3444hunk ./src/allmydata/uri.py 209
3445 
3446     def is_readonly(self):
3447         return True
3448+
3449     def is_mutable(self):
3450         return False
3451hunk ./src/allmydata/uri.py 212
3452+
3453     def get_readonly(self):
3454         return self
3455hunk ./src/allmydata/uri.py 215
3456+
3457     def get_storage_index(self):
3458         return None
3459 
3460hunk ./src/allmydata/uri.py 226
3461     def get_size(self):
3462         return len(self.data)
3463 
3464+
3465 class WriteableSSKFileURI(_BaseURI):
3466     implements(IURI, IMutableFileURI)
3467 
3468hunk ./src/allmydata/uri.py 247
3469     def init_from_human_encoding(cls, uri):
3470         mo = cls.HUMAN_RE.search(uri)
3471         if not mo:
3472-            raise BadURIError("'%s' doesn't look like a cap" % (uri,))
3473+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3474         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
3475 
3476     @classmethod
3477hunk ./src/allmydata/uri.py 268
3478 
3479     def abbrev(self):
3480         return base32.b2a(self.writekey[:5])
3481+
3482     def abbrev_si(self):
3483         return base32.b2a(self.storage_index)[:5]
3484 
3485hunk ./src/allmydata/uri.py 274
3486     def is_readonly(self):
3487         return False
3488+
3489     def is_mutable(self):
3490         return True
3491hunk ./src/allmydata/uri.py 277
3492+
3493     def get_readonly(self):
3494         return ReadonlySSKFileURI(self.readkey, self.fingerprint)
3495hunk ./src/allmydata/uri.py 280
3496+
3497     def get_verify_cap(self):
3498         return SSKVerifierURI(self.storage_index, self.fingerprint)
3499 
3500hunk ./src/allmydata/uri.py 284
3501+
3502 class ReadonlySSKFileURI(_BaseURI):
3503     implements(IURI, IMutableFileURI)
3504 
3505hunk ./src/allmydata/uri.py 302
3506     def init_from_human_encoding(cls, uri):
3507         mo = cls.HUMAN_RE.search(uri)
3508         if not mo:
3509-            raise BadURIError("'%s' doesn't look like a cap" % (uri,))
3510+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3511         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
3512 
3513     @classmethod
3514hunk ./src/allmydata/uri.py 309
3515     def init_from_string(cls, uri):
3516         mo = cls.STRING_RE.search(uri)
3517         if not mo:
3518-            raise BadURIError("'%s' doesn't look like a cap" % (uri,))
3519+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3520         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
3521 
3522     def to_string(self):
3523hunk ./src/allmydata/uri.py 323
3524 
3525     def abbrev(self):
3526         return base32.b2a(self.readkey[:5])
3527+
3528     def abbrev_si(self):
3529         return base32.b2a(self.storage_index)[:5]
3530 
3531hunk ./src/allmydata/uri.py 329
3532     def is_readonly(self):
3533         return True
3534+
3535     def is_mutable(self):
3536         return True
3537hunk ./src/allmydata/uri.py 332
3538+
3539     def get_readonly(self):
3540         return self
3541hunk ./src/allmydata/uri.py 335
3542+
3543     def get_verify_cap(self):
3544         return SSKVerifierURI(self.storage_index, self.fingerprint)
3545 
3546hunk ./src/allmydata/uri.py 339
3547+
3548 class SSKVerifierURI(_BaseURI):
3549     implements(IVerifierURI)
3550 
3551hunk ./src/allmydata/uri.py 355
3552     @classmethod
3553     def init_from_human_encoding(cls, uri):
3554         mo = cls.HUMAN_RE.search(uri)
3555-        assert mo, uri
3556+        if not mo:
3557+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3558         return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
3559 
3560     @classmethod
3561hunk ./src/allmydata/uri.py 362
3562     def init_from_string(cls, uri):
3563         mo = cls.STRING_RE.search(uri)
3564-        assert mo, (uri, cls)
3565+        if not mo:
3566+            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
3567         return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
3568 
3569     def to_string(self):
3570hunk ./src/allmydata/uri.py 372
3571         return 'URI:SSK-Verifier:%s:%s' % (si_b2a(self.storage_index),
3572                                            base32.b2a(self.fingerprint))
3573 
3574+    def is_readonly(self):
3575+        return True
3576+
3577+    def is_mutable(self):
3578+        return False
3579+
3580+    def get_readonly(self):
3581+        return self
3582+
3583+    def get_verify_cap(self):
3584+        return self
3585+
3586 class _DirectoryBaseURI(_BaseURI):
3587     implements(IURI, IDirnodeURI)
3588     def __init__(self, filenode_uri=None):
3589hunk ./src/allmydata/uri.py 423
3590 
3591     def abbrev(self):
3592         return self._filenode_uri.to_string().split(':')[2][:5]
3593+
3594     def abbrev_si(self):
3595         return base32.b2a(self._filenode_uri.storage_index)[:5]
3596 
3597hunk ./src/allmydata/uri.py 427
3598-    def get_filenode_cap(self):
3599-        return self._filenode_uri
3600-
3601     def is_mutable(self):
3602         return True
3603 
3604hunk ./src/allmydata/uri.py 430
3605+    def get_filenode_cap(self):
3606+        return self._filenode_uri
3607+
3608     def get_verify_cap(self):
3609         return DirectoryURIVerifier(self._filenode_uri.get_verify_cap())
3610 
3611hunk ./src/allmydata/uri.py 458
3612     def get_readonly(self):
3613         return ReadonlyDirectoryURI(self._filenode_uri.get_readonly())
3614 
3615+
3616 class ReadonlyDirectoryURI(_DirectoryBaseURI):
3617     implements(IReadonlyDirectoryURI)
3618 
3619hunk ./src/allmydata/uri.py 478
3620     def get_readonly(self):
3621         return self
3622 
3623+
3624 class _ImmutableDirectoryBaseURI(_DirectoryBaseURI):
3625     def __init__(self, filenode_uri=None):
3626         if filenode_uri:
3627hunk ./src/allmydata/uri.py 483
3628             assert isinstance(filenode_uri, self.INNER_URI_CLASS), filenode_uri
3629+            assert not filenode_uri.is_mutable()
3630         _DirectoryBaseURI.__init__(self, filenode_uri)
3631 
3632hunk ./src/allmydata/uri.py 486
3633-    def is_mutable(self):
3634-        return False
3635-
3636     def is_readonly(self):
3637         return True
3638 
3639hunk ./src/allmydata/uri.py 489
3640+    def is_mutable(self):
3641+        return False
3642+
3643     def get_readonly(self):
3644         return self
3645 
3646hunk ./src/allmydata/uri.py 495
3647+
3648 class ImmutableDirectoryURI(_ImmutableDirectoryBaseURI):
3649     BASE_STRING='URI:DIR2-CHK:'
3650     BASE_STRING_RE=re.compile('^'+BASE_STRING)
3651hunk ./src/allmydata/uri.py 501
3652     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK'+SEP)
3653     INNER_URI_CLASS=CHKFileURI
3654+
3655     def get_verify_cap(self):
3656         vcap = self._filenode_uri.get_verify_cap()
3657         return ImmutableDirectoryURIVerifier(vcap)
3658hunk ./src/allmydata/uri.py 512
3659     BASE_STRING_RE=re.compile('^'+BASE_STRING)
3660     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-LIT'+SEP)
3661     INNER_URI_CLASS=LiteralFileURI
3662+
3663     def get_verify_cap(self):
3664         # LIT caps have no verifier, since they aren't distributed
3665         return None
3666hunk ./src/allmydata/uri.py 517
3667 
3668+
3669 def wrap_dirnode_cap(filecap):
3670     if isinstance(filecap, WriteableSSKFileURI):
3671         return DirectoryURI(filecap)
3672hunk ./src/allmydata/uri.py 527
3673         return ImmutableDirectoryURI(filecap)
3674     if isinstance(filecap, LiteralFileURI):
3675         return LiteralDirectoryURI(filecap)
3676-    assert False, "cannot wrap a dirnode around %s" % filecap.__class__
3677+    assert False, "cannot interpret as a directory cap: %s" % filecap.__class__
3678+
3679 
3680 class DirectoryURIVerifier(_DirectoryBaseURI):
3681     implements(IVerifierURI)
3682hunk ./src/allmydata/uri.py 546
3683     def get_filenode_cap(self):
3684         return self._filenode_uri
3685 
3686+    def is_mutable(self):
3687+        return False
3688+
3689+
3690 class ImmutableDirectoryURIVerifier(DirectoryURIVerifier):
3691     implements(IVerifierURI)
3692     BASE_STRING='URI:DIR2-CHK-Verifier:'
3693hunk ./src/allmydata/uri.py 557
3694     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK-VERIFIER'+SEP)
3695     INNER_URI_CLASS=CHKFileVerifierURI
3696 
3697+
3698 class UnknownURI:
3699hunk ./src/allmydata/uri.py 559
3700-    def __init__(self, uri):
3701+    def __init__(self, uri, error=None):
3702         self._uri = uri
3703hunk ./src/allmydata/uri.py 561
3704+        self._error = error
3705+
3706     def to_string(self):
3707         return self._uri
3708 
3709hunk ./src/allmydata/uri.py 566
3710-def from_string(s):
3711-    if not isinstance(s, str):
3712-        raise TypeError("unknown URI type: %s.." % str(s)[:100])
3713-    elif s.startswith('URI:CHK:'):
3714-        return CHKFileURI.init_from_string(s)
3715-    elif s.startswith('URI:CHK-Verifier:'):
3716-        return CHKFileVerifierURI.init_from_string(s)
3717-    elif s.startswith('URI:LIT:'):
3718-        return LiteralFileURI.init_from_string(s)
3719-    elif s.startswith('URI:SSK:'):
3720-        return WriteableSSKFileURI.init_from_string(s)
3721-    elif s.startswith('URI:SSK-RO:'):
3722-        return ReadonlySSKFileURI.init_from_string(s)
3723-    elif s.startswith('URI:SSK-Verifier:'):
3724-        return SSKVerifierURI.init_from_string(s)
3725-    elif s.startswith('URI:DIR2:'):
3726-        return DirectoryURI.init_from_string(s)
3727-    elif s.startswith('URI:DIR2-RO:'):
3728-        return ReadonlyDirectoryURI.init_from_string(s)
3729-    elif s.startswith('URI:DIR2-Verifier:'):
3730-        return DirectoryURIVerifier.init_from_string(s)
3731-    elif s.startswith('URI:DIR2-CHK:'):
3732-        return ImmutableDirectoryURI.init_from_string(s)
3733-    elif s.startswith('URI:DIR2-LIT:'):
3734-        return LiteralDirectoryURI.init_from_string(s)
3735-    return UnknownURI(s)
3736+    def get_readonly(self):
3737+        return None
3738+
3739+    def get_error(self):
3740+        return self._error
3741+
3742+    def get_verify_cap(self):
3743+        return None
3744+
3745+
3746+ALLEGED_READONLY_PREFIX = 'ro.'
3747+ALLEGED_IMMUTABLE_PREFIX = 'imm.'
3748+
3749+def from_string(u, deep_immutable=False, name=u"<unknown name>"):
3750+    if not isinstance(u, str):
3751+        raise TypeError("unknown URI type: %s.." % str(u)[:100])
3752+
3753+    # We allow and check ALLEGED_READONLY_PREFIX or ALLEGED_IMMUTABLE_PREFIX
3754+    # on all URIs, even though we would only strictly need to do so for caps of
3755+    # new formats (post Tahoe-LAFS 1.6). URIs that are not consistent with their
3756+    # prefix are treated as unknown. This should be revisited when we add the
3757+    # new cap formats. See <http://allmydata.org/trac/tahoe/ticket/833#comment:31>.
3758+    s = u
3759+    can_be_mutable = can_be_writeable = not deep_immutable
3760+    if s.startswith(ALLEGED_IMMUTABLE_PREFIX):
3761+        can_be_mutable = can_be_writeable = False
3762+        s = s[len(ALLEGED_IMMUTABLE_PREFIX):]
3763+    elif s.startswith(ALLEGED_READONLY_PREFIX):
3764+        can_be_writeable = False
3765+        s = s[len(ALLEGED_READONLY_PREFIX):]
3766+
3767+    error = None
3768+    kind = "cap"
3769+    try:
3770+        if s.startswith('URI:CHK:'):
3771+            return CHKFileURI.init_from_string(s)
3772+        elif s.startswith('URI:CHK-Verifier:'):
3773+            return CHKFileVerifierURI.init_from_string(s)
3774+        elif s.startswith('URI:LIT:'):
3775+            return LiteralFileURI.init_from_string(s)
3776+        elif s.startswith('URI:SSK:'):
3777+            if can_be_writeable:
3778+                return WriteableSSKFileURI.init_from_string(s)
3779+            kind = "URI:SSK file writecap"
3780+        elif s.startswith('URI:SSK-RO:'):
3781+            if can_be_mutable:
3782+                return ReadonlySSKFileURI.init_from_string(s)
3783+            kind = "URI:SSK-RO readcap to a mutable file"
3784+        elif s.startswith('URI:SSK-Verifier:'):
3785+            return SSKVerifierURI.init_from_string(s)
3786+        elif s.startswith('URI:DIR2:'):
3787+            if can_be_writeable:
3788+                return DirectoryURI.init_from_string(s)
3789+            kind = "URI:DIR2 directory writecap"
3790+        elif s.startswith('URI:DIR2-RO:'):
3791+            if can_be_mutable:
3792+                return ReadonlyDirectoryURI.init_from_string(s)
3793+            kind = "URI:DIR2-RO readcap to a mutable directory"
3794+        elif s.startswith('URI:DIR2-Verifier:'):
3795+            return DirectoryURIVerifier.init_from_string(s)
3796+        elif s.startswith('URI:DIR2-CHK:'):
3797+            return ImmutableDirectoryURI.init_from_string(s)
3798+        elif s.startswith('URI:DIR2-LIT:'):
3799+            return LiteralDirectoryURI.init_from_string(s)
3800+        elif s.startswith('x-tahoe-future-test-writeable:') and not can_be_writeable:
3801+            # For testing how future writeable caps would behave in read-only contexts.
3802+            kind = "x-tahoe-future-test-writeable: testing cap"
3803+        elif s.startswith('x-tahoe-future-test-mutable:') and not can_be_mutable:
3804+            # For testing how future mutable readcaps would behave in immutable contexts.
3805+            kind = "x-tahoe-future-test-mutable: testing cap"
3806+        else:
3807+            return UnknownURI(u)
3808+
3809+        # We fell through because a constraint was not met.
3810+        # Prefer to report the most specific constraint.
3811+        if not can_be_mutable:
3812+            error = MustBeDeepImmutableError(kind + " used in an immutable context", name)
3813+        else:
3814+            error = MustBeReadonlyError(kind + " used in a read-only context", name)
3815+           
3816+    except BadURIError, e:
3817+        error = e
3818+
3819+    return UnknownURI(u, error=error)
3820 
3821 def is_uri(s):
3822     try:
3823hunk ./src/allmydata/uri.py 653
3824-        from_string(s)
3825+        from_string(s, deep_immutable=False)
3826         return True
3827     except (TypeError, AssertionError):
3828         return False
3829hunk ./src/allmydata/uri.py 658
3830 
3831-def from_string_dirnode(s):
3832-    u = from_string(s)
3833+def is_literal_file_uri(s):
3834+    if not isinstance(s, str):
3835+        return False
3836+    return (s.startswith('URI:LIT:') or
3837+            s.startswith(ALLEGED_READONLY_PREFIX + 'URI:LIT:') or
3838+            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:'))
3839+
3840+def has_uri_prefix(s):
3841+    if not isinstance(s, str):
3842+        return False
3843+    return (s.startswith("URI:") or
3844+            s.startswith(ALLEGED_READONLY_PREFIX + 'URI:') or
3845+            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:'))
3846+
3847+
3848+# These take the same keyword arguments as from_string above.
3849+
3850+def from_string_dirnode(s, **kwargs):
3851+    u = from_string(s, **kwargs)
3852     assert IDirnodeURI.providedBy(u)
3853     return u
3854 
3855hunk ./src/allmydata/uri.py 682
3856 registerAdapter(from_string_dirnode, str, IDirnodeURI)
3857 
3858-def from_string_filenode(s):
3859-    u = from_string(s)
3860+def from_string_filenode(s, **kwargs):
3861+    u = from_string(s, **kwargs)
3862     assert IFileURI.providedBy(u)
3863     return u
3864 
3865hunk ./src/allmydata/uri.py 689
3866 registerAdapter(from_string_filenode, str, IFileURI)
3867 
3868-def from_string_mutable_filenode(s):
3869-    u = from_string(s)
3870+def from_string_mutable_filenode(s, **kwargs):
3871+    u = from_string(s, **kwargs)
3872     assert IMutableFileURI.providedBy(u)
3873     return u
3874 registerAdapter(from_string_mutable_filenode, str, IMutableFileURI)
3875hunk ./src/allmydata/uri.py 695
3876 
3877-def from_string_verifier(s):
3878-    u = from_string(s)
3879+def from_string_verifier(s, **kwargs):
3880+    u = from_string(s, **kwargs)
3881     assert IVerifierURI.providedBy(u)
3882     return u
3883 registerAdapter(from_string_verifier, str, IVerifierURI)
3884hunk ./src/allmydata/web/common.py 11
3885 from nevow.util import resource_filename
3886 from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
3887      FileTooLargeError, NotEnoughSharesError, NoSharesError, \
3888-     NotDeepImmutableError, EmptyPathnameComponentError
3889+     EmptyPathnameComponentError, MustBeDeepImmutableError, \
3890+     MustBeReadonlyError, MustNotBeUnknownRWError
3891 from allmydata.mutable.common import UnrecoverableFileError
3892 from allmydata.util import abbreviate # TODO: consolidate
3893 
3894hunk ./src/allmydata/web/common.py 185
3895              "failure, or disk corruption. You should perform a filecheck on "
3896              "this object to learn more.")
3897         return (t, http.GONE)
3898-    if f.check(NotDeepImmutableError):
3899-        t = ("NotDeepImmutableError: a mkdir-immutable operation was given "
3900-             "a child that was not itself immutable: %s" % (f.value,))
3901+    if f.check(MustNotBeUnknownRWError):
3902+        name = f.value.args[1]
3903+        immutable = f.value.args[2]
3904+        if immutable:
3905+            t = ("MustNotBeUnknownRWError: an operation to add a child named "
3906+                 "'%s' to a directory was given an unknown cap in a write slot.\n"
3907+                 "If the cap is actually an immutable readcap, then using a "
3908+                 "webapi server that supports a later version of Tahoe may help.\n\n"
3909+                 "If you are using the webapi directly, then specifying an immutable "
3910+                 "readcap in the read slot (ro_uri) of the JSON PROPDICT, and "
3911+                 "omitting the write slot (rw_uri), would also work in this "
3912+                 "case.") % name.encode("utf-8")
3913+        else:
3914+            t = ("MustNotBeUnknownRWError: an operation to add a child named "
3915+                 "'%s' to a directory was given an unknown cap in a write slot.\n"
3916+                 "Using a webapi server that supports a later version of Tahoe "
3917+                 "may help.\n\n"
3918+                 "If you are using the webapi directly, specifying a readcap in "
3919+                 "the read slot (ro_uri) of the JSON PROPDICT, as well as a "
3920+                 "writecap in the write slot if desired, would also work in this "
3921+                 "case.") % name.encode("utf-8")
3922+        return (t, http.BAD_REQUEST)
3923+    if f.check(MustBeDeepImmutableError):
3924+        name = f.value.args[1]
3925+        t = ("MustBeDeepImmutableError: a cap passed to this operation for "
3926+             "the child named '%s', needed to be immutable but was not. Either "
3927+             "the cap is being added to an immutable directory, or it was "
3928+             "originally retrieved from an immutable directory as an unknown "
3929+             "cap." % name.encode("utf-8"))
3930+        return (t, http.BAD_REQUEST)
3931+    if f.check(MustBeReadonlyError):
3932+        name = f.value.args[1]
3933+        t = ("MustBeReadonlyError: a cap passed to this operation for "
3934+             "the child named '%s', needed to be read-only but was not. "
3935+             "The cap is being passed in a read slot (ro_uri), or was retrieved "
3936+             "from a read slot as an unknown cap." % name.encode("utf-8"))
3937         return (t, http.BAD_REQUEST)
3938     if f.check(WebError):
3939         return (f.value.text, f.value.code)
3940hunk ./src/allmydata/web/directory.py 355
3941         charset = get_arg(req, "_charset", "utf-8")
3942         name = name.decode(charset)
3943         replace = boolean_of_arg(get_arg(req, "replace", "true"))
3944-        d = self.node.set_uri(name, childcap, childcap, overwrite=replace)
3945+       
3946+        # We mustn't pass childcap for the readcap argument because we don't
3947+        # know whether it is a read cap. Passing a read cap as the writecap
3948+        # argument will work (it ends up calling NodeMaker.create_from_cap,
3949+        # which derives a readcap if necessary and possible).
3950+        d = self.node.set_uri(name, childcap, None, overwrite=replace)
3951         d.addCallback(lambda res: childcap)
3952         return d
3953 
3954hunk ./src/allmydata/web/directory.py 371
3955             # won't show up in the resulting encoded form.. the 'name'
3956             # field is completely missing. So to allow deletion of an
3957             # empty file, we have to pretend that None means ''. The only
3958-            # downide of this is a slightly confusing error message if
3959+            # downside of this is a slightly confusing error message if
3960             # someone does a POST without a name= field. For our own HTML
3961hunk ./src/allmydata/web/directory.py 373
3962-            # thisn't a big deal, because we create the 'delete' POST
3963+            # this isn't a big deal, because we create the 'delete' POST
3964             # buttons ourselves.
3965             name = ''
3966         charset = get_arg(req, "_charset", "utf-8")
3967hunk ./src/allmydata/web/directory.py 593
3968     def render_title(self, ctx, data):
3969         si_s = abbreviated_dirnode(self.node)
3970         header = ["Tahoe-LAFS - Directory SI=%s" % si_s]
3971-        if self.node.is_readonly():
3972+        if self.node.is_unknown():
3973+            header.append(" (unknown)")
3974+        elif not self.node.is_mutable():
3975+            header.append(" (immutable)")
3976+        elif self.node.is_readonly():
3977             header.append(" (read-only)")
3978         else:
3979             header.append(" (modifiable)")
3980hunk ./src/allmydata/web/directory.py 606
3981     def render_header(self, ctx, data):
3982         si_s = abbreviated_dirnode(self.node)
3983         header = ["Tahoe-LAFS Directory SI=", T.span(class_="data-chars")[si_s]]
3984-        if self.node.is_readonly():
3985+        if self.node.is_unknown():
3986+            header.append(" (unknown)")
3987+        elif not self.node.is_mutable():
3988+            header.append(" (immutable)")
3989+        elif self.node.is_readonly():
3990             header.append(" (read-only)")
3991         return ctx.tag[header]
3992 
3993hunk ./src/allmydata/web/directory.py 619
3994         return T.div[T.a(href=link)["Return to Welcome page"]]
3995 
3996     def render_show_readonly(self, ctx, data):
3997-        if self.node.is_readonly():
3998+        if self.node.is_unknown() or self.node.is_readonly():
3999             return ""
4000         rocap = self.node.get_readonly_uri()
4001         root = get_root(ctx)
4002hunk ./src/allmydata/web/directory.py 646
4003 
4004         root = get_root(ctx)
4005         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
4006-        if self.node.is_readonly():
4007+        if self.node.is_unknown() or self.node.is_readonly():
4008             delete = "-"
4009             rename = "-"
4010         else:
4011hunk ./src/allmydata/web/directory.py 694
4012         ctx.fillSlots("times", times)
4013 
4014         assert IFilesystemNode.providedBy(target), target
4015-        writecap = target.get_uri() or ""
4016-        quoted_uri = urllib.quote(writecap, safe="") # escape slashes too
4017+        target_uri = target.get_uri() or ""
4018+        quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too
4019 
4020         if IMutableFileNode.providedBy(target):
4021             # to prevent javascript in displayed .html files from stealing a
4022hunk ./src/allmydata/web/directory.py 724
4023 
4024         elif IDirectoryNode.providedBy(target):
4025             # directory
4026-            uri_link = "%s/uri/%s/" % (root, urllib.quote(writecap))
4027+            uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
4028             ctx.fillSlots("filename",
4029                           T.a(href=uri_link)[html.escape(name)])
4030             if not target.is_mutable():
4031hunk ./src/allmydata/web/directory.py 811
4032         kids = {}
4033         for name, (childnode, metadata) in children.iteritems():
4034             assert IFilesystemNode.providedBy(childnode), childnode
4035-            rw_uri = childnode.get_uri()
4036+            rw_uri = childnode.get_write_uri()
4037             ro_uri = childnode.get_readonly_uri()
4038             if IFileNode.providedBy(childnode):
4039hunk ./src/allmydata/web/directory.py 814
4040-                if childnode.is_readonly():
4041-                    rw_uri = None
4042                 kiddata = ("filenode", {'size': childnode.get_size(),
4043                                         'mutable': childnode.is_mutable(),
4044                                         })
4045hunk ./src/allmydata/web/directory.py 818
4046             elif IDirectoryNode.providedBy(childnode):
4047-                if childnode.is_readonly():
4048-                    rw_uri = None
4049                 kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
4050             else:
4051                 kiddata = ("unknown", {})
4052hunk ./src/allmydata/web/directory.py 821
4053+
4054             kiddata[1]["metadata"] = metadata
4055hunk ./src/allmydata/web/directory.py 823
4056-            if ro_uri:
4057-                kiddata[1]["ro_uri"] = ro_uri
4058             if rw_uri:
4059                 kiddata[1]["rw_uri"] = rw_uri
4060hunk ./src/allmydata/web/directory.py 825
4061+            if ro_uri:
4062+                kiddata[1]["ro_uri"] = ro_uri
4063             verifycap = childnode.get_verify_cap()
4064             if verifycap:
4065                 kiddata[1]['verify_uri'] = verifycap.to_string()
4066hunk ./src/allmydata/web/directory.py 830
4067+
4068             kids[name] = kiddata
4069hunk ./src/allmydata/web/directory.py 832
4070-        if dirnode.is_readonly():
4071-            drw_uri = None
4072-            dro_uri = dirnode.get_uri()
4073-        else:
4074-            drw_uri = dirnode.get_uri()
4075-            dro_uri = dirnode.get_readonly_uri()
4076+
4077+        drw_uri = dirnode.get_write_uri()
4078+        dro_uri = dirnode.get_readonly_uri()
4079         contents = { 'children': kids }
4080         if dro_uri:
4081             contents['ro_uri'] = dro_uri
4082hunk ./src/allmydata/web/directory.py 845
4083             contents['verify_uri'] = verifycap.to_string()
4084         contents['mutable'] = dirnode.is_mutable()
4085         data = ("dirnode", contents)
4086-        return simplejson.dumps(data, indent=1) + "\n"
4087+        json = simplejson.dumps(data, indent=1) + "\n"
4088+        return json
4089     d.addCallback(_got)
4090     d.addCallback(text_plain, ctx)
4091     return d
4092hunk ./src/allmydata/web/directory.py 852
4093 
4094 
4095-
4096 def DirectoryURI(ctx, dirnode):
4097     return text_plain(dirnode.get_uri(), ctx)
4098 
4099hunk ./src/allmydata/web/directory.py 1143
4100         self.req.write(j+"\n")
4101         return ""
4102 
4103-class UnknownNodeHandler(RenderMixin, rend.Page):
4104 
4105hunk ./src/allmydata/web/directory.py 1144
4106+class UnknownNodeHandler(RenderMixin, rend.Page):
4107     def __init__(self, client, node, parentnode=None, name=None):
4108         rend.Page.__init__(self)
4109         assert node
4110hunk ./src/allmydata/web/directory.py 1149
4111         self.node = node
4112+        self.parentnode = parentnode
4113+        self.name = name
4114 
4115     def render_GET(self, ctx):
4116         req = IRequest(ctx)
4117hunk ./src/allmydata/web/directory.py 1157
4118         t = get_arg(req, "t", "").strip()
4119         if t == "info":
4120             return MoreInfo(self.node)
4121-        raise WebError("GET unknown URI type: can only do t=info, not t=%s" % t)
4122-
4123+        if t == "json":
4124+            if self.parentnode and self.name:
4125+                d = self.parentnode.get_metadata_for(self.name)
4126+            else:
4127+                d = defer.succeed(None)
4128+            d.addCallback(lambda md: UnknownJSONMetadata(ctx, self.node, md))
4129+            return d
4130+        raise WebError("GET unknown URI type: can only do t=info and t=json, not t=%s.\n"
4131+                       "Using a webapi server that supports a later version of Tahoe "
4132+                       "may help." % t)
4133 
4134hunk ./src/allmydata/web/directory.py 1168
4135+def UnknownJSONMetadata(ctx, filenode, edge_metadata):
4136+    rw_uri = filenode.get_write_uri()
4137+    ro_uri = filenode.get_readonly_uri()
4138+    data = ("unknown", {})
4139+    if ro_uri:
4140+        data[1]['ro_uri'] = ro_uri
4141+    if rw_uri:
4142+        data[1]['rw_uri'] = rw_uri
4143+    if edge_metadata is not None:
4144+        data[1]['metadata'] = edge_metadata
4145+    return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
4146hunk ./src/allmydata/web/filenode.py 9
4147 from nevow import url, rend
4148 from nevow.inevow import IRequest
4149 
4150-from allmydata.interfaces import ExistingChildError, CannotPackUnknownNodeError
4151+from allmydata.interfaces import ExistingChildError
4152 from allmydata.monitor import Monitor
4153 from allmydata.immutable.upload import FileHandle
4154hunk ./src/allmydata/web/filenode.py 12
4155-from allmydata.unknown import UnknownNode
4156 from allmydata.util import log, base32
4157 
4158 from allmydata.web.common import text_plain, WebError, RenderMixin, \
4159hunk ./src/allmydata/web/filenode.py 22
4160 from allmydata.web.info import MoreInfo
4161 
4162 class ReplaceMeMixin:
4163-
4164     def replace_me_with_a_child(self, req, client, replace):
4165         # a new file is being uploaded in our place.
4166         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
4167hunk ./src/allmydata/web/filenode.py 56
4168     def replace_me_with_a_childcap(self, req, client, replace):
4169         req.content.seek(0)
4170         childcap = req.content.read()
4171-        childnode = client.create_node_from_uri(childcap, childcap+"readonly")
4172-        if isinstance(childnode, UnknownNode):
4173-            # don't be willing to pack unknown nodes: we might accidentally
4174-            # put some write-authority into the rocap slot because we don't
4175-            # know how to diminish the URI they gave us. We don't even know
4176-            # if they gave us a readcap or a writecap.
4177-            msg = "cannot attach unknown node as child %s" % str(self.name)
4178-            raise CannotPackUnknownNodeError(msg)
4179+        childnode = client.create_node_from_uri(childcap, None, name=self.name)
4180         d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
4181         d.addCallback(lambda res: childnode.get_uri())
4182         return d
4183hunk ./src/allmydata/web/filenode.py 420
4184 
4185 
4186 def FileJSONMetadata(ctx, filenode, edge_metadata):
4187-    if filenode.is_readonly():
4188-        rw_uri = None
4189-        ro_uri = filenode.get_uri()
4190-    else:
4191-        rw_uri = filenode.get_uri()
4192-        ro_uri = filenode.get_readonly_uri()
4193+    rw_uri = filenode.get_write_uri()
4194+    ro_uri = filenode.get_readonly_uri()
4195     data = ("filenode", {})
4196     data[1]['size'] = filenode.get_size()
4197     if ro_uri:
4198hunk ./src/allmydata/web/info.py 24
4199     def get_type(self):
4200         node = self.original
4201         if IDirectoryNode.providedBy(node):
4202+            if not node.is_mutable():
4203+                return "immutable directory"
4204             return "directory"
4205         if IFileNode.providedBy(node):
4206             si = node.get_storage_index()
4207hunk ./src/allmydata/web/info.py 33
4208                 if node.is_mutable():
4209                     return "mutable file"
4210                 return "immutable file"
4211-            return "LIT file"
4212+            return "immutable LIT file"
4213         return "unknown"
4214 
4215     def render_title(self, ctx, data):
4216hunk ./src/allmydata/web/info.py 73
4217 
4218     def render_directory_writecap(self, ctx, data):
4219         node = self.original
4220-        if node.is_readonly():
4221-            return ""
4222         if not IDirectoryNode.providedBy(node):
4223             return ""
4224hunk ./src/allmydata/web/info.py 75
4225+        if node.is_readonly():
4226+            return ""
4227         return ctx.tag[node.get_uri()]
4228 
4229     def render_directory_readcap(self, ctx, data):
4230hunk ./src/allmydata/web/info.py 91
4231             return ""
4232         return ctx.tag[node.get_verify_cap().to_string()]
4233 
4234-
4235     def render_file_writecap(self, ctx, data):
4236         node = self.original
4237         if IDirectoryNode.providedBy(node):
4238hunk ./src/allmydata/web/info.py 95
4239             node = node._node
4240-        if ((IDirectoryNode.providedBy(node) or IFileNode.providedBy(node))
4241-            and node.is_readonly()):
4242-            return ""
4243-        writecap = node.get_uri()
4244-        if not writecap:
4245+        write_uri = node.get_write_uri()
4246+        if not write_uri:
4247             return ""
4248hunk ./src/allmydata/web/info.py 98
4249-        return ctx.tag[writecap]
4250+        return ctx.tag[write_uri]
4251 
4252     def render_file_readcap(self, ctx, data):
4253         node = self.original
4254hunk ./src/allmydata/web/info.py 104
4255         if IDirectoryNode.providedBy(node):
4256             node = node._node
4257-        readcap = node.get_readonly_uri()
4258-        if not readcap:
4259+        read_uri = node.get_readonly_uri()
4260+        if not read_uri:
4261             return ""
4262hunk ./src/allmydata/web/info.py 107
4263-        return ctx.tag[readcap]
4264+        return ctx.tag[read_uri]
4265 
4266     def render_file_verifycap(self, ctx, data):
4267         node = self.original
4268hunk ./src/allmydata/web/root.py 15
4269 from allmydata import get_package_versions_string
4270 from allmydata import provisioning
4271 from allmydata.util import idlib, log
4272-from allmydata.interfaces import IFileNode, UnhandledCapTypeError
4273+from allmydata.interfaces import IFileNode
4274 from allmydata.web import filenode, directory, unlinked, status, operations
4275 from allmydata.web import reliability, storage
4276 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
4277hunk ./src/allmydata/web/root.py 88
4278         try:
4279             node = self.client.create_node_from_uri(name)
4280             return directory.make_handler_for(node, self.client)
4281-        except (TypeError, UnhandledCapTypeError, AssertionError):
4282+        except (TypeError, AssertionError):
4283             raise WebError("'%s' is not a valid file- or directory- cap"
4284                            % name)
4285 
4286hunk ./src/allmydata/web/root.py 107
4287         # 'name' must be a file URI
4288         try:
4289             node = self.client.create_node_from_uri(name)
4290-        except (TypeError, UnhandledCapTypeError, AssertionError):
4291+        except (TypeError, AssertionError):
4292             # I think this can no longer be reached
4293             raise WebError("'%s' is not a valid file- or directory- cap"
4294                            % name)
4295}
4296
4297Context:
4298
4299[docs: further CREDITS level-ups for Nils, Kevan, David-Sarah
4300zooko@zooko.com**20100126170021
4301 Ignore-this: 1e513e85cf7b7abf57f056e6d7544b38
4302]
4303[ftpd: clearer error message if Twisted needs a patch (by Nils Durner)
4304zooko@zooko.com**20100126143411
4305 Ignore-this: 440e6831ae6da5135c1edd081c93871f
4306]
4307[Add 'docs/performance.txt', which (for the moment) describes mutable file performance issues
4308Kevan Carstensen <kevan@isnotajoke.com>**20100115204500
4309 Ignore-this: ade4e500217db2509aee35aacc8c5dbf
4310]
4311[docs: more CREDITS for François, Kevan, and David-Sarah
4312zooko@zooko.com**20100126132133
4313 Ignore-this: f37d4977c13066fcac088ba98a31b02e
4314]
4315[tahoe_backup.py: display warnings on errors instead of stopping the whole backup. Fix #729.
4316francois@ctrlaltdel.ch**20100120094249
4317 Ignore-this: 7006ea4b0910b6d29af6ab4a3997a8f9
4318 
4319 This patch displays a warning to the user in two cases:
4320   
4321   1. When special files like symlinks, fifos, devices, etc. are found in the
4322      local source.
4323   
4324   2. If files or directories are not readables by the user running the 'tahoe
4325      backup' command.
4326 
4327 In verbose mode, the number of skipped files and directories is printed at the
4328 end of the backup.
4329 
4330 Exit status returned by 'tahoe backup':
4331 
4332   - 0 everything went fine
4333   - 1 the backup failed
4334   - 2 files were skipped during the backup
4335 
4336]
4337[Message saying that we couldn't find bin/tahoe should say where we looked
4338david-sarah@jacaranda.org**20100116204556
4339 Ignore-this: 1068576fd59ea470f1e19196315d1bb
4340]
4341[Change running.html to describe 'tahoe run'
4342david-sarah@jacaranda.org**20100112044409
4343 Ignore-this: 23ad0114643ce31b56e19bb14e011e4f
4344]
4345[cli: split usage strings into groups (patch by David-Sarah Hopwood)
4346zooko@zooko.com**20100126043921
4347 Ignore-this: 51928d266a7292b873f87f7d53c9a01e
4348]
4349[Add create-node CLI command, and make create-client equivalent to create-node --no-storage (fixes #760)
4350david-sarah@jacaranda.org**20100116052055
4351 Ignore-this: 47d08b18c69738685e13ff365738d5a
4352]
4353[contrib/fuse/runtests.py: Fix #888, configure settings in tahoe.cfg and don't treat warnings as failure
4354francois@ctrlaltdel.ch**20100109123010
4355 Ignore-this: 2590d44044acd7dfa3690c416cae945c
4356 
4357 Fix a few bitrotten pieces in the FUSE test script.  It now configures tahoe
4358 node settings by editing tahoe.cfg which is the new supported method.
4359 
4360 It alos tolerate warnings issued by the mount command, the cause of these
4361 warnings is the same as in #876 (contrib/fuse/runtests.py doesn't tolerate
4362 deprecations warnings).
4363 
4364]
4365[Fix webapi t=mkdir with multpart/form-data, as on the Welcome page. Closes #919.
4366Brian Warner <warner@lothar.com>**20100121065052
4367 Ignore-this: 1f20ea0a0f1f6d6c1e8e14f193a92c87
4368]
4369[Fix boodlegrid use of set_children
4370david-sarah@jacaranda.org**20100126063414
4371 Ignore-this: 3aa2d4836f76303b2bacecd09611f999
4372]
4373[Remove replace= parameter to mkdir-immutable and mkdir-with-children
4374david-sarah@jacaranda.org**20100124224325
4375 Ignore-this: 25207bcc946c0c43d9528718e76ba7b
4376]
4377[Warn about test failures due to setting FLOG* env vars
4378david-sarah@jacaranda.org**20100124220629
4379 Ignore-this: 1c25247ca0f0840390a1b7259a9f4a3c
4380]
4381[Patch to accept t=set-children as well as t=set_children
4382david-sarah@jacaranda.org**20100124030020
4383 Ignore-this: 2c061f12af817cdf77feeeb64098ec3a
4384]
4385[tahoe_add_alias.py: minor refactoring
4386Brian Warner <warner@lothar.com>**20100115064220
4387 Ignore-this: 29910e81ad11209c9e493d65fd2dab9b
4388]
4389[test_dirnode.py: reduce scope of a Client instance, suggested by Kevan.
4390Brian Warner <warner@lothar.com>**20100115062713
4391 Ignore-this: b35efd9e6027e43de6c6f509bfb4ccaa
4392]
4393[test_provisioning: STAN is not always a list. Fix by David-Sarah Hopwood.
4394Brian Warner <warner@lothar.com>**20100115014632
4395 Ignore-this: 9989de7f1e00907706d2b63153138219
4396]
4397[web/directory.py mkdir-immutable: hush pyflakes, add TODO for #903 behavior
4398Brian Warner <warner@lothar.com>**20100114222804
4399 Ignore-this: 717cd3b9a1c8aeee76938c9641db7356
4400]
4401[hush pyflakes-0.4.0 warnings: slightly less-trivial fixes. Closes #900.
4402Brian Warner <warner@lothar.com>**20100114221719
4403 Ignore-this: f774f4637e256ad55502659413a811a8
4404 
4405 This includes one fix (in test_web) which was testing the wrong thing.
4406]
4407[hush pyflakes-0.4.0 warnings: remove trivial unused variables. For #900.
4408Brian Warner <warner@lothar.com>**20100114221529
4409 Ignore-this: e96106c8f1a99fbf93306fbfe9a294cf
4410]
4411[tahoe add-alias/create-alias: don't corrupt non-newline-terminated alias
4412Brian Warner <warner@lothar.com>**20100114210246
4413 Ignore-this: 9c994792e53a85159d708760a9b1b000
4414 file. Closes #741.
4415]
4416[change docs and --help to use "grid" instead of "virtual drive": closes #892.
4417Brian Warner <warner@lothar.com>**20100114201119
4418 Ignore-this: a20d4a4dcc4de4e3b404ff72d40fc29b
4419 
4420 Thanks to David-Sarah Hopwood for the patch.
4421]
4422[backupdb.txt: fix ST_CTIME reference
4423Brian Warner <warner@lothar.com>**20100114194052
4424 Ignore-this: 5a189c7a1181b07dd87f0a08ea31b6d3
4425]
4426[client.py: fix/update comments on KeyGenerator
4427Brian Warner <warner@lothar.com>**20100113004226
4428 Ignore-this: 2208adbb3fd6a911c9f44e814583cabd
4429]
4430[Clean up log.err calls, for one of the issues in #889.
4431Brian Warner <warner@lothar.com>**20100112013343
4432 Ignore-this: f58455ce15f1fda647c5fb25d234d2db
4433 
4434 allmydata.util.log.err() either takes a Failure as the first positional
4435 argument, or takes no positional arguments and must be invoked in an
4436 exception handler. Fixed its signature to match both foolscap.logging.log.err
4437 and twisted.python.log.err . Included a brief unit test.
4438]
4439[tidy up DeadReferenceError handling, ignore them in add_lease calls
4440Brian Warner <warner@lothar.com>**20100112000723
4441 Ignore-this: 72f1444e826fd0b9db6d318f89603c38
4442 
4443 Stop checking separately for ConnectionDone/ConnectionLost, since those have
4444 been folded into DeadReferenceError since foolscap-0.3.1 . Write
4445 rrefutil.trap_deadref() in terms of rrefutil.trap_and_discard() to improve
4446 code coverage.
4447]
4448[NEWS: improve "tahoe backup" notes, mention first-backup-after-upgrade duration
4449Brian Warner <warner@lothar.com>**20100111190132
4450 Ignore-this: 10347c590b3375964579ba6c2b0edb4f
4451 
4452 Thanks to Francois Deppierraz for the suggestion.
4453]
4454[test_repairer: add (commented-out) test_each_byte, to see exactly what the
4455Brian Warner <warner@lothar.com>**20100110203552
4456 Ignore-this: 8e84277d5304752edeff052b97821815
4457 Verifier misses
4458 
4459 The results (described in #819) match our expectations: it misses corruption
4460 in unused share fields and in most container fields (which are only visible
4461 to the storage server, not the client). 1265 bytes of a 2753 byte
4462 share (hosting a 56-byte file with an artifically small segment size) are
4463 unused, mostly in the unused tail of the overallocated UEB space (765 bytes),
4464 and the allocated-but-unwritten plaintext_hash_tree (480 bytes).
4465]
4466[repairer: fix some wrong offsets in the randomized verifier tests, debugged by Brian
4467zooko@zooko.com**20100110203721
4468 Ignore-this: 20604a609db8706555578612c1c12feb
4469 fixes #819
4470]
4471[test_repairer: fix colliding basedir names, which caused test inconsistencies
4472Brian Warner <warner@lothar.com>**20100110084619
4473 Ignore-this: b1d56dd27e6ab99a7730f74ba10abd23
4474]
4475[repairer: add deterministic test for #819, mark as TODO
4476zooko@zooko.com**20100110013619
4477 Ignore-this: 4cb8bb30b25246de58ed2b96fa447d68
4478]
4479[contrib/fuse/runtests.py: Tolerate the tahoe CLI returning deprecation warnings
4480francois@ctrlaltdel.ch**20100109175946
4481 Ignore-this: 419c354d9f2f6eaec03deb9b83752aee
4482 
4483 Depending on the versions of external libraries such as Twisted of Foolscap,
4484 the tahoe CLI can display deprecation warnings on stdout.  The tests should
4485 not interpret those warnings as a failure if the node is in fact correctly
4486 started.
4487   
4488 See http://allmydata.org/trac/tahoe/ticket/859 for an example of deprecation
4489 warnings.
4490 
4491 fixes #876
4492]
4493[contrib: fix fuse_impl_c to use new Python API
4494zooko@zooko.com**20100109174956
4495 Ignore-this: 51ca1ec7c2a92a0862e9b99e52542179
4496 original patch by Thomas Delaet, fixed by François, reviewed by Brian, committed by me
4497]
4498[docs: CREDITS: add David-Sarah to the CREDITS file
4499zooko@zooko.com**20100109060435
4500 Ignore-this: 896062396ad85f9d2d4806762632f25a
4501]
4502[mutable/publish: don't loop() right away upon DeadReferenceError. Closes #877
4503Brian Warner <warner@lothar.com>**20100102220841
4504 Ignore-this: b200e707b3f13aa8251981362b8a3e61
4505 
4506 The bug was that a disconnected server could cause us to re-enter the initial
4507 loop() call, sending multiple queries to a single server, provoking an
4508 incorrect UCWE. To fix it, stall the loop() with an eventual.fireEventually()
4509]
4510[immutable/checker.py: oops, forgot some imports. Also hush pyflakes.
4511Brian Warner <warner@lothar.com>**20091229233909
4512 Ignore-this: 4d61bd3f8113015a4773fd4768176e51
4513]
4514[mutable repair: return successful=False when numshares<k (thus repair fails),
4515Brian Warner <warner@lothar.com>**20091229233746
4516 Ignore-this: d881c3275ff8c8bee42f6a80ca48441e
4517 instead of weird errors. Closes #874 and #786.
4518 
4519 Previously, if the file had 0 shares, this would raise TypeError as it tried
4520 to call download_version(None). If the file had some shares but fewer than
4521 'k', it would incorrectly raise MustForceRepairError.
4522 
4523 Added get_successful() to the IRepairResults API, to give repair() a place to
4524 report non-code-bug problems like this.
4525]
4526[node.py/interfaces.py: minor docs fixes
4527Brian Warner <warner@lothar.com>**20091229230409
4528 Ignore-this: c86ad6342ef0f95d50639b4f99cd4ddf
4529]
4530[NEWS: fix 1.4.1 announcement w.r.t. add-lease behavior in older releases
4531Brian Warner <warner@lothar.com>**20091229230310
4532 Ignore-this: bbbbb9c961f3bbcc6e5dbe0b1594822
4533]
4534[checker: don't let failures in add-lease affect checker results. Closes #875.
4535Brian Warner <warner@lothar.com>**20091229230108
4536 Ignore-this: ef1a367b93e4d01298c2b1e6ca59c492
4537 
4538 Mutable servermap updates and the immutable checker, when run with
4539 add_lease=True, send both the do-you-have-block and add-lease commands in
4540 parallel, to avoid an extra round trip time. Many older servers have problems
4541 with add-lease and raise various exceptions, which don't generally matter.
4542 The client-side code was catching+ignoring some of them, but unrecognized
4543 exceptions were passed through to the DYHB code, concealing the DYHB results
4544 from the checker, making it think the server had no shares.
4545 
4546 The fix is to separate the code paths. Both commands are sent at the same
4547 time, but the errback path from add-lease is handled separately. Known
4548 exceptions are ignored, the others (both unknown-remote and all-local) are
4549 logged (log.WEIRD, which will trigger an Incident), but neither will affect
4550 the DYHB results.
4551 
4552 The add-lease message is sent first, and we know that the server handles them
4553 synchronously. So when the checker is done, we can be sure that all the
4554 add-lease messages have been retired. This makes life easier for unit tests.
4555]
4556[test_cli: verify fix for "tahoe get" not creating empty file on error (#121)
4557Brian Warner <warner@lothar.com>**20091227235444
4558 Ignore-this: 6444d52413b68eb7c11bc3dfdc69c55f
4559]
4560[addendum to "Fix 'tahoe ls' on files (#771)"
4561Brian Warner <warner@lothar.com>**20091227232149
4562 Ignore-this: 6dd5e25f8072a3153ba200b7fdd49491
4563 
4564 tahoe_ls.py: tolerate missing metadata
4565 web/filenode.py: minor cleanups
4566 test_cli.py: test 'tahoe ls FILECAP'
4567]
4568[Fix 'tahoe ls' on files (#771). Patch adapted from Kevan Carstensen.
4569Brian Warner <warner@lothar.com>**20091227225443
4570 Ignore-this: 8bf8c7b1cd14ea4b0ebd453434f4fe07
4571 
4572 web/filenode.py: also serve edge metadata when using t=json on a
4573                  DIRCAP/childname object.
4574 tahoe_ls.py: list file objects as if we were listing one-entry directories.
4575              Show edge metadata if we have it, which will be true when doing
4576              'tahoe ls DIRCAP/filename' and false when doing 'tahoe ls
4577              FILECAP'
4578]
4579[tahoe_get: don't create the output file on error. Closes #121.
4580Brian Warner <warner@lothar.com>**20091227220404
4581 Ignore-this: 58d5e793a77ec6e87d9394ade074b926
4582]
4583[webapi: don't accept zero-length childnames during traversal. Closes #358, #676.
4584Brian Warner <warner@lothar.com>**20091227201043
4585 Ignore-this: a9119dec89e1c7741f2289b0cad6497b
4586 
4587 This forbids operations that would implicitly create a directory with a
4588 zero-length (empty string) name, like what you'd get if you did "tahoe put
4589 local /oops/blah" (#358) or "POST /uri/CAP//?t=mkdir" (#676). The error
4590 message is fairly friendly too.
4591 
4592 Also added code to "tahoe put" to catch this error beforehand and suggest the
4593 correct syntax (i.e. without the leading slash).
4594]
4595[CLI: send 'Accept:' header to ask for text/plain tracebacks. Closes #646.
4596Brian Warner <warner@lothar.com>**20091227195828
4597 Ignore-this: 44c258d4d4c7dac0ed58adb22f73331
4598 
4599 The webapi has been looking for an Accept header since 1.4.0, but it treats a
4600 missing header as equal to */* (to honor RFC2616). This change finally
4601 modifies our CLI tools to ask for "text/plain, application/octet-stream",
4602 which seems roughly correct (we either want a plain-text traceback or error
4603 message, or an uninterpreted chunk of binary data to save to disk). Some day
4604 we'll figure out how JSON fits into this scheme.
4605]
4606[Makefile: upload-tarballs: switch from xfer-client to flappclient, closes #350
4607Brian Warner <warner@lothar.com>**20091227163703
4608 Ignore-this: 3beeecdf2ad9c2438ab57f0e33dcb357
4609 
4610 I've also set up a new flappserver on source@allmydata.org to receive the
4611 tarballs. We still need to replace the gutsy buildslave (which is where the
4612 tarballs used to be generated+uploaded) and give it the new FURL.
4613]
4614[misc/ringsim.py: make it deterministic, more detail about grid-is-full behavior
4615Brian Warner <warner@lothar.com>**20091227024832
4616 Ignore-this: a691cc763fb2e98a4ce1767c36e8e73f
4617]
4618[misc/ringsim.py: tool to discuss #302
4619Brian Warner <warner@lothar.com>**20091226060339
4620 Ignore-this: fc171369b8f0d97afeeb8213e29d10ed
4621]
4622[docs: fix helper.txt to describe new config style
4623zooko@zooko.com**20091224223522
4624 Ignore-this: 102e7692dc414a4b466307f7d78601fe
4625]
4626[docs/stats.txt: add TOC, notes about controlling gatherer's listening port
4627Brian Warner <warner@lothar.com>**20091224202133
4628 Ignore-this: 8eef63b0e18db5aa8249c2eafde02c05
4629 
4630 Thanks to Jody Harris for the suggestions.
4631]
4632[Add docs/stats.py, explaining Tahoe stats, the gatherer, and the munin plugins.
4633Brian Warner <warner@lothar.com>**20091223052400
4634 Ignore-this: 7c9eeb6e5644eceda98b59a67730ccd5
4635]
4636[more #859: avoid deprecation warning for unit tests too, hush pyflakes
4637Brian Warner <warner@lothar.com>**20091215000147
4638 Ignore-this: 193622e24d31077da825a11ed2325fd3
4639 
4640 * factor maybe-import-sha logic into util.hashutil
4641]
4642[use hashlib module if available, thus avoiding a DeprecationWarning for importing the old sha module; fixes #859
4643zooko@zooko.com**20091214212703
4644 Ignore-this: 8d0f230a4bf8581dbc1b07389d76029c
4645]
4646[docs: reflow architecture.txt to 78-char lines
4647zooko@zooko.com**20091208232943
4648 Ignore-this: 88f55166415f15192e39407815141f77
4649]
4650[docs: update the about.html a little
4651zooko@zooko.com**20091208212737
4652 Ignore-this: 3fe2d9653c6de0727d3e82bd70f2a8ed
4653]
4654[docs: remove obsolete doc file "codemap.txt"
4655zooko@zooko.com**20091113163033
4656 Ignore-this: 16bc21a1835546e71d1b344c06c61ebb
4657 I started to update this to reflect the current codebase, but then I thought (a) nobody seemed to notice that it hasn't been updated since December 2007, and (b) it will just bit-rot again, so I'm removing it.
4658]
4659[mutable/retrieve.py: stop reaching into private MutableFileNode attributes
4660Brian Warner <warner@lothar.com>**20091208172921
4661 Ignore-this: 61e548798c1105aed66a792bf26ceef7
4662]
4663[mutable/servermap.py: stop reaching into private MutableFileNode attributes
4664Brian Warner <warner@lothar.com>**20091208172608
4665 Ignore-this: b40a6b62f623f9285ad96fda139c2ef2
4666]
4667[mutable/servermap.py: oops, query N+e servers in MODE_WRITE, not k+e
4668Brian Warner <warner@lothar.com>**20091208171156
4669 Ignore-this: 3497f4ab70dae906759007c3cfa43bc
4670 
4671 under normal conditions, this wouldn't cause any problems, but if the shares
4672 are really sparse (perhaps because new servers were added), then
4673 file-modifies might stop looking too early and leave old shares in place
4674]
4675[control.py: fix speedtest: use download_best_version (not read) on mutable nodes
4676Brian Warner <warner@lothar.com>**20091207060512
4677 Ignore-this: 7125eabfe74837e05f9291dd6414f917
4678]
4679[FTP-and-SFTP.txt: fix ssh-keygen pointer
4680Brian Warner <warner@lothar.com>**20091207052803
4681 Ignore-this: bc2a70ee8c58ec314e79c1262ccb22f7
4682]
4683[setup: ignore _darcs in the "test-clean" test and make the "clean" step remove all .egg's in the root dir
4684zooko@zooko.com**20091206184835
4685 Ignore-this: 6066bd160f0db36d7bf60aba405558d2
4686]
4687[remove MutableFileNode.download(), prefer download_best_version() instead
4688Brian Warner <warner@lothar.com>**20091201225438
4689 Ignore-this: 5733eb373a902063e09fd52cc858dec0
4690]
4691[Simplify immutable download API: use just filenode.read(consumer, offset, size)
4692Brian Warner <warner@lothar.com>**20091201225330
4693 Ignore-this: bdedfb488ac23738bf52ae6d4ab3a3fb
4694 
4695 * remove Downloader.download_to_data/download_to_filename/download_to_filehandle
4696 * remove download.Data/FileName/FileHandle targets
4697 * remove filenode.download/download_to_data/download_to_filename methods
4698 * leave Downloader.download (the whole Downloader will go away eventually)
4699 * add util.consumer.MemoryConsumer/download_to_data, for convenience
4700   (this is mostly used by unit tests, but it gets used by enough non-test
4701    code to warrant putting it in allmydata.util)
4702 * update tests
4703 * removes about 180 lines of code. Yay negative code days!
4704 
4705 Overall plan is to rewrite immutable/download.py and leave filenode.read() as
4706 the sole read-side API.
4707]
4708[server.py: undo my bogus 'correction' of David-Sarah's comment fix
4709Brian Warner <warner@lothar.com>**20091201024607
4710 Ignore-this: ff4bb58f6a9e045b900ac3a89d6f506a
4711 
4712 and move it to a better line
4713]
4714[Implement more coherent behavior when copying with dircaps/filecaps (closes #761). Patch by Kevan Carstensen.
4715"Brian Warner <warner@lothar.com>"**20091130211009]
4716[storage.py: update comment
4717"Brian Warner <warner@lothar.com>"**20091130195913]
4718[storage server: detect disk space usage on Windows too (fixes #637)
4719david-sarah@jacaranda.org**20091121055644
4720 Ignore-this: 20fb30498174ce997befac7701fab056
4721]
4722[make status of finished operations consistently "Finished"
4723david-sarah@jacaranda.org**20091121061543
4724 Ignore-this: 97d483e8536ccfc2934549ceff7055a3
4725]
4726[NEWS: update with all user-visible changes since the last release
4727Brian Warner <warner@lothar.com>**20091127224217
4728 Ignore-this: 741da6cd928e939fb6d21a61ea3daf0b
4729]
4730[update "tahoe backup" docs, and webapi.txt's mkdir-with-children
4731Brian Warner <warner@lothar.com>**20091127055900
4732 Ignore-this: defac1fb9a2335b0af3ef9dbbcc67b7e
4733]
4734[Add dirnodes to backupdb and "tahoe backup", closes #606.
4735Brian Warner <warner@lothar.com>**20091126234257
4736 Ignore-this: fa88796fcad1763c6a2bf81f56103223
4737 
4738 * backups now share dirnodes with any previous backup, in any location,
4739   so renames and moves are handled very efficiently
4740 * "tahoe backup" no longer bothers reading the previous snapshot
4741 * if you switch grids, you should delete ~/.tahoe/private/backupdb.sqlite,
4742   to force new uploads of all files and directories
4743]
4744[webapi: fix t=check for DIR2-LIT (i.e. empty immutable directories)
4745Brian Warner <warner@lothar.com>**20091126232731
4746 Ignore-this: 8513c890525c69c1eca0e80d53a231f8
4747]
4748[PipelineError: fix str() on python2.4 . Closes #842.
4749Brian Warner <warner@lothar.com>**20091124212512
4750 Ignore-this: e62c92ea9ede2ab7d11fe63f43b9c942
4751]
4752[test_uri.py: s/NewDirnode/Dirnode/ , now that they aren't "new" anymore
4753Brian Warner <warner@lothar.com>**20091120075553
4754 Ignore-this: 61c8ef5e45a9d966873a610d8349b830
4755]
4756[interface name cleanups: IFileNode, IImmutableFileNode, IMutableFileNode
4757Brian Warner <warner@lothar.com>**20091120075255
4758 Ignore-this: e3d193c229e2463e1d0b0c92306de27f
4759 
4760 The proper hierarchy is:
4761  IFilesystemNode
4762  +IFileNode
4763  ++IMutableFileNode
4764  ++IImmutableFileNode
4765  +IDirectoryNode
4766 
4767 Also expand test_client.py (NodeMaker) to hit all IFilesystemNode types.
4768]
4769[class name cleanups: s/FileNode/ImmutableFileNode/
4770Brian Warner <warner@lothar.com>**20091120072239
4771 Ignore-this: 4b3218f2d0e585c62827e14ad8ed8ac1
4772 
4773 also fix test/bench_dirnode.py for recent dirnode changes
4774]
4775[Use DIR-IMM and t=mkdir-immutable for "tahoe backup", for #828
4776Brian Warner <warner@lothar.com>**20091118192813
4777 Ignore-this: a4720529c9bc6bc8b22a3d3265925491
4778]
4779[web/directory.py: use "DIR-IMM" to describe immutable directories, not DIR-RO
4780Brian Warner <warner@lothar.com>**20091118191832
4781 Ignore-this: aceafd6ab4bf1cc0c2a719ef7319ac03
4782]
4783[web/info.py: hush pyflakes
4784Brian Warner <warner@lothar.com>**20091118191736
4785 Ignore-this: edc5f128a2b8095fb20686a75747c8
4786]
4787[make get_size/get_current_size consistent for all IFilesystemNode classes
4788Brian Warner <warner@lothar.com>**20091118191624
4789 Ignore-this: bd3449cf96e4827abaaf962672c1665a
4790 
4791 * stop caching most_recent_size in dirnode, rely upon backing filenode for it
4792 * start caching most_recent_size in MutableFileNode
4793 * return None when you don't know, not "?"
4794 * only render None as "?" in the web "more info" page
4795 * add get_size/get_current_size to UnknownNode
4796]
4797[ImmutableDirectoryURIVerifier: fix verifycap handling
4798Brian Warner <warner@lothar.com>**20091118164238
4799 Ignore-this: 6bba5c717b54352262eabca6e805d590
4800]
4801[Add t=mkdir-immutable to the webapi. Closes #607.
4802Brian Warner <warner@lothar.com>**20091118070900
4803 Ignore-this: 311e5fab9a5f28b9e8a28d3d08f3c0d
4804 
4805 * change t=mkdir-with-children to not use multipart/form encoding. Instead,
4806   the request body is all JSON. t=mkdir-immutable uses this format too.
4807 * make nodemaker.create_immutable_dirnode() get convergence from SecretHolder,
4808   but let callers override it
4809 * raise NotDeepImmutableError instead of using assert()
4810 * add mutable= argument to DirectoryNode.create_subdirectory(), default True
4811]
4812[move convergence secret into SecretHolder, next to lease secret
4813Brian Warner <warner@lothar.com>**20091118015444
4814 Ignore-this: 312f85978a339f2d04deb5bcb8f511bc
4815]
4816[nodemaker: implement immutable directories (internal interface), for #607
4817Brian Warner <warner@lothar.com>**20091112002233
4818 Ignore-this: d09fccf41813fdf7e0db177ed9e5e130
4819 
4820 * nodemaker.create_from_cap() now handles DIR2-CHK and DIR2-LIT
4821 * client.create_immutable_dirnode() is used to create them
4822 * no webapi yet
4823]
4824[stop using IURI()/etc as an adapter
4825Brian Warner <warner@lothar.com>**20091111224542
4826 Ignore-this: 9611da7ea6a4696de2a3b8c08776e6e0
4827]
4828[clean up uri-vs-cap terminology, emphasize cap instances instead of URI strings
4829Brian Warner <warner@lothar.com>**20091111222619
4830 Ignore-this: 93626385f6e7f039ada71f54feefe267
4831 
4832  * "cap" means a python instance which encapsulates a filecap/dircap (uri.py)
4833  * "uri" means a string with a "URI:" prefix
4834  * FileNode instances are created with (and retain) a cap instance, and
4835    generate uri strings on demand
4836  * .get_cap/get_readcap/get_verifycap/get_repaircap return cap instances
4837  * .get_uri/get_readonly_uri return uri strings
4838 
4839 * add filenode.download_to_filename() for control.py, should find a better way
4840 * use MutableFileNode.init_from_cap, not .init_from_uri
4841 * directory URI instances: use get_filenode_cap, not get_filenode_uri
4842 * update/cleanup bench_dirnode.py to match, add Makefile target to run it
4843]
4844[add parser for immutable directory caps: DIR2-CHK, DIR2-LIT, DIR2-CHK-Verifier
4845Brian Warner <warner@lothar.com>**20091104181351
4846 Ignore-this: 854398cc7a75bada57fa97c367b67518
4847]
4848[wui: s/TahoeLAFS/Tahoe-LAFS/
4849zooko@zooko.com**20091029035050
4850 Ignore-this: 901e64cd862e492ed3132bd298583c26
4851]
4852[tests: bump up the timeout on test_repairer to see if 120 seconds was too short for François's ARM box to do the test even when it was doing it right.
4853zooko@zooko.com**20091027224800
4854 Ignore-this: 95e93dc2e018b9948253c2045d506f56
4855]
4856[dirnode.pack_children(): add deep_immutable= argument
4857Brian Warner <warner@lothar.com>**20091026162809
4858 Ignore-this: d5a2371e47662c4bc6eff273e8181b00
4859 
4860 This will be used by DIR2:CHK to enforce the deep-immutability requirement.
4861]
4862[webapi: use t=mkdir-with-children instead of a children= arg to t=mkdir .
4863Brian Warner <warner@lothar.com>**20091026011321
4864 Ignore-this: 769cab30b6ab50db95000b6c5a524916
4865 
4866 This is safer: in the earlier API, an old webapi server would silently ignore
4867 the initial children, and clients trying to set them would have to fetch the
4868 newly-created directory to discover the incompatibility. In the new API,
4869 clients using t=mkdir-with-children against an old webapi server will get a
4870 clear error.
4871]
4872[nodemaker.create_new_mutable_directory: pack_children() in initial_contents=
4873Brian Warner <warner@lothar.com>**20091020005118
4874 Ignore-this: bd43c4eefe06fd32b7492bcb0a55d07e
4875 instead of creating an empty file and then adding the children later.
4876 
4877 This should speed up mkdir(initial_children) considerably, removing two
4878 roundtrips and an entire read-modify-write cycle, probably bringing it down
4879 to a single roundtrip. A quick test (against the volunteergrid) suggests a
4880 30% speedup.
4881 
4882 test_dirnode: add new tests to enforce the restrictions that interfaces.py
4883 claims for create_new_mutable_directory(): no UnknownNodes, metadata dicts
4884]
4885[test_dirnode.py: add tests of initial_children= args to client.create_dirnode
4886Brian Warner <warner@lothar.com>**20091017194159
4887 Ignore-this: 2e2da28323a4d5d815466387914abc1b
4888 and nodemaker.create_new_mutable_directory
4889]
4890[update many dirnode interfaces to accept dict-of-nodes instead of dict-of-caps
4891Brian Warner <warner@lothar.com>**20091017192829
4892 Ignore-this: b35472285143862a856bf4b361d692f0
4893 
4894 interfaces.py: define INodeMaker, document argument values, change
4895                create_new_mutable_directory() to take dict-of-nodes. Change
4896                dirnode.set_nodes() and dirnode.create_subdirectory() too.
4897 nodemaker.py: use INodeMaker, update create_new_mutable_directory()
4898 client.py: have create_dirnode() delegate initial_children= to nodemaker
4899 dirnode.py (Adder): take dict-of-nodes instead of list-of-nodes, which
4900                     updates set_nodes() and create_subdirectory()
4901 web/common.py (convert_initial_children_json): create dict-of-nodes
4902 web/directory.py: same
4903 web/unlinked.py: same
4904 test_dirnode.py: update tests to match
4905]
4906[dirnode.py: move pack_children() out to a function, for eventual use by others
4907Brian Warner <warner@lothar.com>**20091017180707
4908 Ignore-this: 6a823fb61f2c180fd38d6742d3196a7a
4909]
4910[move dirnode.CachingDict to dictutil.AuxValueDict, generalize method names,
4911Brian Warner <warner@lothar.com>**20091017180005
4912 Ignore-this: b086933cf429df0fcea16a308d2640dd
4913 improve tests. Let dirnode _pack_children accept either dict or AuxValueDict.
4914]
4915[test/common.py: update FakeMutableFileNode to new contents= callable scheme
4916Brian Warner <warner@lothar.com>**20091013052154
4917 Ignore-this: 62f00a76454a2190d1c8641c5993632f
4918]
4919[The initial_children= argument to nodemaker.create_new_mutable_directory is
4920Brian Warner <warner@lothar.com>**20091013031922
4921 Ignore-this: 72e45317c21f9eb9ec3bd79bd4311f48
4922 now enabled.
4923]
4924[client.create_mutable_file(contents=) now accepts a callable, which is
4925Brian Warner <warner@lothar.com>**20091013031232
4926 Ignore-this: 3c89d2f50c1e652b83f20bd3f4f27c4b
4927 invoked with the new MutableFileNode and is supposed to return the initial
4928 contents. This can be used by e.g. a new dirnode which needs the filenode's
4929 writekey to encrypt its initial children.
4930 
4931 create_mutable_file() still accepts a bytestring too, or None for an empty
4932 file.
4933]
4934[webapi: t=mkdir now accepts initial children, using the same JSON that t=json
4935Brian Warner <warner@lothar.com>**20091013023444
4936 Ignore-this: 574a46ed46af4251abf8c9580fd31ef7
4937 emits.
4938 
4939 client.create_dirnode(initial_children=) now works.
4940]
4941[replace dirnode.create_empty_directory() with create_subdirectory(), which
4942Brian Warner <warner@lothar.com>**20091013021520
4943 Ignore-this: 6b57cb51bcfcc6058d0df569fdc8a9cf
4944 takes an initial_children= argument
4945]
4946[dirnode.set_children: change return value: fire with self instead of None
4947Brian Warner <warner@lothar.com>**20091013015026
4948 Ignore-this: f1d14e67e084e4b2a4e25fa849b0e753
4949]
4950[dirnode.set_nodes: change return value: fire with self instead of None
4951Brian Warner <warner@lothar.com>**20091013014546
4952 Ignore-this: b75b3829fb53f7399693f1c1a39aacae
4953]
4954[dirnode.set_children: take a dict, not a list
4955Brian Warner <warner@lothar.com>**20091013002440
4956 Ignore-this: 540ce72ce2727ee053afaae1ff124e21
4957]
4958[dirnode.set_uri/set_children: change signature to take writecap+readcap
4959Brian Warner <warner@lothar.com>**20091012235126
4960 Ignore-this: 5df617b2d379a51c79148a857e6026b1
4961 instead of a single cap. The webapi t=set_children call benefits too.
4962]
4963[replace Client.create_empty_dirnode() with create_dirnode(), in anticipation
4964Brian Warner <warner@lothar.com>**20091012224506
4965 Ignore-this: cbdaa4266ecb3c6496ffceab4f95709d
4966 of adding initial_children= argument.
4967 
4968 Includes stubbed-out initial_children= support.
4969]
4970[test_web.py: use a less-fake client, making test harness smaller
4971Brian Warner <warner@lothar.com>**20091012222808
4972 Ignore-this: 29e95147f8c94282885c65b411d100bb
4973]
4974[webapi.txt: document t=set_children, other small edits
4975Brian Warner <warner@lothar.com>**20091009200446
4976 Ignore-this: 4d7e76b04a7b8eaa0a981879f778ea5d
4977]
4978[Verifier: check the full cryptext-hash tree on each share. Removed .todos
4979Brian Warner <warner@lothar.com>**20091005221849
4980 Ignore-this: 6fb039c5584812017d91725e687323a5
4981 from the last few test_repairer tests that were waiting on this.
4982]
4983[Verifier: check the full block-hash-tree on each share
4984Brian Warner <warner@lothar.com>**20091005214844
4985 Ignore-this: 3f7ccf6d253f32340f1bf1da27803eee
4986 
4987 Removed the .todo from two test_repairer tests that check this. The only
4988 remaining .todos are on the three crypttext-hash-tree tests.
4989]
4990[Verifier: check the full share-hash chain on each share
4991Brian Warner <warner@lothar.com>**20091005213443
4992 Ignore-this: 3d30111904158bec06a4eac22fd39d17
4993 
4994 Removed the .todo from two test_repairer tests that check this.
4995]
4996[test_repairer: rename Verifier test cases to be more precise and less verbose
4997Brian Warner <warner@lothar.com>**20091005201115
4998 Ignore-this: 64be7094e33338c7c2aea9387e138771
4999]
5000[immutable/checker.py: rearrange code a little bit, make it easier to follow
5001Brian Warner <warner@lothar.com>**20091005200252
5002 Ignore-this: 91cc303fab66faf717433a709f785fb5
5003]
5004[test/common.py: wrap docstrings to 80cols so I can read them more easily
5005Brian Warner <warner@lothar.com>**20091005200143
5006 Ignore-this: b180a3a0235cbe309c87bd5e873cbbb3
5007]
5008[immutable/download.py: wrap to 80cols, no functional changes
5009Brian Warner <warner@lothar.com>**20091005192542
5010 Ignore-this: 6b05fe3dc6d78832323e708b9e6a1fe
5011]
5012[CHK-hashes.svg: cross out plaintext hashes, since we don't include
5013Brian Warner <warner@lothar.com>**20091005010803
5014 Ignore-this: bea2e953b65ec7359363aa20de8cb603
5015 them (until we finish #453)
5016]
5017[docs: a few licensing clarifications requested by Ubuntu
5018zooko@zooko.com**20090927033226
5019 Ignore-this: 749fc8c9aeb6dc643669854a3e81baa7
5020]
5021[setup: remove binary WinFUSE modules
5022zooko@zooko.com**20090924211436
5023 Ignore-this: 8aefc571d2ae22b9405fc650f2c2062
5024 I would prefer to have just source code, or indications of what 3rd-party packages are required, under revision control, and have the build process generate o
5025 r acquire the binaries as needed.  Also, having these in our release tarballs is interfering with getting Tahoe-LAFS uploaded into Ubuntu Karmic.  (Technicall
5026 y, they would accept binary modules as long as they came with the accompanying source so that they could satisfy their obligations under GPL2+ and TGPPL1+, bu
5027 t it is easier for now to remove the binaries from the source tree.)
5028 In this case, the binaries are from the tahoe-w32-client project: http://allmydata.org/trac/tahoe-w32-client , from which you can also get the source.
5029]
5030[setup: remove binary _fusemodule.so 's
5031zooko@zooko.com**20090924211130
5032 Ignore-this: 74487bbe27d280762ac5dd5f51e24186
5033 I would prefer to have just source code, or indications of what 3rd-party packages are required, under revision control, and have the build process generate or acquire the binaries as needed.  Also, having these in our release tarballs is interfering with getting Tahoe-LAFS uploaded into Ubuntu Karmic.  (Technically, they would accept binary modules as long as they came with the accompanying source so that they could satisfy their obligations under GPL2+ and TGPPL1+, but it is easier for now to remove the binaries from the source tree.)
5034 In this case, these modules come from the MacFUSE project: http://code.google.com/p/macfuse/
5035]
5036[doc: add a copy of LGPL2 for documentation purposes for ubuntu
5037zooko@zooko.com**20090924054218
5038 Ignore-this: 6a073b48678a7c84dc4fbcef9292ab5b
5039]
5040[setup: remove a convenience copy of figleaf, to ease inclusion into Ubuntu Karmic Koala
5041zooko@zooko.com**20090924053215
5042 Ignore-this: a0b0c990d6e2ee65c53a24391365ac8d
5043 We need to carefully document the licence of figleaf in order to get Tahoe-LAFS into Ubuntu Karmic Koala.  However, figleaf isn't really a part of Tahoe-LAFS per se -- this is just a "convenience copy" of a development tool.  The quickest way to make Tahoe-LAFS acceptable for Karmic then, is to remove figleaf from the Tahoe-LAFS tarball itself.  People who want to run figleaf on Tahoe-LAFS (as everyone should want) can install figleaf themselves.  I haven't tested this -- there may be incompatibilities between upstream figleaf and the copy that we had here...
5044]
5045[setup: shebang for misc/build-deb.py to fail quickly
5046zooko@zooko.com**20090819135626
5047 Ignore-this: 5a1b893234d2d0bb7b7346e84b0a6b4d
5048 Without this patch, when I ran "chmod +x ./misc/build-deb.py && ./misc/build-deb.py" then it hung indefinitely.  (I wonder what it was doing.)
5049]
5050[docs: Shawn Willden grants permission for his contributions under GPL2+|TGPPL1+
5051zooko@zooko.com**20090921164651
5052 Ignore-this: ef1912010d07ff2ffd9678e7abfd0d57
5053]
5054[docs: Csaba Henk granted permission to license fuse.py under the same terms as Tahoe-LAFS itself
5055zooko@zooko.com**20090921154659
5056 Ignore-this: c61ba48dcb7206a89a57ca18a0450c53
5057]
5058[setup: mark setup.py as having utf-8 encoding in it
5059zooko@zooko.com**20090920180343
5060 Ignore-this: 9d3850733700a44ba7291e9c5e36bb91
5061]
5062[doc: licensing cleanups
5063zooko@zooko.com**20090920171631
5064 Ignore-this: 7654f2854bf3c13e6f4d4597633a6630
5065 Use nice utf-8 © instead of "(c)". Remove licensing statements on utility modules that have been assigned to allmydata.com by their original authors. (Nattraverso was not assigned to allmydata.com -- it was LGPL'ed -- but I checked and src/allmydata/util/iputil.py was completely rewritten and doesn't contain any line of code from nattraverso.)  Add notes to misc/debian/copyright about licensing on files that aren't just allmydata.com-licensed.
5066]
5067[build-deb.py: run darcsver early, otherwise we get the wrong version later on
5068Brian Warner <warner@lothar.com>**20090918033620
5069 Ignore-this: 6635c5b85e84f8aed0d8390490c5392a
5070]
5071[new approach for debian packaging, sharing pieces across distributions. Still experimental, still only works for sid.
5072warner@lothar.com**20090818190527
5073 Ignore-this: a75eb63db9106b3269badbfcdd7f5ce1
5074]
5075[new experimental deb-packaging rules. Only works for sid so far.
5076Brian Warner <warner@lothar.com>**20090818014052
5077 Ignore-this: 3a26ad188668098f8f3cc10a7c0c2f27
5078]
5079[setup.py: read _version.py and pass to setup(version=), so more commands work
5080Brian Warner <warner@lothar.com>**20090818010057
5081 Ignore-this: b290eb50216938e19f72db211f82147e
5082 like "setup.py --version" and "setup.py --fullname"
5083]
5084[test/check_speed.py: fix shbang line
5085Brian Warner <warner@lothar.com>**20090818005948
5086 Ignore-this: 7f3a37caf349c4c4de704d0feb561f8d
5087]
5088[setup: remove bundled version of darcsver-1.2.1
5089zooko@zooko.com**20090816233432
5090 Ignore-this: 5357f26d2803db2d39159125dddb963a
5091 That version of darcsver emits a scary error message when the darcs executable or the _darcs subdirectory is not found.
5092 This error is hidden (unless the --loud option is passed) in darcsver >= 1.3.1.
5093 Fixes #788.
5094]
5095[de-Service-ify Helper, pass in storage_broker and secret_holder directly.
5096Brian Warner <warner@lothar.com>**20090815201737
5097 Ignore-this: 86b8ac0f90f77a1036cd604dd1304d8b
5098 This makes it more obvious that the Helper currently generates leases with
5099 the Helper's own secrets, rather than getting values from the client, which
5100 is arguably a bug that will likely be resolved with the Accounting project.
5101]
5102[immutable.Downloader: pass StorageBroker to constructor, stop being a Service
5103Brian Warner <warner@lothar.com>**20090815192543
5104 Ignore-this: af5ab12dbf75377640a670c689838479
5105 child of the client, access with client.downloader instead of
5106 client.getServiceNamed("downloader"). The single "Downloader" instance is
5107 scheduled for demolition anyways, to be replaced by individual
5108 filenode.download calls.
5109]
5110[tests: double the timeout on test_runner.RunNode.test_introducer since feisty hit a timeout
5111zooko@zooko.com**20090815160512
5112 Ignore-this: ca7358bce4bdabe8eea75dedc39c0e67
5113 I'm not sure if this is an actual timing issue (feisty is running on an overloaded VM if I recall correctly), or it there is a deeper bug.
5114]
5115[stop making History be a Service, it wasn't necessary
5116Brian Warner <warner@lothar.com>**20090815114415
5117 Ignore-this: b60449231557f1934a751c7effa93cfe
5118]
5119[Overhaul IFilesystemNode handling, to simplify tests and use POLA internally.
5120Brian Warner <warner@lothar.com>**20090815112846
5121 Ignore-this: 1db1b9c149a60a310228aba04c5c8e5f
5122 
5123 * stop using IURI as an adapter
5124 * pass cap strings around instead of URI instances
5125 * move filenode/dirnode creation duties from Client to new NodeMaker class
5126 * move other Client duties to KeyGenerator, SecretHolder, History classes
5127 * stop passing Client reference to dirnode/filenode constructors
5128   - pass less-powerful references instead, like StorageBroker or Uploader
5129 * always create DirectoryNodes by wrapping a filenode (mutable for now)
5130 * remove some specialized mock classes from unit tests
5131 
5132 Detailed list of changes (done one at a time, then merged together)
5133 
5134 always pass a string to create_node_from_uri(), not an IURI instance
5135 always pass a string to IFilesystemNode constructors, not an IURI instance
5136 stop using IURI() as an adapter, switch on cap prefix in create_node_from_uri()
5137 client.py: move SecretHolder code out to a separate class
5138 test_web.py: hush pyflakes
5139 client.py: move NodeMaker functionality out into a separate object
5140 LiteralFileNode: stop storing a Client reference
5141 immutable Checker: remove Client reference, it only needs a SecretHolder
5142 immutable Upload: remove Client reference, leave SecretHolder and StorageBroker
5143 immutable Repairer: replace Client reference with StorageBroker and SecretHolder
5144 immutable FileNode: remove Client reference
5145 mutable.Publish: stop passing Client
5146 mutable.ServermapUpdater: get StorageBroker in constructor, not by peeking into Client reference
5147 MutableChecker: reference StorageBroker and History directly, not through Client
5148 mutable.FileNode: removed unused indirection to checker classes
5149 mutable.FileNode: remove Client reference
5150 client.py: move RSA key generation into a separate class, so it can be passed to the nodemaker
5151 move create_mutable_file() into NodeMaker
5152 test_dirnode.py: stop using FakeClient mockups, use NoNetworkGrid instead. This simplifies the code, but takes longer to run (17s instead of 6s). This should come down later when other cleanups make it possible to use simpler (non-RSA) fake mutable files for dirnode tests.
5153 test_mutable.py: clean up basedir names
5154 client.py: move create_empty_dirnode() into NodeMaker
5155 dirnode.py: get rid of DirectoryNode.create
5156 remove DirectoryNode.init_from_uri, refactor NodeMaker for customization, simplify test_web's mock Client to match
5157 stop passing Client to DirectoryNode, make DirectoryNode.create_with_mutablefile the normal DirectoryNode constructor, start removing client from NodeMaker
5158 remove Client from NodeMaker
5159 move helper status into History, pass History to web.Status instead of Client
5160 test_mutable.py: fix minor typo
5161]
5162[docs: edits for docs/running.html from Sam Mason
5163zooko@zooko.com**20090809201416
5164 Ignore-this: 2207e80449943ebd4ed50cea57c43143
5165]
5166[docs: install.html: instruct Debian users to use this document and not to go find the DownloadDebianPackages page, ignore the warning at the top of it, and try it
5167zooko@zooko.com**20090804123840
5168 Ignore-this: 49da654f19d377ffc5a1eff0c820e026
5169 http://allmydata.org/pipermail/tahoe-dev/2009-August/002507.html
5170]
5171[docs: relnotes.txt: reflow to 63 chars wide because google groups and some web forms seem to wrap to that
5172zooko@zooko.com**20090802135016
5173 Ignore-this: 53b1493a0491bc30fb2935fad283caeb
5174]
5175[docs: about.html: fix English usage noticed by Amber
5176zooko@zooko.com**20090802050533
5177 Ignore-this: 89965c4650f9bd100a615c401181a956
5178]
5179[docs: fix mis-spelled word in about.html
5180zooko@zooko.com**20090802050320
5181 Ignore-this: fdfd0397bc7cef9edfde425dddeb67e5
5182]
5183[TAG allmydata-tahoe-1.5.0
5184zooko@zooko.com**20090802031303
5185 Ignore-this: 94e5558e7225c39a86aae666ea00f166
5186]
5187Patch bundle hash:
5188107121306a20b57c808f30125dab116da28e8db2