]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_sftp.py
SFTP: further small improvements to test coverage. Also ensure that after a test...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_sftp.py
1
2 import re, struct, traceback, time, calendar
3 from stat import S_IFREG, S_IFDIR
4
5 from twisted.trial import unittest
6 from twisted.internet import defer, reactor
7 from twisted.python.failure import Failure
8 from twisted.internet.error import ProcessDone, ProcessTerminated
9
10 conch_interfaces = None
11 sftp = None
12 sftpd = None
13 have_pycrypto = False
14 try:
15     from Crypto import Util
16     Util  # hush pyflakes
17     have_pycrypto = True
18 except ImportError:
19     pass
20
21 if have_pycrypto:
22     from twisted.conch import interfaces as conch_interfaces
23     from twisted.conch.ssh import filetransfer as sftp
24     from allmydata.frontends import sftpd
25
26 from allmydata.interfaces import IDirectoryNode, ExistingChildError, NoSuchChildError
27 from allmydata.mutable.common import NotWriteableError
28
29 from allmydata.util.consumer import download_to_data
30 from allmydata.immutable import upload
31 from allmydata.test.no_network import GridTestMixin
32 from allmydata.test.common import ShouldFailMixin
33 from allmydata.test.common_util import ReallyEqualMixin
34
35 timeout = 240
36
37 class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCase):
38     """This is a no-network unit test of the SFTPUserHandler and the abstractions it uses."""
39
40     if not have_pycrypto:
41         skip = "SFTP support requires pycrypto, which is not installed"
42
43     def shouldFailWithSFTPError(self, expected_code, which, callable, *args, **kwargs):
44         assert isinstance(expected_code, int), repr(expected_code)
45         assert isinstance(which, str), repr(which)
46         s = traceback.format_stack()
47         d = defer.maybeDeferred(callable, *args, **kwargs)
48         def _done(res):
49             if isinstance(res, Failure):
50                 res.trap(sftp.SFTPError)
51                 self.failUnlessReallyEqual(res.value.code, expected_code,
52                                            "%s was supposed to raise SFTPError(%r), not SFTPError(%r): %s" %
53                                            (which, expected_code, res.value.code, res))
54             else:
55                 print '@' + '@'.join(s)
56                 self.fail("%s was supposed to raise SFTPError(%r), not get %r" %
57                           (which, expected_code, res))
58         d.addBoth(_done)
59         return d
60
61     def failUnlessReallyEqual(self, a, b, msg=None):
62         self.failUnlessEqual(a, b, msg=msg)
63         self.failUnlessEqual(type(a), type(b), msg=msg)
64
65     def _set_up(self, basedir, num_clients=1, num_servers=10):
66         self.basedir = "sftp/" + basedir
67         self.set_up_grid(num_clients=num_clients, num_servers=num_servers)
68
69         self.client = self.g.clients[0]
70         self.username = "alice"
71
72         d = self.client.create_dirnode()
73         def _created_root(node):
74             self.root = node
75             self.root_uri = node.get_uri()
76             sftpd._reload()
77             self.handler = sftpd.SFTPUserHandler(self.client, self.root, self.username)
78         d.addCallback(_created_root)
79         return d
80
81     def _set_up_tree(self):
82         d = self.client.create_mutable_file("mutable file contents")
83         d.addCallback(lambda node: self.root.set_node(u"mutable", node))
84         def _created_mutable(n):
85             self.mutable = n
86             self.mutable_uri = n.get_uri()
87         d.addCallback(_created_mutable)
88
89         d.addCallback(lambda ign:
90                       self.root._create_and_validate_node(None, self.mutable.get_readonly_uri(), name=u"readonly"))
91         d.addCallback(lambda node: self.root.set_node(u"readonly", node))
92         def _created_readonly(n):
93             self.readonly = n
94             self.readonly_uri = n.get_uri()
95         d.addCallback(_created_readonly)
96
97         gross = upload.Data("0123456789" * 101, None)
98         d.addCallback(lambda ign: self.root.add_file(u"gro\u00DF", gross))
99         def _created_gross(n):
100             self.gross = n
101             self.gross_uri = n.get_uri()
102         d.addCallback(_created_gross)
103
104         small = upload.Data("0123456789", None)
105         d.addCallback(lambda ign: self.root.add_file(u"small", small))
106         def _created_small(n):
107             self.small = n
108             self.small_uri = n.get_uri()
109         d.addCallback(_created_small)
110
111         small2 = upload.Data("Small enough for a LIT too", None)
112         d.addCallback(lambda ign: self.root.add_file(u"small2", small2))
113         def _created_small2(n):
114             self.small2 = n
115             self.small2_uri = n.get_uri()
116         d.addCallback(_created_small2)
117
118         empty_litdir_uri = "URI:DIR2-LIT:"
119
120         # contains one child which is itself also LIT:
121         tiny_litdir_uri = "URI:DIR2-LIT:gqytunj2onug64tufqzdcosvkjetutcjkq5gw4tvm5vwszdgnz5hgyzufqydulbshj5x2lbm"
122
123         unknown_uri = "x-tahoe-crazy://I_am_from_the_future."
124
125         d.addCallback(lambda ign: self.root._create_and_validate_node(None, empty_litdir_uri, name=u"empty_lit_dir"))
126         def _created_empty_lit_dir(n):
127             self.empty_lit_dir = n
128             self.empty_lit_dir_uri = n.get_uri()
129             self.root.set_node(u"empty_lit_dir", n)
130         d.addCallback(_created_empty_lit_dir)
131
132         d.addCallback(lambda ign: self.root._create_and_validate_node(None, tiny_litdir_uri, name=u"tiny_lit_dir"))
133         def _created_tiny_lit_dir(n):
134             self.tiny_lit_dir = n
135             self.tiny_lit_dir_uri = n.get_uri()
136             self.root.set_node(u"tiny_lit_dir", n)
137         d.addCallback(_created_tiny_lit_dir)
138
139         d.addCallback(lambda ign: self.root._create_and_validate_node(None, unknown_uri, name=u"unknown"))
140         def _created_unknown(n):
141             self.unknown = n
142             self.unknown_uri = n.get_uri()
143             self.root.set_node(u"unknown", n)
144         d.addCallback(_created_unknown)
145
146         fall_of_the_Berlin_wall = calendar.timegm(time.strptime("1989-11-09 20:00:00 UTC", "%Y-%m-%d %H:%M:%S %Z"))
147         md = {'mtime': fall_of_the_Berlin_wall, 'tahoe': {'linkmotime': fall_of_the_Berlin_wall}}
148         d.addCallback(lambda ign: self.root.set_node(u"loop", self.root, metadata=md))
149         return d
150
151     def test_basic(self):
152         d = self._set_up("basic")
153         def _check(ign):
154             # Test operations that have no side-effects, and don't need the tree.
155
156             version = self.handler.gotVersion(3, {})
157             self.failUnless(isinstance(version, dict))
158
159             self.failUnlessReallyEqual(self.handler._path_from_string(""), [])
160             self.failUnlessReallyEqual(self.handler._path_from_string("/"), [])
161             self.failUnlessReallyEqual(self.handler._path_from_string("."), [])
162             self.failUnlessReallyEqual(self.handler._path_from_string("//"), [])
163             self.failUnlessReallyEqual(self.handler._path_from_string("/."), [])
164             self.failUnlessReallyEqual(self.handler._path_from_string("/./"), [])
165             self.failUnlessReallyEqual(self.handler._path_from_string("foo"), [u"foo"])
166             self.failUnlessReallyEqual(self.handler._path_from_string("/foo"), [u"foo"])
167             self.failUnlessReallyEqual(self.handler._path_from_string("foo/"), [u"foo"])
168             self.failUnlessReallyEqual(self.handler._path_from_string("/foo/"), [u"foo"])
169             self.failUnlessReallyEqual(self.handler._path_from_string("foo/bar"), [u"foo", u"bar"])
170             self.failUnlessReallyEqual(self.handler._path_from_string("/foo/bar"), [u"foo", u"bar"])
171             self.failUnlessReallyEqual(self.handler._path_from_string("foo/bar//"), [u"foo", u"bar"])
172             self.failUnlessReallyEqual(self.handler._path_from_string("/foo/bar//"), [u"foo", u"bar"])
173             self.failUnlessReallyEqual(self.handler._path_from_string("foo/./bar"), [u"foo", u"bar"])
174             self.failUnlessReallyEqual(self.handler._path_from_string("./foo/./bar"), [u"foo", u"bar"])
175             self.failUnlessReallyEqual(self.handler._path_from_string("foo/../bar"), [u"bar"])
176             self.failUnlessReallyEqual(self.handler._path_from_string("/foo/../bar"), [u"bar"])
177             self.failUnlessReallyEqual(self.handler._path_from_string("../bar"), [u"bar"])
178             self.failUnlessReallyEqual(self.handler._path_from_string("/../bar"), [u"bar"])
179
180             self.failUnlessReallyEqual(self.handler.realPath(""), "/")
181             self.failUnlessReallyEqual(self.handler.realPath("/"), "/")
182             self.failUnlessReallyEqual(self.handler.realPath("."), "/")
183             self.failUnlessReallyEqual(self.handler.realPath("//"), "/")
184             self.failUnlessReallyEqual(self.handler.realPath("/."), "/")
185             self.failUnlessReallyEqual(self.handler.realPath("/./"), "/")
186             self.failUnlessReallyEqual(self.handler.realPath("foo"), "/foo")
187             self.failUnlessReallyEqual(self.handler.realPath("/foo"), "/foo")
188             self.failUnlessReallyEqual(self.handler.realPath("foo/"), "/foo")
189             self.failUnlessReallyEqual(self.handler.realPath("/foo/"), "/foo")
190             self.failUnlessReallyEqual(self.handler.realPath("foo/bar"), "/foo/bar")
191             self.failUnlessReallyEqual(self.handler.realPath("/foo/bar"), "/foo/bar")
192             self.failUnlessReallyEqual(self.handler.realPath("foo/bar//"), "/foo/bar")
193             self.failUnlessReallyEqual(self.handler.realPath("/foo/bar//"), "/foo/bar")
194             self.failUnlessReallyEqual(self.handler.realPath("foo/./bar"), "/foo/bar")
195             self.failUnlessReallyEqual(self.handler.realPath("./foo/./bar"), "/foo/bar")
196             self.failUnlessReallyEqual(self.handler.realPath("foo/../bar"), "/bar")
197             self.failUnlessReallyEqual(self.handler.realPath("/foo/../bar"), "/bar")
198             self.failUnlessReallyEqual(self.handler.realPath("../bar"), "/bar")
199             self.failUnlessReallyEqual(self.handler.realPath("/../bar"), "/bar")
200         d.addCallback(_check)
201
202         d.addCallback(lambda ign:
203             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "_path_from_string invalid UTF-8",
204                                          self.handler._path_from_string, "\xFF"))
205         d.addCallback(lambda ign:
206             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "realPath invalid UTF-8",
207                                          self.handler.realPath, "\xFF"))
208
209         return d
210
211     def test_convert_error(self):
212         self.failUnlessReallyEqual(sftpd._convert_error(None, "request"), None)
213         
214         d = defer.succeed(None)
215         d.addCallback(lambda ign:
216             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_convert_error SFTPError",
217                                          sftpd._convert_error, Failure(sftp.SFTPError(sftp.FX_FAILURE, "foo")), "request"))
218         d.addCallback(lambda ign:
219             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "_convert_error NoSuchChildError",
220                                          sftpd._convert_error, Failure(NoSuchChildError("foo")), "request"))
221         d.addCallback(lambda ign:
222             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_convert_error ExistingChildError",
223                                          sftpd._convert_error, Failure(ExistingChildError("foo")), "request"))
224         d.addCallback(lambda ign:
225             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "_convert_error NotWriteableError",
226                                          sftpd._convert_error, Failure(NotWriteableError("foo")), "request"))
227         d.addCallback(lambda ign:
228             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "_convert_error NotImplementedError",
229                                          sftpd._convert_error, Failure(NotImplementedError("foo")), "request"))
230         d.addCallback(lambda ign:
231             self.shouldFailWithSFTPError(sftp.FX_EOF, "_convert_error EOFError",
232                                          sftpd._convert_error, Failure(EOFError("foo")), "request"))
233         d.addCallback(lambda ign:
234             self.shouldFailWithSFTPError(sftp.FX_EOF, "_convert_error defer.FirstError",
235                                          sftpd._convert_error, Failure(defer.FirstError(
236                                                                  Failure(sftp.SFTPError(sftp.FX_EOF, "foo")), 0)), "request"))
237         d.addCallback(lambda ign:
238             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_convert_error AssertionError",
239                                          sftpd._convert_error, Failure(AssertionError("foo")), "request"))
240
241         return d
242
243     def test_not_implemented(self):
244         d = self._set_up("not_implemented")
245
246         d.addCallback(lambda ign:
247             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "readLink link",
248                                          self.handler.readLink, "link"))
249         d.addCallback(lambda ign:
250             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "makeLink link file",
251                                          self.handler.makeLink, "link", "file"))
252
253         return d
254
255     def _compareDirLists(self, actual, expected):
256        actual_list = sorted(actual)
257        expected_list = sorted(expected)
258        self.failUnlessReallyEqual(len(actual_list), len(expected_list),
259                             "%r is wrong length, expecting %r" % (actual_list, expected_list))
260        for (a, b) in zip(actual_list, expected_list):
261            (name, text, attrs) = a
262            (expected_name, expected_text_re, expected_attrs) = b
263            self.failUnlessReallyEqual(name, expected_name)
264            self.failUnless(re.match(expected_text_re, text),
265                            "%r does not match %r in\n%r" % (text, expected_text_re, actual_list))
266            self._compareAttributes(attrs, expected_attrs)
267
268     def _compareAttributes(self, attrs, expected_attrs):
269         # It is ok for there to be extra actual attributes.
270         # TODO: check times
271         for e in expected_attrs:
272             self.failUnless(e in attrs, "%r is not in\n%r" % (e, attrs))
273             self.failUnlessReallyEqual(attrs[e], expected_attrs[e],
274                                        "%r:%r is not %r in\n%r" % (e, attrs[e], expected_attrs[e], attrs))
275
276     def test_openDirectory_and_attrs(self):
277         d = self._set_up("openDirectory_and_attrs")
278         d.addCallback(lambda ign: self._set_up_tree())
279
280         d.addCallback(lambda ign:
281             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openDirectory small",
282                                          self.handler.openDirectory, "small"))
283         d.addCallback(lambda ign:
284             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openDirectory unknown",
285                                          self.handler.openDirectory, "unknown"))
286         d.addCallback(lambda ign:
287             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openDirectory nodir",
288                                          self.handler.openDirectory, "nodir"))
289         d.addCallback(lambda ign:
290             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openDirectory nodir/nodir",
291                                          self.handler.openDirectory, "nodir/nodir"))
292
293         gross = u"gro\u00DF".encode("utf-8")
294         expected_root = [
295             ('empty_lit_dir', r'dr-xr-xr-x .* \? .* empty_lit_dir$',      {'permissions': S_IFDIR | 0555}),
296             (gross,           r'-rw-rw-rw- .* 1010 .* '+gross+'$',        {'permissions': S_IFREG | 0666, 'size': 1010}),
297             # The fall of the Berlin wall may have been on 9th or 10th November 1989 depending on the gateway's timezone.
298             #('loop',          r'drwxrwxrwx .* \? Nov (09|10)  1989 loop$', {'permissions': S_IFDIR | 0777}),
299             ('loop',          r'drwxrwxrwx .* \? .* loop$',               {'permissions': S_IFDIR | 0777}),
300             ('mutable',       r'-rw-rw-rw- .* \? .* mutable$',            {'permissions': S_IFREG | 0666}),
301             ('readonly',      r'-r--r--r-- .* \? .* readonly$',           {'permissions': S_IFREG | 0444}),
302             ('small',         r'-rw-rw-rw- .* 10 .* small$',              {'permissions': S_IFREG | 0666, 'size': 10}),
303             ('small2',        r'-rw-rw-rw- .* 26 .* small2$',             {'permissions': S_IFREG | 0666, 'size': 26}),
304             ('tiny_lit_dir',  r'dr-xr-xr-x .* \? .* tiny_lit_dir$',       {'permissions': S_IFDIR | 0555}),
305             ('unknown',       r'\?--------- .* \? .* unknown$',           {'permissions': 0}),
306         ]
307
308         d.addCallback(lambda ign: self.handler.openDirectory(""))
309         d.addCallback(lambda res: self._compareDirLists(res, expected_root))
310
311         d.addCallback(lambda ign: self.handler.openDirectory("loop"))
312         d.addCallback(lambda res: self._compareDirLists(res, expected_root))
313
314         d.addCallback(lambda ign: self.handler.openDirectory("loop/loop"))
315         d.addCallback(lambda res: self._compareDirLists(res, expected_root))
316
317         d.addCallback(lambda ign: self.handler.openDirectory("empty_lit_dir"))
318         d.addCallback(lambda res: self._compareDirLists(res, []))
319
320         # The UTC epoch may either be in Jan 1 1970 or Dec 31 1969 depending on the gateway's timezone.
321         expected_tiny_lit = [
322             ('short', r'-r--r--r-- .* 8 (Jan 01  1970|Dec 31  1969) short$', {'permissions': S_IFREG | 0444, 'size': 8}),
323         ]
324
325         d.addCallback(lambda ign: self.handler.openDirectory("tiny_lit_dir"))
326         d.addCallback(lambda res: self._compareDirLists(res, expected_tiny_lit))
327
328         d.addCallback(lambda ign: self.handler.getAttrs("small", True))
329         d.addCallback(lambda attrs: self._compareAttributes(attrs, {'permissions': S_IFREG | 0666, 'size': 10}))
330
331         d.addCallback(lambda ign: self.handler.setAttrs("small", {}))
332         d.addCallback(lambda res: self.failUnlessReallyEqual(res, None))
333
334         d.addCallback(lambda ign: self.handler.getAttrs("small", True))
335         d.addCallback(lambda attrs: self._compareAttributes(attrs, {'permissions': S_IFREG | 0666, 'size': 10}))
336
337         d.addCallback(lambda ign:
338             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "setAttrs size",
339                                          self.handler.setAttrs, "small", {'size': 0}))
340
341         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
342         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
343         return d
344
345     def test_openFile_read(self):
346         d = self._set_up("openFile_read")
347         d.addCallback(lambda ign: self._set_up_tree())
348
349         d.addCallback(lambda ign:
350             self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile small 0 bad",
351                                          self.handler.openFile, "small", 0, {}))
352
353         # attempting to open a non-existent file should fail
354         d.addCallback(lambda ign:
355             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile nofile READ nosuch",
356                                          self.handler.openFile, "nofile", sftp.FXF_READ, {}))
357         d.addCallback(lambda ign:
358             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile nodir/file READ nosuch",
359                                          self.handler.openFile, "nodir/file", sftp.FXF_READ, {}))
360
361         d.addCallback(lambda ign:
362             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown READ denied",
363                                          self.handler.openFile, "unknown", sftp.FXF_READ, {}))
364         d.addCallback(lambda ign:
365             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown/file READ denied",
366                                          self.handler.openFile, "unknown/file", sftp.FXF_READ, {}))
367         d.addCallback(lambda ign:
368             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir READ denied",
369                                          self.handler.openFile, "tiny_lit_dir", sftp.FXF_READ, {}))
370         d.addCallback(lambda ign:
371             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown uri READ denied",
372                                          self.handler.openFile, "uri/"+self.unknown_uri, sftp.FXF_READ, {}))
373         d.addCallback(lambda ign:
374             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir uri READ denied",
375                                          self.handler.openFile, "uri/"+self.tiny_lit_dir_uri, sftp.FXF_READ, {}))
376         # FIXME: should be FX_NO_SUCH_FILE?
377         d.addCallback(lambda ign:
378             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile noexist uri READ denied",
379                                          self.handler.openFile, "uri/URI:noexist", sftp.FXF_READ, {}))
380         d.addCallback(lambda ign:
381             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile invalid UTF-8 uri READ denied",
382                                          self.handler.openFile, "uri/URI:\xFF", sftp.FXF_READ, {}))
383
384         # reading an existing file should succeed
385         d.addCallback(lambda ign: self.handler.openFile("small", sftp.FXF_READ, {}))
386         def _read_small(rf):
387             d2 = rf.readChunk(0, 10)
388             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
389
390             d2.addCallback(lambda ign: rf.readChunk(2, 6))
391             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "234567"))
392
393             d2.addCallback(lambda ign: rf.readChunk(1, 0))
394             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
395
396             d2.addCallback(lambda ign: rf.readChunk(8, 4))  # read that starts before EOF is OK
397             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "89"))
398
399             d2.addCallback(lambda ign:
400                 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF (0-byte)",
401                                              rf.readChunk, 10, 0))
402             d2.addCallback(lambda ign:
403                 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF",
404                                              rf.readChunk, 10, 1))
405             d2.addCallback(lambda ign:
406                 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting after EOF",
407                                              rf.readChunk, 11, 1))
408
409             d2.addCallback(lambda ign: rf.getAttrs())
410             d2.addCallback(lambda attrs: self._compareAttributes(attrs, {'permissions': S_IFREG | 0666, 'size': 10}))
411
412             d2.addCallback(lambda ign: self.handler.getAttrs("small", followLinks=0))
413             d2.addCallback(lambda attrs: self._compareAttributes(attrs, {'permissions': S_IFREG | 0666, 'size': 10}))
414
415             d2.addCallback(lambda ign:
416                 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "writeChunk on read-only handle denied",
417                                              rf.writeChunk, 0, "a"))
418             d2.addCallback(lambda ign:
419                 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "setAttrs on read-only handle denied",
420                                              rf.setAttrs, {}))
421
422             d2.addCallback(lambda ign: rf.close())
423
424             d2.addCallback(lambda ign:
425                 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "readChunk on closed file bad",
426                                              rf.readChunk, 0, 1))
427             d2.addCallback(lambda ign:
428                 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "getAttrs on closed file bad",
429                                              rf.getAttrs))
430
431             d2.addCallback(lambda ign: rf.close()) # should be no-op
432             return d2
433         d.addCallback(_read_small)
434
435         # repeat for a large file
436         gross = u"gro\u00DF".encode("utf-8")
437         d.addCallback(lambda ign: self.handler.openFile(gross, sftp.FXF_READ, {}))
438         def _read_gross(rf):
439             d2 = rf.readChunk(0, 10)
440             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
441
442             d2.addCallback(lambda ign: rf.readChunk(2, 6))
443             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "234567"))
444
445             d2.addCallback(lambda ign: rf.readChunk(1, 0))
446             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
447
448             d2.addCallback(lambda ign: rf.readChunk(1008, 4))  # read that starts before EOF is OK
449             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "89"))
450
451             d2.addCallback(lambda ign:
452                 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF (0-byte)",
453                                              rf.readChunk, 1010, 0))
454             d2.addCallback(lambda ign:
455                 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF",
456                                              rf.readChunk, 1010, 1))
457             d2.addCallback(lambda ign:
458                 self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting after EOF",
459                                              rf.readChunk, 1011, 1))
460
461             d2.addCallback(lambda ign: rf.getAttrs())
462             d2.addCallback(lambda attrs: self._compareAttributes(attrs, {'permissions': S_IFREG | 0666, 'size': 1010}))
463
464             d2.addCallback(lambda ign: self.handler.getAttrs(gross, followLinks=0))
465             d2.addCallback(lambda attrs: self._compareAttributes(attrs, {'permissions': S_IFREG | 0666, 'size': 1010}))
466
467             d2.addCallback(lambda ign:
468                 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "writeChunk on read-only handle denied",
469                                              rf.writeChunk, 0, "a"))
470             d2.addCallback(lambda ign:
471                 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "setAttrs on read-only handle denied",
472                                              rf.setAttrs, {}))
473
474             d2.addCallback(lambda ign: rf.close())
475
476             d2.addCallback(lambda ign:
477                 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "readChunk on closed file",
478                                              rf.readChunk, 0, 1))
479             d2.addCallback(lambda ign:
480                 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "getAttrs on closed file",
481                                              rf.getAttrs))
482
483             d2.addCallback(lambda ign: rf.close()) # should be no-op
484             return d2
485         d.addCallback(_read_gross)
486
487         # reading an existing small file via uri/ should succeed
488         d.addCallback(lambda ign: self.handler.openFile("uri/"+self.small_uri, sftp.FXF_READ, {}))
489         def _read_small_uri(rf):
490             d2 = rf.readChunk(0, 10)
491             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
492             d2.addCallback(lambda ign: rf.close())
493             return d2
494         d.addCallback(_read_small_uri)
495
496         # repeat for a large file
497         d.addCallback(lambda ign: self.handler.openFile("uri/"+self.gross_uri, sftp.FXF_READ, {}))
498         def _read_gross_uri(rf):
499             d2 = rf.readChunk(0, 10)
500             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
501             d2.addCallback(lambda ign: rf.close())
502             return d2
503         d.addCallback(_read_gross_uri)
504
505         # repeat for a mutable file
506         d.addCallback(lambda ign: self.handler.openFile("uri/"+self.mutable_uri, sftp.FXF_READ, {}))
507         def _read_mutable_uri(rf):
508             d2 = rf.readChunk(0, 100)
509             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable file contents"))
510             d2.addCallback(lambda ign: rf.close())
511             return d2
512         d.addCallback(_read_mutable_uri)
513
514         # repeat for a file within a directory referenced by URI
515         d.addCallback(lambda ign: self.handler.openFile("uri/"+self.tiny_lit_dir_uri+"/short", sftp.FXF_READ, {}))
516         def _read_short(rf):
517             d2 = rf.readChunk(0, 100)
518             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "The end."))
519             d2.addCallback(lambda ign: rf.close())
520             return d2
521         d.addCallback(_read_short)
522
523         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
524         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
525         return d
526
527     def test_openFile_write(self):
528         d = self._set_up("openFile_write")
529         d.addCallback(lambda ign: self._set_up_tree())
530
531         # '' is an invalid filename
532         d.addCallback(lambda ign:
533             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile '' WRITE|CREAT|TRUNC nosuch",
534                                          self.handler.openFile, "", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
535
536         # TRUNC is not valid without CREAT if the file does not already exist
537         d.addCallback(lambda ign:
538             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile newfile WRITE|TRUNC nosuch",
539                                          self.handler.openFile, "newfile", sftp.FXF_WRITE | sftp.FXF_TRUNC, {}))
540
541         # EXCL is not valid without CREAT
542         d.addCallback(lambda ign:
543             self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile small WRITE|EXCL bad",
544                                          self.handler.openFile, "small", sftp.FXF_WRITE | sftp.FXF_EXCL, {}))
545
546         # cannot write to an existing directory
547         d.addCallback(lambda ign:
548             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir WRITE denied",
549                                          self.handler.openFile, "tiny_lit_dir", sftp.FXF_WRITE, {}))
550
551         # cannot write to an existing unknown
552         d.addCallback(lambda ign:
553             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown WRITE denied",
554                                          self.handler.openFile, "unknown", sftp.FXF_WRITE, {}))
555
556         # cannot create a child of an unknown
557         d.addCallback(lambda ign:
558             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown/newfile WRITE|CREAT denied",
559                                          self.handler.openFile, "unknown/newfile",
560                                          sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
561
562         # cannot write to a new file in an immutable directory
563         d.addCallback(lambda ign:
564             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/newfile WRITE|CREAT|TRUNC denied",
565                                          self.handler.openFile, "tiny_lit_dir/newfile",
566                                          sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
567
568         # cannot write to an existing immutable file in an immutable directory (with or without CREAT and EXCL)
569         d.addCallback(lambda ign:
570             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE denied",
571                                          self.handler.openFile, "tiny_lit_dir/short", sftp.FXF_WRITE, {}))
572         d.addCallback(lambda ign:
573             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE|CREAT denied",
574                                          self.handler.openFile, "tiny_lit_dir/short",
575                                          sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
576
577         # cannot write to a mutable file via a readonly cap (by path or uri)
578         d.addCallback(lambda ign:
579             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly WRITE denied",
580                                          self.handler.openFile, "readonly", sftp.FXF_WRITE, {}))
581         d.addCallback(lambda ign:
582             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly uri WRITE denied",
583                                          self.handler.openFile, "uri/"+self.readonly_uri, sftp.FXF_WRITE, {}))
584
585         # cannot create a file with the EXCL flag if it already exists
586         d.addCallback(lambda ign:
587             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "openFile small WRITE|CREAT|EXCL failure",
588                                          self.handler.openFile, "small",
589                                          sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
590         d.addCallback(lambda ign:
591             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "openFile mutable WRITE|CREAT|EXCL failure",
592                                          self.handler.openFile, "mutable",
593                                          sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
594         d.addCallback(lambda ign:
595             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "openFile mutable uri WRITE|CREAT|EXCL failure",
596                                          self.handler.openFile, "uri/"+self.mutable_uri,
597                                          sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
598         d.addCallback(lambda ign:
599             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "openFile tiny_lit_dir/short WRITE|CREAT|EXCL failure",
600                                          self.handler.openFile, "tiny_lit_dir/short",
601                                          sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
602
603         # cannot write to an immutable file if we don't have its parent (with or without CREAT, TRUNC, or EXCL)
604         d.addCallback(lambda ign:
605             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE denied",
606                                          self.handler.openFile, "uri/"+self.small_uri, sftp.FXF_WRITE, {}))
607         d.addCallback(lambda ign:
608             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT denied",
609                                          self.handler.openFile, "uri/"+self.small_uri,
610                                          sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
611         d.addCallback(lambda ign:
612             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT|TRUNC denied",
613                                          self.handler.openFile, "uri/"+self.small_uri,
614                                          sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
615         d.addCallback(lambda ign:
616             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT|EXCL denied",
617                                          self.handler.openFile, "uri/"+self.small_uri,
618                                          sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
619
620         # test creating a new file with truncation and extension
621         d.addCallback(lambda ign:
622                       self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
623         def _write(wf):
624             d2 = wf.writeChunk(0, "0123456789")
625             d2.addCallback(lambda res: self.failUnlessReallyEqual(res, None))
626
627             d2.addCallback(lambda ign: wf.writeChunk(8, "0123"))
628             d2.addCallback(lambda ign: wf.writeChunk(13, "abc"))
629
630             d2.addCallback(lambda ign: wf.getAttrs())
631             d2.addCallback(lambda attrs: self._compareAttributes(attrs, {'permissions': S_IFREG | 0666, 'size': 16}))
632
633             d2.addCallback(lambda ign: self.handler.getAttrs("newfile", followLinks=0))
634             d2.addCallback(lambda attrs: self._compareAttributes(attrs, {'permissions': S_IFREG | 0666, 'size': 16}))
635
636             d2.addCallback(lambda ign: wf.setAttrs({}))
637
638             d2.addCallback(lambda ign:
639                 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "setAttrs with negative size bad",
640                                              wf.setAttrs, {'size': -1}))
641
642             d2.addCallback(lambda ign: wf.setAttrs({'size': 14}))
643             d2.addCallback(lambda ign: wf.getAttrs())
644             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 14))
645
646             d2.addCallback(lambda ign: wf.setAttrs({'size': 14}))
647             d2.addCallback(lambda ign: wf.getAttrs())
648             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 14))
649
650             d2.addCallback(lambda ign: wf.setAttrs({'size': 17}))
651             d2.addCallback(lambda ign: wf.getAttrs())
652             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 17))
653             d2.addCallback(lambda ign: self.handler.getAttrs("newfile", followLinks=0))
654             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 17))
655
656             d2.addCallback(lambda ign:
657                 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "readChunk on write-only handle denied",
658                                              wf.readChunk, 0, 1))
659
660             d2.addCallback(lambda ign: wf.close())
661
662             d2.addCallback(lambda ign:
663                 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "writeChunk on closed file bad",
664                                              wf.writeChunk, 0, "a"))
665             d2.addCallback(lambda ign:
666                 self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "setAttrs on closed file bad",
667                                              wf.setAttrs, {'size': 0}))
668
669             d2.addCallback(lambda ign: wf.close()) # should be no-op
670             return d2
671         d.addCallback(_write)
672         d.addCallback(lambda ign: self.root.get(u"newfile"))
673         d.addCallback(lambda node: download_to_data(node))
674         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123\x00a\x00\x00\x00"))
675
676         # test APPEND flag, and also replacing an existing file ("newfile" created by the previous test)
677         d.addCallback(lambda ign:
678                       self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT |
679                                                        sftp.FXF_TRUNC | sftp.FXF_APPEND, {}))
680         def _write_append(wf):
681             d2 = wf.writeChunk(0, "0123456789")
682             d2.addCallback(lambda ign: wf.writeChunk(8, "0123"))
683
684             d2.addCallback(lambda ign: wf.setAttrs({'size': 17}))
685             d2.addCallback(lambda ign: wf.getAttrs())
686             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 17))
687
688             d2.addCallback(lambda ign: wf.writeChunk(0, "z"))
689             d2.addCallback(lambda ign: wf.close())
690             return d2
691         d.addCallback(_write_append)
692         d.addCallback(lambda ign: self.root.get(u"newfile"))
693         d.addCallback(lambda node: download_to_data(node))
694         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "01234567890123\x00\x00\x00z"))
695
696         # test WRITE | TRUNC without CREAT, when the file already exists
697         # This is invalid according to section 6.3 of the SFTP spec, but required for interoperability,
698         # since POSIX does allow O_WRONLY | O_TRUNC.
699         d.addCallback(lambda ign:
700                       self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_TRUNC, {}))
701         def _write_trunc(wf):
702             d2 = wf.writeChunk(0, "01234")
703             d2.addCallback(lambda ign: wf.close())
704             return d2
705         d.addCallback(_write_trunc)
706         d.addCallback(lambda ign: self.root.get(u"newfile"))
707         d.addCallback(lambda node: download_to_data(node))
708         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "01234"))
709
710         # test WRITE | TRUNC with permissions: 0
711         d.addCallback(lambda ign:
712                       self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_TRUNC, {'permissions': 0}))
713         d.addCallback(_write_trunc)
714         d.addCallback(lambda ign: self.root.get(u"newfile"))
715         d.addCallback(lambda node: download_to_data(node))
716         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "01234"))
717         d.addCallback(lambda ign: self.root.get_metadata_for(u"newfile"))
718         d.addCallback(lambda metadata: self.failIf(metadata.get('no-write', False), metadata))
719
720         # test EXCL flag
721         d.addCallback(lambda ign:
722                       self.handler.openFile("excl", sftp.FXF_WRITE | sftp.FXF_CREAT |
723                                                     sftp.FXF_TRUNC | sftp.FXF_EXCL, {}))
724         def _write_excl(wf):
725             d2 = self.root.get(u"excl")
726             d2.addCallback(lambda node: download_to_data(node))
727             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
728
729             d2.addCallback(lambda ign: wf.writeChunk(0, "0123456789"))
730             d2.addCallback(lambda ign: wf.close())
731             return d2
732         d.addCallback(_write_excl)
733         d.addCallback(lambda ign: self.root.get(u"excl"))
734         d.addCallback(lambda node: download_to_data(node))
735         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
736
737         # test that writing a zero-length file with EXCL only updates the directory once
738         d.addCallback(lambda ign:
739                       self.handler.openFile("zerolength", sftp.FXF_WRITE | sftp.FXF_CREAT |
740                                                           sftp.FXF_EXCL, {}))
741         def _write_excl_zerolength(wf):
742             d2 = self.root.get(u"zerolength")
743             d2.addCallback(lambda node: download_to_data(node))
744             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
745
746             # FIXME: no API to get the best version number exists (fix as part of #993)
747             """
748             d2.addCallback(lambda ign: self.root.get_best_version_number())
749             def _check_version(version):
750                 d3 = wf.close()
751                 d3.addCallback(lambda ign: self.root.get_best_version_number())
752                 d3.addCallback(lambda new_version: self.failUnlessReallyEqual(new_version, version))
753                 return d3
754             d2.addCallback(_check_version)
755             """
756             d2.addCallback(lambda ign: wf.close())
757             return d2
758         d.addCallback(_write_excl_zerolength)
759         d.addCallback(lambda ign: self.root.get(u"zerolength"))
760         d.addCallback(lambda node: download_to_data(node))
761         d.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
762
763         # test WRITE | CREAT | EXCL | APPEND
764         d.addCallback(lambda ign:
765                       self.handler.openFile("exclappend", sftp.FXF_WRITE | sftp.FXF_CREAT |
766                                                           sftp.FXF_EXCL | sftp.FXF_APPEND, {}))
767         def _write_excl_append(wf):
768             d2 = self.root.get(u"exclappend")
769             d2.addCallback(lambda node: download_to_data(node))
770             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, ""))
771
772             d2.addCallback(lambda ign: wf.writeChunk(10, "0123456789"))
773             d2.addCallback(lambda ign: wf.writeChunk(5, "01234"))
774             d2.addCallback(lambda ign: wf.close())
775             return d2
776         d.addCallback(_write_excl_append)
777         d.addCallback(lambda ign: self.root.get(u"exclappend"))
778         d.addCallback(lambda node: download_to_data(node))
779         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345678901234"))
780
781         # test WRITE | CREAT | APPEND when the file does not already exist
782         d.addCallback(lambda ign:
783                       self.handler.openFile("creatappend", sftp.FXF_WRITE | sftp.FXF_CREAT |
784                                                            sftp.FXF_APPEND, {}))
785         def _write_creat_append_new(wf):
786             d2 = wf.writeChunk(10, "0123456789")
787             d2.addCallback(lambda ign: wf.writeChunk(5, "01234"))
788             d2.addCallback(lambda ign: wf.close())
789             return d2
790         d.addCallback(_write_creat_append_new)
791         d.addCallback(lambda ign: self.root.get(u"creatappend"))
792         d.addCallback(lambda node: download_to_data(node))
793         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345678901234"))
794
795         # ... and when it does exist
796         d.addCallback(lambda ign:
797                       self.handler.openFile("creatappend", sftp.FXF_WRITE | sftp.FXF_CREAT |
798                                                            sftp.FXF_APPEND, {}))
799         def _write_creat_append_existing(wf):
800             d2 = wf.writeChunk(5, "01234")
801             d2.addCallback(lambda ign: wf.close())
802             return d2
803         d.addCallback(_write_creat_append_existing)
804         d.addCallback(lambda ign: self.root.get(u"creatappend"))
805         d.addCallback(lambda node: download_to_data(node))
806         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "01234567890123401234"))
807
808         # test WRITE | CREAT without TRUNC, when the file does not already exist
809         d.addCallback(lambda ign:
810                       self.handler.openFile("newfile2", sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
811         def _write_creat_new(wf):
812             d2 =  wf.writeChunk(0, "0123456789")
813             d2.addCallback(lambda ign: wf.close())
814             return d2
815         d.addCallback(_write_creat_new)
816         d.addCallback(lambda ign: self.root.get(u"newfile2"))
817         d.addCallback(lambda node: download_to_data(node))
818         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
819
820         # ... and when it does exist
821         d.addCallback(lambda ign:
822                       self.handler.openFile("newfile2", sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
823         def _write_creat_existing(wf):
824             d2 =  wf.writeChunk(0, "abcde")
825             d2.addCallback(lambda ign: wf.close())
826             return d2
827         d.addCallback(_write_creat_existing)
828         d.addCallback(lambda ign: self.root.get(u"newfile2"))
829         d.addCallback(lambda node: download_to_data(node))
830         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "abcde56789"))
831
832         d.addCallback(lambda ign: self.root.set_node(u"mutable2", self.mutable))
833
834         # test writing to a mutable file
835         d.addCallback(lambda ign:
836                       self.handler.openFile("mutable", sftp.FXF_WRITE, {}))
837         def _write_mutable(wf):
838             d2 = wf.writeChunk(8, "new!")
839             d2.addCallback(lambda ign: wf.close())
840             return d2
841         d.addCallback(_write_mutable)
842         d.addCallback(lambda ign: self.root.get(u"mutable"))
843         def _check_same_file(node):
844             self.failUnless(node.is_mutable())
845             self.failIf(node.is_readonly())
846             self.failUnlessReallyEqual(node.get_uri(), self.mutable_uri)
847             return node.download_best_version()
848         d.addCallback(_check_same_file)
849         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable new! contents"))
850
851         # ... and with permissions, which should be ignored
852         d.addCallback(lambda ign:
853                       self.handler.openFile("mutable", sftp.FXF_WRITE, {'permissions': 0}))
854         d.addCallback(_write_mutable)
855         d.addCallback(lambda ign: self.root.get(u"mutable"))
856         d.addCallback(_check_same_file)
857         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable new! contents"))
858
859         # ... and with a setAttrs call that diminishes the parent link to read-only, first by path
860         d.addCallback(lambda ign:
861                       self.handler.openFile("mutable", sftp.FXF_WRITE, {}))
862         def _write_mutable_setattr(wf):
863             d2 = wf.writeChunk(8, "read-only link from parent")
864
865             d2.addCallback(lambda ign: self.handler.setAttrs("mutable", {'permissions': 0444}))
866
867             d2.addCallback(lambda ign: self.root.get(u"mutable"))
868             d2.addCallback(lambda node: self.failUnless(node.is_readonly()))
869
870             d2.addCallback(lambda ign: wf.getAttrs())
871             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0666))
872             d2.addCallback(lambda ign: self.handler.getAttrs("mutable", followLinks=0))
873             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0444))
874
875             d2.addCallback(lambda ign: wf.close())
876             return d2
877         d.addCallback(_write_mutable_setattr)
878         d.addCallback(lambda ign: self.root.get(u"mutable"))
879         def _check_readonly_file(node):
880             self.failUnless(node.is_mutable())
881             self.failUnless(node.is_readonly())
882             self.failUnlessReallyEqual(node.get_write_uri(), None)
883             self.failUnlessReallyEqual(node.get_storage_index(), self.mutable.get_storage_index())
884             return node.download_best_version()
885         d.addCallback(_check_readonly_file)
886         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable read-only link from parent"))
887
888         # ... and then by handle
889         d.addCallback(lambda ign:
890                       self.handler.openFile("mutable2", sftp.FXF_WRITE, {}))
891         def _write_mutable2_setattr(wf):
892             d2 = wf.writeChunk(7, "2")
893
894             d2.addCallback(lambda ign: wf.setAttrs({'permissions': 0444, 'size': 8}))
895
896             # The link isn't made read-only until the file is closed.
897             d2.addCallback(lambda ign: self.root.get(u"mutable2"))
898             d2.addCallback(lambda node: self.failIf(node.is_readonly()))
899
900             d2.addCallback(lambda ign: wf.getAttrs())
901             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0444))
902             d2.addCallback(lambda ign: self.handler.getAttrs("mutable2", followLinks=0))
903             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0666))
904
905             d2.addCallback(lambda ign: wf.close())
906             return d2
907         d.addCallback(_write_mutable2_setattr)
908         d.addCallback(lambda ign: self.root.get(u"mutable2"))
909         d.addCallback(_check_readonly_file)  # from above
910         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable2"))
911
912         # test READ | WRITE without CREAT or TRUNC
913         d.addCallback(lambda ign:
914                       self.handler.openFile("small", sftp.FXF_READ | sftp.FXF_WRITE, {}))
915         def _read_write(rwf):
916             d2 = rwf.writeChunk(8, "0123")
917             d2.addCallback(lambda ign: rwf.readChunk(0, 100))
918             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123"))
919             d2.addCallback(lambda ign: rwf.close())
920             return d2
921         d.addCallback(_read_write)
922         d.addCallback(lambda ign: self.root.get(u"small"))
923         d.addCallback(lambda node: download_to_data(node))
924         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123"))
925
926         # test WRITE and rename while still open
927         d.addCallback(lambda ign:
928                       self.handler.openFile("small", sftp.FXF_WRITE, {}))
929         def _write_rename(wf):
930             d2 = wf.writeChunk(0, "abcd")
931             d2.addCallback(lambda ign: self.handler.renameFile("small", "renamed"))
932             d2.addCallback(lambda ign: wf.writeChunk(4, "efgh"))
933             d2.addCallback(lambda ign: wf.close())
934             return d2
935         d.addCallback(_write_rename)
936         d.addCallback(lambda ign: self.root.get(u"renamed"))
937         d.addCallback(lambda node: download_to_data(node))
938         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "abcdefgh0123"))
939         d.addCallback(lambda ign:
940                       self.shouldFail(NoSuchChildError, "rename small while open", "small",
941                                       self.root.get, u"small"))
942
943         # test WRITE | CREAT | EXCL and rename while still open
944         d.addCallback(lambda ign:
945                       self.handler.openFile("newexcl", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
946         def _write_creat_excl_rename(wf):
947             d2 = wf.writeChunk(0, "abcd")
948             d2.addCallback(lambda ign: self.handler.renameFile("newexcl", "renamedexcl"))
949             d2.addCallback(lambda ign: wf.writeChunk(4, "efgh"))
950             d2.addCallback(lambda ign: wf.close())
951             return d2
952         d.addCallback(_write_creat_excl_rename)
953         d.addCallback(lambda ign: self.root.get(u"renamedexcl"))
954         d.addCallback(lambda node: download_to_data(node))
955         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "abcdefgh"))
956         d.addCallback(lambda ign:
957                       self.shouldFail(NoSuchChildError, "rename newexcl while open", "newexcl",
958                                       self.root.get, u"newexcl"))
959
960         # it should be possible to rename even before the open has completed
961         def _open_and_rename_race(ign):
962             slow_open = defer.Deferred()
963             reactor.callLater(1, slow_open.callback, None)
964             d2 = self.handler.openFile("new", sftp.FXF_WRITE | sftp.FXF_CREAT, {}, delay=slow_open)
965
966             # deliberate race between openFile and renameFile
967             d3 = self.handler.renameFile("new", "new2")
968             del d3
969             return d2
970         d.addCallback(_open_and_rename_race)
971         def _write_rename_race(wf):
972             d2 = wf.writeChunk(0, "abcd")
973             d2.addCallback(lambda ign: wf.close())
974             return d2
975         d.addCallback(_write_rename_race)
976         d.addCallback(lambda ign: self.root.get(u"new2"))
977         d.addCallback(lambda node: download_to_data(node))
978         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "abcd"))
979         d.addCallback(lambda ign:
980                       self.shouldFail(NoSuchChildError, "rename new while open", "new",
981                                       self.root.get, u"new"))
982
983         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
984         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
985         return d
986
987     def test_removeFile(self):
988         d = self._set_up("removeFile")
989         d.addCallback(lambda ign: self._set_up_tree())
990
991         d.addCallback(lambda ign:
992             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nofile",
993                                          self.handler.removeFile, "nofile"))
994         d.addCallback(lambda ign:
995             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nofile",
996                                          self.handler.removeFile, "nofile"))
997         d.addCallback(lambda ign:
998             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nodir/file",
999                                          self.handler.removeFile, "nodir/file"))
1000         d.addCallback(lambda ign:
1001             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removefile ''",
1002                                          self.handler.removeFile, ""))
1003             
1004         # removing a directory should fail
1005         d.addCallback(lambda ign:
1006             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeFile tiny_lit_dir",
1007                                          self.handler.removeFile, "tiny_lit_dir"))
1008
1009         # removing a file should succeed
1010         d.addCallback(lambda ign: self.root.get(u"gro\u00DF"))
1011         d.addCallback(lambda ign: self.handler.removeFile(u"gro\u00DF".encode('utf-8')))
1012         d.addCallback(lambda ign:
1013                       self.shouldFail(NoSuchChildError, "removeFile gross", "gro\\xdf",
1014                                       self.root.get, u"gro\u00DF"))
1015
1016         # removing an unknown should succeed
1017         d.addCallback(lambda ign: self.root.get(u"unknown"))
1018         d.addCallback(lambda ign: self.handler.removeFile("unknown"))
1019         d.addCallback(lambda ign:
1020                       self.shouldFail(NoSuchChildError, "removeFile unknown", "unknown",
1021                                       self.root.get, u"unknown"))
1022
1023         # removing a link to an open file should not prevent it from being read
1024         d.addCallback(lambda ign: self.handler.openFile("small", sftp.FXF_READ, {}))
1025         def _remove_and_read_small(rf):
1026             d2 = self.handler.removeFile("small")
1027             d2.addCallback(lambda ign:
1028                            self.shouldFail(NoSuchChildError, "removeFile small", "small",
1029                                            self.root.get, u"small"))
1030             d2.addCallback(lambda ign: rf.readChunk(0, 10))
1031             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
1032             d2.addCallback(lambda ign: rf.close())
1033             return d2
1034         d.addCallback(_remove_and_read_small)
1035
1036         # removing a link to a created file should prevent it from being created
1037         d.addCallback(lambda ign: self.handler.openFile("tempfile", sftp.FXF_READ | sftp.FXF_WRITE |
1038                                                                     sftp.FXF_CREAT, {}))
1039         def _write_remove(rwf):
1040             d2 = rwf.writeChunk(0, "0123456789")
1041             d2.addCallback(lambda ign: self.handler.removeFile("tempfile"))
1042             d2.addCallback(lambda ign: rwf.readChunk(0, 10))
1043             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
1044             d2.addCallback(lambda ign: rwf.close())
1045             return d2
1046         d.addCallback(_write_remove)
1047         d.addCallback(lambda ign:
1048                       self.shouldFail(NoSuchChildError, "removeFile tempfile", "tempfile",
1049                                       self.root.get, u"tempfile"))
1050
1051         # ... even if the link is renamed while open
1052         d.addCallback(lambda ign: self.handler.openFile("tempfile2", sftp.FXF_READ | sftp.FXF_WRITE |
1053                                                                      sftp.FXF_CREAT, {}))
1054         def _write_rename_remove(rwf):
1055             d2 = rwf.writeChunk(0, "0123456789")
1056             d2.addCallback(lambda ign: self.handler.renameFile("tempfile2", "tempfile3"))
1057             d2.addCallback(lambda ign: self.handler.removeFile("tempfile3"))
1058             d2.addCallback(lambda ign: rwf.readChunk(0, 10))
1059             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
1060             d2.addCallback(lambda ign: rwf.close())
1061             return d2
1062         d.addCallback(_write_rename_remove)
1063         d.addCallback(lambda ign:
1064                       self.shouldFail(NoSuchChildError, "removeFile tempfile2", "tempfile2",
1065                                       self.root.get, u"tempfile2"))
1066         d.addCallback(lambda ign:
1067                       self.shouldFail(NoSuchChildError, "removeFile tempfile3", "tempfile3",
1068                                       self.root.get, u"tempfile3"))
1069
1070         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1071         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1072         return d
1073
1074     def test_removeDirectory(self):
1075         d = self._set_up("removeDirectory")
1076         d.addCallback(lambda ign: self._set_up_tree())
1077
1078         d.addCallback(lambda ign:
1079             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory nodir",
1080                                          self.handler.removeDirectory, "nodir"))
1081         d.addCallback(lambda ign:
1082             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory nodir/nodir",
1083                                          self.handler.removeDirectory, "nodir/nodir"))
1084         d.addCallback(lambda ign:
1085             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory ''",
1086                                          self.handler.removeDirectory, ""))
1087
1088         # removing a file should fail
1089         d.addCallback(lambda ign:
1090             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeDirectory gross",
1091                                          self.handler.removeDirectory, u"gro\u00DF".encode('utf-8')))
1092
1093         # removing a directory should succeed
1094         d.addCallback(lambda ign: self.root.get(u"tiny_lit_dir"))
1095         d.addCallback(lambda ign: self.handler.removeDirectory("tiny_lit_dir"))
1096         d.addCallback(lambda ign:
1097                       self.shouldFail(NoSuchChildError, "removeDirectory tiny_lit_dir", "tiny_lit_dir",
1098                                       self.root.get, u"tiny_lit_dir"))
1099
1100         # removing an unknown should succeed
1101         d.addCallback(lambda ign: self.root.get(u"unknown"))
1102         d.addCallback(lambda ign: self.handler.removeDirectory("unknown"))
1103         d.addCallback(lambda err:
1104                       self.shouldFail(NoSuchChildError, "removeDirectory unknown", "unknown",
1105                                       self.root.get, u"unknown"))
1106
1107         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1108         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1109         return d
1110
1111     def test_renameFile(self):
1112         d = self._set_up("renameFile")
1113         d.addCallback(lambda ign: self._set_up_tree())
1114
1115         # renaming a non-existent file should fail
1116         d.addCallback(lambda ign:
1117             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile nofile newfile",
1118                                          self.handler.renameFile, "nofile", "newfile"))
1119         d.addCallback(lambda ign:
1120             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile '' newfile",
1121                                          self.handler.renameFile, "", "newfile"))
1122
1123         # renaming a file to a non-existent path should fail
1124         d.addCallback(lambda ign:
1125             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small nodir/small",
1126                                          self.handler.renameFile, "small", "nodir/small"))
1127
1128         # renaming a file to an invalid UTF-8 name should fail
1129         d.addCallback(lambda ign:
1130             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small invalid",
1131                                          self.handler.renameFile, "small", "\xFF"))
1132
1133         # renaming a file to or from an URI should fail
1134         d.addCallback(lambda ign:
1135             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small from uri",
1136                                          self.handler.renameFile, "uri/"+self.small_uri, "new"))
1137         d.addCallback(lambda ign:
1138             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small to uri",
1139                                          self.handler.renameFile, "small", "uri/fake_uri"))
1140
1141         # renaming a file onto an existing file, directory or unknown should fail
1142         # The SFTP spec isn't clear about what error should be returned, but sshfs depends on
1143         # it being FX_PERMISSION_DENIED.
1144         d.addCallback(lambda ign:
1145             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small small2",
1146                                          self.handler.renameFile, "small", "small2"))
1147         d.addCallback(lambda ign:
1148             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small tiny_lit_dir",
1149                                          self.handler.renameFile, "small", "tiny_lit_dir"))
1150         d.addCallback(lambda ign:
1151             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small unknown",
1152                                          self.handler.renameFile, "small", "unknown"))
1153
1154         # renaming a file to a correct path should succeed
1155         d.addCallback(lambda ign: self.handler.renameFile("small", "new_small"))
1156         d.addCallback(lambda ign: self.root.get(u"new_small"))
1157         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1158
1159         # renaming a file into a subdirectory should succeed (also tests Unicode names)
1160         d.addCallback(lambda ign: self.handler.renameFile(u"gro\u00DF".encode('utf-8'),
1161                                                           u"loop/neue_gro\u00DF".encode('utf-8')))
1162         d.addCallback(lambda ign: self.root.get(u"neue_gro\u00DF"))
1163         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.gross_uri))
1164
1165         # renaming a directory to a correct path should succeed
1166         d.addCallback(lambda ign: self.handler.renameFile("tiny_lit_dir", "new_tiny_lit_dir"))
1167         d.addCallback(lambda ign: self.root.get(u"new_tiny_lit_dir"))
1168         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.tiny_lit_dir_uri))
1169
1170         # renaming an unknown to a correct path should succeed
1171         d.addCallback(lambda ign: self.handler.renameFile("unknown", "new_unknown"))
1172         d.addCallback(lambda ign: self.root.get(u"new_unknown"))
1173         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.unknown_uri))
1174
1175         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1176         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1177         return d
1178
1179     def test_renameFile_posix(self):
1180         def _renameFile(fromPathstring, toPathstring):
1181             extData = (struct.pack('>L', len(fromPathstring)) + fromPathstring +
1182                        struct.pack('>L', len(toPathstring))   + toPathstring)
1183
1184             d2 = self.handler.extendedRequest('posix-rename@openssh.com', extData)
1185             def _check(res):
1186                 res.trap(sftp.SFTPError)
1187                 if res.value.code == sftp.FX_OK:
1188                     return None
1189                 return res
1190             d2.addCallbacks(lambda res: self.fail("posix-rename request was supposed to "
1191                                                   "raise an SFTPError, not get '%r'" % (res,)),
1192                             _check)
1193             return d2
1194
1195         d = self._set_up("renameFile_posix")
1196         d.addCallback(lambda ign: self._set_up_tree())
1197
1198         d.addCallback(lambda ign: self.root.set_node(u"loop2", self.root))
1199         d.addCallback(lambda ign: self.root.set_node(u"unknown2", self.unknown))
1200
1201         # POSIX-renaming a non-existent file should fail
1202         d.addCallback(lambda ign:
1203             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix nofile newfile",
1204                                          _renameFile, "nofile", "newfile"))
1205         d.addCallback(lambda ign:
1206             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix '' newfile",
1207                                          _renameFile, "", "newfile"))
1208
1209         # POSIX-renaming a file to a non-existent path should fail
1210         d.addCallback(lambda ign:
1211             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix small nodir/small",
1212                                          _renameFile, "small", "nodir/small"))
1213
1214         # POSIX-renaming a file to an invalid UTF-8 name should fail
1215         d.addCallback(lambda ign:
1216             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix small invalid",
1217                                          _renameFile, "small", "\xFF"))
1218
1219         # POSIX-renaming a file to or from an URI should fail
1220         d.addCallback(lambda ign:
1221             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix small from uri",
1222                                          _renameFile, "uri/"+self.small_uri, "new"))
1223         d.addCallback(lambda ign:
1224             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix small to uri",
1225                                          _renameFile, "small", "uri/fake_uri"))
1226
1227         # POSIX-renaming a file onto an existing file, directory or unknown should succeed
1228         d.addCallback(lambda ign: _renameFile("small", "small2"))
1229         d.addCallback(lambda ign: self.root.get(u"small2"))
1230         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1231
1232         d.addCallback(lambda ign: _renameFile("small2", "loop2"))
1233         d.addCallback(lambda ign: self.root.get(u"loop2"))
1234         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1235
1236         d.addCallback(lambda ign: _renameFile("loop2", "unknown2"))
1237         d.addCallback(lambda ign: self.root.get(u"unknown2"))
1238         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1239
1240         # POSIX-renaming a file to a correct new path should succeed
1241         d.addCallback(lambda ign: _renameFile("unknown2", "new_small"))
1242         d.addCallback(lambda ign: self.root.get(u"new_small"))
1243         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1244
1245         # POSIX-renaming a file into a subdirectory should succeed (also tests Unicode names)
1246         d.addCallback(lambda ign: _renameFile(u"gro\u00DF".encode('utf-8'),
1247                                               u"loop/neue_gro\u00DF".encode('utf-8')))
1248         d.addCallback(lambda ign: self.root.get(u"neue_gro\u00DF"))
1249         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.gross_uri))
1250
1251         # POSIX-renaming a directory to a correct path should succeed
1252         d.addCallback(lambda ign: _renameFile("tiny_lit_dir", "new_tiny_lit_dir"))
1253         d.addCallback(lambda ign: self.root.get(u"new_tiny_lit_dir"))
1254         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.tiny_lit_dir_uri))
1255
1256         # POSIX-renaming an unknown to a correct path should succeed
1257         d.addCallback(lambda ign: _renameFile("unknown", "new_unknown"))
1258         d.addCallback(lambda ign: self.root.get(u"new_unknown"))
1259         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.unknown_uri))
1260
1261         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1262         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1263         return d
1264
1265     def test_makeDirectory(self):
1266         d = self._set_up("makeDirectory")
1267         d.addCallback(lambda ign: self._set_up_tree())
1268
1269         # making a directory at a correct path should succeed
1270         d.addCallback(lambda ign: self.handler.makeDirectory("newdir", {'ext_foo': 'bar', 'ctime': 42}))
1271
1272         d.addCallback(lambda ign: self.root.get_child_and_metadata(u"newdir"))
1273         def _got( (child, metadata) ):
1274             self.failUnless(IDirectoryNode.providedBy(child))
1275             self.failUnless(child.is_mutable())
1276             # FIXME
1277             #self.failUnless('ctime' in metadata, metadata)
1278             #self.failUnlessReallyEqual(metadata['ctime'], 42)
1279             #self.failUnless('ext_foo' in metadata, metadata)
1280             #self.failUnlessReallyEqual(metadata['ext_foo'], 'bar')
1281             # TODO: child should be empty
1282         d.addCallback(_got)
1283
1284         # making intermediate directories should also succeed
1285         d.addCallback(lambda ign: self.handler.makeDirectory("newparent/newchild", {}))
1286
1287         d.addCallback(lambda ign: self.root.get(u"newparent"))
1288         def _got_newparent(newparent):
1289             self.failUnless(IDirectoryNode.providedBy(newparent))
1290             self.failUnless(newparent.is_mutable())
1291             return newparent.get(u"newchild")
1292         d.addCallback(_got_newparent)
1293
1294         def _got_newchild(newchild):
1295             self.failUnless(IDirectoryNode.providedBy(newchild))
1296             self.failUnless(newchild.is_mutable())
1297         d.addCallback(_got_newchild)
1298
1299         d.addCallback(lambda ign:
1300             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "makeDirectory invalid UTF-8",
1301                                          self.handler.makeDirectory, "\xFF", {}))
1302
1303         # should fail because there is an existing file "small"
1304         d.addCallback(lambda ign:
1305             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "makeDirectory small",
1306                                          self.handler.makeDirectory, "small", {}))
1307
1308         # directories cannot be created read-only via SFTP
1309         d.addCallback(lambda ign:
1310             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "makeDirectory newdir2 permissions:0444 denied",
1311                                          self.handler.makeDirectory, "newdir2",
1312                                          {'permissions': 0444}))
1313
1314         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1315         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1316         return d
1317
1318     def test_execCommand_and_openShell(self):
1319         class FakeProtocol:
1320             def __init__(self):
1321                 self.output = ""
1322                 self.reason = None
1323             def write(self, data):
1324                 self.output += data
1325                 return defer.succeed(None)
1326             def processEnded(self, reason):
1327                 self.reason = reason
1328                 return defer.succeed(None)
1329
1330         d = self._set_up("execCommand_and_openShell")
1331
1332         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
1333         def _exec_df(session):
1334             protocol = FakeProtocol()
1335             d2 = session.execCommand(protocol, "df -P -k /")
1336             d2.addCallback(lambda ign: self.failUnlessIn("1024-blocks", protocol.output))
1337             d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessDone)))
1338             d2.addCallback(lambda ign: session.eofReceived())
1339             d2.addCallback(lambda ign: session.closed())
1340             return d2
1341         d.addCallback(_exec_df)
1342
1343         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
1344         def _exec_error(session):
1345             protocol = FakeProtocol()
1346             d2 = session.execCommand(protocol, "error")
1347             d2.addCallback(lambda ign: session.windowChanged(None))
1348             d2.addCallback(lambda ign: self.failUnlessEqual("", protocol.output))
1349             d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
1350             d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
1351             d2.addCallback(lambda ign: session.closed())
1352             return d2
1353         d.addCallback(_exec_error)
1354
1355         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
1356         def _openShell(session):
1357             protocol = FakeProtocol()
1358             d2 = session.openShell(protocol)
1359             d2.addCallback(lambda ign: self.failUnlessIn("only SFTP", protocol.output))
1360             d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
1361             d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
1362             d2.addCallback(lambda ign: session.closed())
1363             return d2
1364         d.addCallback(_openShell)
1365
1366         return d
1367
1368     def test_extendedRequest(self):
1369         d = self._set_up("extendedRequest")
1370
1371         d.addCallback(lambda ign: self.handler.extendedRequest("statvfs@openssh.com", "/"))
1372         def _check(res):
1373             self.failUnless(isinstance(res, str))
1374             self.failUnlessEqual(len(res), 8*11)
1375         d.addCallback(_check)
1376
1377         d.addCallback(lambda ign:
1378             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "extendedRequest foo bar",
1379                                          self.handler.extendedRequest, "foo", "bar"))
1380
1381         d.addCallback(lambda ign:
1382             self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 1",
1383                                          self.handler.extendedRequest, 'posix-rename@openssh.com', ''))
1384         d.addCallback(lambda ign:
1385             self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 2",
1386                                          self.handler.extendedRequest, 'posix-rename@openssh.com', '\x00\x00\x00\x01'))
1387         d.addCallback(lambda ign:
1388             self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 3",
1389                                          self.handler.extendedRequest, 'posix-rename@openssh.com', '\x00\x00\x00\x01_\x00\x00\x00\x01'))
1390
1391         return d