]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_sftp.py
6095b18f22dce1786f45d0fd82a8e305b53d898f
[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             d2.addCallback(lambda ign: rwf.readChunk(0, 100))
917             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123"))
918             d2.addCallback(lambda ign: rwf.close())
919             return d2
920         d.addCallback(_read_write)
921         d.addCallback(lambda ign: self.root.get(u"small"))
922         d.addCallback(lambda node: download_to_data(node))
923         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123"))
924
925         # test WRITE and rename while still open
926         d.addCallback(lambda ign:
927                       self.handler.openFile("small", sftp.FXF_WRITE, {}))
928         def _write_rename(wf):
929             d2 = wf.writeChunk(0, "abcd")
930             d2.addCallback(lambda ign: self.handler.renameFile("small", "renamed"))
931             d2.addCallback(lambda ign: wf.writeChunk(4, "efgh"))
932             d2.addCallback(lambda ign: wf.close())
933             return d2
934         d.addCallback(_write_rename)
935         d.addCallback(lambda ign: self.root.get(u"renamed"))
936         d.addCallback(lambda node: download_to_data(node))
937         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "abcdefgh0123"))
938         d.addCallback(lambda ign:
939                       self.shouldFail(NoSuchChildError, "rename small while open", "small",
940                                       self.root.get, u"small"))
941
942         # test WRITE | CREAT | EXCL and rename while still open
943         d.addCallback(lambda ign:
944                       self.handler.openFile("newexcl", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
945         def _write_creat_excl_rename(wf):
946             d2 = wf.writeChunk(0, "abcd")
947             d2.addCallback(lambda ign: self.handler.renameFile("newexcl", "renamedexcl"))
948             d2.addCallback(lambda ign: wf.writeChunk(4, "efgh"))
949             d2.addCallback(lambda ign: wf.close())
950             return d2
951         d.addCallback(_write_creat_excl_rename)
952         d.addCallback(lambda ign: self.root.get(u"renamedexcl"))
953         d.addCallback(lambda node: download_to_data(node))
954         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "abcdefgh"))
955         d.addCallback(lambda ign:
956                       self.shouldFail(NoSuchChildError, "rename newexcl while open", "newexcl",
957                                       self.root.get, u"newexcl"))
958
959         # it should be possible to rename even before the open has completed
960         def _open_and_rename_race(ign):
961             slow_open = defer.Deferred()
962             reactor.callLater(1, slow_open.callback, None)
963             d2 = self.handler.openFile("new", sftp.FXF_WRITE | sftp.FXF_CREAT, {}, delay=slow_open)
964
965             # deliberate race between openFile and renameFile
966             d3 = self.handler.renameFile("new", "new2")
967             d3.addErrback(lambda err: self.fail("renameFile failed: %r" % (err,)))
968             return d2
969         d.addCallback(_open_and_rename_race)
970         def _write_rename_race(wf):
971             d2 = wf.writeChunk(0, "abcd")
972             d2.addCallback(lambda ign: wf.close())
973             return d2
974         d.addCallback(_write_rename_race)
975         d.addCallback(lambda ign: self.root.get(u"new2"))
976         d.addCallback(lambda node: download_to_data(node))
977         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "abcd"))
978         d.addCallback(lambda ign:
979                       self.shouldFail(NoSuchChildError, "rename new while open", "new",
980                                       self.root.get, u"new"))
981
982         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
983         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
984         return d
985
986     def test_removeFile(self):
987         d = self._set_up("removeFile")
988         d.addCallback(lambda ign: self._set_up_tree())
989
990         d.addCallback(lambda ign:
991             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nofile",
992                                          self.handler.removeFile, "nofile"))
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 nodir/file",
998                                          self.handler.removeFile, "nodir/file"))
999         d.addCallback(lambda ign:
1000             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removefile ''",
1001                                          self.handler.removeFile, ""))
1002
1003         # removing a directory should fail
1004         d.addCallback(lambda ign:
1005             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeFile tiny_lit_dir",
1006                                          self.handler.removeFile, "tiny_lit_dir"))
1007
1008         # removing a file should succeed
1009         d.addCallback(lambda ign: self.root.get(u"gro\u00DF"))
1010         d.addCallback(lambda ign: self.handler.removeFile(u"gro\u00DF".encode('utf-8')))
1011         d.addCallback(lambda ign:
1012                       self.shouldFail(NoSuchChildError, "removeFile gross", "gro\\xdf",
1013                                       self.root.get, u"gro\u00DF"))
1014
1015         # removing an unknown should succeed
1016         d.addCallback(lambda ign: self.root.get(u"unknown"))
1017         d.addCallback(lambda ign: self.handler.removeFile("unknown"))
1018         d.addCallback(lambda ign:
1019                       self.shouldFail(NoSuchChildError, "removeFile unknown", "unknown",
1020                                       self.root.get, u"unknown"))
1021
1022         # removing a link to an open file should not prevent it from being read
1023         d.addCallback(lambda ign: self.handler.openFile("small", sftp.FXF_READ, {}))
1024         def _remove_and_read_small(rf):
1025             d2 = self.handler.removeFile("small")
1026             d2.addCallback(lambda ign:
1027                            self.shouldFail(NoSuchChildError, "removeFile small", "small",
1028                                            self.root.get, u"small"))
1029             d2.addCallback(lambda ign: rf.readChunk(0, 10))
1030             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
1031             d2.addCallback(lambda ign: rf.close())
1032             return d2
1033         d.addCallback(_remove_and_read_small)
1034
1035         # removing a link to a created file should prevent it from being created
1036         d.addCallback(lambda ign: self.handler.openFile("tempfile", sftp.FXF_READ | sftp.FXF_WRITE |
1037                                                                     sftp.FXF_CREAT, {}))
1038         def _write_remove(rwf):
1039             d2 = rwf.writeChunk(0, "0123456789")
1040             d2.addCallback(lambda ign: self.handler.removeFile("tempfile"))
1041             d2.addCallback(lambda ign: rwf.readChunk(0, 10))
1042             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
1043             d2.addCallback(lambda ign: rwf.close())
1044             return d2
1045         d.addCallback(_write_remove)
1046         d.addCallback(lambda ign:
1047                       self.shouldFail(NoSuchChildError, "removeFile tempfile", "tempfile",
1048                                       self.root.get, u"tempfile"))
1049
1050         # ... even if the link is renamed while open
1051         d.addCallback(lambda ign: self.handler.openFile("tempfile2", sftp.FXF_READ | sftp.FXF_WRITE |
1052                                                                      sftp.FXF_CREAT, {}))
1053         def _write_rename_remove(rwf):
1054             d2 = rwf.writeChunk(0, "0123456789")
1055             d2.addCallback(lambda ign: self.handler.renameFile("tempfile2", "tempfile3"))
1056             d2.addCallback(lambda ign: self.handler.removeFile("tempfile3"))
1057             d2.addCallback(lambda ign: rwf.readChunk(0, 10))
1058             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "0123456789"))
1059             d2.addCallback(lambda ign: rwf.close())
1060             return d2
1061         d.addCallback(_write_rename_remove)
1062         d.addCallback(lambda ign:
1063                       self.shouldFail(NoSuchChildError, "removeFile tempfile2", "tempfile2",
1064                                       self.root.get, u"tempfile2"))
1065         d.addCallback(lambda ign:
1066                       self.shouldFail(NoSuchChildError, "removeFile tempfile3", "tempfile3",
1067                                       self.root.get, u"tempfile3"))
1068
1069         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1070         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1071         return d
1072
1073     def test_removeDirectory(self):
1074         d = self._set_up("removeDirectory")
1075         d.addCallback(lambda ign: self._set_up_tree())
1076
1077         d.addCallback(lambda ign:
1078             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory nodir",
1079                                          self.handler.removeDirectory, "nodir"))
1080         d.addCallback(lambda ign:
1081             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory nodir/nodir",
1082                                          self.handler.removeDirectory, "nodir/nodir"))
1083         d.addCallback(lambda ign:
1084             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory ''",
1085                                          self.handler.removeDirectory, ""))
1086
1087         # removing a file should fail
1088         d.addCallback(lambda ign:
1089             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeDirectory gross",
1090                                          self.handler.removeDirectory, u"gro\u00DF".encode('utf-8')))
1091
1092         # removing a directory should succeed
1093         d.addCallback(lambda ign: self.root.get(u"tiny_lit_dir"))
1094         d.addCallback(lambda ign: self.handler.removeDirectory("tiny_lit_dir"))
1095         d.addCallback(lambda ign:
1096                       self.shouldFail(NoSuchChildError, "removeDirectory tiny_lit_dir", "tiny_lit_dir",
1097                                       self.root.get, u"tiny_lit_dir"))
1098
1099         # removing an unknown should succeed
1100         d.addCallback(lambda ign: self.root.get(u"unknown"))
1101         d.addCallback(lambda ign: self.handler.removeDirectory("unknown"))
1102         d.addCallback(lambda err:
1103                       self.shouldFail(NoSuchChildError, "removeDirectory unknown", "unknown",
1104                                       self.root.get, u"unknown"))
1105
1106         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1107         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1108         return d
1109
1110     def test_renameFile(self):
1111         d = self._set_up("renameFile")
1112         d.addCallback(lambda ign: self._set_up_tree())
1113
1114         # renaming a non-existent file should fail
1115         d.addCallback(lambda ign:
1116             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile nofile newfile",
1117                                          self.handler.renameFile, "nofile", "newfile"))
1118         d.addCallback(lambda ign:
1119             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile '' newfile",
1120                                          self.handler.renameFile, "", "newfile"))
1121
1122         # renaming a file to a non-existent path should fail
1123         d.addCallback(lambda ign:
1124             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small nodir/small",
1125                                          self.handler.renameFile, "small", "nodir/small"))
1126
1127         # renaming a file to an invalid UTF-8 name should fail
1128         d.addCallback(lambda ign:
1129             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small invalid",
1130                                          self.handler.renameFile, "small", "\xFF"))
1131
1132         # renaming a file to or from an URI should fail
1133         d.addCallback(lambda ign:
1134             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small from uri",
1135                                          self.handler.renameFile, "uri/"+self.small_uri, "new"))
1136         d.addCallback(lambda ign:
1137             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small to uri",
1138                                          self.handler.renameFile, "small", "uri/fake_uri"))
1139
1140         # renaming a file onto an existing file, directory or unknown should fail
1141         # The SFTP spec isn't clear about what error should be returned, but sshfs depends on
1142         # it being FX_PERMISSION_DENIED.
1143         d.addCallback(lambda ign:
1144             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small small2",
1145                                          self.handler.renameFile, "small", "small2"))
1146         d.addCallback(lambda ign:
1147             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small tiny_lit_dir",
1148                                          self.handler.renameFile, "small", "tiny_lit_dir"))
1149         d.addCallback(lambda ign:
1150             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small unknown",
1151                                          self.handler.renameFile, "small", "unknown"))
1152
1153         # renaming a file onto a heisenfile should fail, even if the open hasn't completed
1154         def _rename_onto_heisenfile_race(wf):
1155             slow_open = defer.Deferred()
1156             reactor.callLater(1, slow_open.callback, None)
1157
1158             d2 = self.handler.openFile("heisenfile", sftp.FXF_WRITE | sftp.FXF_CREAT, {}, delay=slow_open)
1159
1160             # deliberate race between openFile and renameFile
1161             d3 = self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small heisenfile",
1162                                               self.handler.renameFile, "small", "heisenfile")
1163             d2.addCallback(lambda wf: wf.close())
1164             return deferredutil.gatherResults([d2, d3])
1165         d.addCallback(_rename_onto_heisenfile_race)
1166
1167         # renaming a file to a correct path should succeed
1168         d.addCallback(lambda ign: self.handler.renameFile("small", "new_small"))
1169         d.addCallback(lambda ign: self.root.get(u"new_small"))
1170         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1171
1172         # renaming a file into a subdirectory should succeed (also tests Unicode names)
1173         d.addCallback(lambda ign: self.handler.renameFile(u"gro\u00DF".encode('utf-8'),
1174                                                           u"loop/neue_gro\u00DF".encode('utf-8')))
1175         d.addCallback(lambda ign: self.root.get(u"neue_gro\u00DF"))
1176         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.gross_uri))
1177
1178         # renaming a directory to a correct path should succeed
1179         d.addCallback(lambda ign: self.handler.renameFile("tiny_lit_dir", "new_tiny_lit_dir"))
1180         d.addCallback(lambda ign: self.root.get(u"new_tiny_lit_dir"))
1181         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.tiny_lit_dir_uri))
1182
1183         # renaming an unknown to a correct path should succeed
1184         d.addCallback(lambda ign: self.handler.renameFile("unknown", "new_unknown"))
1185         d.addCallback(lambda ign: self.root.get(u"new_unknown"))
1186         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.unknown_uri))
1187
1188         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1189         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1190         return d
1191
1192     def test_renameFile_posix(self):
1193         def _renameFile(fromPathstring, toPathstring):
1194             extData = (struct.pack('>L', len(fromPathstring)) + fromPathstring +
1195                        struct.pack('>L', len(toPathstring))   + toPathstring)
1196
1197             d2 = self.handler.extendedRequest('posix-rename@openssh.com', extData)
1198             def _check(res):
1199                 res.trap(sftp.SFTPError)
1200                 if res.value.code == sftp.FX_OK:
1201                     return None
1202                 return res
1203             d2.addCallbacks(lambda res: self.fail("posix-rename request was supposed to "
1204                                                   "raise an SFTPError, not get '%r'" % (res,)),
1205                             _check)
1206             return d2
1207
1208         d = self._set_up("renameFile_posix")
1209         d.addCallback(lambda ign: self._set_up_tree())
1210
1211         d.addCallback(lambda ign: self.root.set_node(u"loop2", self.root))
1212         d.addCallback(lambda ign: self.root.set_node(u"unknown2", self.unknown))
1213
1214         # POSIX-renaming a non-existent file should fail
1215         d.addCallback(lambda ign:
1216             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix nofile newfile",
1217                                          _renameFile, "nofile", "newfile"))
1218         d.addCallback(lambda ign:
1219             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix '' newfile",
1220                                          _renameFile, "", "newfile"))
1221
1222         # POSIX-renaming a file to a non-existent path should fail
1223         d.addCallback(lambda ign:
1224             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix small nodir/small",
1225                                          _renameFile, "small", "nodir/small"))
1226
1227         # POSIX-renaming a file to an invalid UTF-8 name should fail
1228         d.addCallback(lambda ign:
1229             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix small invalid",
1230                                          _renameFile, "small", "\xFF"))
1231
1232         # POSIX-renaming a file to or from an URI should fail
1233         d.addCallback(lambda ign:
1234             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix small from uri",
1235                                          _renameFile, "uri/"+self.small_uri, "new"))
1236         d.addCallback(lambda ign:
1237             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile_posix small to uri",
1238                                          _renameFile, "small", "uri/fake_uri"))
1239
1240         # POSIX-renaming a file onto an existing file, directory or unknown should succeed
1241         d.addCallback(lambda ign: _renameFile("small", "small2"))
1242         d.addCallback(lambda ign: self.root.get(u"small2"))
1243         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1244
1245         d.addCallback(lambda ign: _renameFile("small2", "loop2"))
1246         d.addCallback(lambda ign: self.root.get(u"loop2"))
1247         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1248
1249         d.addCallback(lambda ign: _renameFile("loop2", "unknown2"))
1250         d.addCallback(lambda ign: self.root.get(u"unknown2"))
1251         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1252
1253         # POSIX-renaming a file to a correct new path should succeed
1254         d.addCallback(lambda ign: _renameFile("unknown2", "new_small"))
1255         d.addCallback(lambda ign: self.root.get(u"new_small"))
1256         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.small_uri))
1257
1258         # POSIX-renaming a file into a subdirectory should succeed (also tests Unicode names)
1259         d.addCallback(lambda ign: _renameFile(u"gro\u00DF".encode('utf-8'),
1260                                               u"loop/neue_gro\u00DF".encode('utf-8')))
1261         d.addCallback(lambda ign: self.root.get(u"neue_gro\u00DF"))
1262         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.gross_uri))
1263
1264         # POSIX-renaming a directory to a correct path should succeed
1265         d.addCallback(lambda ign: _renameFile("tiny_lit_dir", "new_tiny_lit_dir"))
1266         d.addCallback(lambda ign: self.root.get(u"new_tiny_lit_dir"))
1267         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.tiny_lit_dir_uri))
1268
1269         # POSIX-renaming an unknown to a correct path should succeed
1270         d.addCallback(lambda ign: _renameFile("unknown", "new_unknown"))
1271         d.addCallback(lambda ign: self.root.get(u"new_unknown"))
1272         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.unknown_uri))
1273
1274         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1275         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1276         return d
1277
1278     def test_makeDirectory(self):
1279         d = self._set_up("makeDirectory")
1280         d.addCallback(lambda ign: self._set_up_tree())
1281
1282         # making a directory at a correct path should succeed
1283         d.addCallback(lambda ign: self.handler.makeDirectory("newdir", {'ext_foo': 'bar', 'ctime': 42}))
1284
1285         d.addCallback(lambda ign: self.root.get_child_and_metadata(u"newdir"))
1286         def _got( (child, metadata) ):
1287             self.failUnless(IDirectoryNode.providedBy(child))
1288             self.failUnless(child.is_mutable())
1289             # FIXME
1290             #self.failUnless('ctime' in metadata, metadata)
1291             #self.failUnlessReallyEqual(metadata['ctime'], 42)
1292             #self.failUnless('ext_foo' in metadata, metadata)
1293             #self.failUnlessReallyEqual(metadata['ext_foo'], 'bar')
1294             # TODO: child should be empty
1295         d.addCallback(_got)
1296
1297         # making intermediate directories should also succeed
1298         d.addCallback(lambda ign: self.handler.makeDirectory("newparent/newchild", {}))
1299
1300         d.addCallback(lambda ign: self.root.get(u"newparent"))
1301         def _got_newparent(newparent):
1302             self.failUnless(IDirectoryNode.providedBy(newparent))
1303             self.failUnless(newparent.is_mutable())
1304             return newparent.get(u"newchild")
1305         d.addCallback(_got_newparent)
1306
1307         def _got_newchild(newchild):
1308             self.failUnless(IDirectoryNode.providedBy(newchild))
1309             self.failUnless(newchild.is_mutable())
1310         d.addCallback(_got_newchild)
1311
1312         d.addCallback(lambda ign:
1313             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "makeDirectory invalid UTF-8",
1314                                          self.handler.makeDirectory, "\xFF", {}))
1315
1316         # should fail because there is an existing file "small"
1317         d.addCallback(lambda ign:
1318             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "makeDirectory small",
1319                                          self.handler.makeDirectory, "small", {}))
1320
1321         # directories cannot be created read-only via SFTP
1322         d.addCallback(lambda ign:
1323             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "makeDirectory newdir2 permissions:0444 denied",
1324                                          self.handler.makeDirectory, "newdir2",
1325                                          {'permissions': 0444}))
1326
1327         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
1328         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
1329         return d
1330
1331     def test_execCommand_and_openShell(self):
1332         class MockProtocol:
1333             def __init__(self):
1334                 self.output = ""
1335                 self.error = ""
1336                 self.reason = None
1337
1338             def write(self, data):
1339                 return self.outReceived(data)
1340
1341             def outReceived(self, data):
1342                 self.output += data
1343                 return defer.succeed(None)
1344
1345             def errReceived(self, data):
1346                 self.error += data
1347                 return defer.succeed(None)
1348
1349             def processEnded(self, reason):
1350                 self.reason = reason
1351                 return defer.succeed(None)
1352
1353         def _lines_end_in_crlf(s):
1354             return s.replace('\r\n', '').find('\n') == -1 and s.endswith('\r\n')
1355
1356         d = self._set_up("execCommand_and_openShell")
1357
1358         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
1359         def _exec_df(session):
1360             protocol = MockProtocol()
1361             d2 = session.execCommand(protocol, "df -P -k /")
1362             d2.addCallback(lambda ign: self.failUnlessIn("1024-blocks", protocol.output))
1363             d2.addCallback(lambda ign: self.failUnless(_lines_end_in_crlf(protocol.output), protocol.output))
1364             d2.addCallback(lambda ign: self.failUnlessEqual(protocol.error, ""))
1365             d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessDone)))
1366             d2.addCallback(lambda ign: session.eofReceived())
1367             d2.addCallback(lambda ign: session.closed())
1368             return d2
1369         d.addCallback(_exec_df)
1370
1371         def _check_unsupported(protocol):
1372             d2 = defer.succeed(None)
1373             d2.addCallback(lambda ign: self.failUnlessEqual(protocol.output, ""))
1374             d2.addCallback(lambda ign: self.failUnlessIn("only the SFTP protocol", protocol.error))
1375             d2.addCallback(lambda ign: self.failUnless(_lines_end_in_crlf(protocol.error), protocol.error))
1376             d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
1377             d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
1378             return d2
1379
1380         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
1381         def _exec_error(session):
1382             protocol = MockProtocol()
1383             d2 = session.execCommand(protocol, "error")
1384             d2.addCallback(lambda ign: session.windowChanged(None))
1385             d2.addCallback(lambda ign: _check_unsupported(protocol))
1386             d2.addCallback(lambda ign: session.closed())
1387             return d2
1388         d.addCallback(_exec_error)
1389
1390         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
1391         def _openShell(session):
1392             protocol = MockProtocol()
1393             d2 = session.openShell(protocol)
1394             d2.addCallback(lambda ign: _check_unsupported(protocol))
1395             d2.addCallback(lambda ign: session.closed())
1396             return d2
1397         d.addCallback(_openShell)
1398
1399         return d
1400
1401     def test_extendedRequest(self):
1402         d = self._set_up("extendedRequest")
1403
1404         d.addCallback(lambda ign: self.handler.extendedRequest("statvfs@openssh.com", "/"))
1405         def _check(res):
1406             self.failUnless(isinstance(res, str))
1407             self.failUnlessEqual(len(res), 8*11)
1408         d.addCallback(_check)
1409
1410         d.addCallback(lambda ign:
1411             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "extendedRequest foo bar",
1412                                          self.handler.extendedRequest, "foo", "bar"))
1413
1414         d.addCallback(lambda ign:
1415             self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 1",
1416                                          self.handler.extendedRequest, 'posix-rename@openssh.com', ''))
1417         d.addCallback(lambda ign:
1418             self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 2",
1419                                          self.handler.extendedRequest, 'posix-rename@openssh.com', '\x00\x00\x00\x01'))
1420         d.addCallback(lambda ign:
1421             self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 3",
1422                                          self.handler.extendedRequest, 'posix-rename@openssh.com', '\x00\x00\x00\x01_\x00\x00\x00\x01'))
1423
1424         return d