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