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