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