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