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