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