[MERGE][1.0][Bug 124089] Add root_client_path parameter to SmartWSGIApp, and use it to translate paths received from clients to local paths.

Andrew Bennetts andrew at canonical.com
Thu Dec 6 00:34:15 GMT 2007


Hi all,

The attached change adds a “root_client_path” parameter to SmartServerRequest
and to SmartWSGIApp, and also adds a “translate_client_path” method to
SmartServerRequest, and makes all our SmartServerRequest subclasses use it when
dealing with paths received from a client.  See the docstrings in the diff for
their precise meanings.

This fixes bug 124089, at least at a code level.  The documentation for
configuring a WSGI server still needs to be improved to take this new
configuration option into account.

-Andrew.

-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: andrew.bennetts at canonical.com-20071206002917-\
#   86jl4regw5yr0oot
# target_branch: http://bazaar-vcs.org/bzr/bzr.dev
# testament_sha1: 104357aeb345ab60743a2dea82c12aedb0d3113a
# timestamp: 2007-12-06 11:29:21 +1100
# source_branch: http://people.ubuntu.com/~andrew/bzr/bug-124089
# base_revision_id: pqm at pqm.ubuntu.com-20071204185324-guhluk76wrccurxj
# 
# Begin patch
=== modified file 'NEWS'
--- NEWS	2007-12-04 18:11:51 +0000
+++ NEWS	2007-12-06 00:29:17 +0000
@@ -27,6 +27,13 @@
 
   BUGFIXES:
 
+   * Add ``root_client_path`` parameter to SmartWSGIApp and SmartServerRequest.
+     This makes it possible to publish filesystem locations that don't exactly
+     match URL paths. SmartServerRequest subclasses should now use the new
+     ``translate_client_path`` and ``transport_from_client_path`` methods when
+     dealing with paths received from a client to take this into account.
+     (Andrew Bennetts, #124089)
+
    * Catch OSError 17 (file exists) in final phase of tree transform and show
      filename to user.
      (Alexander Belchenko, #111758)

=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py	2007-12-04 18:11:51 +0000
+++ bzrlib/errors.py	2007-12-05 01:58:00 +0000
@@ -526,11 +526,13 @@
 
 class InvalidURLJoin(PathError):
 
-    _fmt = 'Invalid URL join request: "%(args)s"%(extra)s'
+    _fmt = "Invalid URL join request: %(reason)s: %(base)r + %(join_args)r"
 
-    def __init__(self, msg, base, args):
-        PathError.__init__(self, base, msg)
-        self.args = [base] + list(args)
+    def __init__(self, reason, base, join_args):
+        self.reason = reason
+        self.base = base
+        self.join_args = join_args
+        PathError.__init__(self, base, reason)
 
 
 class UnknownHook(BzrError):

=== modified file 'bzrlib/smart/branch.py'
--- bzrlib/smart/branch.py	2007-11-26 02:06:52 +0000
+++ bzrlib/smart/branch.py	2007-12-05 07:09:42 +0000
@@ -27,14 +27,21 @@
 
 
 class SmartServerBranchRequest(SmartServerRequest):
-    """Base class for handling common branch request logic."""
+    """Base class for handling common branch request logic.
+    """
 
     def do(self, path, *args):
         """Execute a request for a branch at path.
+    
+        All Branch requests take a path to the branch as their first argument.
 
         If the branch is a branch reference, NotBranchError is raised.
+
+        :param path: The path for the repository as received from the
+            client.
+        :return: A SmartServerResponse from self.do_with_branch().
         """
-        transport = self._backing_transport.clone(path)
+        transport = self.transport_from_client_path(path)
         bzrdir = BzrDir.open_from_transport(transport)
         if bzrdir.get_branch_reference() is not None:
             raise errors.NotBranchError(transport.base)

=== modified file 'bzrlib/smart/bzrdir.py'
--- bzrlib/smart/bzrdir.py	2007-04-24 12:20:09 +0000
+++ bzrlib/smart/bzrdir.py	2007-12-05 23:30:57 +0000
@@ -30,15 +30,24 @@
 
     def do(self, path):
         from bzrlib.bzrdir import BzrDirFormat
-        t = self._backing_transport.clone(path)
-        default_format = BzrDirFormat.get_default_format()
-        real_bzrdir = default_format.open(t, _found=True)
         try:
-            real_bzrdir._format.probe_transport(t)
-        except (errors.NotBranchError, errors.UnknownFormatError):
+            t = self.transport_from_client_path(path)
+        except errors.PathNotChild:
+            # The client is trying to ask about a path that they have no access
+            # to.
+            # Ideally we'd return a FailedSmartServerResponse here rather than
+            # a "successful" negative, but we want to be compatibile with
+            # clients that don't anticipate errors from this method.
             answer = 'no'
         else:
-            answer = 'yes'
+            default_format = BzrDirFormat.get_default_format()
+            real_bzrdir = default_format.open(t, _found=True)
+            try:
+                real_bzrdir._format.probe_transport(t)
+            except (errors.NotBranchError, errors.UnknownFormatError):
+                answer = 'no'
+            else:
+                answer = 'yes'
         return SuccessfulSmartServerResponse((answer,))
 
 
@@ -54,12 +63,14 @@
 
         :return: norepository or ok, relpath.
         """
-        bzrdir = BzrDir.open_from_transport(self._backing_transport.clone(path))
+        bzrdir = BzrDir.open_from_transport(
+            self.transport_from_client_path(path))
         try:
             repository = bzrdir.find_repository()
             # the relpath of the bzrdir in the found repository gives us the 
             # path segments to pop-out.
-            relpath = repository.bzrdir.root_transport.relpath(bzrdir.root_transport.base)
+            relpath = repository.bzrdir.root_transport.relpath(
+                bzrdir.root_transport.base)
             if len(relpath):
                 segments = ['..'] * len(relpath.split('/'))
             else:
@@ -85,7 +96,7 @@
         The default format of the server is used.
         :return: SmartServerResponse(('ok', ))
         """
-        target_transport = self._backing_transport.clone(path)
+        target_transport = self.transport_from_client_path(path)
         BzrDirFormat.get_default_format().initialize_on_transport(target_transport)
         return SuccessfulSmartServerResponse(('ok', ))
 
@@ -98,7 +109,8 @@
         If a bzrdir is not present, an exception is propogated
         rather than 'no branch' because these are different conditions.
         """
-        bzrdir = BzrDir.open_from_transport(self._backing_transport.clone(path))
+        bzrdir = BzrDir.open_from_transport(
+            self.transport_from_client_path(path))
         try:
             reference_url = bzrdir.get_branch_reference()
             if reference_url is None:

=== modified file 'bzrlib/smart/medium.py'
--- bzrlib/smart/medium.py	2007-12-03 16:39:11 +0000
+++ bzrlib/smart/medium.py	2007-12-05 23:30:57 +0000
@@ -53,13 +53,14 @@
     which will typically be a LocalTransport looking at the server's filesystem.
     """
 
-    def __init__(self, backing_transport):
+    def __init__(self, backing_transport, root_client_path='/'):
         """Construct new server.
 
         :param backing_transport: Transport for the directory served.
         """
         # backing_transport could be passed to serve instead of __init__
         self.backing_transport = backing_transport
+        self.root_client_path = root_client_path
         self.finished = False
 
     def serve(self):
@@ -91,7 +92,8 @@
             bytes = bytes[len(REQUEST_VERSION_TWO):]
         else:
             protocol_class = SmartServerRequestProtocolOne
-        protocol = protocol_class(self.backing_transport, self._write_out)
+        protocol = protocol_class(
+            self.backing_transport, self._write_out, self.root_client_path)
         protocol.accept_bytes(bytes)
         return protocol
 
@@ -139,13 +141,14 @@
 
 class SmartServerSocketStreamMedium(SmartServerStreamMedium):
 
-    def __init__(self, sock, backing_transport):
+    def __init__(self, sock, backing_transport, root_client_path='/'):
         """Constructor.
 
         :param sock: the socket the server will read from.  It will be put
             into blocking mode.
         """
-        SmartServerStreamMedium.__init__(self, backing_transport)
+        SmartServerStreamMedium.__init__(
+            self, backing_transport, root_client_path=root_client_path)
         self.push_back = ''
         sock.setblocking(True)
         self.socket = sock

=== modified file 'bzrlib/smart/protocol.py'
--- bzrlib/smart/protocol.py	2007-11-09 20:49:54 +0000
+++ bzrlib/smart/protocol.py	2007-12-05 01:58:00 +0000
@@ -73,8 +73,9 @@
 class SmartServerRequestProtocolOne(SmartProtocolBase):
     """Server-side encoding and decoding logic for smart version 1."""
     
-    def __init__(self, backing_transport, write_func):
+    def __init__(self, backing_transport, write_func, root_client_path='/'):
         self._backing_transport = backing_transport
+        self._root_client_path = root_client_path
         self.excess_buffer = ''
         self._finished = False
         self.in_buffer = ''
@@ -100,7 +101,8 @@
                 first_line += '\n'
                 req_args = _decode_tuple(first_line)
                 self.request = request.SmartServerRequestHandler(
-                    self._backing_transport, commands=request.request_handlers)
+                    self._backing_transport, commands=request.request_handlers,
+                    root_client_path=self._root_client_path)
                 self.request.dispatch_command(req_args[0], req_args[1:])
                 if self.request.finished_reading:
                     # trivial request
@@ -471,6 +473,7 @@
     def call(self, *args):
         if 'hpss' in debug.debug_flags:
             mutter('hpss call:   %s', repr(args)[1:-1])
+            mutter('             (to: %r)' % (self._request._medium))
             self._request_start_time = time.time()
         self._write_args(args)
         self._request.finished_writing()

=== modified file 'bzrlib/smart/repository.py'
--- bzrlib/smart/repository.py	2007-11-26 02:08:13 +0000
+++ bzrlib/smart/repository.py	2007-12-05 07:09:42 +0000
@@ -38,14 +38,17 @@
     def do(self, path, *args):
         """Execute a repository request.
         
-        The repository must be at the exact path - no searching is done.
+        All Repository requests take a path to the repository as their first
+        argument.  The repository must be at the exact path given by the
+        client - no searching is done.
 
         The actual logic is delegated to self.do_repository_request.
 
-        :param path: The path for the repository.
-        :return: A smart server from self.do_repository_request().
+        :param client_path: The path for the repository as received from the
+            client.
+        :return: A SmartServerResponse from self.do_repository_request().
         """
-        transport = self._backing_transport.clone(path)
+        transport = self.transport_from_client_path(path)
         bzrdir = BzrDir.open_from_transport(transport)
         repository = bzrdir.open_repository()
         return self.do_repository_request(repository, *args)
@@ -257,15 +260,16 @@
             repository.unlock()
 
     def _do_repository_request(self, repository, revision_ids):
-        stream = repository.get_data_stream(revision_ids)
         filelike = StringIO()
         pack = ContainerWriter(filelike.write)
         pack.begin()
+        stream = repository.get_data_stream(revision_ids)
         try:
             for name_tuple, bytes in stream:
                 pack.add_bytes_record(bytes, [name_tuple])
         except errors.RevisionNotPresent, e:
-            return FailedSmartServerResponse(('NoSuchRevision', e.revision_id))
+            return FailedSmartServerResponse(('NoSuchRevision',
+                                              e.revision_id))
         pack.end()
         return SuccessfulSmartServerResponse(('ok',), filelike.getvalue())
 

=== modified file 'bzrlib/smart/request.py'
--- bzrlib/smart/request.py	2007-10-19 05:38:36 +0000
+++ bzrlib/smart/request.py	2007-12-05 23:30:57 +0000
@@ -24,20 +24,40 @@
     errors,
     registry,
     revision,
+    urlutils,
     )
 from bzrlib.bundle.serializer import write_bundle
+from bzrlib.trace import mutter
+from bzrlib.transport import get_transport
+from bzrlib.transport.chroot import ChrootServer
 
 
 class SmartServerRequest(object):
-    """Base class for request handlers."""
+    """Base class for request handlers.
+    
+    To define a new request, subclass this class and override the `do` method
+    (and if appropriate, `do_body` as well).  Request implementors should take
+    care to call `translate_client_path` and `transport_from_client_path` as
+    appropriate when dealing with paths received from the client.
+    """
 
-    def __init__(self, backing_transport):
+    def __init__(self, backing_transport, root_client_path='/'):
         """Constructor.
 
         :param backing_transport: the base transport to be used when performing
             this request.
+        :param root_client_path: the client path that maps to the root of
+            backing_transport.  This is used to interpret relpaths received
+            from the client.  Clients will not be able to refer to paths above
+            this root.
         """
+        rcp = root_client_path
         self._backing_transport = backing_transport
+        if not root_client_path.startswith('/'):
+            root_client_path = '/' + root_client_path
+        if not root_client_path.endswith('/'):
+            root_client_path += '/'
+        self._root_client_path = root_client_path
 
     def _check_enabled(self):
         """Raises DisabledMethod if this method is disabled."""
@@ -74,6 +94,34 @@
         # this NotImplementedError and translate it into a 'bad request' error
         # to send to the client.
         raise NotImplementedError(self.do_body)
+    
+    def translate_client_path(self, client_path):
+        """Translate a path received from a network client into a local
+        relpath.
+
+        All paths received from the client *must* be translated.
+
+        :returns: a relpath that may be used with self._backing_transport
+        """
+        if not client_path.startswith('/'):
+            client_path = '/' + client_path
+        if client_path.startswith(self._root_client_path):
+            path = client_path[len(self._root_client_path):]
+            relpath = urlutils.joinpath('/', path)
+            assert relpath.startswith('/')
+            return '.' + relpath
+        else:
+            raise errors.PathNotChild(client_path, self._root_client_path)
+
+    def transport_from_client_path(self, client_path):
+        """Get a backing transport corresponding to the location referred to by
+        a network client.
+
+        :seealso: translate_client_path
+        :returns: a transport cloned from self._backing_transport
+        """
+        relpath = self.translate_client_path(client_path)
+        return self._backing_transport.clone(relpath)
 
 
 class SmartServerResponse(object):
@@ -106,8 +154,9 @@
                 other.body_stream is self.body_stream)
 
     def __repr__(self):
-        return "<SmartServerResponse %r args=%r body=%r>" % (
-            self.is_successful(), self.args, self.body)
+        status = {True: 'OK', False: 'ERR'}[self.is_successful()]
+        return "<SmartServerResponse status=%s args=%r body=%r>" % (status,
+            self.args, self.body)
 
 
 class FailedSmartServerResponse(SmartServerResponse):
@@ -143,7 +192,7 @@
     # TODO: Better way of representing the body for commands that take it,
     # and allow it to be streamed into the server.
 
-    def __init__(self, backing_transport, commands):
+    def __init__(self, backing_transport, commands, root_client_path):
         """Constructor.
 
         :param backing_transport: a Transport to handle requests for.
@@ -151,6 +200,7 @@
             subclasses. e.g. bzrlib.transport.smart.vfs.vfs_commands.
         """
         self._backing_transport = backing_transport
+        self._root_client_path = root_client_path
         self._commands = commands
         self._body_bytes = ''
         self.response = None
@@ -180,7 +230,7 @@
             command = self._commands.get(cmd)
         except LookupError:
             raise errors.SmartProtocolError("bad request %r" % (cmd,))
-        self._command = command(self._backing_transport)
+        self._command = command(self._backing_transport, self._root_client_path)
         self._run_handler_code(self._command.execute, args, {})
 
     def _run_handler_code(self, callable, args, kwargs):
@@ -247,7 +297,7 @@
 
     def do(self, path, revision_id):
         # open transport relative to our base
-        t = self._backing_transport.clone(path)
+        t = self.transport_from_client_path(path)
         control, extra_path = bzrdir.BzrDir.open_containing_from_transport(t)
         repo = control.open_repository()
         tmpf = tempfile.TemporaryFile()

=== modified file 'bzrlib/smart/server.py'
--- bzrlib/smart/server.py	2007-07-20 03:20:20 +0000
+++ bzrlib/smart/server.py	2007-12-05 23:30:57 +0000
@@ -63,6 +63,7 @@
         self.backing_transport = backing_transport
         self._started = threading.Event()
         self._stopped = threading.Event()
+        self.root_client_path = '/'
 
     def serve(self):
         self._should_terminate = False
@@ -134,7 +135,8 @@
         # propogates to the newly accepted socket.
         conn.setblocking(True)
         conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
-        handler = SmartServerSocketStreamMedium(conn, self.backing_transport)
+        handler = SmartServerSocketStreamMedium(
+            conn, self.backing_transport, self.root_client_path)
         connection_thread = threading.Thread(None, handler.serve, name='smart-server-child')
         connection_thread.setDaemon(True)
         connection_thread.start()
@@ -204,13 +206,27 @@
 
     def __init__(self):
         SmartTCPServer.__init__(self, None)
+        self.client_path_extra = None
         
     def get_backing_transport(self, backing_transport_server):
         """Get a backing transport from a server we are decorating."""
         return transport.get_transport(backing_transport_server.get_url())
 
-    def setUp(self, backing_transport_server=None):
-        """Set up server for testing"""
+    def setUp(self, backing_transport_server=None,
+              client_path_extra='/extra/'):
+        """Set up server for testing.
+        
+        :param backing_transport_server: backing server to use.  If not
+            specified, a LocalURLServer at the current working directory will
+            be used.
+        :param client_path_extra: a path segment starting with '/' to append to
+            the root URL for this server.  For instance, a value of '/foo/bar/'
+            will mean the root of the backing transport will be published at a
+            URL like `bzr://127.0.0.1:nnnn/foo/bar/`, rather than
+            `bzr://127.0.0.1:nnnn/`.  Default value is `extra`, so that tests
+            by default will fail unless they do the necessary path translation.
+        """
+        assert client_path_extra.startswith('/')
         from bzrlib.transport.chroot import ChrootServer
         if backing_transport_server is None:
             from bzrlib.transport.local import LocalURLServer
@@ -220,12 +236,18 @@
         self.chroot_server.setUp()
         self.backing_transport = transport.get_transport(
             self.chroot_server.get_url())
+        self.root_client_path = self.client_path_extra = client_path_extra
         self.start_background_thread()
 
     def tearDown(self):
         self.stop_background_thread()
         self.chroot_server.tearDown()
 
+    def get_url(self):
+        url = super(SmartTCPServer_for_testing, self).get_url()
+        assert url.endswith('/')
+        return url[:-1] + self.client_path_extra
+
     def get_bogus_url(self):
         """Return a URL which will fail to connect"""
         return 'bzr://127.0.0.1:1/'

=== modified file 'bzrlib/smart/vfs.py'
--- bzrlib/smart/vfs.py	2007-07-04 08:08:13 +0000
+++ bzrlib/smart/vfs.py	2007-12-05 23:30:57 +0000
@@ -63,6 +63,7 @@
 class HasRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         r = self._backing_transport.has(relpath) and 'yes' or 'no'
         return request.SuccessfulSmartServerResponse((r,))
 
@@ -70,6 +71,7 @@
 class GetRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         try:
             backing_bytes = self._backing_transport.get_bytes(relpath)
         except errors.ReadError:
@@ -81,6 +83,7 @@
 class AppendRequest(VfsRequest):
 
     def do(self, relpath, mode):
+        relpath = self.translate_client_path(relpath)
         self._relpath = relpath
         self._mode = _deserialise_optional_mode(mode)
     
@@ -93,6 +96,7 @@
 class DeleteRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         self._backing_transport.delete(relpath)
         return request.SuccessfulSmartServerResponse(('ok', ))
 
@@ -100,6 +104,9 @@
 class IterFilesRecursiveRequest(VfsRequest):
 
     def do(self, relpath):
+        if not relpath.endswith('/'):
+            relpath += '/'
+        relpath = self.translate_client_path(relpath)
         transport = self._backing_transport.clone(relpath)
         filenames = transport.iter_files_recursive()
         return request.SuccessfulSmartServerResponse(('names',) + tuple(filenames))
@@ -108,6 +115,9 @@
 class ListDirRequest(VfsRequest):
 
     def do(self, relpath):
+        if not relpath.endswith('/'):
+            relpath += '/'
+        relpath = self.translate_client_path(relpath)
         filenames = self._backing_transport.list_dir(relpath)
         return request.SuccessfulSmartServerResponse(('names',) + tuple(filenames))
 
@@ -115,6 +125,7 @@
 class MkdirRequest(VfsRequest):
 
     def do(self, relpath, mode):
+        relpath = self.translate_client_path(relpath)
         self._backing_transport.mkdir(relpath,
                                       _deserialise_optional_mode(mode))
         return request.SuccessfulSmartServerResponse(('ok',))
@@ -123,6 +134,8 @@
 class MoveRequest(VfsRequest):
 
     def do(self, rel_from, rel_to):
+        rel_from = self.translate_client_path(rel_from)
+        rel_to = self.translate_client_path(rel_to)
         self._backing_transport.move(rel_from, rel_to)
         return request.SuccessfulSmartServerResponse(('ok',))
 
@@ -130,6 +143,7 @@
 class PutRequest(VfsRequest):
 
     def do(self, relpath, mode):
+        relpath = self.translate_client_path(relpath)
         self._relpath = relpath
         self._mode = _deserialise_optional_mode(mode)
 
@@ -141,6 +155,7 @@
 class PutNonAtomicRequest(VfsRequest):
 
     def do(self, relpath, mode, create_parent, dir_mode):
+        relpath = self.translate_client_path(relpath)
         self._relpath = relpath
         self._dir_mode = _deserialise_optional_mode(dir_mode)
         self._mode = _deserialise_optional_mode(mode)
@@ -159,6 +174,7 @@
 class ReadvRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         self._relpath = relpath
 
     def do_body(self, body_bytes):
@@ -182,6 +198,8 @@
 class RenameRequest(VfsRequest):
 
     def do(self, rel_from, rel_to):
+        rel_from = self.translate_client_path(rel_from)
+        rel_to = self.translate_client_path(rel_to)
         self._backing_transport.rename(rel_from, rel_to)
         return request.SuccessfulSmartServerResponse(('ok', ))
 
@@ -189,6 +207,7 @@
 class RmdirRequest(VfsRequest):
 
     def do(self, relpath):
+        relpath = self.translate_client_path(relpath)
         self._backing_transport.rmdir(relpath)
         return request.SuccessfulSmartServerResponse(('ok', ))
 
@@ -196,6 +215,9 @@
 class StatRequest(VfsRequest):
 
     def do(self, relpath):
+        if not relpath.endswith('/'):
+            relpath += '/'
+        relpath = self.translate_client_path(relpath)
         stat = self._backing_transport.stat(relpath)
         return request.SuccessfulSmartServerResponse(
             ('stat', str(stat.st_size), oct(stat.st_mode)))

=== modified file 'bzrlib/tests/test_errors.py'
--- bzrlib/tests/test_errors.py	2007-12-04 18:11:51 +0000
+++ bzrlib/tests/test_errors.py	2007-12-05 01:58:00 +0000
@@ -409,6 +409,13 @@
             "Unable to create symlink u'\\xb5' on this platform",
             str(err))
 
+    def test_invalid_url_join(self):
+        """Test the formatting of InvalidURLJoin."""
+        e = errors.InvalidURLJoin('Reason', 'base path', ('args',))
+        self.assertEqual(
+            "Invalid URL join request: Reason: 'base path' + ('args',)",
+            str(e))
+
     def test_incorrect_url(self):
         err = errors.InvalidBugTrackerURL('foo', 'http://bug.com/')
         self.assertEquals(

=== modified file 'bzrlib/tests/test_smart.py'
--- bzrlib/tests/test_smart.py	2007-11-26 03:36:30 +0000
+++ bzrlib/tests/test_smart.py	2007-12-05 02:13:22 +0000
@@ -25,17 +25,49 @@
 """
 
 from StringIO import StringIO
-import tempfile
 import tarfile
 
-from bzrlib import bzrdir, errors, pack, smart, tests
-from bzrlib.smart.request import SmartServerResponse
+from bzrlib import (
+    bzrdir,
+    errors,
+    pack,
+    smart,
+    tests,
+    urlutils,
+    )
+from bzrlib.branch import BranchReferenceFormat
+# Some imports so that "smart.branch.foo", etc, work correctly.
+import bzrlib.smart.branch
 import bzrlib.smart.bzrdir
-import bzrlib.smart.branch
 import bzrlib.smart.repository
+from bzrlib.smart.request import (
+    FailedSmartServerResponse,
+    SmartServerRequest,
+    SmartServerResponse,
+    SuccessfulSmartServerResponse,
+    )
+from bzrlib.transport import chroot, get_transport
 from bzrlib.util import bencode
 
 
+class TestCaseWithChrootedTransport(tests.TestCaseWithTransport):
+
+    def setUp(self):
+        tests.TestCaseWithTransport.setUp(self)
+        self._chroot_server = None
+
+    def get_transport(self, relpath=None):
+        if self._chroot_server is None:
+            backing_transport = tests.TestCaseWithTransport.get_transport(self)
+            self._chroot_server = chroot.ChrootServer(backing_transport)
+            self._chroot_server.setUp()
+            self.addCleanup(self._chroot_server.tearDown)
+        t = get_transport(self._chroot_server.get_url())
+        if relpath is not None:
+            t = t.clone(relpath)
+        return t
+
+
 class TestCaseWithSmartMedium(tests.TestCaseWithTransport):
 
     def setUp(self):
@@ -65,8 +97,39 @@
         self.assertNotEqual(None,
             SmartServerResponse(('ok', )))
 
-
-class TestSmartServerRequestFindRepository(tests.TestCaseWithTransport):
+    def test__str__(self):
+        """SmartServerResponses can be stringified."""
+        self.assertEqual(
+            "<SmartServerResponse status=OK args=('args',) body='body'>",
+            str(SuccessfulSmartServerResponse(('args',), 'body')))
+        self.assertEqual(
+            "<SmartServerResponse status=ERR args=('args',) body='body'>",
+            str(FailedSmartServerResponse(('args',), 'body')))
+
+
+class TestSmartServerRequest(tests.TestCaseWithMemoryTransport):
+
+    def test_translate_client_path(self):
+        transport = self.get_transport()
+        request = SmartServerRequest(transport, 'foo/')
+        self.assertEqual('./', request.translate_client_path('foo/'))
+        self.assertRaises(
+            errors.InvalidURLJoin, request.translate_client_path, 'foo/..')
+        self.assertRaises(
+            errors.PathNotChild, request.translate_client_path, '/')
+        self.assertRaises(
+            errors.PathNotChild, request.translate_client_path, 'bar/')
+        self.assertEqual('./baz', request.translate_client_path('foo/baz'))
+
+    def test_transport_from_client_path(self):
+        transport = self.get_transport()
+        request = SmartServerRequest(transport, 'foo/')
+        self.assertEqual(
+            transport.base,
+            request.transport_from_client_path('foo/').base)
+
+
+class TestSmartServerRequestFindRepository(tests.TestCaseWithMemoryTransport):
     """Tests for BzrDir.find_repository."""
 
     def test_no_repository(self):
@@ -75,7 +138,7 @@
         request = smart.bzrdir.SmartServerRequestFindRepository(backing)
         self.make_bzrdir('.')
         self.assertEqual(SmartServerResponse(('norepository', )),
-            request.execute(backing.local_abspath('')))
+            request.execute('/'))
 
     def test_nonshared_repository(self):
         # nonshared repositorys only allow 'find' to return a handle when the 
@@ -84,10 +147,10 @@
         backing = self.get_transport()
         request = smart.bzrdir.SmartServerRequestFindRepository(backing)
         result = self._make_repository_and_result()
-        self.assertEqual(result, request.execute(backing.local_abspath('')))
+        self.assertEqual(result, request.execute(''))
         self.make_bzrdir('subdir')
         self.assertEqual(SmartServerResponse(('norepository', )),
-            request.execute(backing.local_abspath('subdir')))
+            request.execute('subdir'))
 
     def _make_repository_and_result(self, shared=False, format=None):
         """Convenience function to setup a repository.
@@ -110,15 +173,15 @@
         backing = self.get_transport()
         request = smart.bzrdir.SmartServerRequestFindRepository(backing)
         result = self._make_repository_and_result(shared=True)
-        self.assertEqual(result, request.execute(backing.local_abspath('')))
+        self.assertEqual(result, request.execute(''))
         self.make_bzrdir('subdir')
         result2 = SmartServerResponse(result.args[0:1] + ('..', ) + result.args[2:])
         self.assertEqual(result2,
-            request.execute(backing.local_abspath('subdir')))
+            request.execute('subdir'))
         self.make_bzrdir('subdir/deeper')
         result3 = SmartServerResponse(result.args[0:1] + ('../..', ) + result.args[2:])
         self.assertEqual(result3,
-            request.execute(backing.local_abspath('subdir/deeper')))
+            request.execute('subdir/deeper'))
 
     def test_rich_root_and_subtree_encoding(self):
         """Test for the format attributes for rich root and subtree support."""
@@ -128,17 +191,17 @@
         # check the test will be valid
         self.assertEqual('yes', result.args[2])
         self.assertEqual('yes', result.args[3])
-        self.assertEqual(result, request.execute(backing.local_abspath('')))
-
-
-class TestSmartServerRequestInitializeBzrDir(tests.TestCaseWithTransport):
+        self.assertEqual(result, request.execute(''))
+
+
+class TestSmartServerRequestInitializeBzrDir(tests.TestCaseWithMemoryTransport):
 
     def test_empty_dir(self):
         """Initializing an empty dir should succeed and do it."""
         backing = self.get_transport()
         request = smart.bzrdir.SmartServerRequestInitializeBzrDir(backing)
         self.assertEqual(SmartServerResponse(('ok', )),
-            request.execute(backing.local_abspath('.')))
+            request.execute('.'))
         made_dir = bzrdir.BzrDir.open_from_transport(backing)
         # no branch, tree or repository is expected with the current 
         # default formart.
@@ -151,7 +214,7 @@
         backing = self.get_transport()
         request = smart.bzrdir.SmartServerRequestInitializeBzrDir(backing)
         self.assertRaises(errors.NoSuchFile,
-            request.execute, backing.local_abspath('subdir'))
+            request.execute, 'subdir')
 
     def test_initialized_dir(self):
         """Initializing an extant bzrdir should fail like the bzrdir api."""
@@ -159,10 +222,10 @@
         request = smart.bzrdir.SmartServerRequestInitializeBzrDir(backing)
         self.make_bzrdir('subdir')
         self.assertRaises(errors.FileExists,
-            request.execute, backing.local_abspath('subdir'))
-
-
-class TestSmartServerRequestOpenBranch(tests.TestCaseWithTransport):
+            request.execute, 'subdir')
+
+
+class TestSmartServerRequestOpenBranch(TestCaseWithChrootedTransport):
 
     def test_no_branch(self):
         """When there is no branch, ('nobranch', ) is returned."""
@@ -170,7 +233,7 @@
         request = smart.bzrdir.SmartServerRequestOpenBranch(backing)
         self.make_bzrdir('.')
         self.assertEqual(SmartServerResponse(('nobranch', )),
-            request.execute(backing.local_abspath('')))
+            request.execute('/'))
 
     def test_branch(self):
         """When there is a branch, 'ok' is returned."""
@@ -178,7 +241,7 @@
         request = smart.bzrdir.SmartServerRequestOpenBranch(backing)
         self.make_branch('.')
         self.assertEqual(SmartServerResponse(('ok', '')),
-            request.execute(backing.local_abspath('')))
+            request.execute('/'))
 
     def test_branch_reference(self):
         """When there is a branch reference, the reference URL is returned."""
@@ -186,15 +249,13 @@
         request = smart.bzrdir.SmartServerRequestOpenBranch(backing)
         branch = self.make_branch('branch')
         checkout = branch.create_checkout('reference',lightweight=True)
-        # TODO: once we have an API to probe for references of any sort, we
-        # can use it here.
-        reference_url = backing.abspath('branch') + '/'
+        reference_url = BranchReferenceFormat().get_reference(checkout.bzrdir)
         self.assertFileEqual(reference_url, 'reference/.bzr/branch/location')
         self.assertEqual(SmartServerResponse(('ok', reference_url)),
-            request.execute(backing.local_abspath('reference')))
-
-
-class TestSmartServerRequestRevisionHistory(tests.TestCaseWithTransport):
+            request.execute('/reference'))
+
+
+class TestSmartServerRequestRevisionHistory(tests.TestCaseWithMemoryTransport):
 
     def test_empty(self):
         """For an empty branch, the body is empty."""
@@ -202,7 +263,7 @@
         request = smart.branch.SmartServerRequestRevisionHistory(backing)
         self.make_branch('.')
         self.assertEqual(SmartServerResponse(('ok', ), ''),
-            request.execute(backing.local_abspath('')))
+            request.execute(''))
 
     def test_not_empty(self):
         """For a non-empty branch, the body is empty."""
@@ -216,10 +277,10 @@
         tree.unlock()
         self.assertEqual(
             SmartServerResponse(('ok', ), ('\x00'.join([r1, r2]))),
-            request.execute(backing.local_abspath('')))
-
-
-class TestSmartServerBranchRequest(tests.TestCaseWithTransport):
+            request.execute(''))
+
+
+class TestSmartServerBranchRequest(tests.TestCaseWithMemoryTransport):
 
     def test_no_branch(self):
         """When there is a bzrdir and no branch, NotBranchError is raised."""
@@ -227,7 +288,7 @@
         request = smart.branch.SmartServerBranchRequest(backing)
         self.make_bzrdir('.')
         self.assertRaises(errors.NotBranchError,
-            request.execute, backing.local_abspath(''))
+            request.execute, '')
 
     def test_branch_reference(self):
         """When there is a branch reference, NotBranchError is raised."""
@@ -236,10 +297,10 @@
         branch = self.make_branch('branch')
         checkout = branch.create_checkout('reference',lightweight=True)
         self.assertRaises(errors.NotBranchError,
-            request.execute, backing.local_abspath('checkout'))
-
-
-class TestSmartServerBranchRequestLastRevisionInfo(tests.TestCaseWithTransport):
+            request.execute, 'checkout')
+
+
+class TestSmartServerBranchRequestLastRevisionInfo(tests.TestCaseWithMemoryTransport):
 
     def test_empty(self):
         """For an empty branch, the result is ('ok', '0', 'null:')."""
@@ -247,7 +308,7 @@
         request = smart.branch.SmartServerBranchRequestLastRevisionInfo(backing)
         self.make_branch('.')
         self.assertEqual(SmartServerResponse(('ok', '0', 'null:')),
-            request.execute(backing.local_abspath('')))
+            request.execute(''))
 
     def test_not_empty(self):
         """For a non-empty branch, the result is ('ok', 'revno', 'revid')."""
@@ -262,10 +323,10 @@
         tree.unlock()
         self.assertEqual(
             SmartServerResponse(('ok', '2', rev_id_utf8)),
-            request.execute(backing.local_abspath('')))
-
-
-class TestSmartServerBranchRequestGetConfigFile(tests.TestCaseWithTransport):
+            request.execute(''))
+
+
+class TestSmartServerBranchRequestGetConfigFile(tests.TestCaseWithMemoryTransport):
 
     def test_default(self):
         """With no file, we get empty content."""
@@ -275,7 +336,7 @@
         # there should be no file by default
         content = ''
         self.assertEqual(SmartServerResponse(('ok', ), content),
-            request.execute(backing.local_abspath('')))
+            request.execute(''))
 
     def test_with_content(self):
         # SmartServerBranchGetConfigFile should return the content from
@@ -286,10 +347,10 @@
         branch = self.make_branch('.')
         branch.control_files.put_utf8('branch.conf', 'foo bar baz')
         self.assertEqual(SmartServerResponse(('ok', ), 'foo bar baz'),
-            request.execute(backing.local_abspath('')))
-
-
-class TestSmartServerBranchRequestSetLastRevision(tests.TestCaseWithTransport):
+            request.execute(''))
+
+
+class TestSmartServerBranchRequestSetLastRevision(tests.TestCaseWithMemoryTransport):
 
     def test_empty(self):
         backing = self.get_transport()
@@ -301,7 +362,7 @@
         try:
             self.assertEqual(SmartServerResponse(('ok',)),
                 request.execute(
-                    backing.local_abspath(''), branch_token, repo_token,
+                    '', branch_token, repo_token,
                     'null:'))
         finally:
             b.unlock()
@@ -318,7 +379,7 @@
             self.assertEqual(
                 SmartServerResponse(('NoSuchRevision', revision_id)),
                 request.execute(
-                    backing.local_abspath(''), branch_token, repo_token,
+                    '', branch_token, repo_token,
                     revision_id))
         finally:
             b.unlock()
@@ -340,7 +401,7 @@
             self.assertEqual(
                 SmartServerResponse(('ok',)),
                 request.execute(
-                    backing.local_abspath(''), branch_token, repo_token,
+                    '', branch_token, repo_token,
                     rev_id_utf8))
             self.assertEqual([rev_id_utf8], tree.branch.revision_history())
         finally:
@@ -364,17 +425,17 @@
             self.assertEqual(
                 SmartServerResponse(('ok',)),
                 request.execute(
-                    backing.local_abspath(''), branch_token, repo_token,
+                    '', branch_token, repo_token,
                     rev_id_utf8))
             self.assertEqual([rev_id_utf8], tree.branch.revision_history())
         finally:
             tree.branch.unlock()
 
 
-class TestSmartServerBranchRequestLockWrite(tests.TestCaseWithTransport):
+class TestSmartServerBranchRequestLockWrite(tests.TestCaseWithMemoryTransport):
 
     def setUp(self):
-        tests.TestCaseWithTransport.setUp(self)
+        tests.TestCaseWithMemoryTransport.setUp(self)
         self.reduceLockdirTimeout()
 
     def test_lock_write_on_unlocked_branch(self):
@@ -382,7 +443,7 @@
         request = smart.branch.SmartServerBranchRequestLockWrite(backing)
         branch = self.make_branch('.', format='knit')
         repository = branch.repository
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         branch_nonce = branch.control_files._lock.peek().get('nonce')
         repository_nonce = repository.control_files._lock.peek().get('nonce')
         self.assertEqual(
@@ -400,7 +461,7 @@
         branch.lock_write()
         branch.leave_lock_in_place()
         branch.unlock()
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         self.assertEqual(
             SmartServerResponse(('LockContention',)), response)
 
@@ -414,7 +475,7 @@
         branch.leave_lock_in_place()
         branch.repository.leave_lock_in_place()
         branch.unlock()
-        response = request.execute(backing.local_abspath(''),
+        response = request.execute('',
                                    branch_token, repo_token)
         self.assertEqual(
             SmartServerResponse(('ok', branch_token, repo_token)), response)
@@ -429,7 +490,7 @@
         branch.leave_lock_in_place()
         branch.repository.leave_lock_in_place()
         branch.unlock()
-        response = request.execute(backing.local_abspath(''),
+        response = request.execute('',
                                    branch_token+'xxx', repo_token)
         self.assertEqual(
             SmartServerResponse(('TokenMismatch',)), response)
@@ -441,7 +502,7 @@
         branch.repository.lock_write()
         branch.repository.leave_lock_in_place()
         branch.repository.unlock()
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         self.assertEqual(
             SmartServerResponse(('LockContention',)), response)
 
@@ -449,16 +510,18 @@
         backing = self.get_readonly_transport()
         request = smart.branch.SmartServerBranchRequestLockWrite(backing)
         branch = self.make_branch('.')
-        response = request.execute('')
+        root = self.get_transport().clone('/')
+        path = urlutils.relative_url(root.base, self.get_transport().base)
+        response = request.execute(path)
         error_name, lock_str, why_str = response.args
         self.assertFalse(response.is_successful())
         self.assertEqual('LockFailed', error_name)
 
 
-class TestSmartServerBranchRequestUnlock(tests.TestCaseWithTransport):
+class TestSmartServerBranchRequestUnlock(tests.TestCaseWithMemoryTransport):
 
     def setUp(self):
-        tests.TestCaseWithTransport.setUp(self)
+        tests.TestCaseWithMemoryTransport.setUp(self)
         self.reduceLockdirTimeout()
 
     def test_unlock_on_locked_branch_and_repo(self):
@@ -474,7 +537,7 @@
         branch.leave_lock_in_place()
         branch.repository.leave_lock_in_place()
         branch.unlock()
-        response = request.execute(backing.local_abspath(''),
+        response = request.execute('',
                                    branch_token, repo_token)
         self.assertEqual(
             SmartServerResponse(('ok',)), response)
@@ -489,7 +552,7 @@
         request = smart.branch.SmartServerBranchRequestUnlock(backing)
         branch = self.make_branch('.', format='knit')
         response = request.execute(
-            backing.local_abspath(''), 'branch token', 'repo token')
+            '', 'branch token', 'repo token')
         self.assertEqual(
             SmartServerResponse(('TokenMismatch',)), response)
 
@@ -504,12 +567,12 @@
         # Issue branch lock_write request on the unlocked branch (with locked
         # repo).
         response = request.execute(
-            backing.local_abspath(''), 'branch token', repo_token)
+            '', 'branch token', repo_token)
         self.assertEqual(
             SmartServerResponse(('TokenMismatch',)), response)
 
 
-class TestSmartServerRepositoryRequest(tests.TestCaseWithTransport):
+class TestSmartServerRepositoryRequest(tests.TestCaseWithMemoryTransport):
 
     def test_no_repository(self):
         """Raise NoRepositoryPresent when there is a bzrdir and no repo."""
@@ -522,10 +585,10 @@
         self.make_repository('.', shared=True)
         self.make_bzrdir('subdir')
         self.assertRaises(errors.NoRepositoryPresent,
-            request.execute, backing.local_abspath('subdir'))
-
-
-class TestSmartServerRepositoryGetRevisionGraph(tests.TestCaseWithTransport):
+            request.execute, 'subdir')
+
+
+class TestSmartServerRepositoryGetRevisionGraph(tests.TestCaseWithMemoryTransport):
 
     def test_none_argument(self):
         backing = self.get_transport()
@@ -540,7 +603,7 @@
         # the lines of revision_id->revision_parent_list has no guaranteed
         # order coming out of a dict, so sort both our test and response
         lines = sorted([' '.join([r2, r1]), r1])
-        response = request.execute(backing.local_abspath(''), '')
+        response = request.execute('', '')
         response.body = '\n'.join(sorted(response.body.split('\n')))
 
         self.assertEqual(
@@ -558,7 +621,7 @@
         tree.unlock()
 
         self.assertEqual(SmartServerResponse(('ok', ), rev_id_utf8),
-            request.execute(backing.local_abspath(''), rev_id_utf8))
+            request.execute('', rev_id_utf8))
     
     def test_no_such_revision(self):
         backing = self.get_transport()
@@ -572,10 +635,10 @@
         # Note that it still returns body (of zero bytes).
         self.assertEqual(
             SmartServerResponse(('nosuchrevision', 'missingrevision', ), ''),
-            request.execute(backing.local_abspath(''), 'missingrevision'))
-
-
-class TestSmartServerRequestHasRevision(tests.TestCaseWithTransport):
+            request.execute('', 'missingrevision'))
+
+
+class TestSmartServerRequestHasRevision(tests.TestCaseWithMemoryTransport):
 
     def test_missing_revision(self):
         """For a missing revision, ('no', ) is returned."""
@@ -583,7 +646,7 @@
         request = smart.repository.SmartServerRequestHasRevision(backing)
         self.make_repository('.')
         self.assertEqual(SmartServerResponse(('no', )),
-            request.execute(backing.local_abspath(''), 'revid'))
+            request.execute('', 'revid'))
 
     def test_present_revision(self):
         """For a present revision, ('yes', ) is returned."""
@@ -597,10 +660,10 @@
         tree.unlock()
         self.assertTrue(tree.branch.repository.has_revision(rev_id_utf8))
         self.assertEqual(SmartServerResponse(('yes', )),
-            request.execute(backing.local_abspath(''), rev_id_utf8))
-
-
-class TestSmartServerRepositoryGatherStats(tests.TestCaseWithTransport):
+            request.execute('', rev_id_utf8))
+
+
+class TestSmartServerRepositoryGatherStats(tests.TestCaseWithMemoryTransport):
 
     def test_empty_revid(self):
         """With an empty revid, we get only size an number and revisions"""
@@ -611,7 +674,7 @@
         size = stats['size']
         expected_body = 'revisions: 0\nsize: %d\n' % size
         self.assertEqual(SmartServerResponse(('ok', ), expected_body),
-                         request.execute(backing.local_abspath(''), '', 'no'))
+                         request.execute('', '', 'no'))
 
     def test_revid_with_committers(self):
         """For a revid we get more infos."""
@@ -634,7 +697,7 @@
                          'revisions: 2\n'
                          'size: %d\n' % size)
         self.assertEqual(SmartServerResponse(('ok', ), expected_body),
-                         request.execute(backing.local_abspath(''),
+                         request.execute('',
                                          rev_id_utf8, 'no'))
 
     def test_not_empty_repository_with_committers(self):
@@ -660,11 +723,11 @@
                          'revisions: 2\n'
                          'size: %d\n' % size)
         self.assertEqual(SmartServerResponse(('ok', ), expected_body),
-                         request.execute(backing.local_abspath(''),
+                         request.execute('',
                                          rev_id_utf8, 'yes'))
 
 
-class TestSmartServerRepositoryIsShared(tests.TestCaseWithTransport):
+class TestSmartServerRepositoryIsShared(tests.TestCaseWithMemoryTransport):
 
     def test_is_shared(self):
         """For a shared repository, ('yes', ) is returned."""
@@ -672,7 +735,7 @@
         request = smart.repository.SmartServerRepositoryIsShared(backing)
         self.make_repository('.', shared=True)
         self.assertEqual(SmartServerResponse(('yes', )),
-            request.execute(backing.local_abspath(''), ))
+            request.execute('', ))
 
     def test_is_not_shared(self):
         """For a shared repository, ('no', ) is returned."""
@@ -680,20 +743,20 @@
         request = smart.repository.SmartServerRepositoryIsShared(backing)
         self.make_repository('.', shared=False)
         self.assertEqual(SmartServerResponse(('no', )),
-            request.execute(backing.local_abspath(''), ))
-
-
-class TestSmartServerRepositoryLockWrite(tests.TestCaseWithTransport):
+            request.execute('', ))
+
+
+class TestSmartServerRepositoryLockWrite(tests.TestCaseWithMemoryTransport):
 
     def setUp(self):
-        tests.TestCaseWithTransport.setUp(self)
+        tests.TestCaseWithMemoryTransport.setUp(self)
         self.reduceLockdirTimeout()
 
     def test_lock_write_on_unlocked_repo(self):
         backing = self.get_transport()
         request = smart.repository.SmartServerRepositoryLockWrite(backing)
         repository = self.make_repository('.', format='knit')
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         nonce = repository.control_files._lock.peek().get('nonce')
         self.assertEqual(SmartServerResponse(('ok', nonce)), response)
         # The repository is now locked.  Verify that with a new repository
@@ -708,7 +771,7 @@
         repository.lock_write()
         repository.leave_lock_in_place()
         repository.unlock()
-        response = request.execute(backing.local_abspath(''))
+        response = request.execute('')
         self.assertEqual(
             SmartServerResponse(('LockContention',)), response)
 
@@ -721,10 +784,10 @@
         self.assertEqual('LockFailed', response.args[0])
 
 
-class TestSmartServerRepositoryUnlock(tests.TestCaseWithTransport):
+class TestSmartServerRepositoryUnlock(tests.TestCaseWithMemoryTransport):
 
     def setUp(self):
-        tests.TestCaseWithTransport.setUp(self)
+        tests.TestCaseWithMemoryTransport.setUp(self)
         self.reduceLockdirTimeout()
 
     def test_unlock_on_locked_repo(self):
@@ -734,7 +797,7 @@
         token = repository.lock_write()
         repository.leave_lock_in_place()
         repository.unlock()
-        response = request.execute(backing.local_abspath(''), token)
+        response = request.execute('', token)
         self.assertEqual(
             SmartServerResponse(('ok',)), response)
         # The repository is now unlocked.  Verify that with a new repository
@@ -747,7 +810,7 @@
         backing = self.get_transport()
         request = smart.repository.SmartServerRepositoryUnlock(backing)
         repository = self.make_repository('.', format='knit')
-        response = request.execute(backing.local_abspath(''), 'some token')
+        response = request.execute('', 'some token')
         self.assertEqual(
             SmartServerResponse(('TokenMismatch',)), response)
 
@@ -761,7 +824,7 @@
         # make some extraneous junk in the repository directory which should
         # not be copied
         self.build_tree(['.bzr/repository/extra-junk'])
-        response = request.execute(backing.local_abspath(''), 'bz2')
+        response = request.execute('', 'bz2')
         self.assertEqual(('ok',), response.args)
         # body should be a tbz2
         body_file = StringIO(response.body)
@@ -776,7 +839,7 @@
             "extraneous file present in tar file")
 
 
-class TestSmartServerRepositoryStreamKnitData(tests.TestCaseWithTransport):
+class TestSmartServerRepositoryStreamKnitData(tests.TestCaseWithMemoryTransport):
 
     def test_fetch_revisions(self):
         backing = self.get_transport()
@@ -790,7 +853,7 @@
         r1 = tree.commit('2nd commit', rev_id=rev_id2_utf8)
         tree.unlock()
 
-        response = request.execute(backing.local_abspath(''), rev_id2_utf8)
+        response = request.execute('', rev_id2_utf8)
         self.assertEqual(('ok',), response.args)
         from cStringIO import StringIO
         unpacker = pack.ContainerReader(StringIO(response.body))
@@ -808,13 +871,13 @@
         request = smart.repository.SmartServerRepositoryStreamKnitDataForRevisions(backing)
         repo = self.make_repository('.')
         rev_id1_utf8 = u'\xc8'.encode('utf-8')
-        response = request.execute(backing.local_abspath(''), rev_id1_utf8)
+        response = request.execute('', rev_id1_utf8)
         self.assertEqual(
             SmartServerResponse(('NoSuchRevision', rev_id1_utf8)),
             response)
 
 
-class TestSmartServerIsReadonly(tests.TestCaseWithTransport):
+class TestSmartServerIsReadonly(tests.TestCaseWithMemoryTransport):
 
     def test_is_readonly_no(self):
         backing = self.get_transport()

=== modified file 'bzrlib/tests/test_smart_transport.py'
--- bzrlib/tests/test_smart_transport.py	2007-11-09 20:49:54 +0000
+++ bzrlib/tests/test_smart_transport.py	2007-12-05 06:49:15 +0000
@@ -34,7 +34,6 @@
         client,
         medium,
         protocol,
-        request,
         request as _mod_request,
         server,
         vfs,
@@ -1113,7 +1112,7 @@
     """
         
     def test_hello(self):
-        cmd = request.HelloRequest(None)
+        cmd = _mod_request.HelloRequest(None, '/')
         response = cmd.execute()
         self.assertEqual(('ok', '2'), response.args)
         self.assertEqual(None, response.body)
@@ -1125,7 +1124,7 @@
         wt.add('hello')
         rev_id = wt.commit('add hello')
         
-        cmd = request.GetBundleRequest(self.get_transport())
+        cmd = _mod_request.GetBundleRequest(self.get_transport(), '/')
         response = cmd.execute('.', rev_id)
         bundle = serializer.read_bundle(StringIO(response.body))
         self.assertEqual((), response.args)
@@ -1140,12 +1139,13 @@
 
     def build_handler(self, transport):
         """Returns a handler for the commands in protocol version one."""
-        return request.SmartServerRequestHandler(transport,
-                                                 request.request_handlers)
+        return _mod_request.SmartServerRequestHandler(
+            transport, _mod_request.request_handlers, '/')
 
     def test_construct_request_handler(self):
         """Constructing a request handler should be easy and set defaults."""
-        handler = request.SmartServerRequestHandler(None, None)
+        handler = _mod_request.SmartServerRequestHandler(None, commands=None,
+                root_client_path='/')
         self.assertFalse(handler.finished_reading)
 
     def test_hello(self):
@@ -1157,7 +1157,7 @@
     def test_disable_vfs_handler_classes_via_environment(self):
         # VFS handler classes will raise an error from "execute" if
         # BZR_NO_SMART_VFS is set.
-        handler = vfs.HasRequest(None)
+        handler = vfs.HasRequest(None, '/')
         # set environment variable after construction to make sure it's
         # examined.
         # Note that we can safely clobber BZR_NO_SMART_VFS here, because setUp
@@ -1222,7 +1222,7 @@
         handler.accept_body('100,1')
         handler.end_of_body()
         self.assertTrue(handler.finished_reading)
-        self.assertEqual(('ShortReadvError', 'a-file', '100', '1', '0'),
+        self.assertEqual(('ShortReadvError', './a-file', '100', '1', '0'),
             handler.response.args)
         self.assertEqual(None, handler.response.body)
 
@@ -1313,8 +1313,8 @@
             self.to_server)
         self.client_protocol = self.client_protocol_class(self.client_medium)
         self.smart_server = InstrumentedServerProtocol(self.server_to_client)
-        self.smart_server_request = request.SmartServerRequestHandler(
-            None, request.request_handlers)
+        self.smart_server_request = _mod_request.SmartServerRequestHandler(
+            None, _mod_request.request_handlers, root_client_path='/')
 
     def assertOffsetSerialisation(self, expected_offsets, expected_serialised,
         client):
@@ -1329,7 +1329,7 @@
         """
         # XXX: '_deserialise_offsets' should be a method of the
         # SmartServerRequestProtocol in future.
-        readv_cmd = vfs.ReadvRequest(None)
+        readv_cmd = vfs.ReadvRequest(None, '/')
         offsets = readv_cmd._deserialise_offsets(expected_serialised)
         self.assertEqual(expected_offsets, offsets)
         serialised = client._serialise_offsets(offsets)
@@ -1344,7 +1344,7 @@
             def do_body(cmd, body_bytes):
                 self.end_received = True
                 self.assertEqual('abcdefg', body_bytes)
-                return request.SuccessfulSmartServerResponse(('ok', ))
+                return _mod_request.SuccessfulSmartServerResponse(('ok', ))
         smart_protocol.request._command = FakeCommand()
         # Call accept_bytes to make sure that internal state like _body_decoder
         # is initialised.  This test should probably be given a clearer
@@ -1509,7 +1509,7 @@
             None, lambda x: None)
         self.assertEqual(1, smart_protocol.next_read_size())
         smart_protocol._send_response(
-            request.SuccessfulSmartServerResponse(('x',)))
+            _mod_request.SuccessfulSmartServerResponse(('x',)))
         self.assertEqual(0, smart_protocol.next_read_size())
 
     def test__send_response_errors_with_base_response(self):
@@ -1517,7 +1517,7 @@
         smart_protocol = protocol.SmartServerRequestProtocolOne(
             None, lambda x: None)
         self.assertRaises(AttributeError, smart_protocol._send_response,
-            request.SmartServerResponse(('x',)))
+            _mod_request.SmartServerResponse(('x',)))
 
     def test_query_version(self):
         """query_version on a SmartClientProtocolOne should return a number.
@@ -1704,7 +1704,7 @@
 
     def test_body_stream_error_serialistion(self):
         stream = ['first chunk',
-                  request.FailedSmartServerResponse(
+                  _mod_request.FailedSmartServerResponse(
                       ('FailureName', 'failure arg'))]
         expected_bytes = (
             'chunked\n' + 'b\nfirst chunk' +
@@ -1796,7 +1796,7 @@
             None, lambda x: None)
         self.assertEqual(1, smart_protocol.next_read_size())
         smart_protocol._send_response(
-            request.SuccessfulSmartServerResponse(('x',)))
+            _mod_request.SuccessfulSmartServerResponse(('x',)))
         self.assertEqual(0, smart_protocol.next_read_size())
 
     def test__send_response_with_body_stream_sets_finished_reading(self):
@@ -1804,7 +1804,7 @@
             None, lambda x: None)
         self.assertEqual(1, smart_protocol.next_read_size())
         smart_protocol._send_response(
-            request.SuccessfulSmartServerResponse(('x',), body_stream=[]))
+            _mod_request.SuccessfulSmartServerResponse(('x',), body_stream=[]))
         self.assertEqual(0, smart_protocol.next_read_size())
 
     def test__send_response_errors_with_base_response(self):
@@ -1812,7 +1812,7 @@
         smart_protocol = protocol.SmartServerRequestProtocolTwo(
             None, lambda x: None)
         self.assertRaises(AttributeError, smart_protocol._send_response,
-            request.SmartServerResponse(('x',)))
+            _mod_request.SmartServerResponse(('x',)))
 
     def test__send_response_includes_failure_marker(self):
         """FailedSmartServerResponse have 'failed\n' after the version."""
@@ -1820,7 +1820,7 @@
         smart_protocol = protocol.SmartServerRequestProtocolTwo(
             None, out_stream.write)
         smart_protocol._send_response(
-            request.FailedSmartServerResponse(('x',)))
+            _mod_request.FailedSmartServerResponse(('x',)))
         self.assertEqual(protocol.RESPONSE_VERSION_TWO + 'failed\nx\n',
                          out_stream.getvalue())
 
@@ -1830,7 +1830,7 @@
         smart_protocol = protocol.SmartServerRequestProtocolTwo(
             None, out_stream.write)
         smart_protocol._send_response(
-            request.SuccessfulSmartServerResponse(('x',)))
+            _mod_request.SuccessfulSmartServerResponse(('x',)))
         self.assertEqual(protocol.RESPONSE_VERSION_TWO + 'success\nx\n',
                          out_stream.getvalue())
 
@@ -1991,7 +1991,7 @@
         smart_protocol.read_response_tuple(True)
         expected_chunks = [
             'aaaa',
-            request.FailedSmartServerResponse(('error arg1', 'arg2'))]
+            _mod_request.FailedSmartServerResponse(('error arg1', 'arg2'))]
         stream = smart_protocol.read_streamed_body()
         self.assertEqual(expected_chunks, list(stream))
 
@@ -2252,7 +2252,7 @@
         decoder.accept_bytes(chunk_one + error_signal + error_chunks + finish)
         self.assertTrue(decoder.finished_reading)
         self.assertEqual('first chunk', decoder.read_next_chunk())
-        expected_failure = request.FailedSmartServerResponse(
+        expected_failure = _mod_request.FailedSmartServerResponse(
             ('part1', 'part2'))
         self.assertEqual(expected_failure, decoder.read_next_chunk())
 
@@ -2268,19 +2268,19 @@
 class TestSuccessfulSmartServerResponse(tests.TestCase):
 
     def test_construct_no_body(self):
-        response = request.SuccessfulSmartServerResponse(('foo', 'bar'))
+        response = _mod_request.SuccessfulSmartServerResponse(('foo', 'bar'))
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual(None, response.body)
 
     def test_construct_with_body(self):
-        response = request.SuccessfulSmartServerResponse(
-            ('foo', 'bar'), 'bytes')
+        response = _mod_request.SuccessfulSmartServerResponse(('foo', 'bar'),
+                                                              'bytes')
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual('bytes', response.body)
 
     def test_construct_with_body_stream(self):
         bytes_iterable = ['abc']
-        response = request.SuccessfulSmartServerResponse(
+        response = _mod_request.SuccessfulSmartServerResponse(
             ('foo', 'bar'), body_stream=bytes_iterable)
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual(bytes_iterable, response.body_stream)
@@ -2289,27 +2289,27 @@
         """'body' and 'body_stream' are mutually exclusive."""
         self.assertRaises(
             errors.BzrError,
-            request.SuccessfulSmartServerResponse, (), 'body', ['stream'])
+            _mod_request.SuccessfulSmartServerResponse, (), 'body', ['stream'])
 
     def test_is_successful(self):
         """is_successful should return True for SuccessfulSmartServerResponse."""
-        response = request.SuccessfulSmartServerResponse(('error',))
+        response = _mod_request.SuccessfulSmartServerResponse(('error',))
         self.assertEqual(True, response.is_successful())
 
 
 class TestFailedSmartServerResponse(tests.TestCase):
 
     def test_construct(self):
-        response = request.FailedSmartServerResponse(('foo', 'bar'))
+        response = _mod_request.FailedSmartServerResponse(('foo', 'bar'))
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual(None, response.body)
-        response = request.FailedSmartServerResponse(('foo', 'bar'), 'bytes')
+        response = _mod_request.FailedSmartServerResponse(('foo', 'bar'), 'bytes')
         self.assertEqual(('foo', 'bar'), response.args)
         self.assertEqual('bytes', response.body)
 
     def test_is_successful(self):
         """is_successful should return False for FailedSmartServerResponse."""
-        response = request.FailedSmartServerResponse(('error',))
+        response = _mod_request.FailedSmartServerResponse(('error',))
         self.assertEqual(False, response.is_successful())
 
 

=== modified file 'bzrlib/tests/test_transport_implementations.py'
--- bzrlib/tests/test_transport_implementations.py	2007-11-25 14:42:29 +0000
+++ bzrlib/tests/test_transport_implementations.py	2007-12-05 23:30:57 +0000
@@ -169,6 +169,11 @@
         self.assertEqual(True, t.has_any(['b', 'b', 'b']))
 
     def test_has_root_works(self):
+        from bzrlib.smart import server
+        if self.transport_server is server.SmartTCPServer_for_testing:
+            raise TestNotApplicable(
+                "SmartTCPServer_for_testing intentionally does not allow "
+                "access to /.")
         current_transport = self.get_transport()
         self.assertTrue(current_transport.has('/'))
         root = current_transport.clone('/')

=== modified file 'bzrlib/transport/chroot.py'
--- bzrlib/transport/chroot.py	2007-08-22 08:09:05 +0000
+++ bzrlib/transport/chroot.py	2007-12-05 01:58:00 +0000
@@ -116,6 +116,9 @@
     def has(self, relpath):
         return self._call('has', relpath)
 
+    def is_readonly(self):
+        return self.server.backing_transport.is_readonly()
+
     def iter_files_recursive(self):
         backing_transport = self.server.backing_transport.clone(
             self._safe_relpath('.'))

=== modified file 'bzrlib/transport/http/wsgi.py'
--- bzrlib/transport/http/wsgi.py	2007-08-15 19:16:00 +0000
+++ bzrlib/transport/http/wsgi.py	2007-12-05 01:58:00 +0000
@@ -85,11 +85,14 @@
 class SmartWSGIApp(object):
     """A WSGI application for the bzr smart server."""
 
-    def __init__(self, backing_transport):
+    def __init__(self, backing_transport, root_client_path='/'):
         """Constructor.
 
         :param backing_transport: a transport.  Requests will be processed
             relative to this transport.
+        :param root_client_path: the client path that maps to the root of
+            backing_transport.  This is used to interpret relpaths received from
+            the client.
         """
         # Use a ChrootTransportDecorator so that this web application won't
         # accidentally let people access locations they shouldn't.
@@ -98,6 +101,7 @@
         self.chroot_server = chroot.ChrootServer(backing_transport)
         self.chroot_server.setUp()
         self.backing_transport = get_transport(self.chroot_server.get_url())
+        self.root_client_path = root_client_path
         # While the chroot server can technically be torn down at this point,
         # as all it does is remove the scheme registration from transport's 
         # protocol dictionary, we don't *just in case* there are parts of 
@@ -139,6 +143,7 @@
             request_bytes = request_bytes[len(protocol.REQUEST_VERSION_TWO):]
         else:
             protocol_class = protocol.SmartServerRequestProtocolOne
-        server_protocol = protocol_class(transport, write_func)
+        server_protocol = protocol_class(
+            transport, write_func, self.root_client_path)
         server_protocol.accept_bytes(request_bytes)
         return server_protocol

# Begin bundle
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


More information about the bazaar mailing list