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