Ticket #833: all-with-json-fix-diff.txt

File all-with-json-fix-diff.txt, 268.1 KB (added by davidsarah, at 2010-01-24T06:10:54Z)

Diff for everything including t=json fix (code + tests + docs)

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