Ticket #833: new-all-diff.2.txt

File new-all-diff.2.txt, 197.3 KB (added by davidsarah, at 2010-01-25T12:24:18Z)

New diff for everything (code + tests + docs) -- review this version

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-25 11:54:53.023000000 +0000
3+++ new-tahoe/contrib/fuse/impl_c/blackmatch.py 2010-01-25 11:54:58.114000000 +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-25 11:54:53.204000000 +0000
51+++ new-tahoe/docs/frontends/webapi.txt 2010-01-25 11:54:58.321000000 +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/relnotes.txt new-tahoe/relnotes.txt
492--- old-tahoe/relnotes.txt      2010-01-25 11:54:55.260000000 +0000
493+++ new-tahoe/relnotes.txt      2010-01-25 11:55:00.443000000 +0000
494@@ -1,7 +1,7 @@
495-ANNOUNCING Tahoe, the Lofty-Atmospheric Filesystem, v1.5
496+ANNOUNCING Tahoe, the Lofty-Atmospheric Filesystem, v1.6
497 
498 The Tahoe-LAFS team is pleased to announce the immediate
499-availability of version 1.5 of Tahoe, the Lofty Atmospheric
500+availability of version 1.6 of Tahoe, the Lofty Atmospheric
501 File System.
502 
503 Tahoe-LAFS is the first cloud storage technology which offers
504@@ -29,15 +29,20 @@
505 
506 COMPATIBILITY
507 
508-Version 1.5 is fully compatible with the version 1 series of
509-Tahoe-LAFS. Files written by v1.5 clients can be read by
510-clients of all versions back to v1.0. v1.5 clients can read
511-files produced by clients of all versions since v1.0.  v1.5
512-servers can serve clients of all versions back to v1.0 and v1.5
513+Version 1.6 is fully compatible with the version 1 series of
514+Tahoe-LAFS. Files written by v1.6 clients can be read by
515+clients of all versions back to v1.0. v1.6 clients can read
516+files produced by clients of all versions since v1.0.  v1.6
517+servers can serve clients of all versions back to v1.0 and v1.6
518 clients can use servers of all versions back to v1.0.
519 
520-This is the sixth release in the version 1 series. The version
521-1 series of Tahoe-LAFS will be actively supported and
522+In addition, version 1.6 improves forward-compatibility with
523+planned future cap formats, allowing updates to a directory
524+containing both current and future caps, without loss of
525+information.
526+
527+This is the seventh major release in the version 1 series. The
528+version 1 series of Tahoe-LAFS will be actively supported and
529 maintained for the forseeable future, and future versions of
530 Tahoe-LAFS will retain the ability to read and write files
531 compatible with Tahoe-LAFS v1.
532diff -rN -u old-tahoe/src/allmydata/client.py new-tahoe/src/allmydata/client.py
533--- old-tahoe/src/allmydata/client.py   2010-01-25 11:54:55.348000000 +0000
534+++ new-tahoe/src/allmydata/client.py   2010-01-25 11:55:00.506000000 +0000
535@@ -471,13 +471,16 @@
536     # dirnodes. The first takes a URI and produces a filenode or (new-style)
537     # dirnode. The other three create brand-new filenodes/dirnodes.
538 
539-    def create_node_from_uri(self, writecap, readcap=None):
540-        # this returns synchronously.
541-        return self.nodemaker.create_from_cap(writecap, readcap)
542+    def create_node_from_uri(self, write_uri, read_uri=None, deep_immutable=False, name="<unknown name>"):
543+        # This returns synchronously.
544+        # Note that it does *not* validate the write_uri and read_uri; instead we
545+        # may get an opaque node if there were any problems.
546+        return self.nodemaker.create_from_cap(write_uri, read_uri, deep_immutable=deep_immutable, name=name)
547 
548     def create_dirnode(self, initial_children={}):
549         d = self.nodemaker.create_new_mutable_directory(initial_children)
550         return d
551+
552     def create_immutable_dirnode(self, children, convergence=None):
553         return self.nodemaker.create_immutable_directory(children, convergence)
554 
555diff -rN -u old-tahoe/src/allmydata/control.py new-tahoe/src/allmydata/control.py
556--- old-tahoe/src/allmydata/control.py  2010-01-25 11:54:55.368000000 +0000
557+++ new-tahoe/src/allmydata/control.py  2010-01-25 11:55:00.523000000 +0000
558@@ -5,7 +5,7 @@
559 from twisted.internet import defer
560 from twisted.internet.interfaces import IConsumer
561 from foolscap.api import Referenceable
562-from allmydata.interfaces import RIControlClient
563+from allmydata.interfaces import RIControlClient, IFileNode
564 from allmydata.util import fileutil, mathutil
565 from allmydata.immutable import upload
566 from twisted.python import log
567@@ -67,7 +67,9 @@
568         return d
569 
570     def remote_download_from_uri_to_file(self, uri, filename):
571-        filenode = self.parent.create_node_from_uri(uri)
572+        filenode = self.parent.create_node_from_uri(uri, name=filename)
573+        if not IFileNode.providedBy(filenode):
574+            raise AssertionError("The URI does not reference a file.")
575         c = FileWritingConsumer(filename)
576         d = filenode.read(c)
577         d.addCallback(lambda res: filename)
578@@ -199,6 +201,8 @@
579             if i >= self.count:
580                 return
581             n = self.parent.create_node_from_uri(self.uris[i])
582+            if not IFileNode.providedBy(n):
583+                raise AssertionError("The URI does not reference a file.")
584             if n.is_mutable():
585                 d1 = n.download_best_version()
586             else:
587diff -rN -u old-tahoe/src/allmydata/dirnode.py new-tahoe/src/allmydata/dirnode.py
588--- old-tahoe/src/allmydata/dirnode.py  2010-01-25 11:54:55.383000000 +0000
589+++ new-tahoe/src/allmydata/dirnode.py  2010-01-25 11:55:00.535000000 +0000
590@@ -5,13 +5,13 @@
591 from twisted.internet import defer
592 from foolscap.api import fireEventually
593 import simplejson
594-from allmydata.mutable.common import NotMutableError
595+from allmydata.mutable.common import NotWriteableError
596 from allmydata.mutable.filenode import MutableFileNode
597-from allmydata.unknown import UnknownNode
598+from allmydata.unknown import UnknownNode, strip_prefix_for_ro
599 from allmydata.interfaces import IFilesystemNode, IDirectoryNode, IFileNode, \
600      IImmutableFileNode, IMutableFileNode, \
601      ExistingChildError, NoSuchChildError, ICheckable, IDeepCheckable, \
602-     CannotPackUnknownNodeError
603+     MustBeDeepImmutableError, CapConstraintError
604 from allmydata.check_results import DeepCheckResults, \
605      DeepCheckAndRepairResults
606 from allmydata.monitor import Monitor
607@@ -23,6 +23,11 @@
608 from pycryptopp.cipher.aes import AES
609 from allmydata.util.dictutil import AuxValueDict
610 
611+
612+# TODO: {Deleter,MetadataSetter,Adder}.modify all start by unpacking the
613+# contents and end by repacking them. It might be better to apply them to
614+# the unpacked contents.
615+
616 class Deleter:
617     def __init__(self, node, name, must_exist=True):
618         self.node = node
619@@ -40,6 +45,7 @@
620         new_contents = self.node._pack_contents(children)
621         return new_contents
622 
623+
624 class MetadataSetter:
625     def __init__(self, node, name, metadata):
626         self.node = node
627@@ -75,6 +81,11 @@
628         for (name, (child, new_metadata)) in self.entries.iteritems():
629             precondition(isinstance(name, unicode), name)
630             precondition(IFilesystemNode.providedBy(child), child)
631+
632+            # Strictly speaking this is redundant because we would raise the
633+            # error again in pack_children.
634+            child.raise_error()
635+
636             if name in children:
637                 if not self.overwrite:
638                     raise ExistingChildError("child '%s' already exists" % name)
639@@ -123,25 +134,21 @@
640         new_contents = self.node._pack_contents(children)
641         return new_contents
642 
643-def _encrypt_rwcap(filenode, rwcap):
644-    assert isinstance(rwcap, str)
645+def _encrypt_rw_uri(filenode, rw_uri):
646+    assert isinstance(rw_uri, str)
647     writekey = filenode.get_writekey()
648     if not writekey:
649         return ""
650-    salt = hashutil.mutable_rwcap_salt_hash(rwcap)
651+    salt = hashutil.mutable_rwcap_salt_hash(rw_uri)
652     key = hashutil.mutable_rwcap_key_hash(salt, writekey)
653     cryptor = AES(key)
654-    crypttext = cryptor.process(rwcap)
655+    crypttext = cryptor.process(rw_uri)
656     mac = hashutil.hmac(key, salt + crypttext)
657     assert len(mac) == 32
658     return salt + crypttext + mac
659     # The MAC is not checked by readers in Tahoe >= 1.3.0, but we still
660     # produce it for the sake of older readers.
661 
662-class MustBeDeepImmutable(Exception):
663-    """You tried to add a non-deep-immutable node to a deep-immutable
664-    directory."""
665-
666 def pack_children(filenode, children, deep_immutable=False):
667     """Take a dict that maps:
668          children[unicode_name] = (IFileSystemNode, metadata_dict)
669@@ -152,7 +159,7 @@
670     time.
671 
672     If deep_immutable is True, I will require that all my children are deeply
673-    immutable, and will raise a MustBeDeepImmutable exception if not.
674+    immutable, and will raise a MustBeDeepImmutableError if not.
675     """
676 
677     has_aux = isinstance(children, AuxValueDict)
678@@ -161,25 +168,29 @@
679         assert isinstance(name, unicode)
680         entry = None
681         (child, metadata) = children[name]
682-        if deep_immutable and child.is_mutable():
683-            # TODO: consider adding IFileSystemNode.is_deep_immutable()
684-            raise MustBeDeepImmutable("child '%s' is mutable" % (name,))
685+        child.raise_error()
686+        if deep_immutable and not child.is_allowed_in_immutable_directory():
687+            raise MustBeDeepImmutableError("child '%s' is not allowed in an immutable directory" % (name,), name)
688         if has_aux:
689             entry = children.get_aux(name)
690         if not entry:
691             assert IFilesystemNode.providedBy(child), (name,child)
692             assert isinstance(metadata, dict)
693-            rwcap = child.get_uri() # might be RO if the child is not writeable
694-            if rwcap is None:
695-                rwcap = ""
696-            assert isinstance(rwcap, str), rwcap
697-            rocap = child.get_readonly_uri()
698-            if rocap is None:
699-                rocap = ""
700-            assert isinstance(rocap, str), rocap
701+            rw_uri = child.get_write_uri()
702+            if rw_uri is None:
703+                rw_uri = ""
704+            assert isinstance(rw_uri, str), rw_uri
705+           
706+            # should be prevented by MustBeDeepImmutableError check above
707+            assert not (rw_uri and deep_immutable)
708+
709+            ro_uri = child.get_readonly_uri()
710+            if ro_uri is None:
711+                ro_uri = ""
712+            assert isinstance(ro_uri, str), ro_uri
713             entry = "".join([netstring(name.encode("utf-8")),
714-                             netstring(rocap),
715-                             netstring(_encrypt_rwcap(filenode, rwcap)),
716+                             netstring(strip_prefix_for_ro(ro_uri, deep_immutable)),
717+                             netstring(_encrypt_rw_uri(filenode, rw_uri)),
718                              netstring(simplejson.dumps(metadata))])
719         entries.append(netstring(entry))
720     return "".join(entries)
721@@ -230,38 +241,68 @@
722         plaintext = cryptor.process(crypttext)
723         return plaintext
724 
725-    def _create_node(self, rwcap, rocap):
726-        return self._nodemaker.create_from_cap(rwcap, rocap)
727+    def _create_and_validate_node(self, rw_uri, ro_uri, name):
728+        #print "mutable? %r\n" % self.is_mutable()
729+        #print "_create_and_validate_node(rw_uri=%r, ro_uri=%r, name=%r)\n" % (rw_uri, ro_uri, name)
730+        node = self._nodemaker.create_from_cap(rw_uri, ro_uri,
731+                                               deep_immutable=not self.is_mutable(),
732+                                               name=name)
733+        node.raise_error()
734+        return node
735 
736     def _unpack_contents(self, data):
737         # the directory is serialized as a list of netstrings, one per child.
738-        # Each child is serialized as a list of four netstrings: (name,
739-        # rocap, rwcap, metadata), in which the name,rocap,metadata are in
740-        # cleartext. The 'name' is UTF-8 encoded. The rwcap is formatted as:
741-        # pack("16ss32s", iv, AES(H(writekey+iv), plaintextrwcap), mac)
742+        # Each child is serialized as a list of four netstrings: (name, ro_uri,
743+        # rwcapdata, metadata), in which the name, ro_uri, metadata are in
744+        # cleartext. The 'name' is UTF-8 encoded. The rwcapdata is formatted as:
745+        # pack("16ss32s", iv, AES(H(writekey+iv), plaintext_rw_uri), mac)
746         assert isinstance(data, str), (repr(data), type(data))
747         # an empty directory is serialized as an empty string
748         if data == "":
749             return AuxValueDict()
750         writeable = not self.is_readonly()
751+        mutable = self.is_mutable()
752         children = AuxValueDict()
753         position = 0
754         while position < len(data):
755             entries, position = split_netstring(data, 1, position)
756             entry = entries[0]
757-            (name, rocap, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
758+            (name, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
759+            if not mutable and len(rwcapdata) > 0:
760+                raise ValueError("the rwcapdata field of a dirnode in an immutable directory was not empty")
761             name = name.decode("utf-8")
762-            rwcap = None
763+            rw_uri = ""
764             if writeable:
765-                rwcap = self._decrypt_rwcapdata(rwcapdata)
766-            if not rwcap:
767-                rwcap = None # rwcap is None or a non-empty string
768-            if not rocap:
769-                rocap = None # rocap is None or a non-empty string
770-            child = self._create_node(rwcap, rocap)
771-            metadata = simplejson.loads(metadata_s)
772-            assert isinstance(metadata, dict)
773-            children.set_with_aux(name, (child, metadata), auxilliary=entry)
774+                rw_uri = self._decrypt_rwcapdata(rwcapdata)
775+            #print "mutable=%r, writeable=%r, rw_uri=%r, ro_uri=%r, name=%r" % (mutable, writeable, rw_uri, ro_uri, name)
776+
777+            # Since the encryption uses CTR mode, it currently leaks the length of the
778+            # plaintext rw_uri -- and therefore whether it is present, i.e. whether the
779+            # dirnode is writable (ticket #925). By stripping spaces in Tahoe >= 1.6.0,
780+            # we may make it easier for future versions to plug this leak.
781+            rw_uri = rw_uri.strip(' ')
782+            if not rw_uri:
783+                rw_uri = None  # rw_uri is None or a non-empty string
784+
785+            # Treat ro_uri in the same way for consistency.
786+            ro_uri = ro_uri.strip(' ')
787+            if not ro_uri:
788+                ro_uri = None  # ro_uri is None or a non-empty string
789+
790+            try:
791+                child = self._create_and_validate_node(rw_uri, ro_uri, name)
792+                #print "%r.is_allowed_in_immutable_directory() = %r" % (child, child.is_allowed_in_immutable_directory())
793+                if mutable or child.is_allowed_in_immutable_directory():
794+                    metadata = simplejson.loads(metadata_s)
795+                    assert isinstance(metadata, dict)
796+                    children[name] = (child, metadata)
797+                    children.set_with_aux(name, (child, metadata), auxilliary=entry)
798+            except CapConstraintError, e:
799+                #print "unmet constraint: (%s, %s)" % (e.args[0], e.args[1].encode("utf-8"))
800+                log.msg(format="unmet constraint on cap for child '%(name)s' unpacked from a directory:\n"
801+                               "%(message)s", message=e.args[0], name=e.args[1].encode("utf-8"),
802+                               facility="tahoe.webish", level=log.UNUSUAL)
803+
804         return children
805 
806     def _pack_contents(self, children):
807@@ -270,21 +311,39 @@
808 
809     def is_readonly(self):
810         return self._node.is_readonly()
811+
812     def is_mutable(self):
813         return self._node.is_mutable()
814 
815+    def is_unknown(self):
816+        return False
817+
818+    def is_allowed_in_immutable_directory(self):
819+        return not self._node.is_mutable()
820+
821+    def raise_error(self):
822+        pass
823+
824     def get_uri(self):
825         return self._uri.to_string()
826 
827+    def get_write_uri(self):
828+        if self.is_readonly():
829+            return None
830+        return self._uri.to_string()
831+
832     def get_readonly_uri(self):
833         return self._uri.get_readonly().to_string()
834 
835     def get_cap(self):
836         return self._uri
837+
838     def get_readcap(self):
839         return self._uri.get_readonly()
840+
841     def get_verify_cap(self):
842         return self._uri.get_verify_cap()
843+
844     def get_repair_cap(self):
845         if self._node.is_readonly():
846             return None # readonly (mutable) dirnodes are not yet repairable
847@@ -350,7 +409,7 @@
848     def set_metadata_for(self, name, metadata):
849         assert isinstance(name, unicode)
850         if self.is_readonly():
851-            return defer.fail(NotMutableError())
852+            return defer.fail(NotWriteableError())
853         assert isinstance(metadata, dict)
854         s = MetadataSetter(self, name, metadata)
855         d = self._node.modify(s.modify)
856@@ -398,14 +457,10 @@
857         precondition(isinstance(name, unicode), name)
858         precondition(isinstance(writecap, (str,type(None))), writecap)
859         precondition(isinstance(readcap, (str,type(None))), readcap)
860-        child_node = self._create_node(writecap, readcap)
861-        if isinstance(child_node, UnknownNode):
862-            # don't be willing to pack unknown nodes: we might accidentally
863-            # put some write-authority into the rocap slot because we don't
864-            # know how to diminish the URI they gave us. We don't even know
865-            # if they gave us a readcap or a writecap.
866-            msg = "cannot pack unknown node as child %s" % str(name)
867-            raise CannotPackUnknownNodeError(msg)
868+           
869+        # We now allow packing unknown nodes, provided they are valid
870+        # for this type of directory.
871+        child_node = self._create_and_validate_node(writecap, readcap, name)
872         d = self.set_node(name, child_node, metadata, overwrite)
873         d.addCallback(lambda res: child_node)
874         return d
875@@ -423,10 +478,10 @@
876                 writecap, readcap, metadata = e
877             precondition(isinstance(writecap, (str,type(None))), writecap)
878             precondition(isinstance(readcap, (str,type(None))), readcap)
879-            child_node = self._create_node(writecap, readcap)
880-            if isinstance(child_node, UnknownNode):
881-                msg = "cannot pack unknown node as child %s" % str(name)
882-                raise CannotPackUnknownNodeError(msg)
883+           
884+            # We now allow packing unknown nodes, provided they are valid
885+            # for this type of directory.
886+            child_node = self._create_and_validate_node(writecap, readcap, name)
887             a.set_node(name, child_node, metadata)
888         d = self._node.modify(a.modify)
889         d.addCallback(lambda ign: self)
890@@ -439,12 +494,12 @@
891         same name.
892 
893         If this directory node is read-only, the Deferred will errback with a
894-        NotMutableError."""
895+        NotWriteableError."""
896 
897         precondition(IFilesystemNode.providedBy(child), child)
898 
899         if self.is_readonly():
900-            return defer.fail(NotMutableError())
901+            return defer.fail(NotWriteableError())
902         assert isinstance(name, unicode)
903         assert IFilesystemNode.providedBy(child), child
904         a = Adder(self, overwrite=overwrite)
905@@ -456,7 +511,7 @@
906     def set_nodes(self, entries, overwrite=True):
907         precondition(isinstance(entries, dict), entries)
908         if self.is_readonly():
909-            return defer.fail(NotMutableError())
910+            return defer.fail(NotWriteableError())
911         a = Adder(self, entries, overwrite=overwrite)
912         d = self._node.modify(a.modify)
913         d.addCallback(lambda res: self)
914@@ -470,10 +525,10 @@
915         the operation completes."""
916         assert isinstance(name, unicode)
917         if self.is_readonly():
918-            return defer.fail(NotMutableError())
919+            return defer.fail(NotWriteableError())
920         d = self._uploader.upload(uploadable)
921-        d.addCallback(lambda results: results.uri)
922-        d.addCallback(self._nodemaker.create_from_cap)
923+        d.addCallback(lambda results:
924+                      self._create_and_validate_node(results.uri, None, name))
925         d.addCallback(lambda node:
926                       self.set_node(name, node, metadata, overwrite))
927         return d
928@@ -483,7 +538,7 @@
929         fires (with the node just removed) when the operation finishes."""
930         assert isinstance(name, unicode)
931         if self.is_readonly():
932-            return defer.fail(NotMutableError())
933+            return defer.fail(NotWriteableError())
934         deleter = Deleter(self, name)
935         d = self._node.modify(deleter.modify)
936         d.addCallback(lambda res: deleter.old_child)
937@@ -493,7 +548,7 @@
938                             mutable=True):
939         assert isinstance(name, unicode)
940         if self.is_readonly():
941-            return defer.fail(NotMutableError())
942+            return defer.fail(NotWriteableError())
943         if mutable:
944             d = self._nodemaker.create_new_mutable_directory(initial_children)
945         else:
946@@ -515,7 +570,7 @@
947         Deferred that fires when the operation finishes."""
948         assert isinstance(current_child_name, unicode)
949         if self.is_readonly() or new_parent.is_readonly():
950-            return defer.fail(NotMutableError())
951+            return defer.fail(NotWriteableError())
952         if new_child_name is None:
953             new_child_name = current_child_name
954         assert isinstance(new_child_name, unicode)
955diff -rN -u old-tahoe/src/allmydata/immutable/filenode.py new-tahoe/src/allmydata/immutable/filenode.py
956--- old-tahoe/src/allmydata/immutable/filenode.py       2010-01-25 11:54:55.626000000 +0000
957+++ new-tahoe/src/allmydata/immutable/filenode.py       2010-01-25 11:55:00.764000000 +0000
958@@ -17,6 +17,9 @@
959 class _ImmutableFileNodeBase(object):
960     implements(IImmutableFileNode, ICheckable)
961 
962+    def get_write_uri(self):
963+        return None
964+
965     def get_readonly_uri(self):
966         return self.get_uri()
967 
968@@ -26,6 +29,15 @@
969     def is_readonly(self):
970         return True
971 
972+    def is_unknown(self):
973+        return False
974+
975+    def is_allowed_in_immutable_directory(self):
976+        return True
977+
978+    def raise_error(self):
979+        pass
980+
981     def __hash__(self):
982         return self.u.__hash__()
983     def __eq__(self, other):
984diff -rN -u old-tahoe/src/allmydata/interfaces.py new-tahoe/src/allmydata/interfaces.py
985--- old-tahoe/src/allmydata/interfaces.py       2010-01-25 11:54:55.672000000 +0000
986+++ new-tahoe/src/allmydata/interfaces.py       2010-01-25 11:55:00.785000000 +0000
987@@ -426,6 +426,7 @@
988         """Return True if the data can be modified by *somebody* (perhaps
989         someone who has a more powerful URI than this one)."""
990 
991+    # TODO: rename to get_read_cap()
992     def get_readonly():
993         """Return another IURI instance, which represents a read-only form of
994         this one. If is_readonly() is True, this returns self."""
995@@ -456,7 +457,6 @@
996 class IDirnodeURI(Interface):
997     """I am a URI which represents a dirnode."""
998 
999-
1000 class IFileURI(Interface):
1001     """I am a URI which represents a filenode."""
1002     def get_size():
1003@@ -467,21 +467,28 @@
1004 
1005 class IMutableFileURI(Interface):
1006     """I am a URI which represents a mutable filenode."""
1007+
1008 class IDirectoryURI(Interface):
1009     pass
1010+
1011 class IReadonlyDirectoryURI(Interface):
1012     pass
1013 
1014-class CannotPackUnknownNodeError(Exception):
1015-    """UnknownNodes (using filecaps from the future that we don't understand)
1016-    cannot yet be copied safely, so I refuse to copy them."""
1017-
1018-class UnhandledCapTypeError(Exception):
1019-    """I recognize the cap/URI, but I cannot create an IFilesystemNode for
1020-    it."""
1021+class CapConstraintError(Exception):
1022+    """A constraint on a cap was violated."""
1023 
1024-class NotDeepImmutableError(Exception):
1025-    """Deep-immutable directories can only contain deep-immutable children"""
1026+class MustBeDeepImmutableError(CapConstraintError):
1027+    """Mutable children cannot be added to an immutable directory.
1028+    Also, caps obtained from an immutable directory can trigger this error
1029+    if they are later found to refer to a mutable object and then used."""
1030+
1031+class MustBeReadonlyError(CapConstraintError):
1032+    """Known write caps cannot be specified in a ro_uri field. Also,
1033+    caps obtained from a ro_uri field can trigger this error if they
1034+    are later found to be write caps and then used."""
1035+
1036+class MustNotBeUnknownRWError(CapConstraintError):
1037+    """Cannot add an unknown child cap specified in a rw_uri field."""
1038 
1039 # The hierarchy looks like this:
1040 #  IFilesystemNode
1041@@ -518,9 +525,8 @@
1042         """
1043 
1044     def get_uri():
1045-        """
1046-        Return the URI string that can be used by others to get access to
1047-        this node. If this node is read-only, the URI will only offer
1048+        """Return the URI string corresponding to the strongest cap associated
1049+        with this node. If this node is read-only, the URI will only offer
1050         read-only access. If this node is read-write, the URI will offer
1051         read-write access.
1052 
1053@@ -528,6 +534,11 @@
1054         read-only access with others, use get_readonly_uri().
1055         """
1056 
1057+    def get_write_uri(n):
1058+        """Return the URI string that can be used by others to get write
1059+        access to this node, if it is writeable. If this is a read-only node,
1060+        return None."""
1061+
1062     def get_readonly_uri():
1063         """Return the URI string that can be used by others to get read-only
1064         access to this node. The result is a read-only URI, regardless of
1065@@ -557,6 +568,18 @@
1066         file.
1067         """
1068 
1069+    def is_unknown():
1070+        """Return True if this is an unknown node."""
1071+
1072+    def is_allowed_in_immutable_directory():
1073+        """Return True if this node is allowed as a child of a deep-immutable
1074+        directory. This is true if either the node is of a known-immutable type,
1075+        or it is unknown and read-only.
1076+        """
1077+
1078+    def raise_error():
1079+        """Raise any error associated with this node."""
1080+
1081     def get_size():
1082         """Return the length (in bytes) of the data this node represents. For
1083         directory nodes, I return the size of the backing store. I return
1084@@ -902,7 +925,7 @@
1085         ctime/mtime semantics of traditional filesystems.
1086 
1087         If this directory node is read-only, the Deferred will errback with a
1088-        NotMutableError."""
1089+        NotWriteableError."""
1090 
1091     def set_children(entries, overwrite=True):
1092         """Add multiple children (by writecap+readcap) to a directory node.
1093@@ -928,7 +951,7 @@
1094         ctime/mtime semantics of traditional filesystems.
1095 
1096         If this directory node is read-only, the Deferred will errback with a
1097-        NotMutableError."""
1098+        NotWriteableError."""
1099 
1100     def set_nodes(entries, overwrite=True):
1101         """Add multiple children to a directory node. Takes a dict mapping
1102@@ -2074,7 +2097,7 @@
1103     Tahoe process will typically have a single NodeMaker, but unit tests may
1104     create simplified/mocked forms for testing purposes.
1105     """
1106-    def create_from_cap(writecap, readcap=None):
1107+    def create_from_cap(writecap, readcap=None, **kwargs):
1108         """I create an IFilesystemNode from the given writecap/readcap. I can
1109         only provide nodes for existing file/directory objects: use my other
1110         methods to create new objects. I return synchronously."""
1111diff -rN -u old-tahoe/src/allmydata/mutable/common.py new-tahoe/src/allmydata/mutable/common.py
1112--- old-tahoe/src/allmydata/mutable/common.py   2010-01-25 11:54:55.746000000 +0000
1113+++ new-tahoe/src/allmydata/mutable/common.py   2010-01-25 11:55:00.832000000 +0000
1114@@ -8,7 +8,7 @@
1115                           # creation
1116 MODE_READ = "MODE_READ"
1117 
1118-class NotMutableError(Exception):
1119+class NotWriteableError(Exception):
1120     pass
1121 
1122 class NeedMoreDataError(Exception):
1123diff -rN -u old-tahoe/src/allmydata/mutable/filenode.py new-tahoe/src/allmydata/mutable/filenode.py
1124--- old-tahoe/src/allmydata/mutable/filenode.py 2010-01-25 11:54:55.753000000 +0000
1125+++ new-tahoe/src/allmydata/mutable/filenode.py 2010-01-25 11:55:00.837000000 +0000
1126@@ -214,6 +214,12 @@
1127 
1128     def get_uri(self):
1129         return self._uri.to_string()
1130+
1131+    def get_write_uri(self):
1132+        if self.is_readonly():
1133+            return None
1134+        return self._uri.to_string()
1135+
1136     def get_readonly_uri(self):
1137         return self._uri.get_readonly().to_string()
1138 
1139@@ -227,9 +233,19 @@
1140 
1141     def is_mutable(self):
1142         return self._uri.is_mutable()
1143+
1144     def is_readonly(self):
1145         return self._uri.is_readonly()
1146 
1147+    def is_unknown(self):
1148+        return False
1149+
1150+    def is_allowed_in_immutable_directory(self):
1151+        return not self._uri.is_mutable()
1152+
1153+    def raise_error(self):
1154+        pass
1155+
1156     def __hash__(self):
1157         return hash((self.__class__, self._uri))
1158     def __cmp__(self, them):
1159diff -rN -u old-tahoe/src/allmydata/nodemaker.py new-tahoe/src/allmydata/nodemaker.py
1160--- old-tahoe/src/allmydata/nodemaker.py        2010-01-25 11:54:55.808000000 +0000
1161+++ new-tahoe/src/allmydata/nodemaker.py        2010-01-25 11:55:00.872000000 +0000
1162@@ -1,7 +1,7 @@
1163 import weakref
1164 from zope.interface import implements
1165 from allmydata.util.assertutil import precondition
1166-from allmydata.interfaces import INodeMaker, NotDeepImmutableError
1167+from allmydata.interfaces import INodeMaker, MustBeDeepImmutableError
1168 from allmydata.immutable.filenode import ImmutableFileNode, LiteralFileNode
1169 from allmydata.immutable.upload import Data
1170 from allmydata.mutable.filenode import MutableFileNode
1171@@ -44,28 +44,36 @@
1172     def _create_dirnode(self, filenode):
1173         return DirectoryNode(filenode, self, self.uploader)
1174 
1175-    def create_from_cap(self, writecap, readcap=None):
1176+    def create_from_cap(self, writecap, readcap=None, deep_immutable=False, name=u"<unknown name>"):
1177         # this returns synchronously. It starts with a "cap string".
1178         assert isinstance(writecap, (str, type(None))), type(writecap)
1179         assert isinstance(readcap,  (str, type(None))), type(readcap)
1180+        #import traceback
1181+        #traceback.print_stack()
1182+        #print '%r.create_from_cap(%r, %r, %r)' % (self, writecap, readcap, kwargs)
1183+       
1184         bigcap = writecap or readcap
1185         if not bigcap:
1186             # maybe the writecap was hidden because we're in a readonly
1187             # directory, and the future cap format doesn't have a readcap, or
1188             # something.
1189-            return UnknownNode(writecap, readcap)
1190-        if bigcap in self._node_cache:
1191-            return self._node_cache[bigcap]
1192-        cap = uri.from_string(bigcap)
1193-        node = self._create_from_cap(cap)
1194+            return UnknownNode(None, None)  # deep_immutable and name not needed
1195+
1196+        # The name doesn't matter for caching since it's only used in the error
1197+        # attribute of an UnknownNode, and we don't cache those.
1198+        memokey = ("I" if deep_immutable else "M") + bigcap
1199+        if memokey in self._node_cache:
1200+            return self._node_cache[memokey]
1201+        cap = uri.from_string(bigcap, deep_immutable=deep_immutable, name=name)
1202+        node = self._create_from_single_cap(cap)
1203         if node:
1204-            self._node_cache[bigcap] = node  # note: WeakValueDictionary
1205+            self._node_cache[memokey] = node  # note: WeakValueDictionary
1206         else:
1207-            node = UnknownNode(writecap, readcap) # don't cache UnknownNode
1208+            # don't cache UnknownNode
1209+            node = UnknownNode(writecap, readcap, deep_immutable=deep_immutable, name=name)
1210         return node
1211 
1212-    def _create_from_cap(self, cap):
1213-        # This starts with a "cap instance"
1214+    def _create_from_single_cap(self, cap):
1215         if isinstance(cap, uri.LiteralFileURI):
1216             return self._create_lit(cap)
1217         if isinstance(cap, uri.CHKFileURI):
1218@@ -76,7 +84,7 @@
1219                             uri.ReadonlyDirectoryURI,
1220                             uri.ImmutableDirectoryURI,
1221                             uri.LiteralDirectoryURI)):
1222-            filenode = self._create_from_cap(cap.get_filenode_cap())
1223+            filenode = self._create_from_single_cap(cap.get_filenode_cap())
1224             return self._create_dirnode(filenode)
1225         return None
1226 
1227@@ -89,13 +97,11 @@
1228         return d
1229 
1230     def create_new_mutable_directory(self, initial_children={}):
1231-        # initial_children must have metadata (i.e. {} instead of None), and
1232-        # should not contain UnknownNodes
1233+        # initial_children must have metadata (i.e. {} instead of None)
1234         for (name, (node, metadata)) in initial_children.iteritems():
1235-            precondition(not isinstance(node, UnknownNode),
1236-                         "create_new_mutable_directory does not accept UnknownNode", node)
1237             precondition(isinstance(metadata, dict),
1238                          "create_new_mutable_directory requires metadata to be a dict, not None", metadata)
1239+            node.raise_error()
1240         d = self.create_mutable_file(lambda n:
1241                                      pack_children(n, initial_children))
1242         d.addCallback(self._create_dirnode)
1243@@ -105,19 +111,15 @@
1244         if convergence is None:
1245             convergence = self.secret_holder.get_convergence_secret()
1246         for (name, (node, metadata)) in children.iteritems():
1247-            precondition(not isinstance(node, UnknownNode),
1248-                         "create_immutable_directory does not accept UnknownNode", node)
1249             precondition(isinstance(metadata, dict),
1250                          "create_immutable_directory requires metadata to be a dict, not None", metadata)
1251-            if node.is_mutable():
1252-                raise NotDeepImmutableError("%s is not immutable" % (node,))
1253+            node.raise_error()
1254+            if not node.is_allowed_in_immutable_directory():
1255+                raise MustBeDeepImmutableError("%s is not immutable" % (node,), name)
1256         n = DummyImmutableFileNode() # writekey=None
1257         packed = pack_children(n, children)
1258         uploadable = Data(packed, convergence)
1259         d = self.uploader.upload(uploadable, history=self.history)
1260-        def _uploaded(results):
1261-            filecap = self.create_from_cap(results.uri)
1262-            return filecap
1263-        d.addCallback(_uploaded)
1264+        d.addCallback(lambda results: self.create_from_cap(None, results.uri))
1265         d.addCallback(self._create_dirnode)
1266         return d
1267diff -rN -u old-tahoe/src/allmydata/scripts/common.py new-tahoe/src/allmydata/scripts/common.py
1268--- old-tahoe/src/allmydata/scripts/common.py   2010-01-25 11:54:55.874000000 +0000
1269+++ new-tahoe/src/allmydata/scripts/common.py   2010-01-25 11:55:00.920000000 +0000
1270@@ -128,12 +128,14 @@
1271     pass
1272 
1273 def get_alias(aliases, path, default):
1274+    from allmydata import uri
1275     # transform "work:path/filename" into (aliases["work"], "path/filename").
1276     # If default=None, then an empty alias is indicated by returning
1277-    # DefaultAliasMarker. We special-case "URI:" to make it easy to access
1278-    # specific files/directories by their read-cap.
1279+    # DefaultAliasMarker. We special-case strings with a recognized cap URI
1280+    # prefix, to make it easy to access specific files/directories by their
1281+    # caps.
1282     path = path.strip()
1283-    if path.startswith("URI:"):
1284+    if uri.has_uri_prefix(path):
1285         # The only way to get a sub-path is to use URI:blah:./foo, and we
1286         # strip out the :./ sequence.
1287         sep = path.find(":./")
1288diff -rN -u old-tahoe/src/allmydata/scripts/tahoe_cp.py new-tahoe/src/allmydata/scripts/tahoe_cp.py
1289--- old-tahoe/src/allmydata/scripts/tahoe_cp.py 2010-01-25 11:54:55.971000000 +0000
1290+++ new-tahoe/src/allmydata/scripts/tahoe_cp.py 2010-01-25 11:55:01.030000000 +0000
1291@@ -258,8 +258,7 @@
1292                 readcap = ascii_or_none(data[1].get("ro_uri"))
1293                 self.children[name] = TahoeFileSource(self.nodeurl, mutable,
1294                                                       writecap, readcap)
1295-            else:
1296-                assert data[0] == "dirnode"
1297+            elif data[0] == "dirnode":
1298                 writecap = ascii_or_none(data[1].get("rw_uri"))
1299                 readcap = ascii_or_none(data[1].get("ro_uri"))
1300                 if writecap and writecap in self.cache:
1301@@ -277,6 +276,11 @@
1302                     if recurse:
1303                         child.populate(True)
1304                 self.children[name] = child
1305+            else:
1306+                # TODO: there should be an option to skip unknown nodes.
1307+                raise TahoeError("Cannot copy unknown nodes (ticket #839). "
1308+                                 "You probably need to use a later version of "
1309+                                 "Tahoe-LAFS to copy this directory.")
1310 
1311 class TahoeMissingTarget:
1312     def __init__(self, url):
1313@@ -353,8 +357,7 @@
1314                                                    urllib.quote(name.encode('utf-8'))])
1315                 self.children[name] = TahoeFileTarget(self.nodeurl, mutable,
1316                                                       writecap, readcap, url)
1317-            else:
1318-                assert data[0] == "dirnode"
1319+            elif data[0] == "dirnode":
1320                 writecap = ascii_or_none(data[1].get("rw_uri"))
1321                 readcap = ascii_or_none(data[1].get("ro_uri"))
1322                 if writecap and writecap in self.cache:
1323@@ -372,6 +375,11 @@
1324                     if recurse:
1325                         child.populate(True)
1326                 self.children[name] = child
1327+            else:
1328+                # TODO: there should be an option to skip unknown nodes.
1329+                raise TahoeError("Cannot copy unknown nodes (ticket #839). "
1330+                                 "You probably need to use a later version of "
1331+                                 "Tahoe-LAFS to copy this directory.")
1332 
1333     def get_child_target(self, name):
1334         # return a new target for a named subdirectory of this dir
1335@@ -407,9 +415,11 @@
1336         set_data = {}
1337         for (name, filecap) in self.new_children.items():
1338             # it just so happens that ?t=set_children will accept both file
1339-            # read-caps and write-caps as ['rw_uri'], and will handle eithe
1340+            # read-caps and write-caps as ['rw_uri'], and will handle either
1341             # correctly. So don't bother trying to figure out whether the one
1342             # we have is read-only or read-write.
1343+            # TODO: think about how this affects forward-compatibility for
1344+            # unknown caps
1345             set_data[name] = ["filenode", {"rw_uri": filecap}]
1346         body = simplejson.dumps(set_data)
1347         POST(url, body)
1348@@ -770,6 +780,7 @@
1349 #  local-file-in-the-way
1350 #   touch proposed
1351 #   tahoe cp -r my:docs/proposed/denver.txt proposed/denver.txt
1352+#  handling of unknown nodes
1353 
1354 # things that maybe should be errors but aren't
1355 #  local-dir-in-the-way
1356diff -rN -u old-tahoe/src/allmydata/scripts/tahoe_put.py new-tahoe/src/allmydata/scripts/tahoe_put.py
1357--- old-tahoe/src/allmydata/scripts/tahoe_put.py        2010-01-25 11:54:56.014000000 +0000
1358+++ new-tahoe/src/allmydata/scripts/tahoe_put.py        2010-01-25 11:55:01.059000000 +0000
1359@@ -40,6 +40,7 @@
1360         #  DIRCAP:./subdir/foo : DIRCAP/subdir/foo
1361         #  MUTABLE-FILE-WRITECAP : filecap
1362 
1363+        # FIXME: this shouldn't rely on a particular prefix.
1364         if to_file.startswith("URI:SSK:"):
1365             url = nodeurl + "uri/%s" % urllib.quote(to_file)
1366         else:
1367diff -rN -u old-tahoe/src/allmydata/test/common.py new-tahoe/src/allmydata/test/common.py
1368--- old-tahoe/src/allmydata/test/common.py      2010-01-25 11:54:56.294000000 +0000
1369+++ new-tahoe/src/allmydata/test/common.py      2010-01-25 11:55:01.529000000 +0000
1370@@ -51,6 +51,8 @@
1371 
1372     def get_uri(self):
1373         return self.my_uri.to_string()
1374+    def get_write_uri(self):
1375+        return None
1376     def get_readonly_uri(self):
1377         return self.my_uri.to_string()
1378     def get_cap(self):
1379@@ -103,6 +105,12 @@
1380         return False
1381     def is_readonly(self):
1382         return True
1383+    def is_unknown(self):
1384+        return False
1385+    def is_allowed_in_immutable_directory(self):
1386+        return True
1387+    def raise_error(self):
1388+        pass
1389 
1390     def get_size(self):
1391         try:
1392@@ -190,6 +198,10 @@
1393         return self.my_uri.get_readonly()
1394     def get_uri(self):
1395         return self.my_uri.to_string()
1396+    def get_write_uri(self):
1397+        if self.is_readonly():
1398+            return None
1399+        return self.my_uri.to_string()
1400     def get_readonly(self):
1401         return self.my_uri.get_readonly()
1402     def get_readonly_uri(self):
1403@@ -200,6 +212,12 @@
1404         return self.my_uri.is_readonly()
1405     def is_mutable(self):
1406         return self.my_uri.is_mutable()
1407+    def is_unknown(self):
1408+        return False
1409+    def is_allowed_in_immutable_directory(self):
1410+        return not self.my_uri.is_mutable()
1411+    def raise_error(self):
1412+        pass
1413     def get_writekey(self):
1414         return "\x00"*16
1415     def get_size(self):
1416diff -rN -u old-tahoe/src/allmydata/test/test_client.py new-tahoe/src/allmydata/test/test_client.py
1417--- old-tahoe/src/allmydata/test/test_client.py 2010-01-25 11:54:56.512000000 +0000
1418+++ new-tahoe/src/allmydata/test/test_client.py 2010-01-25 11:55:01.810000000 +0000
1419@@ -288,11 +288,15 @@
1420         self.failUnless(n.is_readonly())
1421         self.failUnless(n.is_mutable())
1422 
1423-        future = "x-tahoe-crazy://future_cap_format."
1424-        n = c.create_node_from_uri(future)
1425+        unknown_rw = "lafs://from_the_future"
1426+        unknown_ro = "lafs://readonly_from_the_future"
1427+        n = c.create_node_from_uri(unknown_rw, unknown_ro)
1428         self.failUnless(IFilesystemNode.providedBy(n))
1429         self.failIf(IFileNode.providedBy(n))
1430         self.failIf(IImmutableFileNode.providedBy(n))
1431         self.failIf(IMutableFileNode.providedBy(n))
1432         self.failIf(IDirectoryNode.providedBy(n))
1433-        self.failUnlessEqual(n.get_uri(), future)
1434+        self.failUnless(n.is_unknown())
1435+        self.failUnlessEqual(n.get_uri(), unknown_rw)
1436+        self.failUnlessEqual(n.get_write_uri(), unknown_rw)
1437+        self.failUnlessEqual(n.get_readonly_uri(), "ro." + unknown_ro)
1438diff -rN -u old-tahoe/src/allmydata/test/test_dirnode.py new-tahoe/src/allmydata/test/test_dirnode.py
1439--- old-tahoe/src/allmydata/test/test_dirnode.py        2010-01-25 11:54:56.560000000 +0000
1440+++ new-tahoe/src/allmydata/test/test_dirnode.py        2010-01-25 11:55:01.887000000 +0000
1441@@ -7,8 +7,9 @@
1442 from allmydata.client import Client
1443 from allmydata.immutable import upload
1444 from allmydata.interfaces import IImmutableFileNode, IMutableFileNode, \
1445-     ExistingChildError, NoSuchChildError, NotDeepImmutableError, \
1446-     IDeepCheckResults, IDeepCheckAndRepairResults, CannotPackUnknownNodeError
1447+     ExistingChildError, NoSuchChildError, MustNotBeUnknownRWError, \
1448+     MustBeDeepImmutableError, MustBeReadonlyError, \
1449+     IDeepCheckResults, IDeepCheckAndRepairResults
1450 from allmydata.mutable.filenode import MutableFileNode
1451 from allmydata.mutable.common import UncoordinatedWriteError
1452 from allmydata.util import hashutil, base32
1453@@ -16,7 +17,7 @@
1454 from allmydata.test.common import make_chk_file_uri, make_mutable_file_uri, \
1455      ErrorMixin
1456 from allmydata.test.no_network import GridTestMixin
1457-from allmydata.unknown import UnknownNode
1458+from allmydata.unknown import UnknownNode, strip_prefix_for_ro
1459 from allmydata.nodemaker import NodeMaker
1460 from base64 import b32decode
1461 import common_util as testutil
1462@@ -32,6 +33,11 @@
1463         d = c.create_dirnode()
1464         def _done(res):
1465             self.failUnless(isinstance(res, dirnode.DirectoryNode))
1466+            self.failUnless(res.is_mutable())
1467+            self.failIf(res.is_readonly())
1468+            self.failIf(res.is_unknown())
1469+            self.failIf(res.is_allowed_in_immutable_directory())
1470+            res.raise_error()
1471             rep = str(res)
1472             self.failUnless("RW-MUT" in rep)
1473         d.addCallback(_done)
1474@@ -44,36 +50,74 @@
1475         nm = c.nodemaker
1476         setup_py_uri = "URI:CHK:n7r3m6wmomelk4sep3kw5cvduq:os7ijw5c3maek7pg65e5254k2fzjflavtpejjyhshpsxuqzhcwwq:3:20:14861"
1477         one_uri = "URI:LIT:n5xgk" # LIT for "one"
1478+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1479+        mut_read_uri = "URI:SSK-RO:jf6wkflosyvntwxqcdo7a54jvm:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1480+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
1481+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
1482         kids = {u"one": (nm.create_from_cap(one_uri), {}),
1483                 u"two": (nm.create_from_cap(setup_py_uri),
1484                          {"metakey": "metavalue"}),
1485+                u"mut": (nm.create_from_cap(mut_write_uri, mut_read_uri), {}),
1486+                u"fut": (nm.create_from_cap(future_write_uri, future_read_uri), {}),
1487+                u"fro": (nm.create_from_cap(None, future_read_uri), {}),
1488                 }
1489         d = c.create_dirnode(kids)
1490+       
1491         def _created(dn):
1492             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1493+            self.failUnless(dn.is_mutable())
1494+            self.failIf(dn.is_readonly())
1495+            self.failIf(dn.is_unknown())
1496+            self.failIf(dn.is_allowed_in_immutable_directory())
1497+            dn.raise_error()
1498             rep = str(dn)
1499             self.failUnless("RW-MUT" in rep)
1500             return dn.list()
1501         d.addCallback(_created)
1502+       
1503         def _check_kids(children):
1504-            self.failUnlessEqual(sorted(children.keys()), [u"one", u"two"])
1505+            self.failUnlessEqual(sorted(children.keys()),
1506+                                 [u"fro", u"fut", u"mut", u"one", u"two"])
1507             one_node, one_metadata = children[u"one"]
1508             two_node, two_metadata = children[u"two"]
1509+            mut_node, mut_metadata = children[u"mut"]
1510+            fut_node, fut_metadata = children[u"fut"]
1511+            fro_node, fro_metadata = children[u"fro"]
1512+           
1513             self.failUnlessEqual(one_node.get_size(), 3)
1514-            self.failUnlessEqual(two_node.get_size(), 14861)
1515+            self.failUnlessEqual(one_node.get_uri(), one_uri)
1516+            self.failUnlessEqual(one_node.get_readonly_uri(), one_uri)
1517             self.failUnless(isinstance(one_metadata, dict), one_metadata)
1518+           
1519+            self.failUnlessEqual(two_node.get_size(), 14861)
1520+            self.failUnlessEqual(two_node.get_uri(), setup_py_uri)
1521+            self.failUnlessEqual(two_node.get_readonly_uri(), setup_py_uri)
1522             self.failUnlessEqual(two_metadata["metakey"], "metavalue")
1523+           
1524+            self.failUnlessEqual(mut_node.get_uri(), mut_write_uri)
1525+            self.failUnlessEqual(mut_node.get_readonly_uri(), mut_read_uri)
1526+            self.failUnless(isinstance(mut_metadata, dict), mut_metadata)
1527+           
1528+            self.failUnless(fut_node.is_unknown())
1529+            self.failUnlessEqual(fut_node.get_uri(), future_write_uri)
1530+            self.failUnlessEqual(fut_node.get_readonly_uri(), "ro." + future_read_uri)
1531+            self.failUnless(isinstance(fut_metadata, dict), fut_metadata)
1532+           
1533+            self.failUnless(fro_node.is_unknown())
1534+            self.failUnlessEqual(fro_node.get_uri(), "ro." + future_read_uri)
1535+            self.failUnlessEqual(fut_node.get_readonly_uri(), "ro." + future_read_uri)
1536+            self.failUnless(isinstance(fro_metadata, dict), fro_metadata)
1537         d.addCallback(_check_kids)
1538+
1539         d.addCallback(lambda ign: nm.create_new_mutable_directory(kids))
1540         d.addCallback(lambda dn: dn.list())
1541         d.addCallback(_check_kids)
1542-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
1543-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
1544-        future_node = UnknownNode(future_writecap, future_readcap)
1545-        bad_kids1 = {u"one": (future_node, {})}
1546+
1547+        bad_future_node = UnknownNode(future_write_uri, None)
1548+        bad_kids1 = {u"one": (bad_future_node, {})}
1549         d.addCallback(lambda ign:
1550-                      self.shouldFail(AssertionError, "bad_kids1",
1551-                                      "does not accept UnknownNode",
1552+                      self.shouldFail(MustNotBeUnknownRWError, "bad_kids1",
1553+                                      "cannot attach unknown",
1554                                       nm.create_new_mutable_directory,
1555                                       bad_kids1))
1556         bad_kids2 = {u"one": (nm.create_from_cap(one_uri), None)}
1557@@ -91,17 +135,24 @@
1558         nm = c.nodemaker
1559         setup_py_uri = "URI:CHK:n7r3m6wmomelk4sep3kw5cvduq:os7ijw5c3maek7pg65e5254k2fzjflavtpejjyhshpsxuqzhcwwq:3:20:14861"
1560         one_uri = "URI:LIT:n5xgk" # LIT for "one"
1561-        mut_readcap = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1562-        mut_writecap = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1563+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1564+        mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1565+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
1566+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
1567         kids = {u"one": (nm.create_from_cap(one_uri), {}),
1568                 u"two": (nm.create_from_cap(setup_py_uri),
1569                          {"metakey": "metavalue"}),
1570+                u"fut": (nm.create_from_cap(None, future_read_uri), {}),
1571                 }
1572         d = c.create_immutable_dirnode(kids)
1573+       
1574         def _created(dn):
1575             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1576             self.failIf(dn.is_mutable())
1577             self.failUnless(dn.is_readonly())
1578+            self.failIf(dn.is_unknown())
1579+            self.failUnless(dn.is_allowed_in_immutable_directory())
1580+            dn.raise_error()
1581             rep = str(dn)
1582             self.failUnless("RO-IMM" in rep)
1583             cap = dn.get_cap()
1584@@ -109,50 +160,73 @@
1585             self.cap = cap
1586             return dn.list()
1587         d.addCallback(_created)
1588+       
1589         def _check_kids(children):
1590-            self.failUnlessEqual(sorted(children.keys()), [u"one", u"two"])
1591+            self.failUnlessEqual(sorted(children.keys()), [u"fut", u"one", u"two"])
1592             one_node, one_metadata = children[u"one"]
1593             two_node, two_metadata = children[u"two"]
1594+            fut_node, fut_metadata = children[u"fut"]
1595+
1596             self.failUnlessEqual(one_node.get_size(), 3)
1597-            self.failUnlessEqual(two_node.get_size(), 14861)
1598+            self.failUnlessEqual(one_node.get_uri(), one_uri)
1599+            self.failUnlessEqual(one_node.get_readonly_uri(), one_uri)
1600             self.failUnless(isinstance(one_metadata, dict), one_metadata)
1601+
1602+            self.failUnlessEqual(two_node.get_size(), 14861)
1603+            self.failUnlessEqual(two_node.get_uri(), setup_py_uri)
1604+            self.failUnlessEqual(two_node.get_readonly_uri(), setup_py_uri)
1605             self.failUnlessEqual(two_metadata["metakey"], "metavalue")
1606+
1607+            self.failUnless(fut_node.is_unknown())
1608+            self.failUnlessEqual(fut_node.get_uri(), "imm." + future_read_uri)
1609+            self.failUnlessEqual(fut_node.get_readonly_uri(), "imm." + future_read_uri)
1610+            self.failUnless(isinstance(fut_metadata, dict), fut_metadata)
1611         d.addCallback(_check_kids)
1612+       
1613         d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string()))
1614         d.addCallback(lambda dn: dn.list())
1615         d.addCallback(_check_kids)
1616-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
1617-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
1618-        future_node = UnknownNode(future_writecap, future_readcap)
1619-        bad_kids1 = {u"one": (future_node, {})}
1620+
1621+        bad_future_node1 = UnknownNode(future_write_uri, None)
1622+        bad_kids1 = {u"one": (bad_future_node1, {})}
1623         d.addCallback(lambda ign:
1624-                      self.shouldFail(AssertionError, "bad_kids1",
1625-                                      "does not accept UnknownNode",
1626+                      self.shouldFail(MustNotBeUnknownRWError, "bad_kids1",
1627+                                      "cannot attach unknown",
1628                                       c.create_immutable_dirnode,
1629                                       bad_kids1))
1630-        bad_kids2 = {u"one": (nm.create_from_cap(one_uri), None)}
1631+        bad_future_node2 = UnknownNode(future_write_uri, future_read_uri)
1632+        bad_kids2 = {u"one": (bad_future_node2, {})}
1633         d.addCallback(lambda ign:
1634-                      self.shouldFail(AssertionError, "bad_kids2",
1635-                                      "requires metadata to be a dict",
1636+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids2",
1637+                                      "is not immutable",
1638                                       c.create_immutable_dirnode,
1639                                       bad_kids2))
1640-        bad_kids3 = {u"one": (nm.create_from_cap(mut_writecap), {})}
1641+        bad_kids3 = {u"one": (nm.create_from_cap(one_uri), None)}
1642         d.addCallback(lambda ign:
1643-                      self.shouldFail(NotDeepImmutableError, "bad_kids3",
1644-                                      "is not immutable",
1645+                      self.shouldFail(AssertionError, "bad_kids3",
1646+                                      "requires metadata to be a dict",
1647                                       c.create_immutable_dirnode,
1648                                       bad_kids3))
1649-        bad_kids4 = {u"one": (nm.create_from_cap(mut_readcap), {})}
1650+        bad_kids4 = {u"one": (nm.create_from_cap(mut_write_uri), {})}
1651         d.addCallback(lambda ign:
1652-                      self.shouldFail(NotDeepImmutableError, "bad_kids4",
1653+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids4",
1654                                       "is not immutable",
1655                                       c.create_immutable_dirnode,
1656                                       bad_kids4))
1657+        bad_kids5 = {u"one": (nm.create_from_cap(mut_read_uri), {})}
1658+        d.addCallback(lambda ign:
1659+                      self.shouldFail(MustBeDeepImmutableError, "bad_kids5",
1660+                                      "is not immutable",
1661+                                      c.create_immutable_dirnode,
1662+                                      bad_kids5))
1663         d.addCallback(lambda ign: c.create_immutable_dirnode({}))
1664         def _created_empty(dn):
1665             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1666             self.failIf(dn.is_mutable())
1667             self.failUnless(dn.is_readonly())
1668+            self.failIf(dn.is_unknown())
1669+            self.failUnless(dn.is_allowed_in_immutable_directory())
1670+            dn.raise_error()
1671             rep = str(dn)
1672             self.failUnless("RO-IMM" in rep)
1673             cap = dn.get_cap()
1674@@ -168,6 +242,9 @@
1675             self.failUnless(isinstance(dn, dirnode.DirectoryNode))
1676             self.failIf(dn.is_mutable())
1677             self.failUnless(dn.is_readonly())
1678+            self.failIf(dn.is_unknown())
1679+            self.failUnless(dn.is_allowed_in_immutable_directory())
1680+            dn.raise_error()
1681             rep = str(dn)
1682             self.failUnless("RO-IMM" in rep)
1683             cap = dn.get_cap()
1684@@ -193,9 +270,9 @@
1685             d.addCallback(_check_kids)
1686             d.addCallback(lambda ign: n.get(u"subdir"))
1687             d.addCallback(lambda sd: self.failIf(sd.is_mutable()))
1688-            bad_kids = {u"one": (nm.create_from_cap(mut_writecap), {})}
1689+            bad_kids = {u"one": (nm.create_from_cap(mut_write_uri), {})}
1690             d.addCallback(lambda ign:
1691-                          self.shouldFail(NotDeepImmutableError, "YZ",
1692+                          self.shouldFail(MustBeDeepImmutableError, "YZ",
1693                                           "is not immutable",
1694                                           n.create_subdirectory,
1695                                           u"sub2", bad_kids, mutable=False))
1696@@ -203,7 +280,6 @@
1697         d.addCallback(_made_parent)
1698         return d
1699 
1700-
1701     def test_check(self):
1702         self.basedir = "dirnode/Dirnode/test_check"
1703         self.set_up_grid()
1704@@ -337,24 +413,27 @@
1705             ro_dn = c.create_node_from_uri(ro_uri)
1706             self.failUnless(ro_dn.is_readonly())
1707             self.failUnless(ro_dn.is_mutable())
1708+            self.failIf(ro_dn.is_unknown())
1709+            self.failIf(ro_dn.is_allowed_in_immutable_directory())
1710+            ro_dn.raise_error()
1711 
1712-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1713+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1714                             ro_dn.set_uri, u"newchild", filecap, filecap)
1715-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1716+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1717                             ro_dn.set_node, u"newchild", filenode)
1718-            self.shouldFail(dirnode.NotMutableError, "set_nodes ro", None,
1719+            self.shouldFail(dirnode.NotWriteableError, "set_nodes ro", None,
1720                             ro_dn.set_nodes, { u"newchild": (filenode, None) })
1721-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1722+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1723                             ro_dn.add_file, u"newchild", uploadable)
1724-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1725+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1726                             ro_dn.delete, u"child")
1727-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1728+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1729                             ro_dn.create_subdirectory, u"newchild")
1730-            self.shouldFail(dirnode.NotMutableError, "set_metadata_for ro", None,
1731+            self.shouldFail(dirnode.NotWriteableError, "set_metadata_for ro", None,
1732                             ro_dn.set_metadata_for, u"child", {})
1733-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1734+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1735                             ro_dn.move_child_to, u"child", rw_dn)
1736-            self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
1737+            self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None,
1738                             rw_dn.move_child_to, u"child", ro_dn)
1739             return ro_dn.list()
1740         d.addCallback(_ready)
1741@@ -901,8 +980,8 @@
1742         nodemaker = NodeMaker(None, None, None,
1743                               None, None, None,
1744                               {"k": 3, "n": 10}, None)
1745-        writecap = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1746-        filenode = nodemaker.create_from_cap(writecap)
1747+        write_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
1748+        filenode = nodemaker.create_from_cap(write_uri)
1749         node = dirnode.DirectoryNode(filenode, nodemaker, None)
1750         children = node._unpack_contents(known_tree)
1751         self._check_children(children)
1752@@ -975,23 +1054,23 @@
1753         self.failUnlessIn("lit", packed)
1754 
1755         kids = self._make_kids(nm, ["imm", "lit", "write"])
1756-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1757+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1758                               dirnode.pack_children,
1759                               fn, kids, deep_immutable=True)
1760 
1761         # read-only is not enough: all children must be immutable
1762         kids = self._make_kids(nm, ["imm", "lit", "read"])
1763-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1764+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1765                               dirnode.pack_children,
1766                               fn, kids, deep_immutable=True)
1767 
1768         kids = self._make_kids(nm, ["imm", "lit", "dirwrite"])
1769-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1770+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1771                               dirnode.pack_children,
1772                               fn, kids, deep_immutable=True)
1773 
1774         kids = self._make_kids(nm, ["imm", "lit", "dirread"])
1775-        self.failUnlessRaises(dirnode.MustBeDeepImmutable,
1776+        self.failUnlessRaises(dirnode.MustBeDeepImmutableError,
1777                               dirnode.pack_children,
1778                               fn, kids, deep_immutable=True)
1779 
1780@@ -1017,16 +1096,31 @@
1781 
1782     def get_cap(self):
1783         return self.uri
1784+
1785     def get_uri(self):
1786         return self.uri.to_string()
1787+
1788+    def get_write_uri(self):
1789+        return self.uri.to_string()
1790+
1791     def download_best_version(self):
1792         return defer.succeed(self.data)
1793+
1794     def get_writekey(self):
1795         return "writekey"
1796+
1797     def is_readonly(self):
1798         return False
1799+
1800     def is_mutable(self):
1801         return True
1802+
1803+    def is_unknown(self):
1804+        return False
1805+
1806+    def is_allowed_in_immutable_directory(self):
1807+        return False
1808+
1809     def modify(self, modifier):
1810         self.data = modifier(self.data, None, True)
1811         return defer.succeed(None)
1812@@ -1049,50 +1143,186 @@
1813         self.nodemaker = client.nodemaker
1814 
1815     def test_from_future(self):
1816-        # create a dirnode that contains unknown URI types, and make sure we
1817-        # tolerate them properly. Since dirnodes aren't allowed to add
1818-        # unknown node types, we have to be tricky.
1819+        # Create a mutable directory that contains unknown URI types, and make sure
1820+        # we tolerate them properly.
1821         d = self.nodemaker.create_new_mutable_directory()
1822-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
1823-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
1824-        future_node = UnknownNode(future_writecap, future_readcap)
1825+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
1826+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
1827+        future_imm_uri = "x-tahoe-crazy-immutable://I_am_from_the_future."
1828+        future_node = UnknownNode(future_write_uri, future_read_uri)
1829         def _then(n):
1830             self._node = n
1831             return n.set_node(u"future", future_node)
1832         d.addCallback(_then)
1833 
1834-        # we should be prohibited from adding an unknown URI to a directory,
1835-        # since we don't know how to diminish the cap to a readcap (for the
1836-        # dirnode's rocap slot), and we don't want to accidentally grant
1837-        # write access to a holder of the dirnode's readcap.
1838+        # We should be prohibited from adding an unknown URI to a directory
1839+        # just in the rw_uri slot, since we don't know how to diminish the cap
1840+        # to a readcap (for the ro_uri slot).
1841         d.addCallback(lambda ign:
1842-             self.shouldFail(CannotPackUnknownNodeError,
1843+             self.shouldFail(MustNotBeUnknownRWError,
1844                              "copy unknown",
1845-                             "cannot pack unknown node as child add",
1846+                             "cannot attach unknown rw cap as child",
1847                              self._node.set_uri, u"add",
1848-                             future_writecap, future_readcap))
1849+                             future_write_uri, None))
1850+
1851+        # However, we should be able to add both rw_uri and ro_uri as a pair of
1852+        # unknown URIs.
1853+        d.addCallback(lambda ign: self._node.set_uri(u"add-pair",
1854+                                                     future_write_uri, future_read_uri))
1855+
1856+        # and to add an URI prefixed with "ro." or "imm." when it is given in a
1857+        # write slot (or URL parameter).
1858+        d.addCallback(lambda ign: self._node.set_uri(u"add-ro",
1859+                                                     "ro." + future_read_uri, None))
1860+        d.addCallback(lambda ign: self._node.set_uri(u"add-imm",
1861+                                                     "imm." + future_imm_uri, None))
1862+
1863         d.addCallback(lambda ign: self._node.list())
1864         def _check(children):
1865-            self.failUnlessEqual(len(children), 1)
1866+            self.failUnlessEqual(len(children), 4)
1867             (fn, metadata) = children[u"future"]
1868             self.failUnless(isinstance(fn, UnknownNode), fn)
1869-            self.failUnlessEqual(fn.get_uri(), future_writecap)
1870-            self.failUnlessEqual(fn.get_readonly_uri(), future_readcap)
1871-            # but we *should* be allowed to copy this node, because the
1872-            # UnknownNode contains all the information that was in the
1873-            # original directory (readcap and writecap), so we're preserving
1874-            # everything.
1875+            self.failUnlessEqual(fn.get_uri(), future_write_uri)
1876+            self.failUnlessEqual(fn.get_write_uri(), future_write_uri)
1877+            self.failUnlessEqual(fn.get_readonly_uri(), "ro." + future_read_uri)
1878+
1879+            (fn2, metadata2) = children[u"add-pair"]
1880+            self.failUnless(isinstance(fn2, UnknownNode), fn2)
1881+            self.failUnlessEqual(fn2.get_uri(), future_write_uri)
1882+            self.failUnlessEqual(fn2.get_write_uri(), future_write_uri)
1883+            self.failUnlessEqual(fn2.get_readonly_uri(), "ro." + future_read_uri)
1884+
1885+            (fn3, metadata3) = children[u"add-ro"]
1886+            self.failUnless(isinstance(fn3, UnknownNode), fn3)
1887+            self.failUnlessEqual(fn3.get_uri(), "ro." + future_read_uri)
1888+            self.failUnlessEqual(fn3.get_write_uri(), None)
1889+            self.failUnlessEqual(fn3.get_readonly_uri(), "ro." + future_read_uri)
1890+
1891+            (fn4, metadata4) = children[u"add-imm"]
1892+            self.failUnless(isinstance(fn4, UnknownNode), fn4)
1893+            self.failUnlessEqual(fn4.get_uri(), "imm." + future_imm_uri)
1894+            self.failUnlessEqual(fn4.get_write_uri(), None)
1895+            self.failUnlessEqual(fn4.get_readonly_uri(), "imm." + future_imm_uri)
1896+
1897+            # We should also be allowed to copy the "future" UnknownNode, because
1898+            # it contains all the information that was in the original directory
1899+            # (readcap and writecap), so we're preserving everything.
1900             return self._node.set_node(u"copy", fn)
1901         d.addCallback(_check)
1902+
1903         d.addCallback(lambda ign: self._node.list())
1904         def _check2(children):
1905-            self.failUnlessEqual(len(children), 2)
1906+            self.failUnlessEqual(len(children), 5)
1907             (fn, metadata) = children[u"copy"]
1908             self.failUnless(isinstance(fn, UnknownNode), fn)
1909-            self.failUnlessEqual(fn.get_uri(), future_writecap)
1910-            self.failUnlessEqual(fn.get_readonly_uri(), future_readcap)
1911+            self.failUnlessEqual(fn.get_uri(), future_write_uri)
1912+            self.failUnlessEqual(fn.get_write_uri(), future_write_uri)
1913+            self.failUnlessEqual(fn.get_readonly_uri(), "ro." + future_read_uri)
1914+        d.addCallback(_check2)
1915         return d
1916 
1917+    def test_unknown_strip_prefix_for_ro(self):
1918+        self.failUnlessEqual(strip_prefix_for_ro("foo",     False), "foo")
1919+        self.failUnlessEqual(strip_prefix_for_ro("ro.foo",  False), "foo")
1920+        self.failUnlessEqual(strip_prefix_for_ro("imm.foo", False), "imm.foo")
1921+        self.failUnlessEqual(strip_prefix_for_ro("foo",     True),  "foo")
1922+        self.failUnlessEqual(strip_prefix_for_ro("ro.foo",  True),  "foo")
1923+        self.failUnlessEqual(strip_prefix_for_ro("imm.foo", True),  "foo")
1924+
1925+    def test_unknownnode(self):
1926+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1927+        mut_read_uri = "URI:SSK-RO:jf6wkflosyvntwxqcdo7a54jvm:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
1928+        lit_uri = "URI:LIT:n5xgk"
1929+
1930+        # This does not attempt to be exhaustive.
1931+        no_no        = [# Opaque node, but not an error.
1932+                        ( 0, UnknownNode(None, None)),
1933+                        ( 1, UnknownNode(None, None, deep_immutable=True)),
1934+                       ]
1935+        unknown_rw   = [# These are errors because we're only given a rw_uri, and we can't
1936+                        # diminish it.
1937+                        ( 2, UnknownNode("foo", None)),
1938+                        ( 3, UnknownNode("foo", None, deep_immutable=True)),
1939+                        ( 4, UnknownNode("ro.foo", None, deep_immutable=True)),
1940+                        ( 5, UnknownNode("ro." + mut_read_uri, None, deep_immutable=True)),
1941+                        ( 6, UnknownNode("URI:SSK-RO:foo", None, deep_immutable=True)),
1942+                        ( 7, UnknownNode("URI:SSK:foo", None)),
1943+                       ]
1944+        must_be_ro   = [# These are errors because a readonly constraint is not met.
1945+                        ( 8, UnknownNode("ro." + mut_write_uri, None)),
1946+                        ( 9, UnknownNode(None, "ro." + mut_write_uri)),
1947+                       ]
1948+        must_be_imm  = [# These are errors because an immutable constraint is not met.
1949+                        (10, UnknownNode(None, "ro.URI:SSK-RO:foo", deep_immutable=True)),
1950+                        (11, UnknownNode(None, "imm.URI:SSK:foo")),
1951+                        (12, UnknownNode(None, "imm.URI:SSK-RO:foo")),
1952+                        (13, UnknownNode("bar", "ro.foo", deep_immutable=True)),
1953+                        (14, UnknownNode("bar", "imm.foo", deep_immutable=True)),
1954+                        (15, UnknownNode("bar", "imm." + lit_uri, deep_immutable=True)),
1955+                        (16, UnknownNode("imm." + mut_write_uri, None)),
1956+                        (17, UnknownNode("imm." + mut_read_uri, None)),
1957+                        (18, UnknownNode("bar", "imm.foo")),
1958+                       ]
1959+        bad_uri      = [# These are errors because the URI is bad once we've stripped the prefix.
1960+                        (19, UnknownNode("ro.URI:SSK-RO:foo", None)),
1961+                        (20, UnknownNode("imm.URI:CHK:foo", None, deep_immutable=True)),
1962+                       ]
1963+        ro_prefixed  = [# These are valid, and the readcap should end up with a ro. prefix.
1964+                        (21, UnknownNode(None, "foo")),
1965+                        (22, UnknownNode(None, "ro.foo")),
1966+                        (32, UnknownNode(None, "ro." + lit_uri)),
1967+                        (23, UnknownNode("bar", "foo")),
1968+                        (24, UnknownNode("bar", "ro.foo")),
1969+                        (32, UnknownNode("bar", "ro." + lit_uri)),
1970+                        (25, UnknownNode("ro.foo", None)),
1971+                        (30, UnknownNode("ro." + lit_uri, None)),
1972+                       ]
1973+        imm_prefixed = [# These are valid, and the readcap should end up with an imm. prefix.
1974+                        (26, UnknownNode(None, "foo", deep_immutable=True)),
1975+                        (27, UnknownNode(None, "ro.foo", deep_immutable=True)),
1976+                        (28, UnknownNode(None, "imm.foo")),
1977+                        (29, UnknownNode(None, "imm.foo", deep_immutable=True)),
1978+                        (31, UnknownNode("imm." + lit_uri, None)),
1979+                        (31, UnknownNode("imm." + lit_uri, None, deep_immutable=True)),
1980+                        (33, UnknownNode(None, "imm." + lit_uri)),
1981+                        (33, UnknownNode(None, "imm." + lit_uri, deep_immutable=True)),
1982+                       ]
1983+        error = unknown_rw + must_be_ro + must_be_imm + bad_uri
1984+        ok = ro_prefixed + imm_prefixed
1985+
1986+        for (i, n) in no_no + error + ok:
1987+            self.failUnless(n.is_unknown(), i)
1988+
1989+        for (i, n) in no_no + error:
1990+            self.failUnless(n.get_uri() is None, i)
1991+            self.failUnless(n.get_write_uri() is None, i)
1992+            self.failUnless(n.get_readonly_uri() is None, i)
1993+
1994+        for (i, n) in no_no + ok:
1995+            n.raise_error()
1996+
1997+        for (i, n) in unknown_rw:
1998+            self.failUnlessRaises(MustNotBeUnknownRWError, lambda: n.raise_error())
1999+
2000+        for (i, n) in must_be_ro:
2001+            self.failUnlessRaises(MustBeReadonlyError, lambda: n.raise_error())
2002+
2003+        for (i, n) in must_be_imm:
2004+            self.failUnlessRaises(MustBeDeepImmutableError, lambda: n.raise_error())
2005+
2006+        for (i, n) in bad_uri:
2007+            self.failUnlessRaises(uri.BadURIError, lambda: n.raise_error())
2008+
2009+        for (i, n) in ok:
2010+            self.failIf(n.get_readonly_uri() is None, i)
2011+
2012+        for (i, n) in ro_prefixed:
2013+            self.failUnless(n.get_readonly_uri().startswith("ro."), i)
2014+
2015+        for (i, n) in imm_prefixed:
2016+            self.failUnless(n.get_readonly_uri().startswith("imm."), i)
2017+
2018+
2019 class DeepStats(unittest.TestCase):
2020     timeout = 240 # It takes longer than 120 seconds on Francois's arm box.
2021     def test_stats(self):
2022diff -rN -u old-tahoe/src/allmydata/test/test_filenode.py new-tahoe/src/allmydata/test/test_filenode.py
2023--- old-tahoe/src/allmydata/test/test_filenode.py       2010-01-25 11:54:56.584000000 +0000
2024+++ new-tahoe/src/allmydata/test/test_filenode.py       2010-01-25 11:55:01.908000000 +0000
2025@@ -41,14 +41,21 @@
2026         self.failUnlessEqual(fn1.get_readcap(), u)
2027         self.failUnlessEqual(fn1.is_readonly(), True)
2028         self.failUnlessEqual(fn1.is_mutable(), False)
2029+        self.failUnlessEqual(fn1.is_unknown(), False)
2030+        self.failUnlessEqual(fn1.is_allowed_in_immutable_directory(), True)
2031+        self.failUnlessEqual(fn1.get_write_uri(), None)
2032         self.failUnlessEqual(fn1.get_readonly_uri(), u.to_string())
2033         self.failUnlessEqual(fn1.get_size(), 1000)
2034         self.failUnlessEqual(fn1.get_storage_index(), u.storage_index)
2035+        fn1.raise_error()
2036+        fn2.raise_error()
2037         d = {}
2038         d[fn1] = 1 # exercise __hash__
2039         v = fn1.get_verify_cap()
2040         self.failUnless(isinstance(v, uri.CHKFileVerifierURI))
2041         self.failUnlessEqual(fn1.get_repair_cap(), v)
2042+        self.failUnlessEqual(v.is_readonly(), True)
2043+        self.failUnlessEqual(v.is_mutable(), False)
2044 
2045 
2046     def test_literal_filenode(self):
2047@@ -64,9 +71,14 @@
2048         self.failUnlessEqual(fn1.get_readcap(), u)
2049         self.failUnlessEqual(fn1.is_readonly(), True)
2050         self.failUnlessEqual(fn1.is_mutable(), False)
2051+        self.failUnlessEqual(fn1.is_unknown(), False)
2052+        self.failUnlessEqual(fn1.is_allowed_in_immutable_directory(), True)
2053+        self.failUnlessEqual(fn1.get_write_uri(), None)
2054         self.failUnlessEqual(fn1.get_readonly_uri(), u.to_string())
2055         self.failUnlessEqual(fn1.get_size(), len(DATA))
2056         self.failUnlessEqual(fn1.get_storage_index(), None)
2057+        fn1.raise_error()
2058+        fn2.raise_error()
2059         d = {}
2060         d[fn1] = 1 # exercise __hash__
2061 
2062@@ -99,24 +111,29 @@
2063         self.failUnlessEqual(n.get_writekey(), wk)
2064         self.failUnlessEqual(n.get_readkey(), rk)
2065         self.failUnlessEqual(n.get_storage_index(), si)
2066-        # these itmes are populated on first read (or create), so until that
2067+        # these items are populated on first read (or create), so until that
2068         # happens they'll be None
2069         self.failUnlessEqual(n.get_privkey(), None)
2070         self.failUnlessEqual(n.get_encprivkey(), None)
2071         self.failUnlessEqual(n.get_pubkey(), None)
2072 
2073         self.failUnlessEqual(n.get_uri(), u.to_string())
2074+        self.failUnlessEqual(n.get_write_uri(), u.to_string())
2075         self.failUnlessEqual(n.get_readonly_uri(), u.get_readonly().to_string())
2076         self.failUnlessEqual(n.get_cap(), u)
2077         self.failUnlessEqual(n.get_readcap(), u.get_readonly())
2078         self.failUnlessEqual(n.is_mutable(), True)
2079         self.failUnlessEqual(n.is_readonly(), False)
2080+        self.failUnlessEqual(n.is_unknown(), False)
2081+        self.failUnlessEqual(n.is_allowed_in_immutable_directory(), False)
2082+        n.raise_error()
2083 
2084         n2 = MutableFileNode(None, None, client.get_encoding_parameters(),
2085                              None).init_from_cap(u)
2086         self.failUnlessEqual(n, n2)
2087         self.failIfEqual(n, "not even the right type")
2088         self.failIfEqual(n, u) # not the right class
2089+        n.raise_error()
2090         d = {n: "can these be used as dictionary keys?"}
2091         d[n2] = "replace the old one"
2092         self.failUnlessEqual(len(d), 1)
2093@@ -127,12 +144,16 @@
2094         self.failUnlessEqual(nro.get_readonly(), nro)
2095         self.failUnlessEqual(nro.get_cap(), u.get_readonly())
2096         self.failUnlessEqual(nro.get_readcap(), u.get_readonly())
2097+        self.failUnlessEqual(nro.is_mutable(), True)
2098+        self.failUnlessEqual(nro.is_readonly(), True)
2099+        self.failUnlessEqual(nro.is_unknown(), False)
2100+        self.failUnlessEqual(nro.is_allowed_in_immutable_directory(), False)
2101         nro_u = nro.get_uri()
2102         self.failUnlessEqual(nro_u, nro.get_readonly_uri())
2103         self.failUnlessEqual(nro_u, u.get_readonly().to_string())
2104-        self.failUnlessEqual(nro.is_mutable(), True)
2105-        self.failUnlessEqual(nro.is_readonly(), True)
2106+        self.failUnlessEqual(nro.get_write_uri(), None)
2107         self.failUnlessEqual(nro.get_repair_cap(), None) # RSAmut needs writecap
2108+        nro.raise_error()
2109 
2110         v = n.get_verify_cap()
2111         self.failUnless(isinstance(v, uri.SSKVerifierURI))
2112diff -rN -u old-tahoe/src/allmydata/test/test_system.py new-tahoe/src/allmydata/test/test_system.py
2113--- old-tahoe/src/allmydata/test/test_system.py 2010-01-25 11:54:56.895000000 +0000
2114+++ new-tahoe/src/allmydata/test/test_system.py 2010-01-25 11:55:02.199000000 +0000
2115@@ -17,7 +17,7 @@
2116 from allmydata.interfaces import IDirectoryNode, IFileNode, \
2117      NoSuchChildError, NoSharesError
2118 from allmydata.monitor import Monitor
2119-from allmydata.mutable.common import NotMutableError
2120+from allmydata.mutable.common import NotWriteableError
2121 from allmydata.mutable import layout as mutable_layout
2122 from foolscap.api import DeadReferenceError
2123 from twisted.python.failure import Failure
2124@@ -890,11 +890,11 @@
2125             d1.addCallback(lambda res: dirnode.list())
2126             d1.addCallback(self.log, "dirnode.list")
2127 
2128-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mkdir(nope)", None, dirnode.create_subdirectory, u"nope"))
2129+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mkdir(nope)", None, dirnode.create_subdirectory, u"nope"))
2130 
2131             d1.addCallback(self.log, "doing add_file(ro)")
2132             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)")
2133-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "add_file(nope)", None, dirnode.add_file, u"hope", ut))
2134+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "add_file(nope)", None, dirnode.add_file, u"hope", ut))
2135 
2136             d1.addCallback(self.log, "doing get(ro)")
2137             d1.addCallback(lambda res: dirnode.get(u"mydata992"))
2138@@ -902,17 +902,17 @@
2139                            self.failUnless(IFileNode.providedBy(filenode)))
2140 
2141             d1.addCallback(self.log, "doing delete(ro)")
2142-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "delete(nope)", None, dirnode.delete, u"mydata992"))
2143+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "delete(nope)", None, dirnode.delete, u"mydata992"))
2144 
2145-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "set_uri(nope)", None, dirnode.set_uri, u"hopeless", self.uri, self.uri))
2146+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "set_uri(nope)", None, dirnode.set_uri, u"hopeless", self.uri, self.uri))
2147 
2148             d1.addCallback(lambda res: self.shouldFail2(NoSuchChildError, "get(missing)", "missing", dirnode.get, u"missing"))
2149 
2150             personal = self._personal_node
2151-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mv from readonly", None, dirnode.move_child_to, u"mydata992", personal, u"nope"))
2152+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mv from readonly", None, dirnode.move_child_to, u"mydata992", personal, u"nope"))
2153 
2154             d1.addCallback(self.log, "doing move_child_to(ro)2")
2155-            d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mv to readonly", None, personal.move_child_to, u"sekrit data", dirnode, u"nope"))
2156+            d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mv to readonly", None, personal.move_child_to, u"sekrit data", dirnode, u"nope"))
2157 
2158             d1.addCallback(self.log, "finished with _got_s2ro")
2159             return d1
2160diff -rN -u old-tahoe/src/allmydata/test/test_uri.py new-tahoe/src/allmydata/test/test_uri.py
2161--- old-tahoe/src/allmydata/test/test_uri.py    2010-01-25 11:54:56.910000000 +0000
2162+++ new-tahoe/src/allmydata/test/test_uri.py    2010-01-25 11:55:02.216000000 +0000
2163@@ -3,7 +3,7 @@
2164 from allmydata import uri
2165 from allmydata.util import hashutil, base32
2166 from allmydata.interfaces import IURI, IFileURI, IDirnodeURI, IMutableFileURI, \
2167-    IVerifierURI
2168+    IVerifierURI, CapConstraintError
2169 
2170 class Literal(unittest.TestCase):
2171     def _help_test(self, data):
2172@@ -22,8 +22,16 @@
2173         self.failIf(IDirnodeURI.providedBy(u2))
2174         self.failUnlessEqual(u2.data, data)
2175         self.failUnlessEqual(u2.get_size(), len(data))
2176-        self.failUnless(u.is_readonly())
2177-        self.failIf(u.is_mutable())
2178+        self.failUnless(u2.is_readonly())
2179+        self.failIf(u2.is_mutable())
2180+
2181+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
2182+        self.failUnless(IFileURI.providedBy(u2i))
2183+        self.failIf(IDirnodeURI.providedBy(u2i))
2184+        self.failUnlessEqual(u2i.data, data)
2185+        self.failUnlessEqual(u2i.get_size(), len(data))
2186+        self.failUnless(u2i.is_readonly())
2187+        self.failIf(u2i.is_mutable())
2188 
2189         u3 = u.get_readonly()
2190         self.failUnlessIdentical(u, u3)
2191@@ -51,18 +59,36 @@
2192         fileURI = 'URI:CHK:f5ahxa25t4qkktywz6teyfvcx4:opuioq7tj2y6idzfp6cazehtmgs5fdcebcz3cygrxyydvcozrmeq:3:10:345834'
2193         chk1 = uri.CHKFileURI.init_from_string(fileURI)
2194         chk2 = uri.CHKFileURI.init_from_string(fileURI)
2195+        unk = uri.UnknownURI("lafs://from_the_future")
2196         self.failIfEqual(lit1, chk1)
2197         self.failUnlessEqual(chk1, chk2)
2198         self.failIfEqual(chk1, "not actually a URI")
2199         # these should be hashable too
2200-        s = set([lit1, chk1, chk2])
2201-        self.failUnlessEqual(len(s), 2) # since chk1==chk2
2202+        s = set([lit1, chk1, chk2, unk])
2203+        self.failUnlessEqual(len(s), 3) # since chk1==chk2
2204 
2205     def test_is_uri(self):
2206         lit1 = uri.LiteralFileURI("some data").to_string()
2207         self.failUnless(uri.is_uri(lit1))
2208         self.failIf(uri.is_uri(None))
2209 
2210+    def test_is_literal_file_uri(self):
2211+        lit1 = uri.LiteralFileURI("some data").to_string()
2212+        self.failUnless(uri.is_literal_file_uri(lit1))
2213+        self.failIf(uri.is_literal_file_uri(None))
2214+        self.failIf(uri.is_literal_file_uri("foo"))
2215+        self.failIf(uri.is_literal_file_uri("ro.foo"))
2216+        self.failIf(uri.is_literal_file_uri("URI:LITfoo"))
2217+        self.failUnless(uri.is_literal_file_uri("ro.URI:LIT:foo"))
2218+        self.failUnless(uri.is_literal_file_uri("imm.URI:LIT:foo"))
2219+
2220+    def test_has_uri_prefix(self):
2221+        self.failUnless(uri.has_uri_prefix("URI:foo"))
2222+        self.failUnless(uri.has_uri_prefix("ro.URI:foo"))
2223+        self.failUnless(uri.has_uri_prefix("imm.URI:foo"))
2224+        self.failIf(uri.has_uri_prefix(None))
2225+        self.failIf(uri.has_uri_prefix("foo"))
2226+
2227 class CHKFile(unittest.TestCase):
2228     def test_pack(self):
2229         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
2230@@ -88,8 +114,7 @@
2231         self.failUnless(IFileURI.providedBy(u))
2232         self.failIf(IDirnodeURI.providedBy(u))
2233         self.failUnlessEqual(u.get_size(), 1234)
2234-        self.failUnless(u.is_readonly())
2235-        self.failIf(u.is_mutable())
2236+
2237         u_ro = u.get_readonly()
2238         self.failUnlessIdentical(u, u_ro)
2239         he = u.to_human_encoding()
2240@@ -109,11 +134,19 @@
2241         self.failUnless(IFileURI.providedBy(u2))
2242         self.failIf(IDirnodeURI.providedBy(u2))
2243         self.failUnlessEqual(u2.get_size(), 1234)
2244-        self.failUnless(u2.is_readonly())
2245-        self.failIf(u2.is_mutable())
2246+
2247+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
2248+        self.failUnlessEqual(u.to_string(), u2i.to_string())
2249+        u2ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u.to_string())
2250+        self.failUnlessEqual(u.to_string(), u2ro.to_string())
2251+        u2imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u.to_string())
2252+        self.failUnlessEqual(u.to_string(), u2imm.to_string())
2253 
2254         v = u.get_verify_cap()
2255         self.failUnless(isinstance(v.to_string(), str))
2256+        self.failUnless(v.is_readonly())
2257+        self.failIf(v.is_mutable())
2258+
2259         v2 = uri.from_string(v.to_string())
2260         self.failUnlessEqual(v, v2)
2261         he = v.to_human_encoding()
2262@@ -126,6 +159,8 @@
2263                                     total_shares=10,
2264                                     size=1234)
2265         self.failUnless(isinstance(v3.to_string(), str))
2266+        self.failUnless(v3.is_readonly())
2267+        self.failIf(v3.is_mutable())
2268 
2269     def test_pack_badly(self):
2270         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
2271@@ -179,13 +214,20 @@
2272         self.failUnlessEqual(readable["UEB_hash"],
2273                              base32.b2a(hashutil.uri_extension_hash(ext)))
2274 
2275-class Invalid(unittest.TestCase):
2276+class Unknown(unittest.TestCase):
2277     def test_from_future(self):
2278         # any URI type that we don't recognize should be treated as unknown
2279         future_uri = "I am a URI from the future. Whatever you do, don't "
2280         u = uri.from_string(future_uri)
2281         self.failUnless(isinstance(u, uri.UnknownURI))
2282         self.failUnlessEqual(u.to_string(), future_uri)
2283+        self.failUnless(u.get_readonly() is None)
2284+        self.failUnless(u.get_error() is None)
2285+
2286+        u2 = uri.UnknownURI(future_uri, error=CapConstraintError("..."))
2287+        self.failUnlessEqual(u.to_string(), future_uri)
2288+        self.failUnless(u2.get_readonly() is None)
2289+        self.failUnless(isinstance(u2.get_error(), CapConstraintError))
2290 
2291 class Constraint(unittest.TestCase):
2292     def test_constraint(self):
2293@@ -226,6 +268,13 @@
2294         self.failUnless(IMutableFileURI.providedBy(u2))
2295         self.failIf(IDirnodeURI.providedBy(u2))
2296 
2297+        u2i = uri.from_string(u.to_string(), deep_immutable=True)
2298+        self.failUnless(isinstance(u2i, uri.UnknownURI), u2i)
2299+        u2ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u.to_string())
2300+        self.failUnless(isinstance(u2ro, uri.UnknownURI), u2ro)
2301+        u2imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u.to_string())
2302+        self.failUnless(isinstance(u2imm, uri.UnknownURI), u2imm)
2303+
2304         u3 = u2.get_readonly()
2305         readkey = hashutil.ssk_readkey_hash(writekey)
2306         self.failUnlessEqual(u3.fingerprint, fingerprint)
2307@@ -236,6 +285,13 @@
2308         self.failUnless(IMutableFileURI.providedBy(u3))
2309         self.failIf(IDirnodeURI.providedBy(u3))
2310 
2311+        u3i = uri.from_string(u3.to_string(), deep_immutable=True)
2312+        self.failUnless(isinstance(u3i, uri.UnknownURI), u3i)
2313+        u3ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u3.to_string())
2314+        self.failUnlessEqual(u3.to_string(), u3ro.to_string())
2315+        u3imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u3.to_string())
2316+        self.failUnless(isinstance(u3imm, uri.UnknownURI), u3imm)
2317+
2318         he = u3.to_human_encoding()
2319         u3_h = uri.ReadonlySSKFileURI.init_from_human_encoding(he)
2320         self.failUnlessEqual(u3, u3_h)
2321@@ -249,6 +305,13 @@
2322         self.failUnless(IMutableFileURI.providedBy(u4))
2323         self.failIf(IDirnodeURI.providedBy(u4))
2324 
2325+        u4i = uri.from_string(u4.to_string(), deep_immutable=True)
2326+        self.failUnless(isinstance(u4i, uri.UnknownURI), u4i)
2327+        u4ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u4.to_string())
2328+        self.failUnlessEqual(u4.to_string(), u4ro.to_string())
2329+        u4imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u4.to_string())
2330+        self.failUnless(isinstance(u4imm, uri.UnknownURI), u4imm)
2331+
2332         u4a = uri.from_string(u4.to_string())
2333         self.failUnlessEqual(u4a, u4)
2334         self.failUnless("ReadonlySSKFileURI" in str(u4a))
2335@@ -291,12 +354,19 @@
2336         self.failIf(IFileURI.providedBy(u2))
2337         self.failUnless(IDirnodeURI.providedBy(u2))
2338 
2339+        u2i = uri.from_string(u1.to_string(), deep_immutable=True)
2340+        self.failUnless(isinstance(u2i, uri.UnknownURI))
2341+
2342         u3 = u2.get_readonly()
2343         self.failUnless(u3.is_readonly())
2344         self.failUnless(u3.is_mutable())
2345         self.failUnless(IURI.providedBy(u3))
2346         self.failIf(IFileURI.providedBy(u3))
2347         self.failUnless(IDirnodeURI.providedBy(u3))
2348+
2349+        u3i = uri.from_string(u2.to_string(), deep_immutable=True)
2350+        self.failUnless(isinstance(u3i, uri.UnknownURI))
2351+
2352         u3n = u3._filenode_uri
2353         self.failUnless(u3n.is_readonly())
2354         self.failUnless(u3n.is_mutable())
2355@@ -363,10 +433,16 @@
2356         self.failIf(IFileURI.providedBy(u2))
2357         self.failUnless(IDirnodeURI.providedBy(u2))
2358 
2359+        u2i = uri.from_string(u1.to_string(), deep_immutable=True)
2360+        self.failUnlessEqual(u1.to_string(), u2i.to_string())
2361+
2362         u3 = u2.get_readonly()
2363         self.failUnlessEqual(u3.to_string(), u2.to_string())
2364         self.failUnless(str(u3))
2365 
2366+        u3i = uri.from_string(u2.to_string(), deep_immutable=True)
2367+        self.failUnlessEqual(u2.to_string(), u3i.to_string())
2368+
2369         u2_verifier = u2.get_verify_cap()
2370         self.failUnless(isinstance(u2_verifier,
2371                                    uri.ImmutableDirectoryURIVerifier),
2372diff -rN -u old-tahoe/src/allmydata/test/test_web.py new-tahoe/src/allmydata/test/test_web.py
2373--- old-tahoe/src/allmydata/test/test_web.py    2010-01-25 11:54:56.933000000 +0000
2374+++ new-tahoe/src/allmydata/test/test_web.py    2010-01-25 11:55:02.253000000 +0000
2375@@ -7,7 +7,7 @@
2376 from twisted.web import client, error, http
2377 from twisted.python import failure, log
2378 from nevow import rend
2379-from allmydata import interfaces, uri, webish
2380+from allmydata import interfaces, uri, webish, dirnode
2381 from allmydata.storage.shares import get_share_file
2382 from allmydata.storage_client import StorageFarmBroker
2383 from allmydata.immutable import upload, download
2384@@ -18,6 +18,7 @@
2385 from allmydata.scripts.debug import CorruptShareOptions, corrupt_share
2386 from allmydata.util import fileutil, base32
2387 from allmydata.util.consumer import download_to_data
2388+from allmydata.util.netstring import split_netstring
2389 from allmydata.test.common import FakeCHKFileNode, FakeMutableFileNode, \
2390      create_chk_filenode, WebErrorMixin, ShouldFailMixin, make_mutable_file_uri
2391 from allmydata.interfaces import IMutableFileNode
2392@@ -699,11 +700,15 @@
2393                              self.PUT, base, "")
2394         return d
2395 
2396+    # TODO: version of this with a Unicode filename
2397     def test_GET_FILEURL_save(self):
2398-        d = self.GET(self.public_url + "/foo/bar.txt?filename=bar.txt&save=true")
2399-        # TODO: look at the headers, expect a Content-Disposition: attachment
2400-        # header.
2401-        d.addCallback(self.failUnlessIsBarDotTxt)
2402+        d = self.GET(self.public_url + "/foo/bar.txt?filename=bar.txt&save=true",
2403+                     return_response=True)
2404+        def _got((res, statuscode, headers)):
2405+            content_disposition = headers["content-disposition"][0]
2406+            self.failUnless(content_disposition == 'attachment; filename="bar.txt"', content_disposition)
2407+            self.failUnlessIsBarDotTxt(res)
2408+        d.addCallback(_got)
2409         return d
2410 
2411     def test_GET_FILEURL_missing(self):
2412@@ -735,7 +740,7 @@
2413         d = self.PUT(self.public_url + "/foo/new.txt", self.NEWFILE_CONTENTS)
2414         # TODO: we lose the response code, so we can't check this
2415         #self.failUnlessEqual(responsecode, 201)
2416-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2417+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
2418         d.addCallback(lambda res:
2419                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
2420                                                       self.NEWFILE_CONTENTS))
2421@@ -746,7 +751,7 @@
2422                      self.NEWFILE_CONTENTS)
2423         # TODO: we lose the response code, so we can't check this
2424         #self.failUnlessEqual(responsecode, 201)
2425-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2426+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
2427         d.addCallback(lambda res:
2428                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
2429                                                       self.NEWFILE_CONTENTS))
2430@@ -776,7 +781,7 @@
2431             self.failIf(u.is_readonly())
2432             return res
2433         d.addCallback(_check_uri)
2434-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2435+        d.addCallback(self.failUnlessURIMatchesRWChild, self._foo_node, u"new.txt")
2436         d.addCallback(lambda res:
2437                       self.failUnlessMutableChildContentsAre(self._foo_node,
2438                                                              u"new.txt",
2439@@ -796,7 +801,7 @@
2440         d = self.PUT(self.public_url + "/foo/bar.txt", self.NEWFILE_CONTENTS)
2441         # TODO: we lose the response code, so we can't check this
2442         #self.failUnlessEqual(responsecode, 200)
2443-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt")
2444+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt")
2445         d.addCallback(lambda res:
2446                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
2447                                                       self.NEWFILE_CONTENTS))
2448@@ -821,7 +826,7 @@
2449     def test_PUT_NEWFILEURL_mkdirs(self):
2450         d = self.PUT(self.public_url + "/foo/newdir/new.txt", self.NEWFILE_CONTENTS)
2451         fn = self._foo_node
2452-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"newdir/new.txt")
2453+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"newdir/new.txt")
2454         d.addCallback(lambda res: self.failIfNodeHasChild(fn, u"new.txt"))
2455         d.addCallback(lambda res: self.failUnlessNodeHasChild(fn, u"newdir"))
2456         d.addCallback(lambda res:
2457@@ -954,7 +959,7 @@
2458             self.failUnless(re.search(get_sub, res), res)
2459         d.addCallback(_check)
2460 
2461-        # look at a directory which is readonly
2462+        # look at a readonly directory
2463         d.addCallback(lambda res:
2464                       self.GET(self.public_url + "/reedownlee", followRedirect=True))
2465         def _check2(res):
2466@@ -1167,23 +1172,33 @@
2467         return d
2468 
2469     def test_POST_NEWDIRURL_initial_children(self):
2470-        (newkids, filecap1, filecap2, filecap3,
2471-         dircap) = self._create_initial_children()
2472+        (newkids, caps) = self._create_initial_children()
2473         d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-with-children",
2474                        simplejson.dumps(newkids))
2475         def _check(uri):
2476             n = self.s.create_node_from_uri(uri.strip())
2477             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2478             d2.addCallback(lambda ign:
2479-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2480+                           self.failUnlessROChildURIIs(n, u"child-imm",
2481+                                                       caps['filecap1']))
2482+            d2.addCallback(lambda ign:
2483+                           self.failUnlessRWChildURIIs(n, u"child-mutable",
2484+                                                       caps['filecap2']))
2485+            d2.addCallback(lambda ign:
2486+                           self.failUnlessROChildURIIs(n, u"child-mutable-ro",
2487+                                                       caps['filecap3']))
2488             d2.addCallback(lambda ign:
2489-                           self.failUnlessChildURIIs(n, u"child-mutable",
2490-                                                     filecap2))
2491+                           self.failUnlessROChildURIIs(n, u"unknownchild-ro",
2492+                                                       caps['unknown_rocap']))
2493             d2.addCallback(lambda ign:
2494-                           self.failUnlessChildURIIs(n, u"child-mutable-ro",
2495-                                                     filecap3))
2496+                           self.failUnlessRWChildURIIs(n, u"unknownchild-rw",
2497+                                                       caps['unknown_rwcap']))
2498             d2.addCallback(lambda ign:
2499-                           self.failUnlessChildURIIs(n, u"dirchild", dircap))
2500+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2501+                                                       caps['unknown_immcap']))
2502+            d2.addCallback(lambda ign:
2503+                           self.failUnlessRWChildURIIs(n, u"dirchild",
2504+                                                       caps['dircap']))
2505             return d2
2506         d.addCallback(_check)
2507         d.addCallback(lambda res:
2508@@ -1191,21 +1206,25 @@
2509         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2510         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2511         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2512-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2513+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2514         return d
2515 
2516     def test_POST_NEWDIRURL_immutable(self):
2517-        (newkids, filecap1, immdircap) = self._create_immutable_children()
2518+        (newkids, caps) = self._create_immutable_children()
2519         d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-immutable",
2520                        simplejson.dumps(newkids))
2521         def _check(uri):
2522             n = self.s.create_node_from_uri(uri.strip())
2523             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2524             d2.addCallback(lambda ign:
2525-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2526+                           self.failUnlessROChildURIIs(n, u"child-imm",
2527+                                                       caps['filecap1']))
2528+            d2.addCallback(lambda ign:
2529+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2530+                                                       caps['unknown_immcap']))
2531             d2.addCallback(lambda ign:
2532-                           self.failUnlessChildURIIs(n, u"dirchild-imm",
2533-                                                     immdircap))
2534+                           self.failUnlessROChildURIIs(n, u"dirchild-imm",
2535+                                                       caps['immdircap']))
2536             return d2
2537         d.addCallback(_check)
2538         d.addCallback(lambda res:
2539@@ -1213,18 +1232,19 @@
2540         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2541         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2542         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2543-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2544+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2545         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2546-        d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap)
2547+        d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap'])
2548+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2549+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap'])
2550         d.addErrback(self.explain_web_error)
2551         return d
2552 
2553     def test_POST_NEWDIRURL_immutable_bad(self):
2554-        (newkids, filecap1, filecap2, filecap3,
2555-         dircap) = self._create_initial_children()
2556+        (newkids, caps) = self._create_initial_children()
2557         d = self.shouldFail2(error.Error, "test_POST_NEWDIRURL_immutable_bad",
2558                              "400 Bad Request",
2559-                             "a mkdir-immutable operation was given a child that was not itself immutable",
2560+                             "needed to be immutable but was not",
2561                              self.POST2,
2562                              self.public_url + "/foo/newdir?t=mkdir-immutable",
2563                              simplejson.dumps(newkids))
2564@@ -1346,19 +1366,51 @@
2565         d.addCallback(_check)
2566         return d
2567 
2568-    def failUnlessChildURIIs(self, node, name, expected_uri):
2569+    def failUnlessRWChildURIIs(self, node, name, expected_uri):
2570         assert isinstance(name, unicode)
2571         d = node.get_child_at_path(name)
2572         def _check(child):
2573+            self.failUnless(child.is_unknown() or not child.is_readonly())
2574             self.failUnlessEqual(child.get_uri(), expected_uri.strip())
2575+            self.failUnlessEqual(child.get_write_uri(), expected_uri.strip())
2576+            expected_ro_uri = self._make_readonly(expected_uri)
2577+            if expected_ro_uri:
2578+                self.failUnlessEqual(child.get_readonly_uri(), expected_ro_uri.strip())
2579+        d.addCallback(_check)
2580+        return d
2581+
2582+    def failUnlessROChildURIIs(self, node, name, expected_uri):
2583+        assert isinstance(name, unicode)
2584+        d = node.get_child_at_path(name)
2585+        def _check(child):
2586+            self.failUnless(child.is_unknown() or child.is_readonly())
2587+            self.failUnlessEqual(child.get_write_uri(), None)
2588+            self.failUnlessEqual(child.get_uri(), expected_uri.strip())
2589+            self.failUnlessEqual(child.get_readonly_uri(), expected_uri.strip())
2590+        d.addCallback(_check)
2591+        return d
2592+
2593+    def failUnlessURIMatchesRWChild(self, got_uri, node, name):
2594+        assert isinstance(name, unicode)
2595+        d = node.get_child_at_path(name)
2596+        def _check(child):
2597+            self.failUnless(child.is_unknown() or not child.is_readonly())
2598+            self.failUnlessEqual(child.get_uri(), got_uri.strip())
2599+            self.failUnlessEqual(child.get_write_uri(), got_uri.strip())
2600+            expected_ro_uri = self._make_readonly(got_uri)
2601+            if expected_ro_uri:
2602+                self.failUnlessEqual(child.get_readonly_uri(), expected_ro_uri.strip())
2603         d.addCallback(_check)
2604         return d
2605 
2606-    def failUnlessURIMatchesChild(self, got_uri, node, name):
2607+    def failUnlessURIMatchesROChild(self, got_uri, node, name):
2608         assert isinstance(name, unicode)
2609         d = node.get_child_at_path(name)
2610         def _check(child):
2611+            self.failUnless(child.is_unknown() or child.is_readonly())
2612+            self.failUnlessEqual(child.get_write_uri(), None)
2613             self.failUnlessEqual(got_uri.strip(), child.get_uri())
2614+            self.failUnlessEqual(got_uri.strip(), child.get_readonly_uri())
2615         d.addCallback(_check)
2616         return d
2617 
2618@@ -1369,7 +1421,7 @@
2619         d = self.POST(self.public_url + "/foo", t="upload",
2620                       file=("new.txt", self.NEWFILE_CONTENTS))
2621         fn = self._foo_node
2622-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2623+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt")
2624         d.addCallback(lambda res:
2625                       self.failUnlessChildContentsAre(fn, u"new.txt",
2626                                                       self.NEWFILE_CONTENTS))
2627@@ -1380,7 +1432,7 @@
2628         d = self.POST(self.public_url + "/foo", t="upload",
2629                       file=(filename, self.NEWFILE_CONTENTS))
2630         fn = self._foo_node
2631-        d.addCallback(self.failUnlessURIMatchesChild, fn, filename)
2632+        d.addCallback(self.failUnlessURIMatchesROChild, fn, filename)
2633         d.addCallback(lambda res:
2634                       self.failUnlessChildContentsAre(fn, filename,
2635                                                       self.NEWFILE_CONTENTS))
2636@@ -1397,7 +1449,7 @@
2637                       name=filename,
2638                       file=("overridden", self.NEWFILE_CONTENTS))
2639         fn = self._foo_node
2640-        d.addCallback(self.failUnlessURIMatchesChild, fn, filename)
2641+        d.addCallback(self.failUnlessURIMatchesROChild, fn, filename)
2642         d.addCallback(lambda res:
2643                       self.failUnlessChildContentsAre(fn, filename,
2644                                                       self.NEWFILE_CONTENTS))
2645@@ -1499,7 +1551,7 @@
2646         d = self.POST(self.public_url + "/foo", t="upload", mutable="true",
2647                       file=("new.txt", self.NEWFILE_CONTENTS))
2648         fn = self._foo_node
2649-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2650+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2651         d.addCallback(lambda res:
2652                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2653                                                              self.NEWFILE_CONTENTS))
2654@@ -1518,7 +1570,7 @@
2655                       self.POST(self.public_url + "/foo", t="upload",
2656                                 mutable="true",
2657                                 file=("new.txt", NEWER_CONTENTS)))
2658-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2659+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2660         d.addCallback(lambda res:
2661                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2662                                                              NEWER_CONTENTS))
2663@@ -1534,7 +1586,7 @@
2664         NEW2_CONTENTS = NEWER_CONTENTS + "overwrite with PUT\n"
2665         d.addCallback(lambda res:
2666                       self.PUT(self.public_url + "/foo/new.txt", NEW2_CONTENTS))
2667-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2668+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
2669         d.addCallback(lambda res:
2670                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
2671                                                              NEW2_CONTENTS))
2672@@ -1663,7 +1715,7 @@
2673         d = self.POST(self.public_url + "/foo", t="upload",
2674                       file=("bar.txt", self.NEWFILE_CONTENTS))
2675         fn = self._foo_node
2676-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"bar.txt")
2677+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"bar.txt")
2678         d.addCallback(lambda res:
2679                       self.failUnlessChildContentsAre(fn, u"bar.txt",
2680                                                       self.NEWFILE_CONTENTS))
2681@@ -1714,7 +1766,7 @@
2682         fn = self._foo_node
2683         d = self.POST(self.public_url + "/foo", t="upload",
2684                       name="new.txt", file=self.NEWFILE_CONTENTS)
2685-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
2686+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt")
2687         d.addCallback(lambda res:
2688                       self.failUnlessChildContentsAre(fn, u"new.txt",
2689                                                       self.NEWFILE_CONTENTS))
2690@@ -1977,7 +2029,7 @@
2691         return d
2692 
2693     def test_POST_mkdir_initial_children(self):
2694-        newkids, filecap1, ign, ign, ign = self._create_initial_children()
2695+        (newkids, caps) = self._create_initial_children()
2696         d = self.POST2(self.public_url +
2697                        "/foo?t=mkdir-with-children&name=newdir",
2698                        simplejson.dumps(newkids))
2699@@ -1986,11 +2038,11 @@
2700         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2701         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2702         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2703-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2704+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2705         return d
2706 
2707     def test_POST_mkdir_immutable(self):
2708-        (newkids, filecap1, immdircap) = self._create_immutable_children()
2709+        (newkids, caps) = self._create_immutable_children()
2710         d = self.POST2(self.public_url +
2711                        "/foo?t=mkdir-immutable&name=newdir",
2712                        simplejson.dumps(newkids))
2713@@ -1999,17 +2051,18 @@
2714         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2715         d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
2716         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2717-        d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1)
2718+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
2719+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2720+        d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap'])
2721         d.addCallback(lambda res: self._foo_node.get(u"newdir"))
2722-        d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap)
2723+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap'])
2724         return d
2725 
2726     def test_POST_mkdir_immutable_bad(self):
2727-        (newkids, filecap1, filecap2, filecap3,
2728-         dircap) = self._create_initial_children()
2729+        (newkids, caps) = self._create_initial_children()
2730         d = self.shouldFail2(error.Error, "test_POST_mkdir_immutable_bad",
2731                              "400 Bad Request",
2732-                             "a mkdir-immutable operation was given a child that was not itself immutable",
2733+                             "needed to be immutable but was not",
2734                              self.POST2,
2735                              self.public_url +
2736                              "/foo?t=mkdir-immutable&name=newdir",
2737@@ -2049,21 +2102,43 @@
2738         d.addCallback(_check_target)
2739         return d
2740 
2741+    def _make_readonly(self, u):
2742+        ro_uri = uri.from_string(u).get_readonly()
2743+        if ro_uri is None:
2744+            return None
2745+        return ro_uri.to_string()
2746+
2747     def _create_initial_children(self):
2748         contents, n, filecap1 = self.makefile(12)
2749         md1 = {"metakey1": "metavalue1"}
2750         filecap2 = make_mutable_file_uri()
2751         node3 = self.s.create_node_from_uri(make_mutable_file_uri())
2752         filecap3 = node3.get_readonly_uri()
2753+        unknown_rwcap = "lafs://from_the_future"
2754+        unknown_rocap = "ro.lafs://readonly_from_the_future"
2755+        unknown_immcap = "imm.lafs://immutable_from_the_future"
2756         node4 = self.s.create_node_from_uri(make_mutable_file_uri())
2757         dircap = DirectoryNode(node4, None, None).get_uri()
2758-        newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1,
2759-                                               "metadata": md1, }],
2760-                   u"child-mutable": ["filenode", {"rw_uri": filecap2}],
2761+        newkids = {u"child-imm":        ["filenode", {"rw_uri": filecap1,
2762+                                                      "ro_uri": self._make_readonly(filecap1),
2763+                                                      "metadata": md1, }],
2764+                   u"child-mutable":    ["filenode", {"rw_uri": filecap2,
2765+                                                      "ro_uri": self._make_readonly(filecap2)}],
2766                    u"child-mutable-ro": ["filenode", {"ro_uri": filecap3}],
2767-                   u"dirchild": ["dirnode", {"rw_uri": dircap}],
2768+                   u"unknownchild-rw":  ["unknown",  {"rw_uri": unknown_rwcap,
2769+                                                      "ro_uri": unknown_rocap}],
2770+                   u"unknownchild-ro":  ["unknown",  {"ro_uri": unknown_rocap}],
2771+                   u"unknownchild-imm": ["unknown",  {"ro_uri": unknown_immcap}],
2772+                   u"dirchild":         ["dirnode",  {"rw_uri": dircap,
2773+                                                      "ro_uri": self._make_readonly(dircap)}],
2774                    }
2775-        return newkids, filecap1, filecap2, filecap3, dircap
2776+        return newkids, {'filecap1': filecap1,
2777+                         'filecap2': filecap2,
2778+                         'filecap3': filecap3,
2779+                         'unknown_rwcap': unknown_rwcap,
2780+                         'unknown_rocap': unknown_rocap,
2781+                         'unknown_immcap': unknown_immcap,
2782+                         'dircap': dircap}
2783 
2784     def _create_immutable_children(self):
2785         contents, n, filecap1 = self.makefile(12)
2786@@ -2071,31 +2146,45 @@
2787         tnode = create_chk_filenode("immutable directory contents\n"*10)
2788         dnode = DirectoryNode(tnode, None, None)
2789         assert not dnode.is_mutable()
2790+        unknown_immcap = "imm.lafs://immutable_from_the_future"
2791         immdircap = dnode.get_uri()
2792-        newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1,
2793-                                               "metadata": md1, }],
2794-                   u"dirchild-imm": ["dirnode", {"ro_uri": immdircap}],
2795+        newkids = {u"child-imm":        ["filenode", {"ro_uri": filecap1,
2796+                                                      "metadata": md1, }],
2797+                   u"unknownchild-imm": ["unknown",  {"ro_uri": unknown_immcap}],
2798+                   u"dirchild-imm":     ["dirnode",  {"ro_uri": immdircap}],
2799                    }
2800-        return newkids, filecap1, immdircap
2801+        return newkids, {'filecap1': filecap1,
2802+                         'unknown_immcap': unknown_immcap,
2803+                         'immdircap': immdircap}
2804 
2805     def test_POST_mkdir_no_parentdir_initial_children(self):
2806-        (newkids, filecap1, filecap2, filecap3,
2807-         dircap) = self._create_initial_children()
2808+        (newkids, caps) = self._create_initial_children()
2809         d = self.POST2("/uri?t=mkdir-with-children", simplejson.dumps(newkids))
2810         def _after_mkdir(res):
2811             self.failUnless(res.startswith("URI:DIR"), res)
2812             n = self.s.create_node_from_uri(res)
2813             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2814             d2.addCallback(lambda ign:
2815-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2816+                           self.failUnlessROChildURIIs(n, u"child-imm",
2817+                                                       caps['filecap1']))
2818+            d2.addCallback(lambda ign:
2819+                           self.failUnlessRWChildURIIs(n, u"child-mutable",
2820+                                                       caps['filecap2']))
2821+            d2.addCallback(lambda ign:
2822+                           self.failUnlessROChildURIIs(n, u"child-mutable-ro",
2823+                                                       caps['filecap3']))
2824+            d2.addCallback(lambda ign:
2825+                           self.failUnlessRWChildURIIs(n, u"unknownchild-rw",
2826+                                                       caps['unknown_rwcap']))
2827             d2.addCallback(lambda ign:
2828-                           self.failUnlessChildURIIs(n, u"child-mutable",
2829-                                                     filecap2))
2830+                           self.failUnlessROChildURIIs(n, u"unknownchild-ro",
2831+                                                       caps['unknown_rocap']))
2832             d2.addCallback(lambda ign:
2833-                           self.failUnlessChildURIIs(n, u"child-mutable-ro",
2834-                                                     filecap3))
2835+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2836+                                                       caps['unknown_immcap']))
2837             d2.addCallback(lambda ign:
2838-                           self.failUnlessChildURIIs(n, u"dirchild", dircap))
2839+                           self.failUnlessRWChildURIIs(n, u"dirchild",
2840+                                                       caps['dircap']))
2841             return d2
2842         d.addCallback(_after_mkdir)
2843         return d
2844@@ -2103,8 +2192,7 @@
2845     def test_POST_mkdir_no_parentdir_unexpected_children(self):
2846         # the regular /uri?t=mkdir operation is specified to ignore its body.
2847         # Only t=mkdir-with-children pays attention to it.
2848-        (newkids, filecap1, filecap2, filecap3,
2849-         dircap) = self._create_initial_children()
2850+        (newkids, caps) = self._create_initial_children()
2851         d = self.shouldHTTPError("POST t=mkdir unexpected children",
2852                                  400, "Bad Request",
2853                                  "t=mkdir does not accept children=, "
2854@@ -2121,28 +2209,31 @@
2855         return d
2856 
2857     def test_POST_mkdir_no_parentdir_immutable(self):
2858-        (newkids, filecap1, immdircap) = self._create_immutable_children()
2859+        (newkids, caps) = self._create_immutable_children()
2860         d = self.POST2("/uri?t=mkdir-immutable", simplejson.dumps(newkids))
2861         def _after_mkdir(res):
2862             self.failUnless(res.startswith("URI:DIR"), res)
2863             n = self.s.create_node_from_uri(res)
2864             d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
2865             d2.addCallback(lambda ign:
2866-                           self.failUnlessChildURIIs(n, u"child-imm", filecap1))
2867+                           self.failUnlessROChildURIIs(n, u"child-imm",
2868+                                                          caps['filecap1']))
2869             d2.addCallback(lambda ign:
2870-                           self.failUnlessChildURIIs(n, u"dirchild-imm",
2871-                                                     immdircap))
2872+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
2873+                                                          caps['unknown_immcap']))
2874+            d2.addCallback(lambda ign:
2875+                           self.failUnlessROChildURIIs(n, u"dirchild-imm",
2876+                                                          caps['immdircap']))
2877             return d2
2878         d.addCallback(_after_mkdir)
2879         return d
2880 
2881     def test_POST_mkdir_no_parentdir_immutable_bad(self):
2882-        (newkids, filecap1, filecap2, filecap3,
2883-         dircap) = self._create_initial_children()
2884+        (newkids, caps) = self._create_initial_children()
2885         d = self.shouldFail2(error.Error,
2886                              "test_POST_mkdir_no_parentdir_immutable_bad",
2887                              "400 Bad Request",
2888-                             "a mkdir-immutable operation was given a child that was not itself immutable",
2889+                             "needed to be immutable but was not",
2890                              self.POST2,
2891                              "/uri?t=mkdir-immutable",
2892                              simplejson.dumps(newkids))
2893@@ -2152,7 +2243,12 @@
2894         # Fetch the welcome page.
2895         d = self.GET("/")
2896         def _after_get_welcome_page(res):
2897-            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)
2898+            MKDIR_BUTTON_RE = re.compile(
2899+                '<form action="([^"]*)" method="post".*?'
2900+                '<input type="hidden" name="t" value="([^"]*)" />'
2901+                '<input type="hidden" name="([^"]*)" value="([^"]*)" />'
2902+                '<input type="submit" value="Create a directory" />',
2903+                re.I)
2904             mo = MKDIR_BUTTON_RE.search(res)
2905             formaction = mo.group(1)
2906             formt = mo.group(2)
2907@@ -2250,9 +2346,9 @@
2908 
2909         d = client.getPage(url, method="POST", postdata=reqbody)
2910         def _then(res):
2911-            self.failUnlessURIMatchesChild(newuri9, self._foo_node, u"atomic_added_1")
2912-            self.failUnlessURIMatchesChild(newuri10, self._foo_node, u"atomic_added_2")
2913-            self.failUnlessURIMatchesChild(newuri11, self._foo_node, u"atomic_added_3")
2914+            self.failUnlessURIMatchesROChild(newuri9, self._foo_node, u"atomic_added_1")
2915+            self.failUnlessURIMatchesROChild(newuri10, self._foo_node, u"atomic_added_2")
2916+            self.failUnlessURIMatchesROChild(newuri11, self._foo_node, u"atomic_added_3")
2917 
2918         d.addCallback(_then)
2919         d.addErrback(self.dump_error)
2920@@ -2264,7 +2360,7 @@
2921     def test_POST_put_uri(self):
2922         contents, n, newuri = self.makefile(8)
2923         d = self.POST(self.public_url + "/foo", t="uri", name="new.txt", uri=newuri)
2924-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
2925+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
2926         d.addCallback(lambda res:
2927                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
2928                                                       contents))
2929@@ -2273,7 +2369,7 @@
2930     def test_POST_put_uri_replace(self):
2931         contents, n, newuri = self.makefile(8)
2932         d = self.POST(self.public_url + "/foo", t="uri", name="bar.txt", uri=newuri)
2933-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt")
2934+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt")
2935         d.addCallback(lambda res:
2936                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
2937                                                       contents))
2938@@ -2502,9 +2598,9 @@
2939             d.addCallback(lambda res:
2940                           self.failUnlessEqual(res.strip(), new_uri))
2941             d.addCallback(lambda res:
2942-                          self.failUnlessChildURIIs(self.public_root,
2943-                                                    u"foo",
2944-                                                    new_uri))
2945+                          self.failUnlessRWChildURIIs(self.public_root,
2946+                                                      u"foo",
2947+                                                      new_uri))
2948             return d
2949         d.addCallback(_made_dir)
2950         return d
2951@@ -2521,9 +2617,9 @@
2952                                  self.public_url + "/foo?t=uri&replace=false",
2953                                  new_uri)
2954             d.addCallback(lambda res:
2955-                          self.failUnlessChildURIIs(self.public_root,
2956-                                                    u"foo",
2957-                                                    self._foo_uri))
2958+                          self.failUnlessRWChildURIIs(self.public_root,
2959+                                                      u"foo",
2960+                                                      self._foo_uri))
2961             return d
2962         d.addCallback(_made_dir)
2963         return d
2964@@ -2533,9 +2629,9 @@
2965                                  "400 Bad Request", "PUT to a directory",
2966                                  self.PUT, self.public_url + "/foo?t=BOGUS", "")
2967         d.addCallback(lambda res:
2968-                      self.failUnlessChildURIIs(self.public_root,
2969-                                                u"foo",
2970-                                                self._foo_uri))
2971+                      self.failUnlessRWChildURIIs(self.public_root,
2972+                                                  u"foo",
2973+                                                  self._foo_uri))
2974         return d
2975 
2976     def test_PUT_NEWFILEURL_uri(self):
2977@@ -3062,71 +3158,246 @@
2978         d.addErrback(self.explain_web_error)
2979         return d
2980 
2981-    def test_unknown(self):
2982+    def test_unknown(self, immutable=False):
2983         self.basedir = "web/Grid/unknown"
2984+        if immutable:
2985+            self.basedir = "web/Grid/unknown-immutable"
2986+
2987         self.set_up_grid()
2988         c0 = self.g.clients[0]
2989         self.uris = {}
2990         self.fileurls = {}
2991 
2992-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
2993-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
2994+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
2995+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
2996         # the future cap format may contain slashes, which must be tolerated
2997-        expected_info_url = "uri/%s?t=info" % urllib.quote(future_writecap,
2998+        expected_info_url = "uri/%s?t=info" % urllib.quote(future_write_uri,
2999                                                            safe="")
3000-        future_node = UnknownNode(future_writecap, future_readcap)
3001 
3002-        d = c0.create_dirnode()
3003+        if immutable:
3004+            name = u"future-imm"
3005+            future_node = UnknownNode(None, future_read_uri, deep_immutable=True)
3006+            d = c0.create_immutable_dirnode({name: (future_node, {})})
3007+        else:
3008+            name = u"future"
3009+            future_node = UnknownNode(future_write_uri, future_read_uri)
3010+            d = c0.create_dirnode()
3011+
3012         def _stash_root_and_create_file(n):
3013             self.rootnode = n
3014             self.rooturl = "uri/" + urllib.quote(n.get_uri()) + "/"
3015             self.rourl = "uri/" + urllib.quote(n.get_readonly_uri()) + "/"
3016-            return self.rootnode.set_node(u"future", future_node)
3017+            if not immutable:
3018+                return self.rootnode.set_node(name, future_node)
3019         d.addCallback(_stash_root_and_create_file)
3020+
3021         # make sure directory listing tolerates unknown nodes
3022         d.addCallback(lambda ign: self.GET(self.rooturl))
3023-        def _check_html(res):
3024-            self.failUnlessIn("<td>future</td>", res)
3025-            # find the More Info link for "future", should be relative
3026+        def _check_directory_html(res):
3027+            self.failUnlessIn("<td>%s</td>" % (str(name),), res)
3028+            # find the More Info link for name, should be relative
3029             mo = re.search(r'<a href="([^"]+)">More Info</a>', res)
3030             info_url = mo.group(1)
3031-            self.failUnlessEqual(info_url, "future?t=info")
3032+            self.failUnlessEqual(info_url, "%s?t=info" % (str(name),))
3033+        d.addCallback(_check_directory_html)
3034 
3035-        d.addCallback(_check_html)
3036         d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json"))
3037-        def _check_json(res, expect_writecap):
3038+        def _check_directory_json(res, expect_rw_uri):
3039             data = simplejson.loads(res)
3040             self.failUnlessEqual(data[0], "dirnode")
3041-            f = data[1]["children"]["future"]
3042+            f = data[1]["children"][name]
3043             self.failUnlessEqual(f[0], "unknown")
3044-            if expect_writecap:
3045-                self.failUnlessEqual(f[1]["rw_uri"], future_writecap)
3046+            if expect_rw_uri:
3047+                self.failUnlessEqual(f[1]["rw_uri"], future_write_uri)
3048             else:
3049                 self.failIfIn("rw_uri", f[1])
3050-            self.failUnlessEqual(f[1]["ro_uri"], future_readcap)
3051+            self.failUnlessEqual(f[1]["ro_uri"],
3052+                                 ("imm." if immutable else "ro.") + future_read_uri)
3053             self.failUnless("metadata" in f[1])
3054-        d.addCallback(_check_json, expect_writecap=True)
3055-        d.addCallback(lambda ign: self.GET(expected_info_url))
3056-        def _check_info(res, expect_readcap):
3057+        d.addCallback(_check_directory_json, expect_rw_uri=not immutable)
3058+
3059+        def _check_info(res, expect_rw_uri, expect_ro_uri):
3060             self.failUnlessIn("Object Type: <span>unknown</span>", res)
3061-            self.failUnlessIn(future_writecap, res)
3062-            if expect_readcap:
3063-                self.failUnlessIn(future_readcap, res)
3064+            if expect_rw_uri:
3065+                self.failUnlessIn(future_write_uri, res)
3066+            if expect_ro_uri:
3067+                self.failUnlessIn(future_read_uri, res)
3068+            else:
3069+                self.failIfIn(future_read_uri, res)
3070             self.failIfIn("Raw data as", res)
3071             self.failIfIn("Directory writecap", res)
3072             self.failIfIn("Checker Operations", res)
3073             self.failIfIn("Mutable File Operations", res)
3074             self.failIfIn("Directory Operations", res)
3075-        d.addCallback(_check_info, expect_readcap=False)
3076-        d.addCallback(lambda ign: self.GET(self.rooturl+"future?t=info"))
3077-        d.addCallback(_check_info, expect_readcap=True)
3078+
3079+        # FIXME: these should have expect_rw_uri=not immutable; I don't know
3080+        # why they fail. Possibly related to ticket #922.
3081+
3082+        d.addCallback(lambda ign: self.GET(expected_info_url))
3083+        d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=False)
3084+        d.addCallback(lambda ign: self.GET("%s%s?t=info" % (self.rooturl, str(name))))
3085+        d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=True)
3086+
3087+        def _check_json(res, expect_rw_uri):
3088+            data = simplejson.loads(res)
3089+            self.failUnlessEqual(data[0], "unknown")
3090+            if expect_rw_uri:
3091+                self.failUnlessEqual(data[1]["rw_uri"], future_write_uri)
3092+            else:
3093+                self.failIfIn("rw_uri", data[1])
3094+            self.failUnlessEqual(data[1]["ro_uri"],
3095+                                 ("imm." if immutable else "ro.") + future_read_uri)
3096+            # TODO: check metadata contents
3097+            self.failUnless("metadata" in data[1])
3098+
3099+        d.addCallback(lambda ign: self.GET("%s%s?t=json" % (self.rooturl, str(name))))
3100+        d.addCallback(_check_json, expect_rw_uri=not immutable)
3101 
3102         # and make sure that a read-only version of the directory can be
3103-        # rendered too. This version will not have future_writecap
3104+        # rendered too. This version will not have future_write_uri, whether
3105+        # or not future_node was immutable.
3106         d.addCallback(lambda ign: self.GET(self.rourl))
3107-        d.addCallback(_check_html)
3108+        d.addCallback(_check_directory_html)
3109         d.addCallback(lambda ign: self.GET(self.rourl+"?t=json"))
3110-        d.addCallback(_check_json, expect_writecap=False)
3111+        d.addCallback(_check_directory_json, expect_rw_uri=False)
3112+
3113+        d.addCallback(lambda ign: self.GET("%s%s?t=json" % (self.rourl, str(name))))
3114+        d.addCallback(_check_json, expect_rw_uri=False)
3115+       
3116+        # TODO: check that getting t=info from the Info link in the ro directory
3117+        # works, and does not include the writecap URI.
3118+        return d
3119+
3120+    def test_immutable_unknown(self):
3121+        return self.test_unknown(immutable=True)
3122+
3123+    def test_mutant_dirnodes_are_omitted(self):
3124+        self.basedir = "web/Grid/mutant_dirnodes_are_omitted"
3125+
3126+        self.set_up_grid()
3127+        c = self.g.clients[0]
3128+        nm = c.nodemaker
3129+        self.uris = {}
3130+        self.fileurls = {}
3131+
3132+        lonely_uri = "URI:LIT:n5xgk" # LIT for "one"
3133+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
3134+        mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
3135+       
3136+        # This method tests mainly dirnode, but we'd have to duplicate code in order to
3137+        # test the dirnode and web layers separately.
3138+       
3139+        # 'lonely' is a valid LIT child, 'ro' is a mutant child with an SSK-RO readcap,
3140+        # and 'write-in-ro' is a mutant child with an SSK writecap in the ro_uri field.
3141+        # When the directory is read, the mutants should be silently disposed of, leaving
3142+        # their lonely sibling.
3143+        # We don't test the case of a retrieving a cap from the encrypted rw_uri field,
3144+        # because immutable directories don't have a writecap and therefore that field
3145+        # isn't (and can't be) decrypted.
3146+        # TODO: The field still exists in the netstring. Technically we should check what
3147+        # happens if something is put there (it should be ignored), but that can wait.
3148+
3149+        lonely_child = nm.create_from_cap(lonely_uri)
3150+        mutant_ro_child = nm.create_from_cap(mut_read_uri)
3151+        mutant_write_in_ro_child = nm.create_from_cap(mut_write_uri)
3152+
3153+        def _by_hook_or_by_crook():
3154+            return True
3155+        for n in [mutant_ro_child, mutant_write_in_ro_child]:
3156+            n.is_allowed_in_immutable_directory = _by_hook_or_by_crook
3157+
3158+        mutant_write_in_ro_child.get_write_uri    = lambda: None
3159+        mutant_write_in_ro_child.get_readonly_uri = lambda: mut_write_uri
3160+
3161+        kids = {u"lonely":      (lonely_child, {}),
3162+                u"ro":          (mutant_ro_child, {}),
3163+                u"write-in-ro": (mutant_write_in_ro_child, {}),
3164+                }
3165+        d = c.create_immutable_dirnode(kids)
3166+       
3167+        def _created(dn):
3168+            self.failUnless(isinstance(dn, dirnode.DirectoryNode))
3169+            self.failIf(dn.is_mutable())
3170+            self.failUnless(dn.is_readonly())
3171+            # This checks that if we somehow ended up calling dn._decrypt_rwcapdata, it would fail.
3172+            self.failIf(hasattr(dn._node, 'get_writekey'))
3173+            rep = str(dn)
3174+            self.failUnless("RO-IMM" in rep)
3175+            cap = dn.get_cap()
3176+            self.failUnlessIn("CHK", cap.to_string())
3177+            self.cap = cap
3178+            self.rootnode = dn
3179+            self.rooturl = "uri/" + urllib.quote(dn.get_uri()) + "/"
3180+            return download_to_data(dn._node)
3181+        d.addCallback(_created)
3182+
3183+        def _check_data(data):
3184+            # Decode the netstring representation of the directory to check that all children
3185+            # are present. This is a bit of an abstraction violation, but there's not really
3186+            # any other way to do it given that the real DirectoryNode._unpack_contents would
3187+            # strip the mutant children out (which is what we're trying to test, later).
3188+            position = 0
3189+            numkids = 0
3190+            while position < len(data):
3191+                entries, position = split_netstring(data, 1, position)
3192+                entry = entries[0]
3193+                (name, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
3194+                name = name.decode("utf-8")
3195+                self.failUnless(rwcapdata == "")
3196+                ro_uri = ro_uri.strip()
3197+                if name in kids:
3198+                    self.failIfEqual(ro_uri, "")
3199+                    (expected_child, ign) = kids[name]
3200+                    self.failUnlessEqual(ro_uri, expected_child.get_readonly_uri())
3201+                    numkids += 1
3202+
3203+            self.failUnlessEqual(numkids, 3)
3204+            return self.rootnode.list()
3205+        d.addCallback(_check_data)
3206+       
3207+        # Now when we use the real directory listing code, the mutants should be absent.
3208+        def _check_kids(children):
3209+            self.failUnlessEqual(sorted(children.keys()), [u"lonely"])
3210+            lonely_node, lonely_metadata = children[u"lonely"]
3211+
3212+            self.failUnlessEqual(lonely_node.get_write_uri(), None)
3213+            self.failUnlessEqual(lonely_node.get_readonly_uri(), lonely_uri)
3214+        d.addCallback(_check_kids)
3215+
3216+        d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string()))
3217+        d.addCallback(lambda n: n.list())
3218+        d.addCallback(_check_kids)  # again with dirnode recreated from cap
3219+
3220+        # Make sure the lonely child can be listed in HTML...
3221+        d.addCallback(lambda ign: self.GET(self.rooturl))
3222+        def _check_html(res):
3223+            self.failIfIn("URI:SSK", res)
3224+            get_lonely = "".join([r'<td>FILE</td>',
3225+                                  r'\s+<td>',
3226+                                  r'<a href="[^"]+%s[^"]+">lonely</a>' % (urllib.quote(lonely_uri),),
3227+                                  r'</td>',
3228+                                  r'\s+<td>%d</td>' % len("one"),
3229+                                  ])
3230+            self.failUnless(re.search(get_lonely, res), res)
3231+
3232+            # find the More Info link for name, should be relative
3233+            mo = re.search(r'<a href="([^"]+)">More Info</a>', res)
3234+            info_url = mo.group(1)
3235+            self.failUnless(info_url.endswith(urllib.quote(lonely_uri) + "?t=info"), info_url)
3236+        d.addCallback(_check_html)
3237+
3238+        # ... and in JSON.
3239+        d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json"))
3240+        def _check_json(res):
3241+            data = simplejson.loads(res)
3242+            self.failUnlessEqual(data[0], "dirnode")
3243+            listed_children = data[1]["children"]
3244+            self.failUnlessEqual(sorted(listed_children.keys()), [u"lonely"])
3245+            ll_type, ll_data = listed_children[u"lonely"]
3246+            self.failUnlessEqual(ll_type, "filenode")
3247+            self.failIf("rw_uri" in ll_data)
3248+            self.failUnlessEqual(ll_data["ro_uri"], lonely_uri)
3249+        d.addCallback(_check_json)
3250         return d
3251 
3252     def test_deep_check(self):
3253@@ -3159,10 +3430,10 @@
3254 
3255         # this tests that deep-check and stream-manifest will ignore
3256         # UnknownNode instances. Hopefully this will also cover deep-stats.
3257-        future_writecap = "x-tahoe-crazy://I_am_from_the_future."
3258-        future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future."
3259-        future_node = UnknownNode(future_writecap, future_readcap)
3260-        d.addCallback(lambda ign: self.rootnode.set_node(u"future",future_node))
3261+        future_write_uri = "x-tahoe-crazy://I_am_from_the_future."
3262+        future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future."
3263+        future_node = UnknownNode(future_write_uri, future_read_uri)
3264+        d.addCallback(lambda ign: self.rootnode.set_node(u"future", future_node))
3265 
3266         def _clobber_shares(ignored):
3267             self.delete_shares_numbered(self.uris["sick"], [0,1])
3268diff -rN -u old-tahoe/src/allmydata/unknown.py new-tahoe/src/allmydata/unknown.py
3269--- old-tahoe/src/allmydata/unknown.py  2010-01-25 11:54:56.948000000 +0000
3270+++ new-tahoe/src/allmydata/unknown.py  2010-01-25 11:55:02.278000000 +0000
3271@@ -1,29 +1,183 @@
3272+
3273 from zope.interface import implements
3274 from twisted.internet import defer
3275-from allmydata.interfaces import IFilesystemNode
3276+from allmydata.interfaces import IFilesystemNode, MustNotBeUnknownRWError, \
3277+    MustBeDeepImmutableError
3278+from allmydata import uri
3279+from allmydata.uri import ALLEGED_READONLY_PREFIX, ALLEGED_IMMUTABLE_PREFIX
3280+
3281+
3282+# See ticket #833 for design rationale of UnknownNodes.
3283+
3284+"""Strip prefixes when storing an URI in a ro_uri slot."""
3285+def strip_prefix_for_ro(ro_uri, deep_immutable):
3286+    # It is possible for an alleged-immutable URI to be put into a
3287+    # mutable directory. In that case the ALLEGED_IMMUTABLE_PREFIX
3288+    # should not be stripped. In other cases, the prefix can safely
3289+    # be stripped because it is implied by the context.
3290+
3291+    if ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX):
3292+        if not deep_immutable:
3293+            return ro_uri
3294+        return ro_uri[len(ALLEGED_IMMUTABLE_PREFIX):]
3295+    elif ro_uri.startswith(ALLEGED_READONLY_PREFIX):
3296+        return ro_uri[len(ALLEGED_READONLY_PREFIX):]
3297+    else:
3298+        return ro_uri
3299 
3300 class UnknownNode:
3301     implements(IFilesystemNode)
3302-    def __init__(self, writecap, readcap):
3303-        assert writecap is None or isinstance(writecap, str)
3304-        self.writecap = writecap
3305-        assert readcap is None or isinstance(readcap, str)
3306-        self.readcap = readcap
3307+
3308+    def __init__(self, given_rw_uri, given_ro_uri, deep_immutable=False,
3309+                 name=u"<unknown name>"):
3310+        #import traceback
3311+        #traceback.print_stack()
3312+        #print '%r.__init__(%r, %r, deep_immutable=%r, name=%r)' % (self, given_rw_uri, given_ro_uri, deep_immutable, name)
3313+        assert given_rw_uri is None or isinstance(given_rw_uri, str)
3314+        assert given_ro_uri is None or isinstance(given_ro_uri, str)
3315+
3316+        # We don't raise errors when creating an UnknownNode; we instead create an
3317+        # opaque node (with rw_uri and ro_uri both None) that records the error.
3318+        # This avoids breaking operations that never store the opaque node.
3319+        # Note that this means that if a stored dirnode has only a rw_uri, it
3320+        # might be dropped. Any future "write-only" cap formats should have a dummy
3321+        # unusable readcap to stop that from happening.
3322+
3323+        self.error = None
3324+        self.rw_uri = self.ro_uri = None
3325+        if given_rw_uri is not None:
3326+            if deep_immutable:
3327+                if given_rw_uri.startswith(ALLEGED_IMMUTABLE_PREFIX) and given_ro_uri is None:
3328+                    # We needed an immutable cap, and were given one. It was given in the
3329+                    # rw_uri slot, but that's fine; we'll move it to ro_uri below.
3330+                    pass
3331+                elif given_ro_uri is None:
3332+                    self.error = MustNotBeUnknownRWError("cannot attach unknown rw cap as immutable child",
3333+                                                         name, True)
3334+                    return  # node will be opaque
3335+                else:
3336+                    # We could report either error, but this probably makes more sense.
3337+                    self.error = MustBeDeepImmutableError("cannot attach unknown rw cap as immutable child",
3338+                                                         name)
3339+                    return  # node will be opaque
3340+
3341+            if given_ro_uri is None:
3342+                # We were given a single cap argument, or a rw_uri with no ro_uri.
3343+
3344+                if not (given_rw_uri.startswith(ALLEGED_READONLY_PREFIX)
3345+                        or given_rw_uri.startswith(ALLEGED_IMMUTABLE_PREFIX)):
3346+                    # If the single cap is unprefixed, then we cannot tell whether it is a
3347+                    # writecap, and we don't know how to diminish it to a readcap if it is one.
3348+                    # If it didn't *already* have at least an ALLEGED_READONLY_PREFIX, then
3349+                    # prefixing it would be a bad idea because we have been given no reason
3350+                    # to believe that it is a readcap, so we might be letting a client
3351+                    # inadvertently grant excess write authority.
3352+                    self.error = MustNotBeUnknownRWError("cannot attach unknown rw cap as child",
3353+                                                         name, False)
3354+                    return  # node will be opaque
3355+
3356+                # OTOH, if the single cap already had a prefix (which is of the required
3357+                # strength otherwise an error would have been thrown above), then treat it
3358+                # as though it had been given in the ro_uri slot. This has a similar effect
3359+                # to the use for known caps of 'bigcap = writecap or readcap' in
3360+                # nodemaker.py: create_from_cap. It enables copying of unknown readcaps to
3361+                # work in as many cases as we can securely allow.
3362+                given_ro_uri = given_rw_uri
3363+                given_rw_uri = None
3364+            elif given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX):
3365+                # Strange corner case: we were given a cap in both slots, with the ro_uri
3366+                # alleged to be immutable. A real immutable object wouldn't have a writecap.
3367+                self.error = MustBeDeepImmutableError("cannot accept a child entry that specifies "
3368+                                                      "both rw_uri, and ro_uri with an imm. prefix",
3369+                                                      name)
3370+                return  # node will be opaque
3371+
3372+        # If the ro_uri definitely fails the constraint, it should be treated as opaque and
3373+        # the error recorded.
3374+        if given_ro_uri is not None:
3375+            read_cap = uri.from_string(given_ro_uri, deep_immutable=deep_immutable, name=name)
3376+            if isinstance(read_cap, uri.UnknownURI):
3377+                self.error = read_cap.get_error()
3378+                if self.error:
3379+                    assert self.rw_uri is None and self.ro_uri is None
3380+                    return
3381+
3382+        if deep_immutable:
3383+            assert self.rw_uri is None
3384+            # strengthen the constraint on ro_uri to ALLEGED_IMMUTABLE_PREFIX
3385+            if given_ro_uri is not None:
3386+                if given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX):
3387+                    self.ro_uri = given_ro_uri
3388+                elif given_ro_uri.startswith(ALLEGED_READONLY_PREFIX):
3389+                    self.ro_uri = ALLEGED_IMMUTABLE_PREFIX + given_ro_uri[len(ALLEGED_READONLY_PREFIX):]
3390+                else:
3391+                    self.ro_uri = ALLEGED_IMMUTABLE_PREFIX + given_ro_uri
3392+        else:
3393+            # not immutable, so a writecap is allowed
3394+            self.rw_uri = given_rw_uri
3395+            # strengthen the constraint on ro_uri to ALLEGED_READONLY_PREFIX
3396+            if given_ro_uri is not None:
3397+                if (given_ro_uri.startswith(ALLEGED_READONLY_PREFIX) or
3398+                    given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX)):
3399+                    self.ro_uri = given_ro_uri
3400+                else:
3401+                    self.ro_uri = ALLEGED_READONLY_PREFIX + given_ro_uri
3402+
3403+        #print 'self.(error, rw_uri, ro_uri) = (%r, %r, %r)' % (self.error, self.rw_uri, self.ro_uri)
3404+
3405+    def get_cap(self):
3406+        return uri.UnknownURI(self.rw_uri or self.ro_uri)
3407+
3408+    def get_readcap(self):
3409+        return uri.UnknownURI(self.ro_uri)
3410+
3411+    def is_readonly(self):
3412+        raise AssertionError("an UnknownNode might be either read-only or "
3413+                             "read/write, so we shouldn't be calling is_readonly")
3414+
3415+    def is_mutable(self):
3416+        raise AssertionError("an UnknownNode might be either mutable or immutable, "
3417+                             "so we shouldn't be calling is_mutable")
3418+
3419+    def is_unknown(self):
3420+        return True
3421+
3422+    def is_allowed_in_immutable_directory(self):
3423+        # An UnknownNode consisting only of a ro_uri is allowed in an
3424+        # immutable directory, even though we do not know that it is
3425+        # immutable (or even read-only), provided that no error was detected.
3426+        return not self.error and not self.rw_uri
3427+
3428+    def raise_error(self):
3429+        if self.error is not None:
3430+            raise self.error
3431+
3432     def get_uri(self):
3433-        return self.writecap
3434+        return self.rw_uri or self.ro_uri
3435+
3436+    def get_write_uri(self):
3437+        return self.rw_uri
3438+
3439     def get_readonly_uri(self):
3440-        return self.readcap
3441+        return self.ro_uri
3442+
3443     def get_storage_index(self):
3444         return None
3445+
3446     def get_verify_cap(self):
3447         return None
3448+
3449     def get_repair_cap(self):
3450         return None
3451+
3452     def get_size(self):
3453         return None
3454+
3455     def get_current_size(self):
3456         return defer.succeed(None)
3457+
3458     def check(self, monitor, verify, add_lease):
3459         return defer.succeed(None)
3460+
3461     def check_and_repair(self, monitor, verify, add_lease):
3462         return defer.succeed(None)
3463diff -rN -u old-tahoe/src/allmydata/uri.py new-tahoe/src/allmydata/uri.py
3464--- old-tahoe/src/allmydata/uri.py      2010-01-25 11:54:56.956000000 +0000
3465+++ new-tahoe/src/allmydata/uri.py      2010-01-25 11:55:02.284000000 +0000
3466@@ -5,14 +5,16 @@
3467 from allmydata.storage.server import si_a2b, si_b2a
3468 from allmydata.util import base32, hashutil
3469 from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IImmutableFileURI, \
3470-    IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI
3471+    IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI, \
3472+    MustBeDeepImmutableError, MustBeReadonlyError, CapConstraintError
3473 
3474-class BadURIError(Exception):
3475+class BadURIError(CapConstraintError):
3476     pass
3477 
3478-# the URI shall be an ascii representation of the file. It shall contain
3479-# enough information to retrieve and validate the contents. It shall be
3480-# expressed in a limited character set (namely [TODO]).
3481+# The URI shall be an ASCII representation of a reference to the file/directory.
3482+# It shall contain enough information to retrieve and validate the contents.
3483+# It shall be expressed in a limited character set (currently base32 plus ':' and
3484+# capital letters, but future URIs might use a larger charset).
3485 
3486 BASE32STR_128bits = '(%s{25}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_3bits)
3487 BASE32STR_256bits = '(%s{51}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_1bits)
3488@@ -39,6 +41,10 @@
3489             return self.to_string() != them.to_string()
3490         else:
3491             return True
3492+
3493+    def is_unknown(self):
3494+        return False
3495+
3496     def to_human_encoding(self):
3497         return 'http://127.0.0.1:3456/uri/'+self.to_string()
3498 
3499@@ -97,8 +103,10 @@
3500 
3501     def is_readonly(self):
3502         return True
3503+
3504     def is_mutable(self):
3505         return False
3506+
3507     def get_readonly(self):
3508         return self
3509 
3510@@ -157,6 +165,18 @@
3511                  self.total_shares,
3512                  self.size))
3513 
3514+    def is_readonly(self):
3515+        return True
3516+
3517+    def is_mutable(self):
3518+        return False
3519+
3520+    def get_readonly(self):
3521+        return self
3522+
3523+    def get_verify_cap(self):
3524+        return self
3525+
3526 
3527 class LiteralFileURI(_BaseURI):
3528     implements(IURI, IImmutableFileURI)
3529@@ -297,10 +317,13 @@
3530 
3531     def is_readonly(self):
3532         return True
3533+
3534     def is_mutable(self):
3535         return True
3536+
3537     def get_readonly(self):
3538         return self
3539+
3540     def get_verify_cap(self):
3541         return SSKVerifierURI(self.storage_index, self.fingerprint)
3542 
3543@@ -334,6 +357,15 @@
3544         return 'URI:SSK-Verifier:%s:%s' % (si_b2a(self.storage_index),
3545                                            base32.b2a(self.fingerprint))
3546 
3547+    def is_readonly(self):
3548+        return True
3549+    def is_mutable(self):
3550+        return False
3551+    def get_readonly(self):
3552+        return self
3553+    def get_verify_cap(self):
3554+        return self
3555+
3556 class _DirectoryBaseURI(_BaseURI):
3557     implements(IURI, IDirnodeURI)
3558     def __init__(self, filenode_uri=None):
3559@@ -376,12 +408,12 @@
3560     def abbrev_si(self):
3561         return base32.b2a(self._filenode_uri.storage_index)[:5]
3562 
3563-    def get_filenode_cap(self):
3564-        return self._filenode_uri
3565-
3566     def is_mutable(self):
3567         return True
3568 
3569+    def get_filenode_cap(self):
3570+        return self._filenode_uri
3571+
3572     def get_verify_cap(self):
3573         return DirectoryURIVerifier(self._filenode_uri.get_verify_cap())
3574 
3575@@ -432,12 +464,12 @@
3576             assert isinstance(filenode_uri, self.INNER_URI_CLASS), filenode_uri
3577         _DirectoryBaseURI.__init__(self, filenode_uri)
3578 
3579-    def is_mutable(self):
3580-        return False
3581-
3582     def is_readonly(self):
3583         return True
3584 
3585+    def is_mutable(self):
3586+        return False
3587+
3588     def get_readonly(self):
3589         return self
3590 
3591@@ -460,6 +492,7 @@
3592         # LIT caps have no verifier, since they aren't distributed
3593         return None
3594 
3595+
3596 def wrap_dirnode_cap(filecap):
3597     if isinstance(filecap, WriteableSSKFileURI):
3598         return DirectoryURI(filecap)
3599@@ -469,7 +502,8 @@
3600         return ImmutableDirectoryURI(filecap)
3601     if isinstance(filecap, LiteralFileURI):
3602         return LiteralDirectoryURI(filecap)
3603-    assert False, "cannot wrap a dirnode around %s" % filecap.__class__
3604+    assert False, "cannot interpret as a directory cap: %s" % filecap.__class__
3605+
3606 
3607 class DirectoryURIVerifier(_DirectoryBaseURI):
3608     implements(IVerifierURI)
3609@@ -487,6 +521,10 @@
3610     def get_filenode_cap(self):
3611         return self._filenode_uri
3612 
3613+    def is_mutable(self):
3614+        return False
3615+
3616+
3617 class ImmutableDirectoryURIVerifier(DirectoryURIVerifier):
3618     implements(IVerifierURI)
3619     BASE_STRING='URI:DIR2-CHK-Verifier:'
3620@@ -494,68 +532,141 @@
3621     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK-VERIFIER'+SEP)
3622     INNER_URI_CLASS=CHKFileVerifierURI
3623 
3624+
3625 class UnknownURI:
3626-    def __init__(self, uri):
3627+    def __init__(self, uri, error=None):
3628         self._uri = uri
3629+        self._error = error
3630+
3631     def to_string(self):
3632         return self._uri
3633 
3634-def from_string(s):
3635-    if not isinstance(s, str):
3636-        raise TypeError("unknown URI type: %s.." % str(s)[:100])
3637-    elif s.startswith('URI:CHK:'):
3638-        return CHKFileURI.init_from_string(s)
3639-    elif s.startswith('URI:CHK-Verifier:'):
3640-        return CHKFileVerifierURI.init_from_string(s)
3641-    elif s.startswith('URI:LIT:'):
3642-        return LiteralFileURI.init_from_string(s)
3643-    elif s.startswith('URI:SSK:'):
3644-        return WriteableSSKFileURI.init_from_string(s)
3645-    elif s.startswith('URI:SSK-RO:'):
3646-        return ReadonlySSKFileURI.init_from_string(s)
3647-    elif s.startswith('URI:SSK-Verifier:'):
3648-        return SSKVerifierURI.init_from_string(s)
3649-    elif s.startswith('URI:DIR2:'):
3650-        return DirectoryURI.init_from_string(s)
3651-    elif s.startswith('URI:DIR2-RO:'):
3652-        return ReadonlyDirectoryURI.init_from_string(s)
3653-    elif s.startswith('URI:DIR2-Verifier:'):
3654-        return DirectoryURIVerifier.init_from_string(s)
3655-    elif s.startswith('URI:DIR2-CHK:'):
3656-        return ImmutableDirectoryURI.init_from_string(s)
3657-    elif s.startswith('URI:DIR2-LIT:'):
3658-        return LiteralDirectoryURI.init_from_string(s)
3659-    return UnknownURI(s)
3660+    def get_readonly(self):
3661+        return None
3662+
3663+    def get_error(self):
3664+        return self._error
3665+
3666+
3667+ALLEGED_READONLY_PREFIX = 'ro.'
3668+ALLEGED_IMMUTABLE_PREFIX = 'imm.'
3669+
3670+def from_string(u, deep_immutable=False, name=u"<unknown name>"):
3671+    if not isinstance(u, str):
3672+        raise TypeError("unknown URI type: %s.." % str(u)[:100])
3673+
3674+    # We allow and check ALLEGED_READONLY_PREFIX or ALLEGED_IMMUTABLE_PREFIX
3675+    # on all URIs, even though we would only strictly need to do so for caps of
3676+    # new formats (post Tahoe-LAFS 1.6). URIs that are not consistent with their
3677+    # prefix are treated as unknown. This should be revisited when we add the
3678+    # new cap formats. See <http://allmydata.org/trac/tahoe/ticket/833#comment:31>.
3679+    s = u
3680+    can_be_mutable = can_be_writeable = not deep_immutable
3681+    if s.startswith(ALLEGED_IMMUTABLE_PREFIX):
3682+        can_be_mutable = can_be_writeable = False
3683+        s = s[len(ALLEGED_IMMUTABLE_PREFIX):]
3684+    elif s.startswith(ALLEGED_READONLY_PREFIX):
3685+        can_be_writeable = False
3686+        s = s[len(ALLEGED_READONLY_PREFIX):]
3687+
3688+    #print "can_be_mutable = %r, can_be_writeable = %r, s = %r" % (can_be_mutable, can_be_writeable, s)
3689+    error = None
3690+    kind = "cap"
3691+    try:
3692+        if s.startswith('URI:CHK:'):
3693+            return CHKFileURI.init_from_string(s)
3694+        elif s.startswith('URI:CHK-Verifier:'):
3695+            return CHKFileVerifierURI.init_from_string(s)
3696+        elif s.startswith('URI:LIT:'):
3697+            return LiteralFileURI.init_from_string(s)
3698+        elif s.startswith('URI:SSK:'):
3699+            if can_be_writeable:
3700+                return WriteableSSKFileURI.init_from_string(s)
3701+            kind = "URI:SSK file writecap"
3702+        elif s.startswith('URI:SSK-RO:'):
3703+            if can_be_mutable:
3704+                return ReadonlySSKFileURI.init_from_string(s)
3705+            kind = "URI:SSK-RO readcap to a mutable file"
3706+        elif s.startswith('URI:SSK-Verifier:'):
3707+            return SSKVerifierURI.init_from_string(s)
3708+        elif s.startswith('URI:DIR2:'):
3709+            if can_be_writeable:
3710+                return DirectoryURI.init_from_string(s)
3711+            kind = "URI:DIR2 directory writecap"
3712+        elif s.startswith('URI:DIR2-RO:'):
3713+            if can_be_mutable:
3714+                return ReadonlyDirectoryURI.init_from_string(s)
3715+            kind = "URI:DIR2-RO readcap to a mutable directory"
3716+        elif s.startswith('URI:DIR2-Verifier:'):
3717+            return DirectoryURIVerifier.init_from_string(s)
3718+        elif s.startswith('URI:DIR2-CHK:'):
3719+            return ImmutableDirectoryURI.init_from_string(s)
3720+        elif s.startswith('URI:DIR2-LIT:'):
3721+            return LiteralDirectoryURI.init_from_string(s)
3722+        elif s.startswith('x-tahoe-future-test-writeable:') and not can_be_writeable:
3723+            # For testing how future writeable caps would behave in read-only contexts.
3724+            kind = "x-tahoe-future-test-writeable: testing cap"
3725+        elif s.startswith('x-tahoe-future-test-mutable:') and not can_be_mutable:
3726+            # For testing how future mutable readcaps would behave in immutable contexts.
3727+            kind = "x-tahoe-future-test-mutable: testing cap"
3728+        else:
3729+            return UnknownURI(u)
3730+
3731+        # we fell through because a constraint was not met
3732+        if not can_be_mutable:
3733+            error = MustBeDeepImmutableError(kind + " used in an immutable context", name)
3734+        else:
3735+            error = MustBeReadonlyError(kind + " used in a read-only context", name)
3736+           
3737+    except BadURIError, e:
3738+        error = e
3739+
3740+    #if error: print error
3741+    return UnknownURI(u, error=error)
3742 
3743 def is_uri(s):
3744     try:
3745-        from_string(s)
3746+        from_string(s, deep_immutable=False)
3747         return True
3748     except (TypeError, AssertionError):
3749         return False
3750 
3751-def from_string_dirnode(s):
3752-    u = from_string(s)
3753+def is_literal_file_uri(s):
3754+    if not isinstance(s, str):
3755+        return False
3756+    return (s.startswith('URI:LIT:') or
3757+            s.startswith(ALLEGED_READONLY_PREFIX + 'URI:LIT:') or
3758+            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:'))
3759+
3760+def has_uri_prefix(s):
3761+    if not isinstance(s, str):
3762+        return False
3763+    return (s.startswith("URI:") or
3764+            s.startswith(ALLEGED_READONLY_PREFIX + 'URI:') or
3765+            s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:'))
3766+
3767+def from_string_dirnode(s, **kwargs):
3768+    u = from_string(s, **kwargs)
3769     assert IDirnodeURI.providedBy(u)
3770     return u
3771 
3772 registerAdapter(from_string_dirnode, str, IDirnodeURI)
3773 
3774-def from_string_filenode(s):
3775-    u = from_string(s)
3776+def from_string_filenode(s, **kwargs):
3777+    u = from_string(s, **kwargs)
3778     assert IFileURI.providedBy(u)
3779     return u
3780 
3781 registerAdapter(from_string_filenode, str, IFileURI)
3782 
3783-def from_string_mutable_filenode(s):
3784-    u = from_string(s)
3785+def from_string_mutable_filenode(s, **kwargs):
3786+    u = from_string(s, **kwargs)
3787     assert IMutableFileURI.providedBy(u)
3788     return u
3789 registerAdapter(from_string_mutable_filenode, str, IMutableFileURI)
3790 
3791-def from_string_verifier(s):
3792-    u = from_string(s)
3793+def from_string_verifier(s, **kwargs):
3794+    u = from_string(s, **kwargs)
3795     assert IVerifierURI.providedBy(u)
3796     return u
3797 registerAdapter(from_string_verifier, str, IVerifierURI)
3798diff -rN -u old-tahoe/src/allmydata/web/common.py new-tahoe/src/allmydata/web/common.py
3799--- old-tahoe/src/allmydata/web/common.py       2010-01-25 11:54:57.339000000 +0000
3800+++ new-tahoe/src/allmydata/web/common.py       2010-01-25 11:55:02.584000000 +0000
3801@@ -8,7 +8,8 @@
3802 from nevow.util import resource_filename
3803 from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
3804      FileTooLargeError, NotEnoughSharesError, NoSharesError, \
3805-     NotDeepImmutableError, EmptyPathnameComponentError
3806+     EmptyPathnameComponentError, MustBeDeepImmutableError, \
3807+     MustBeReadonlyError, MustNotBeUnknownRWError
3808 from allmydata.mutable.common import UnrecoverableFileError
3809 from allmydata.util import abbreviate # TODO: consolidate
3810 
3811@@ -181,9 +182,42 @@
3812              "failure, or disk corruption. You should perform a filecheck on "
3813              "this object to learn more.")
3814         return (t, http.GONE)
3815-    if f.check(NotDeepImmutableError):
3816-        t = ("NotDeepImmutableError: a mkdir-immutable operation was given "
3817-             "a child that was not itself immutable: %s" % (f.value,))
3818+    if f.check(MustNotBeUnknownRWError):
3819+        name = f.value.args[1]
3820+        immutable = f.value.args[2]
3821+        if immutable:
3822+            t = ("MustNotBeUnknownRWError: an operation to add a child named "
3823+                 "'%s' to a directory was given an unknown cap in a write slot.\n"
3824+                 "If the cap is actually an immutable readcap, then using a "
3825+                 "webapi server that supports a later version of Tahoe may help.\n\n"
3826+                 "If you are using the webapi directly, then specifying an immutable "
3827+                 "readcap in the read slot (ro_uri) of the JSON PROPDICT, and "
3828+                 "omitting the write slot (rw_uri), would also work in this "
3829+                 "case.") % name.encode("utf-8")
3830+        else:
3831+            t = ("MustNotBeUnknownRWError: an operation to add a child named "
3832+                 "'%s' to a directory was given an unknown cap in a write slot.\n"
3833+                 "Using a webapi server that supports a later version of Tahoe "
3834+                 "may help.\n\n"
3835+                 "If you are using the webapi directly, specifying a readcap in "
3836+                 "the read slot (ro_uri) of the JSON PROPDICT, as well as a "
3837+                 "writecap in the write slot if desired, would also work in this "
3838+                 "case.") % name.encode("utf-8")
3839+        return (t, http.BAD_REQUEST)
3840+    if f.check(MustBeDeepImmutableError):
3841+        name = f.value.args[1]
3842+        t = ("MustBeDeepImmutableError: a cap passed to this operation for "
3843+             "the child named '%s', needed to be immutable but was not. Either "
3844+             "the cap is being added to an immutable directory, or it was "
3845+             "originally retrieved from an immutable directory as an unknown "
3846+             "cap." % name.encode("utf-8"))
3847+        return (t, http.BAD_REQUEST)
3848+    if f.check(MustBeReadonlyError):
3849+        name = f.value.args[1]
3850+        t = ("MustBeReadonlyError: a cap passed to this operation for "
3851+             "the child named '%s', needed to be read-only but was not. "
3852+             "The cap is being passed in a read slot (ro_uri), or was retrieved "
3853+             "from a read slot as an unknown cap." % name.encode("utf-8"))
3854         return (t, http.BAD_REQUEST)
3855     if f.check(WebError):
3856         return (f.value.text, f.value.code)
3857diff -rN -u old-tahoe/src/allmydata/web/directory.py new-tahoe/src/allmydata/web/directory.py
3858--- old-tahoe/src/allmydata/web/directory.py    2010-01-25 11:54:57.375000000 +0000
3859+++ new-tahoe/src/allmydata/web/directory.py    2010-01-25 11:55:02.619000000 +0000
3860@@ -352,7 +352,12 @@
3861         charset = get_arg(req, "_charset", "utf-8")
3862         name = name.decode(charset)
3863         replace = boolean_of_arg(get_arg(req, "replace", "true"))
3864-        d = self.node.set_uri(name, childcap, childcap, overwrite=replace)
3865+       
3866+        # We mustn't pass childcap for the readcap argument because we don't
3867+        # know whether it is a read cap. Passing a read cap as the writecap
3868+        # argument will work (it ends up calling NodeMaker.create_from_cap,
3869+        # which derives a readcap if necessary and possible).
3870+        d = self.node.set_uri(name, childcap, None, overwrite=replace)
3871         d.addCallback(lambda res: childcap)
3872         return d
3873 
3874@@ -363,9 +368,9 @@
3875             # won't show up in the resulting encoded form.. the 'name'
3876             # field is completely missing. So to allow deletion of an
3877             # empty file, we have to pretend that None means ''. The only
3878-            # downide of this is a slightly confusing error message if
3879+            # downside of this is a slightly confusing error message if
3880             # someone does a POST without a name= field. For our own HTML
3881-            # thisn't a big deal, because we create the 'delete' POST
3882+            # this isn't a big deal, because we create the 'delete' POST
3883             # buttons ourselves.
3884             name = ''
3885         charset = get_arg(req, "_charset", "utf-8")
3886@@ -585,7 +590,11 @@
3887     def render_title(self, ctx, data):
3888         si_s = abbreviated_dirnode(self.node)
3889         header = ["Tahoe-LAFS - Directory SI=%s" % si_s]
3890-        if self.node.is_readonly():
3891+        if self.node.is_unknown():
3892+            header.append(" (unknown)")
3893+        elif not self.node.is_mutable():
3894+            header.append(" (immutable)")
3895+        elif self.node.is_readonly():
3896             header.append(" (read-only)")
3897         else:
3898             header.append(" (modifiable)")
3899@@ -594,7 +603,11 @@
3900     def render_header(self, ctx, data):
3901         si_s = abbreviated_dirnode(self.node)
3902         header = ["Tahoe-LAFS Directory SI=", T.span(class_="data-chars")[si_s]]
3903-        if self.node.is_readonly():
3904+        if self.node.is_unknown():
3905+            header.append(" (unknown)")
3906+        elif not self.node.is_mutable():
3907+            header.append(" (immutable)")
3908+        elif self.node.is_readonly():
3909             header.append(" (read-only)")
3910         return ctx.tag[header]
3911 
3912@@ -603,7 +616,7 @@
3913         return T.div[T.a(href=link)["Return to Welcome page"]]
3914 
3915     def render_show_readonly(self, ctx, data):
3916-        if self.node.is_readonly():
3917+        if self.node.is_unknown() or self.node.is_readonly():
3918             return ""
3919         rocap = self.node.get_readonly_uri()
3920         root = get_root(ctx)
3921@@ -630,7 +643,7 @@
3922 
3923         root = get_root(ctx)
3924         here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
3925-        if self.node.is_readonly():
3926+        if self.node.is_unknown() or self.node.is_readonly():
3927             delete = "-"
3928             rename = "-"
3929         else:
3930@@ -678,8 +691,8 @@
3931         ctx.fillSlots("times", times)
3932 
3933         assert IFilesystemNode.providedBy(target), target
3934-        writecap = target.get_uri() or ""
3935-        quoted_uri = urllib.quote(writecap, safe="") # escape slashes too
3936+        target_uri = target.get_uri() or ""
3937+        quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too
3938 
3939         if IMutableFileNode.providedBy(target):
3940             # to prevent javascript in displayed .html files from stealing a
3941@@ -708,7 +721,7 @@
3942 
3943         elif IDirectoryNode.providedBy(target):
3944             # directory
3945-            uri_link = "%s/uri/%s/" % (root, urllib.quote(writecap))
3946+            uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri))
3947             ctx.fillSlots("filename",
3948                           T.a(href=uri_link)[html.escape(name)])
3949             if not target.is_mutable():
3950@@ -795,35 +808,30 @@
3951         kids = {}
3952         for name, (childnode, metadata) in children.iteritems():
3953             assert IFilesystemNode.providedBy(childnode), childnode
3954-            rw_uri = childnode.get_uri()
3955+            rw_uri = childnode.get_write_uri()
3956             ro_uri = childnode.get_readonly_uri()
3957             if IFileNode.providedBy(childnode):
3958-                if childnode.is_readonly():
3959-                    rw_uri = None
3960                 kiddata = ("filenode", {'size': childnode.get_size(),
3961                                         'mutable': childnode.is_mutable(),
3962                                         })
3963             elif IDirectoryNode.providedBy(childnode):
3964-                if childnode.is_readonly():
3965-                    rw_uri = None
3966                 kiddata = ("dirnode", {'mutable': childnode.is_mutable()})
3967             else:
3968                 kiddata = ("unknown", {})
3969+
3970             kiddata[1]["metadata"] = metadata
3971-            if ro_uri:
3972-                kiddata[1]["ro_uri"] = ro_uri
3973             if rw_uri:
3974                 kiddata[1]["rw_uri"] = rw_uri
3975+            if ro_uri:
3976+                kiddata[1]["ro_uri"] = ro_uri
3977             verifycap = childnode.get_verify_cap()
3978             if verifycap:
3979                 kiddata[1]['verify_uri'] = verifycap.to_string()
3980+
3981             kids[name] = kiddata
3982-        if dirnode.is_readonly():
3983-            drw_uri = None
3984-            dro_uri = dirnode.get_uri()
3985-        else:
3986-            drw_uri = dirnode.get_uri()
3987-            dro_uri = dirnode.get_readonly_uri()
3988+
3989+        drw_uri = dirnode.get_write_uri()
3990+        dro_uri = dirnode.get_readonly_uri()
3991         contents = { 'children': kids }
3992         if dro_uri:
3993             contents['ro_uri'] = dro_uri
3994@@ -834,13 +842,14 @@
3995             contents['verify_uri'] = verifycap.to_string()
3996         contents['mutable'] = dirnode.is_mutable()
3997         data = ("dirnode", contents)
3998-        return simplejson.dumps(data, indent=1) + "\n"
3999+        json = simplejson.dumps(data, indent=1) + "\n"
4000+        #print json
4001+        return json
4002     d.addCallback(_got)
4003     d.addCallback(text_plain, ctx)
4004     return d
4005 
4006 
4007-
4008 def DirectoryURI(ctx, dirnode):
4009     return text_plain(dirnode.get_uri(), ctx)
4010 
4011@@ -1132,18 +1141,39 @@
4012         self.req.write(j+"\n")
4013         return ""
4014 
4015-class UnknownNodeHandler(RenderMixin, rend.Page):
4016 
4017+class UnknownNodeHandler(RenderMixin, rend.Page):
4018     def __init__(self, client, node, parentnode=None, name=None):
4019         rend.Page.__init__(self)
4020         assert node
4021         self.node = node
4022+        self.parentnode = parentnode
4023+        self.name = name
4024 
4025     def render_GET(self, ctx):
4026         req = IRequest(ctx)
4027         t = get_arg(req, "t", "").strip()
4028         if t == "info":
4029             return MoreInfo(self.node)
4030-        raise WebError("GET unknown URI type: can only do t=info, not t=%s" % t)
4031-
4032-
4033+        if t == "json":
4034+            if self.parentnode and self.name:
4035+                d = self.parentnode.get_metadata_for(self.name)
4036+            else:
4037+                d = defer.succeed(None)
4038+            d.addCallback(lambda md: UnknownJSONMetadata(ctx, self.node, md))
4039+            return d
4040+        raise WebError("GET unknown URI type: can only do t=info and t=json, not t=%s.\n"
4041+                       "Using a webapi server that supports a later version of Tahoe "
4042+                       "may help." % t)
4043+
4044+def UnknownJSONMetadata(ctx, filenode, edge_metadata):
4045+    rw_uri = filenode.get_write_uri()
4046+    ro_uri = filenode.get_readonly_uri()
4047+    data = ("unknown", {})
4048+    if ro_uri:
4049+        data[1]['ro_uri'] = ro_uri
4050+    if rw_uri:
4051+        data[1]['rw_uri'] = rw_uri
4052+    if edge_metadata is not None:
4053+        data[1]['metadata'] = edge_metadata
4054+    return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx)
4055diff -rN -u old-tahoe/src/allmydata/web/filenode.py new-tahoe/src/allmydata/web/filenode.py
4056--- old-tahoe/src/allmydata/web/filenode.py     2010-01-25 11:54:57.405000000 +0000
4057+++ new-tahoe/src/allmydata/web/filenode.py     2010-01-25 11:55:02.779000000 +0000
4058@@ -6,10 +6,9 @@
4059 from nevow import url, rend
4060 from nevow.inevow import IRequest
4061 
4062-from allmydata.interfaces import ExistingChildError, CannotPackUnknownNodeError
4063+from allmydata.interfaces import ExistingChildError
4064 from allmydata.monitor import Monitor
4065 from allmydata.immutable.upload import FileHandle
4066-from allmydata.unknown import UnknownNode
4067 from allmydata.util import log, base32
4068 
4069 from allmydata.web.common import text_plain, WebError, RenderMixin, \
4070@@ -20,7 +19,6 @@
4071 from allmydata.web.info import MoreInfo
4072 
4073 class ReplaceMeMixin:
4074-
4075     def replace_me_with_a_child(self, req, client, replace):
4076         # a new file is being uploaded in our place.
4077         mutable = boolean_of_arg(get_arg(req, "mutable", "false"))
4078@@ -55,14 +53,7 @@
4079     def replace_me_with_a_childcap(self, req, client, replace):
4080         req.content.seek(0)
4081         childcap = req.content.read()
4082-        childnode = client.create_node_from_uri(childcap, childcap+"readonly")
4083-        if isinstance(childnode, UnknownNode):
4084-            # don't be willing to pack unknown nodes: we might accidentally
4085-            # put some write-authority into the rocap slot because we don't
4086-            # know how to diminish the URI they gave us. We don't even know
4087-            # if they gave us a readcap or a writecap.
4088-            msg = "cannot attach unknown node as child %s" % str(self.name)
4089-            raise CannotPackUnknownNodeError(msg)
4090+        childnode = client.create_node_from_uri(childcap, None, name=self.name)
4091         d = self.parentnode.set_node(self.name, childnode, overwrite=replace)
4092         d.addCallback(lambda res: childnode.get_uri())
4093         return d
4094@@ -426,12 +417,8 @@
4095 
4096 
4097 def FileJSONMetadata(ctx, filenode, edge_metadata):
4098-    if filenode.is_readonly():
4099-        rw_uri = None
4100-        ro_uri = filenode.get_uri()
4101-    else:
4102-        rw_uri = filenode.get_uri()
4103-        ro_uri = filenode.get_readonly_uri()
4104+    rw_uri = filenode.get_write_uri()
4105+    ro_uri = filenode.get_readonly_uri()
4106     data = ("filenode", {})
4107     data[1]['size'] = filenode.get_size()
4108     if ro_uri:
4109diff -rN -u old-tahoe/src/allmydata/web/info.py new-tahoe/src/allmydata/web/info.py
4110--- old-tahoe/src/allmydata/web/info.py 2010-01-25 11:54:57.421000000 +0000
4111+++ new-tahoe/src/allmydata/web/info.py 2010-01-25 11:55:02.793000000 +0000
4112@@ -21,6 +21,8 @@
4113     def get_type(self):
4114         node = self.original
4115         if IDirectoryNode.providedBy(node):
4116+            if not node.is_mutable():
4117+                return "immutable directory"
4118             return "directory"
4119         if IFileNode.providedBy(node):
4120             si = node.get_storage_index()
4121@@ -28,7 +30,7 @@
4122                 if node.is_mutable():
4123                     return "mutable file"
4124                 return "immutable file"
4125-            return "LIT file"
4126+            return "immutable LIT file"
4127         return "unknown"
4128 
4129     def render_title(self, ctx, data):
4130@@ -68,10 +70,10 @@
4131 
4132     def render_directory_writecap(self, ctx, data):
4133         node = self.original
4134-        if node.is_readonly():
4135-            return ""
4136         if not IDirectoryNode.providedBy(node):
4137             return ""
4138+        if node.is_readonly():
4139+            return ""
4140         return ctx.tag[node.get_uri()]
4141 
4142     def render_directory_readcap(self, ctx, data):
4143@@ -86,27 +88,24 @@
4144             return ""
4145         return ctx.tag[node.get_verify_cap().to_string()]
4146 
4147-
4148     def render_file_writecap(self, ctx, data):
4149         node = self.original
4150         if IDirectoryNode.providedBy(node):
4151             node = node._node
4152-        if ((IDirectoryNode.providedBy(node) or IFileNode.providedBy(node))
4153-            and node.is_readonly()):
4154-            return ""
4155-        writecap = node.get_uri()
4156-        if not writecap:
4157+        write_uri = node.get_write_uri()
4158+        #print "write_uri = %r, node = %r" % (write_uri, node)
4159+        if not write_uri:
4160             return ""
4161-        return ctx.tag[writecap]
4162+        return ctx.tag[write_uri]
4163 
4164     def render_file_readcap(self, ctx, data):
4165         node = self.original
4166         if IDirectoryNode.providedBy(node):
4167             node = node._node
4168-        readcap = node.get_readonly_uri()
4169-        if not readcap:
4170+        read_uri = node.get_readonly_uri()
4171+        if not read_uri:
4172             return ""
4173-        return ctx.tag[readcap]
4174+        return ctx.tag[read_uri]
4175 
4176     def render_file_verifycap(self, ctx, data):
4177         node = self.original
4178diff -rN -u old-tahoe/src/allmydata/web/root.py new-tahoe/src/allmydata/web/root.py
4179--- old-tahoe/src/allmydata/web/root.py 2010-01-25 11:54:57.564000000 +0000
4180+++ new-tahoe/src/allmydata/web/root.py 2010-01-25 11:55:03.004000000 +0000
4181@@ -12,7 +12,7 @@
4182 from allmydata import get_package_versions_string
4183 from allmydata import provisioning
4184 from allmydata.util import idlib, log
4185-from allmydata.interfaces import IFileNode, UnhandledCapTypeError
4186+from allmydata.interfaces import IFileNode
4187 from allmydata.web import filenode, directory, unlinked, status, operations
4188 from allmydata.web import reliability, storage
4189 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
4190@@ -85,7 +85,7 @@
4191         try:
4192             node = self.client.create_node_from_uri(name)
4193             return directory.make_handler_for(node, self.client)
4194-        except (TypeError, UnhandledCapTypeError, AssertionError):
4195+        except (TypeError, AssertionError):
4196             raise WebError("'%s' is not a valid file- or directory- cap"
4197                            % name)
4198 
4199@@ -104,7 +104,7 @@
4200         # 'name' must be a file URI
4201         try:
4202             node = self.client.create_node_from_uri(name)
4203-        except (TypeError, UnhandledCapTypeError, AssertionError):
4204+        except (TypeError, AssertionError):
4205             # I think this can no longer be reached
4206             raise WebError("'%s' is not a valid file- or directory- cap"
4207                            % name)