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