[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
IyBCYXphYXIgcmV2aXNpb24gYnVuZGxlIHY0CiMKQlpoOTFBWSZTWbYPEWwAN/F/gGR1QgBb////
f///+v////pgQ/7vSbzXe9u83Qe7iVwAAe3e4aHQd93O8feNaeXt1575vo+806b77bTZle2u8+94
qEqKfc3J7YC5gDH3OKDr7m97UL1m83IHR10Cl7nDoV7mAdc8vtE1AANBeuq7nlTx99h88+uS5x2S
71481vctvdwcySdegPa9tbilAAKqmgb3cDdgKzW3XcN3O61J7e7EHsDSVRJoVFKqEkiEYhoMgEwT
QaMjRoDQEDSZoJPJR+qfpTRpoJQQATQQk0yTGoEaeqZqjeptKA0B6gDTATIekBpiE0SUCabUT0yj
T1M1Gmj1AAGgAAAADQCTShECEaNTyEp+EyJMTep6p6RptBNPUAAbETT1Bo0ESiBATTEaaAAE00Jp
kxBpoTTaTSeJpqaZAaAVKEAmI0ImgTTFT09TU9T1PCh5TxDRGgAAAAMYn7kBeYg7FQkVCAKBr+MP
94j/p2iHo88e3Aj3fR6QfcFx7npnUCPTpyFVXRGaGXM6ym54O+dvG/eCf7dUPV7aB8dv3HtYLtaR
dxFgoYAMIwB47TPkxMg4vJ/ZTVdT3NJUurm6uL+ErvW+y2KPSy0DNFkztLJZLBggs0cKlEwUFM26
GW7b5Nu/4u7t7q/c3/xtWczpSw8ZJ+3/2cfq8KfX2U87Z4TbOZkVP1y1T7Jyl4GXExsMsbGvGzle
IslTl376UpSs2Gw1o7cd9Cad3acEerL/zDh0Nl9zaZms5RahJcH8Pt+flDuL3+CYq0f7G7fo3rph
hvZeIN3nq/t5wOx4UheL+JDQ06NSUPixGSKMScJ9uCkyySntxd07B0e5vRC+lLPPezhQ4x0ojoov
qndp7dSSX2sgRpPb5HFckbfPhkr8sSR1TfrlKgkkmASAYPmurGODHdUeev8D2/BqM7+P2cCPaShq
jk4v1rEiyNUjIc1e5K3AVc7r2CPIhYf7/EHj83YfhRZ/Nr7edUN41wpF3ihWFWVlBJFYqG6jFfvQ
I+jEBRA7vAM0uPbnzMNLzX368FZeVtnWSpIFJre71uxKj19YToIk/x/Z6fjvJBORABlRN1EsomEn
UkOxOtCGGZYYYdiTZkMobJ1MqHiZDCBuzNsRN04cbPGzdMMhMu6bMwpqJOsURGPdtypWzZZi9Wxd
5SYvl5Wnfffbfhc3YUQukISRg+mKofn0Yi/LipSY0zZEUlKEkAUU0z2Hy/Lqe/6pnnB14S+qxdk7
sdiJSGF6eJDSoVWqNlXa6l6VZWljyhsgTeozcGcrZoMxJE7wtY8sqc3Q2rGsqZUSHmqFje34IEb2
MPC93QpjKqlUyZzW6tjLSSHLD1ognTu1ZrC7etlmt0W0RyBHq9XfrRyEbnpZlEopQT7b7lqW9Lvc
cTS05UjLfEJABaSQfmYBEZFAgKLAgskiwCLFgnjFSpUhJWbynL3dOnv8MHx8vwh28FOHrRwcsyrz
UHgjeRZBbJ8ISBTcMtToRrt+sLHeGzc0h6c4WW8esUeQAO0yxZAZCQkGEooRIKCKwWQikAWCwUUU
YyIwWLESKCgosikEZFIjCQkUhACRkAIRDVpOhQQ7uju7rAO7Gsp7f44Txc5OgTNJ1KKDJBVIjAV3
1y4kxZR9ghlEDlyF1Fe7dHbCDIsgTSAQFNLDoXYqrqYnDbG2sKUK9rZRtqJq2J2avQwLA6zJlCcW
5TIs5Kwye8uTzXHp8KhmjTNudoHVOCaNMKOGyStISsSe3AtgagHekBQJ3osWQ9LlnjchuBwjhZR4
c08AEcvnN7nUg8HM0Fp9QF1K4scvQ2ZAsb2xhjWcslC3W5rRTze1ROqJu2EwooOp2puwM0KBqRRG
ZTAsWgGeogcupmVtK+YRUgPEJN4htZO8uGUpqVvMmJ2sIiiUhqafftCzhJirmcrgiH1OuhrfS4hw
no64oUg7HGJk4VNsBZOp4bLN5jjVUWBUxo65QkykNa1VVmFI0Rq9U9VWugJc8fLp4s51sCmqK2Tj
2TiddONEaJrHw7IOWxvE6YaIPLerrc9Dd9LkYJsPm2OEEC9cRmI2OlGjw86AnbRmuGtNDje7nK45
nDsM0huUJSw7qtSMMTKvaLQJE2UQZwUyVOhNUMHXQqs6ug+hPLdjYpW8tdWllScVMjRVZDhvrWPS
ZKZQoNOU3pFishoX526TwfxqD/m3s6N8w/Id8Qrd9axnR8nA8OHhsAWf9M3ByegoTBN0s448ec6T
sFww6hk3TRo6K0xI3d1nZf8Uc+vI7mK1HwEUtaVyF90kHFGS0fEfeeWK0zAh0hKKaQuE9pKRsInZ
RFYkYZccQtmc3l7rLU2iwyrJvznMlT3/3Ed7IJKe8fTDfz/zTsv+dLvbmNt3z6ud9kTpWUjQr36P
q1n8yuJMhTzhDN1BUUSJxbtE01E8l4VFZg/HJPyxUdvNo+0WSmkE3ZLzTw/gluV5q8loHH9BDFGi
4zFtlKqhrN8xEoftW1HR6EcyOG6EkIF2Uvzb0W74rWsD++TMqQVf8Ixv4r12SsdIJG0qPKSZUbg1
u3nb3EpnLCr29cxZYkdDIIqCBRajKv+3aZZhG6qS4Z4cYzhBAlQVcDC+QptA436ruvJvc6JVKupu
FFCPn6dtWCioqFgsdX+90IoqIKhYhx3gHlkgdEUUUUUUUUUUUGcufYyLWwm1azOhkRqb5hFq3gFb
QkMQFFR2REI1T58c7EMyGQRz7bMqr74SB2tdd5m8/FyamEE72L36ERAgWW6dI4vbo2wsX8iTGEgR
qMm1gisZSuII+8VgV/LesZTZJsQnJZEfTE4nFNp2NTn3xNSMu2e1znPZPdvNNH86CU1MjEh4V1V3
cd9JteKntTwp2De69PNz83LwrmeQu2bIIinTdDBDDSpQPhdU6Om/jaKm+5H/mptbdvLvv1/riYxN
GI86iTZsmlD2+uIY6YsTyNDNsznEwCwH4Hd06M6P5HZ9b43d8rPg8X1djzTszKqk+WfzZuliefu3
pOSylBng742O3N995TjplpmvK3vaPUu8qQy5ekJRaetc44uudPnnqGuOeav2k8bEWFL09BjsHDkP
SfiOmzlAAMvxKCi3FYa5ZuXL16dH2I7iOp0aKhU/lChSIqlhAXe+CEbqRpLJSgFkYen/n1I2C/q+
aGufkaOE9dnJC3CMghnIUgfmETfxFyzIUzh6ozissHJC1tnJtpgLsNgj3vFLle+OvamJgRUDoh+m
uy3Au4BI8Z8x/R0oc9nsxDxTqH+UiPd5RWQ86tBQsGRtZ1d3QxlTzWkZDYQ/GnkyBRFYtTdPQzM8
UZL/PtKfCdnPsn/bzM3W/ZmWAGDFEYrMeSZzIUEtZaxZdBDcvrjfpKFMMKDXZ+OW5JqWbffv9nTW
vTrc2zNlSVJKjyme2nSHHt6+zPEXUBlg2e23XZ3uoZRna8Haj7U5Pj11rT+hn6yPX975Z8aKcUqd
SIlqVL10xhZaURT7rPFq5TSNtO8phfwOY1vaOYcc88barnDtgsBQVv39P9D5IJWcJnvIFyJC8hkV
S8OJwk7za70v/lTcnVIe6ThcVHVxasjLRhVVTM5jk1TXuxXjrDSIWjS09GLDs7JyzSKCkVXBg63V
mCCBkTOkp0qVesEjQsB/X0wPB567hB44LK9vXXmmDpBjg4Xp8QaB8EUaVGd0V3Y7QQFWCKKK8iDg
OCTAYQ9gnu2RHRzQFsZjPQ6Qd/jWg9gRRi47Bggn7VOcMSqNFZWs5WEXHRVBP3iIgbICofkqHsVD
l2Br1f90x1mapWtKwpGUCJZYWCTUKakfNZiv+EpSkrRo0KnwPIH2BcpT0iQKSCQIny9LULSBKy2U
IBOZ9kGdIn7xZ+JsfbEr+IuYxLpB5YQ9zKXWTIsZvozxMUP0k9sTKJqkYPdFToRojgYZAcQZAcpF
UxWClIRHONtpLSUCFdsSsZ3u3We2bM5zYbVJ5y3XZ6JZYAvtLJJ2b21nM8JqNjHZTfnAjYbC7hRn
+dOjRv127uwuEukAib8Uoo8tetAllAWhOCsIn4Cis6Y4S1tKHlQP8hZRVKBGBAFTCrTKFqqTNGPL
iEtfFcjveGDEB0AQh+AFQTBzqp6EqTVU1qmc9jgwQSbghk3CkmQ0BSbk3JnimSdKYk2M288Q64Z9
vENba2NoAyg2QyA8SWwXUUolCJii8nXZ3HMf40Wij/0j3Z8vt57+7uoS2yFtLaW0tpbS2ltLaFtk
tpbfL1zW/b+l2pgXxnLn093z8P2wnvZIHLlyt513HW4YY6aE1+nTZsTnb8MbV/8RRaOJtFXhdqaC
gsPHtBA/iuV9s+mBIffYaakMcKQ6DxYRTKBU0yTdOBq4ZN2Rt0hxzQMsrDkkM44Y4JWASADZBLoJ
bELYjiwwqVgXxW+I1ULTLRn4eF6NOyCeS0QRQQIiBJ09C6lCssYU2Z0RfhIKqpFIi+bFIixRqqVC
pCtZVRAKawwobDDqwaF91ZioKwy257o77VRhkQCAlXaMyOSJY+AJAzmNqVQEtOCExiX3Qpm2oYEs
nOQXkyLNcHNq1ZAjQHC5KyYS4wnJOglUgFxQWZAXAfx/ZQR0sUdWVRaM4YEpIkQRHLApKQRTIo4N
cQhSpZVVLhIqEQiowYMN00l1iRevdMZIYFjRUTfVo0nJcw01dFHjfuyUXbViNiXQ1SoRSGaq+Feu
FoSDTdBLEMmEu9doXpV1g2HkIzTEREkgCoggI44kNFRHNEyRYLEE8ZNiVM2s8IswQZ3IVK3qkjRK
EjRWy/dTFJhs2atC6l9meexFrkhLtsZEiUQDJIQK0cwc894QLlyuSEIbJgQoUFbEbP/Dn/7tcMpQ
fzcslkukRaeXDCNyKPQ7EyM1+L0FWzV5lfBW+q2auNloZvAxWBsFza6QKEC4tl0SJH0iedzY+zkz
I9fWBuo4dma11z+36CfIUJ9PY+BPeN4mf6PtuxPrDsDk+X1imTScyB+yM5L+LEKU4wUeiKhZweBw
zyY5SgMHJfNsZmC64ZrjNJNxhDymqeocOknRDdMuWbsnBnG8cW3DlNOk5IGyJyqyo4Qyr3qQNJcK
IFYqrnOwEQOnrkf1fKSIhURDGOyCQrmykXht8Y3kIhgIgxAVVSMi4w5/5JGDCSyp8diqtlWMKW6v
eswSKOdKs9GPt3974nnd89JGG91aceN+6RirskUsVWKJHC6q4rZR1pLkLFE5se/LJ2UK2T4joYRU
2Fhs47qYmzAn43OxsNOs0Sz7Qvvjj0IJUqxYKErLOirwaK3p/f6IlzxXoRBf2isIoilbWMP2bD18
l5dfG84w2hG5LEY3g97cS5hO0cWxLM7YvOyIJ7CIPncyiibDJBt4kUoKFhhiYIowqTFTcfycW5qh
k0WHXLURCuImLpGTmgQR+O8kdJKaFqo8yKD2HPLzgF3PN7bhnIngvaOxjtMZdxQ5k2uWlKsLm67C
RHdXC8q1brmsgpm7R64mOGs6105Q0Ek8h3DGzIbCoZ7A3jgiYDcn2+KUTJIljy0GBxZhaMJEUMLq
vBQypJypPMo2UN2JkQoLyvRyGkuPon9O0Z0ZHl+uAPyyVFScEZJEYjpA2UImKxRz3edRzo0O856V
LZgsM4o4k6PGQ5BOkXKolRBRRSIoljtoc2LHRM0aPdSJxjAdViJ/Nv6fVteuepymqvg6nV4t2TxV
POdCZ0WJjTOxc2NgwVPL02Kgn+Rj0vAmTnYuZCEMHCfbwVOg6M9CpAU2+i6FnKxM/l+79F+kTZxW
EJ1SvdLdO+I4FghyiRCxYKccToJbDiC+6eSRNA0g13tTAmShBMLkQOxiBUoTiUsdtNhbntl3Ms6P
VT1R5W75zet1e529ZfBpPfOcWGyeu8IF7ejWu2/zogG6CRETcARkEnDLxpHL9cIJJIcTmSSRgthe
l9uJomBOrFpZMKyOqqsSDk5FUj3LlYjVZplaxe6+hmthtvllQwQJTGGp2kjpIEgO5cLmR/gteh/g
q4LdrFEU3KQFNwS0pCJsJl0fSjIKKgKKUUgtypwNHkNE5auXAblhkkE4LQS8TyGTJoLxtjnyx0L6
kX7JGDhZcsXROOrOxNjmxNzeBYgoayOFYGJlT0LhPeplqnmEzmpYS9Zq1R2k7lkhx8+e8jY4GUFU
BTg4TuReF1Rs5zE2oHYYoKLVihEqdxSY5AYxJa7vaDbxRIVYg4q7BYZiuNihM33mdzoSQWCa8tUF
3oAxwLqY3YznMgyaFttk/BiJJERA6T/N8s2DyGFNHBnuMQH0QIEDsTPvCpU2ISyXKtXRg4KkJXmX
FIE3IA56BGBQpsZP3WHInkSNtW70NFXNovbv84n2eGn45vdE3ibTySPCu9lw80mInSjd93dPw6b8
iQuzCyZ3UVHVYwFimo1NXJTdcDGJGULUt6qnVynS03IkTT2KrL1g9WWCwWRpeoUBbIAsp4dVxFx+
srYkEyoiEyxQaQn1KZoTMF0pChJRWWjstYjQaaIIVYkd5aiJ5GT6vqYsKWKJsxXaDWRoAisGAU9D
6/UhGk2ZQeYOIhYoR3xLkhyQPUfaR3GOeY87tJka7jPQY0SySsdG5NV4sySlHhwg25hxQzNw+CIA
qGyqtGVQnvyU8L0cFSYpAX5aCYpuQ5QJM6Kcq0OBYlSzSyyQ5PATBzB2uTqNHDOHJ22EiWKktlId
CnJfot0oT8uSOxbWEEzEoRHN+gSewzm4p9/6f1/2/1/ZOp3NF/Xk8zw8vNp3Z+lpp6LmSru3Ysj8
seZQo2evCHReele88VZ9HV2bNVVGL2Kuaz57s17hzcLMWbwc73OYsblKuHRVytEpq+WD5KXQfXcm
yeKBiB9SPHgjz6N7Vr00pDPbC5GzZ0a9WK/grhSsotTwCeICvF2qSCYBzMrsnQRXLurzc6OplctW
N6VkXsSS9yavd1utmingoBD0QImRkdMzMtnsHw2ctJRLlbmxUSSyERr0BBIH7Zaks5OZnARB0aTO
X1jaq/VRHWkKpsKD9zkO+T6q5uFNManBlZ4QQBZR7cRHMokZjD9iXMSxvIc1IG4EOgYooAiwRgFD
4wiI55A8vClTfhSmD1JwMIJIVMEjojFBUVUsrEsj2IEATEd2IGi6ZMDjEmLCcCkkEkdqYItAGYLj
EqULkEIKEpGwwOiqq7IJraQRIpFYi7rU0T3ND2NGUE1nK3eYiKglCMJiwFDY0Ma/hOwIkL6bB0pJ
BL5JgkYIJuThkZK4+BuUQYIlv9ad/K5o5t+jqudlGrydFmLwZrKvY6TR4DkyyXIkxk1pihErQ0bA
3YuhAU3GKFNiqqx3wlIkKhVeDyaaMiJeTJgQs3npA3WEjutTMSqPOCY73tez4Krynya0VAebUaUv
dy0TXnt1rbvDG+SaWzOPKqbewnVZrNWGGmHhYCukyE4wrLsiJ5qB5qkaRlYEYGvGw2SzCAzRRAGz
EoDGCWBz1cW3LwY85hzY1QzRVMcQLilUq7T5p659ROjtDmxXu2jh3598S6br15O+LxaaQvMTrp7y
5gfIoWM3ncawwoSkhosZHlvldzvkRQw0X7rK5JF5u5XdHkS1neatNGp5SRVhdovhzRA0F4BAiEyS
b6tejfnEyYMYMhQhlBLP1OAgmjwq7mOuChi3ZGaYgWD3PuCLsEAa5HwwgahCDeCJsTPiclKUsp3i
YfctYuRjFZCh+tBP4f2gnHGUkme0a5EQzmco0JnZonGEStqPGPEycMD4kjzcqZUYb6EKK1bFTccR
S6YikXS/YssWhMFSSwWq4SYSOC/AXyqXzd0RnJiVYLLd3rdX6d8FzhesyharN3o4ccccJE0+ksQM
jlRxSN3MlAwe8obAox2KGtMZNz7k8OVjtotuYNG5FyRoyYqTLli3sUSQn0Qp51XybiQHxnv6Ap8A
p0BoQucC7hQrxYu3reM+t61+VTQUuElsju3CHl0vkQIx3ZfW52jxihrt0dYTVnQbQq2SsxB7dJ5e
wQtTUutYK8PvhKhRESyNaucGHnOAI08IIfJT4kxgiTHrQjJK5Nlkx5MfY1acuqi5Z8MuMmg0UEKO
NevrXw5N9W6nS98IPySkSOHKZsNufTS5aJobioDHbrYlWOBxTsRRwxI6Ke1dubNtZYQn0e0jfOC1
yxMgaFLHJPFBi9LxVpcGSJaSCQP5UEo4J1csWNDljfYqQ8lrrqRIjy2cSgyPQyS4OTNyExEkXCI5
2LkyJMUVSeTchzMkRFMs2lHMQvBeuMnBje4zDDnByYDAzUzByKwWSvK4uLsiUbl0/xQCifFBNf4U
JxOEbc2dzI4RMVSgiqXSkFG/OhQwIQuH/XB7H8vpZrForpw4viqD++Qw57w9SQ52SrHqTOFPdjqp
IUQmetrVPpsXhMkSLkjX4WyCe8D2/V9LwwnbrAfgffc1LmXPyCHuXM6RZ2Fy7hAeDgZFAVUt7qRg
DPfyaLTjF1RiuCaFzy9XBF6T2rJ5WrdUK2Cr1avTFxnhZSo81AYRCglBhxoO6Xb7PrygmVrYdkKN
GdS0nkqrISkiPTctBli0ds2c5ZaKUJ7WmVGGFAQExCWVrVxZEhyJuVJEpSuVh86mDgjm0SKjTBdh
0s/BuESpoLSpJR2VhlcnkUba+0QuepAzonQssGGBnyaKRiglD0KG5FGNm3rveCFQmcUo6RWRmlcX
nA0KUF3mR5Xg3GONwc+4Kkd1XW25BRZinJA5B5m5E0ZFB4qIZGukxjl0mSKHJsRCEzairnEY4ynJ
TWi6jdo5JIaQ/4+n6+MHg7urdc3X+PyVdx3mcFBOwxwWJz7FCRI7EDubGHLmRTEipY2FoTIkFFgX
OiJgYYhgwVLSJ93IFjFdUpizdHgz444WbsmLjZudGTUzwfLB+e2kHwsUTKKb/b1fVwDvhpybTg1m
l2hdZdSspQujSAeOsYXfPxNmswi36x1sovWGoCapPMHA9XMjY2ygbN5QOM4b2Bua1wEjFD1zgXQ9
cQFdUmvgypTzCt0EURUElNcUD+9Y6k9UQCzEdDaJiMMGNiJUpo/VmotSIqIHBJNzODNKwWgOyLkm
btx8Zk10O1Shw6IIVyKOSxUnS0qiUkMuhT3kNig5kkaNzcuZ2nZkupBVdtrG0iOjGVY5LTVnW5qz
PenSVpNWzN6/Xczar85svckKNcMj8DlRyBqbEbl5mkslFvaQv8iYOuA5HNFKyNzJ8AOIputMq2xx
11CUgx4J5k0V8OoqqnPau4xsR0eCpU6Owac2GK5Ox4eMJGleBbBY2gaOxNBOIuAJeqx/H9oJje5f
cc/ATdyaclLOrJ0kfP3edcwZsFlXU1mjBfZyDkmVQTR2MBg7GS4r1OC6l6MQKjGNzBoYqKDsOfvm
QpLIKDkDprIW7UE5CmDxgnFenLFE3mifYfOwE+LZl+cTHcAYEN6MVoKShUQNkdalvc5zd9TS6K2X
jTBzNPaqgdTNStIaFUeMo3zkFguTJwGn8cusmLDF1QTogJyeY3GIZGsokZHvPu5CYdII1NFdtiS3
LsCMJSXAGTPRY9CZAfMxUpU8yx7R3ufFBMZhjng7jkSJo024SByGx/F9T6uuRqO3VRlOkAwQQSBB
OhfH5fxPXnKMW8kCzeBzJTJXYgIGiY0n8dEiOJVZjAJkGFhIUqQEJioQ7TpPG0Dk7pv1cUUMFyIe
8T8wyb5WjbN0aGLnnccpFmTJ52Ob4maPf1caBc7wNdEylzSCfv1/Sv/XDfW1ix59HW8vMU2PHy9O
HXZR9BMVWbu8lXiq5r1m2nCy5zZKuCZxIgfo9T3zIj8rsaKGD8Jj8HQqCepo5Nxy7hxoSAqHLLps
bAsiR1Xu4r+/iDvBSBl9RJSsgvWXEjnIZm1qHt++uE8p0DJKHuSW7TKiFlpB8uekZK9f9rzrVMIL
uwogBEeIR286UoCNh/gfIKhchQOYqhSt4I9iqGEdaj9ztgWWDkEKVCZC1JkhI/tCYEqOvy01dTn+
tex69GD2RWBCeE9sU/PCWgKDFFgyREUjsEZSMIxZCDCciNDoKk61IwSKxYsWLFixjEjFFLMAFjGF
ClKFKUSoIjGMiALJBEYxjFiIie30Pok92DB4fNaa5YMTd+hmNVTjB8VGRAxI2o0AqjkRqI1MyPpy
dxajcjejclRRXdeE9WcGiGs+RHk9s2avl1fnmqcT0LL5kVuDw+Xm/iktb2y97xCvtpvDH61bjlrT
Y0L8OMUPE5KB+X0HeKdjJBQkhAYSIvPAWjJEDhx64P0IXzyIXRUrma2Jbe91+JxuicVno9uOyEIJ
WNBxNzO8YIHjQhH2tqJvr0QaKVvbbQuBCxIXJPEtrIK/DalqzXhaGPb9vjwIXOhMhff7Y4dFtCe8
QsvCheW3ChddNB0dVXr6dHl14BHG2g7dX0+3FpzoV3lfPPs39Y3a5H4fZotcKgsnDsdsxUIX1onQ
sZcELakgtIVOmkg1c3JPs7uye9chzwIVvp7ZNnDM5qEL23+OXzkBZpq6sNzoWh9nvQqlfh3evp8+
woSQMIVLfno3ncuivPjLRFXt2pDgoclo8OCzVh1Rb7zDeV15lK13lsbDr3bWPw+HcKz79vz4vZx0
lc83YDc+CFPmgdSreslp9JC9umnY9bhxpI499i/r8961r2iF3c4hXNSE5C1UGswDGnk36SFg8frP
7ZVgXiv2zyjpwh1f47M/jZ93VPy17GHwrmukkg9NnNuZuNCupIKA/i8Hv/jJhsy8Xx4tkfrn2oXs
84rvwzS4+PpQsB7kAtXMZlwtg3seA6XXqEnNJck33wU+twnMhhhhkANUiZXO7xn39Xb7wP5xZ0Lx
qvhJwQseTPk3ELw76GYQB/AbXVcjhKozHyYdmiFPZ9kT8aKqoppbY7Xjt8G/l3wex8V+TpYYmQSN
vaernPhGGrw7L3MIWa1arzCE5CMjAerWcVLlv6ro5kA5gaH/6r1oX1+N19rRo7kK4IVlexiut29m
+l/FbmcgmJs2zX3oXyiO9kuzn6qXT2Rbf4Quvx7M10Cc+kfJz99kEvnVJafgdG5tyeP0mkrz+zPr
3bO/1Xp/d8/SCfj+J1d4+Tm9+z6QYdlieX/f8n5n9PxJ+xGL0YDbWQzDvsFAQKu0LOZG2vgwRNye
lIMXwRIIAq0gy2EtWv5L6jCTP9epdL+WhYNTkrK6LKpCiWII2ISwFJBgIhgxidUB1qSQN//MPrjI
kIMSFlyCEirj0F5jVs0W8u4hNk3oHi3kmkEZFFBGKCxRVGArn/Wt2AbgY0wDB6mRkdRRSGeoBWAz
JGoJixHq/N304dn/HE684Pq8TqeT/B7kNlXtHdDOGS+jCI/Svw2atM8eo6f6PMrDiA957ZRVBxmB
8587/AiVfg1cmr1NtGUB6V7d6n2xNMjRwxfGmT9Of9H7Nd2eh+ZVV07tnRu8N6vFRq5PBQiYNawX
O9C1tGiZMgZkUGMGCApEqdSU2ISLEy9hTwT5HKlxjAQFMZNjkUsYLFzdg2Jqvar16iryf1Yfb6rt
GLF2buqmiTiLguTPhVRiYpINpho7ESpuZOCZyaLGfC1OMX4MRH65ZVw5NHzQUaqWeTt8NvXMHZmh
cn3xPSveZsxezjVn4vRm8p581mB2eb5kSkkQ0I+3crl+9SFD9ny84ed6Py+gSioruB2jCwr1Tjsl
c1biZRg2gjjRqRHvMC2tiKZMT+kKXv2sT+6Pg+Zu8BaKVQI1qjQSoEWwE4MNgJgGGwEwDDYCYBhs
BMAw8BcRmMBcAzGCWYBmMQKTEZjAXALWrS9yiiuNEE/7PPfnsp7Zec3lSIxuPMB/8lLsmi1FAr9G
v3FB/zZUgevMCLcPVYuwzn4kR/2BYNUbNxfkUcfunyo1/2M2mGFAkSEJQAP8euhdj106yKtBET+6
RHiP3oJttg1teTh0hvITkcYtYKxyiRFmgDkvmFkKya+uLtgEqR0VTqunHplOkuGEeViYhHlaZ0PF
AQUjaV67RleM5LnRZkXLijRWFGTJq589XNqkx++5moybuimC9s34VaOHDBSFTo9U344vNFGWbcc5
3KmiJsTJ8lBzcJzG5Im5IgfyCVGMlzq7O1lW8NmKqn4pvty6sXDJRd5bfV9zu5qTIbwXGkzm6Wmc
83aESR8IeuVkSeBSxpUkqXnyJokrbUgXwqR70tP6o7pxmw4DSaiH+6Fzd1YP5fY591/i8Xmevo5s
Wb72xVYv2xXt2KbDz8OGz9HN0e34nnSJJVI6nD2M3JTzLO6qPXzmnXU2G9Y7A5U2BzXG7sdVslxd
1o2R5WNoRYRfpCqAs4oapicYmIRVQ5H+0rbAru7YX11YTLbFCPGBX1kbeEkCGsst2641mvC+2Zzc
YzYSEhHH9CR9g8RjR7DFSKuSJIJ9ZrThLAxj7Bj7LmMGbHsJk+wsQPt930kRLkjk4NGYCm43RMYd
Sga6PJIFjRuSOTRY7OHPPJcgTHNyBKV7hycN6NobZlONqOFlF69mzZ84Zsr2CQjNnTdSPx2cevc5
KhKVAsTFwxamEhJNlDuEhJcD1OYcOQqDYyUvjjB4ITiuQkVIwmIqGMiKZOYzJ88kp5G7yclXk9qq
jBzvsot6J2cGq91eN7Vnc6WXOTSVWc9XDKkj9S9Wkk7N7N2bm92cKuYYBFbE4OTnu6MqxnGMaRaq
Ta6xaq3mQtwcwQBKnu+r1/IibjszkpmimYliyNICOeVJCpVGQfGom+IX6AmQu5m9ZDvkLoBALKCm
KHJPTv89TEatUIdvcDbiCxVK8eowsueq/W3mMxAEVfNvhB3nEIDtQ+7f7+L2pR1xA9wiD4VOfYse
3bo9NEd23qUKkDTkDgkKRI/cMU6JGHr4LGCZNxWIEqED7Ch7fpcsKWLMTQCpRqudW2uzFo/K8oiZ
enHFeyWUd0kOz3lOi9uyfDjVe6PSkc0ThZ37+aJ2iUc+Tus3MDEQ4uAQAy7nnfZ57L0bnb+tU5VG
xzG+Zw4jo99noetc7WO/xKsmeNz7vBZ63viZM1y98TVSbyP5kPQicHaJXcW457UJBAY/Z37ZOTeg
xYU9JfhMTBZHExy84gpHFfcThx35DATwjE4OJx48LDu7C4Q9oI9NR4kiszbR+YQ6Aem9G3d+eEou
R8bQQAv5qAUsOeYojUT36bjrc3n88JIdl+lbopoOdgCm0kknO8a4QLxiHIKUEY9ooX60kCzdPXGY
pZmKSLWgAzOjS3V8YkpaafSto3/BROzn7qmi7EZ2mJANJFqoW0KllCwLvXbUYWRQtu3jxkFfF659
P3sc5BSCqqqqrFEzM0ot4Lw2k3xhxagIC1agnpbkuW1j1Npkfa5WfSigaNqVraPqcB9ZuVKny1mh
M/L8uihgkUGOx3qbikzWRSw5k2FKIDkDuUJErPz2z+7uXeNL1tH8rytTx1N9sWzj72jIj2e32yrP
zv4t+XaO2jkoaO3g8Ex/f7izDTIPLct/d7iop49PRiRSJghVRRjYqYMmjREUnQ7Lwf1wLJQ51kiY
FO5ombHz9lU+PxRArIDFDvBHyCN4ZICdqMFgSLCSQS3+GYy4uv3Pfkq5wWmQAKqhRVCld/Fcbpgv
lqcvkI85Hl4jNO6eia7GYB4SqByV+e0WoCNkzYyMxl4tlUsPqqxgnIh5YIzETkhbJkGCE2XBoDwV
Tis2FAvgY7tgTsurSugyGozm6bVMS7NQ/04pHI9JstKaDGQqcWzT5XadQhcpkVgFxecYzD/gwSku
UuEayDEBGUquCaIDU2edGIkQceEQRopCJyRJAGvggQkc3XiMYYkkUTbrcoQ77hImNJ7E/iL4Cit+
jvA2CzCUgyiIUARoGeZVQ9hYWgI/KGyimgikWbTHQMIikFGBCMIw4HL4i9oqHUOTvvbosZSoPJ40
tRgAF8Al6b+W1rb73T9ENvtpQj91978FVTLg/xibB6UjsRK/6UlKRRQkBAgdqiTnOspya2jajyiP
Eg8aAZA5M85hGAFD0OMif1iNEIojQFiEjASUWMFC1GLQgLgFp8aOjuU/jY1mnQh84pw5R0o4gQAx
qw6gooaWCoRgi1gsiorc8ff3qp6UaaQEeROaH2dpuI9lHjacp7vTZbYeVakfRRaKwg1hZW86CtXc
w9mO20nMo1GIDhBBssh7fVD1naF4estK3vj4QYX80/UJGZ+kiTMlyh+dD9hpw2qL+47EDerwVnlB
1XPj45lzdXVm69G6zJuxYKVZLaKqclSZokQkEhR/90zBMxMHJh9QNzksbExOViQhJnFKAVn3lU/E
o6Ep1NHXtVqwTnR5SEMo4uua55ngvYPP+xS9PM8qtNmbxZua8q9tzxeTd4p5mTDx7dmmYMDmpwNj
cuXgfP5qvvHHco1QqYsSJ+pDFfJqe/NsuVVY2X9muNmRE8qHGftJBg80kGBAjiqOLM6EG5GdO5p2
iNQ0aVIILAijLHx+LYWEc7ng0X0SwU8yHq2A586G8+RXqEcDk1AB90wRgb3y5UclOVH6Npf8O36t
3n5LvLbzVACeOaCPfooqhzvE20WkhGdNVC1EyFgFhq0q3OE0aFeC1QsEdnysj2QqyoD/YnNYaghB
DNkHIZwN5H8f1+/2A5oAEJJnI0iwgkIIQgAckRSqVRoMEga6oLL3hvlAqLgLn2VEKUGYRZYqt7GS
F0PQOdkQolw0SIiw8z4POfZYp7q9AO6KWAZYjCMiebIZKmBno4odKO/Z2hhaY0khkFModifpYGsz
o6IbOtRMgmVgwgXoWvs9xl+dXIIdfKGI/B/tPmVUYGwZmFiBoBT+3ruRr/PJ4dmLzOABTWZa4hRW
22iNS7oe5GIi1CF0zmSe8tEVxCPvDp7BKI2G+CO9uYnFmR0I078M21qFioe2c8VDXFQuUh2ppGpB
HU6h4GIdJIBf96qGOnal220xaKU3Jtr93BRsuIUZAAt1zWomhDc0YKRfj+aNv2o6EbUdGRHNECRR
Ai/tx6B1ET1QIEBk8m+YzfkAkIn0SbU1BnBUL/53HGJwJxq/eCyuoRtvRwDONmp3UOjJytiiWqhn
OiA5vVk/x205qfPoRsR7BTJkdukQ4+w9gHv7fYm065zTtpkUDzqByaveG6KWKBqpyPYLwQ6a7iTE
MMwMmSJ+Z4XAXaVtIcEWvogf007H6+fu2b9voxfds9COwy4kcJ33CGr3x4OIWhi6lUJTFRUOG9zC
RJrfCunkSA80KiB40P75gs8JM+awM4B9rHRbGlGjZY1AOuCpAgCpZcIfwB7u1FvgcAkYRJBkruB3
R9cvN+3PtIVd1dwVywD2D3D6HaC5xAutNGM3lE6mEgCa/AoAndFMof3sE6cZaQVXD20Q7fhpAMeo
dKCub+R829745GzvSzIljn7l1SvO6RS53RKTLQ6qYgslnw5QwRTJZE0ZbDA6Qyr8yPxR2VcYjceI
buU3x7AQ4r5NbSn9N/AvRx0CRSGgjQUmWUKRRVge0H5PN1fnzd4YrwfGjpR/H7tzOCc78Eh3iJ8R
QIFqgV/enbXD7D14+4OwU91XQCOHu/H0zzpajs5Dld6Xo/gj2I3mQKI7qBwBn3lEt/TIj96NoJ9+
fSo47txUxTMjhgCFVSB326+65H+W+/+TySReFaEHKV6tc5bhYgc5lbhM7MIujlsF1dGojM6cGBXF
oKGOGCStInh5JFcVID6Y+GWd0vyLiyJMoORClm6CPIjYfdeZ4xQZC5VCyX0KqewWIKtqWNsRbMKS
qe9TQCAFQsLOGKa0YLpigafpoomv98BRW9H7f0uQx/00wvc5N0EAKHOaDqNYorudNgP25ifhkLZY
pHiIguXEiCWQoqSHpCbkQULAspQgCSAo6bAdaNuZG5H36koAm89mD/JvG0DuZP1JK/iif1M+67bq
va3cxDbjtMQNtvpBYJFm8Fcjg7j1/pt/1/Hu+fdOW0MdfwFMkjRT9WVDFh59moTH+3ne9R6gaZeA
Ti4yS6hTZiPKmByg6O5eKQic/jui/WB2Aw7ePaFQdYjEYRRVgJ+XDtmS0Uob+IApqaFGKMiquPmd
0IBbSh+aNPTi7xRW5pgOCC5B3YBoANCEYjICvqSKjX+kGgwLVUMZ+R7gsRujCAsmQvvipjUeFGKq
SINQR1KkVoC04XJGSQhAgxA2Br0RKgkoxEZLLLj6PJnVacWK5N2vzj4e0GtZ9aBs9WuQ/eC97+zR
f3+/WjY65d823y92z9/qGioelH+CNRUOnmXLDayh9NpEk+wpkMUoVA/DfVkIFBFVPrgrG226wGCG
FioYYFZJbGHktrJ9heYQAhc17h2sGWLdeCFzeWgUgUu3oEjdkF4+sADcYCAixRYSETfJCZPmOT1U
7P5A/jysjI4+50nr6UekRiO4c/JjLCGgx+jcLJB82y8AVOpC+ddAJchDLe5Lq+F/qBU0J3bY7Pzm
hz4uGfD6pEWidYnB8p2kH5pB5Cfwo6IQW0hFYF6u+Un3T4o0x0O/1/cBADAzcTQPHPGM7ixiDraE
22DWxWbTEPnRVyaM0fh1sf0zBG8tuoXSgyQgELHEVgGzWB9O/efX22DqLR97ZtQ+YYGGcgun7aDU
dBo7rPmgi78HWFBKUVQy/SYAZw+MZFSIQPgYIISERIIyAxIKfz+QuTh5Ap7O3xRzmpE5qXNY8XwC
WQ4mF1krE/6spWOdvJt1kGncatWtqokou11Z/5x+3Zqoo2X9C6JSq1yS6w7xttEg73Q3SflnVGdX
Ec6apCMOcRiH7xWiiIoCMNpN5GT69jUNJvE58QJ1B1y7D3XUrH6wqGf60eT+YjYqGhHNRBMtyvzg
gBxTlDAG0YdYLVrCu03U7KvDpdjSM4AwnIX6gIZ1Eqjsh/pn4VNh9yGbqAHkOF+qMIT0DAsnq4L3
iuKU9luKIexk/f9vAmxmk27BR4lWKfZyPDwo7a7KUPhCM8kVKhGSAjCUsCVkD1tZbZCFYRQSQZGI
KqMiRRI8cEPSzOSQBJBCgDKMSgj8vyHju2IA5lA6UaKJmDrxiGOByEAeihQFJ0hZZ89n4RGuGm1E
l3BXZ7s1xApGsAiKgo0woKfSKRlJDuNMqmPBQJR/TSodCM4mM5Dj1vPSukr1HL3YkckJdsyFByqV
UKxZ1aFfpaxPug/pibJ9lDX8dnS6iaJMSNwjEYKK9vr/Ti/bfN2n58RtMc3lH+vUHHtAUA1Rsozx
pIWDEULzlhJ7MQ+F95FkPegUv4fk9+v89rrbGK4EZjvcDnMI2qkwh9uCUFkhjK36ID9XPyAGD4Ci
HpKXfPKBjlpqv1uC2GyxXiQtN4QlRygj9COMEbMFehuLUZYonkRiAvmEc2CoeTpFMv349wVDlDAQ
N3c4ZrFKyZRIb0o2Pi81Dlzrw9gi5VAD7aB4dRS4BkkOEKKNECK5R2HB42HD+dG2vZVQkRUMJIoJ
mr2kZMz16ESKAypJBNQrEdtxskaMAOfmQoCIXx77Bnn7IT3dp7PeiCYNzv4PhE31cXvxi0+fRjzu
donkTcpZ5RmcmuXnx26sdiVl79w6bGdEjNqHQ2yzGZn4k7zEzzjRzaUk33BaXWeXPYxjbeayTTuh
bKCxinmskKMrNG8oG2rMbnzkRJmBkCcZDIbplDMx4ZqQmJRDgnbmSQ54EZLJopEKSWAxGDTCtAxo
/FGtpyi2I3iMuby0FoFM95DbUnyap+d0oOE3mAALCGKFsCnr4QM6gQ0UnGbAFnZCBZaBCZ1MsZ0I
oWglitljKhUMhmCqjvwcH9aJdgpeRSJoETKRKZBGCtXoMuOyg72Ms4chACz2TsO6A+s+USjLEjGU
a2E9kVPmfREN4JSkFQ3xHWG6pqIAkISIxCSALCAIskEGKhMgT0jIKSxSk0BVDVcjdXRviNg3iOo1
WUbi6IdiiWjYGNHs336/wu50crvEDKRQoTXuiUCsgEyqoW7oRGGStAaowCI9mMm1tDjQ+PiDIHK2
foKYgeYEpmWipA6zwsg+gsK6YRkHRuWYTiHpjNdc35Yz1wj2eYpubaWxR4bBGoorb5KioUSwL0b9
0Z0nECAF6OOxGqsT0+hGpydmVG+I50fKKd5/CFEPiBQ/iqheqFV/5ERAZNSwgvAUBhQtc3fFHyB4
EQrBkcggh9dcSPGGYHWHp2uCiYw+/vV8CCPK6QjkHGX5qCFxf+R103AfxRrw5TTyX5wfEznKjn8n
fByknyEbVeBIaB9gUbF2ma2NyqYBuo5R5rw7rEYUFOL2PkSohevTdQsYKTd+0+ClAqgTBRTFUHIW
CWjI2Fsj/owOJUKiqWE5BxnyntITnMNy+6gKhpC30zIV2zTSA4cKFqDJ7LcYvCmhfTDQXCtg0RFO
Ge5eckmYSElUaQB8lvjms4/rd+Or2H9bm9YxniwPJXC2sJ4jTS0pAPNAkR0BoKZZ15UhFG3xCIrY
EKtxGIhg/Sj4I+nmHDT3Cni/YuhG/0Cn3e8NqgZo8Bvk2f9YefQnMYBkkiogoQDKdYQgQuYQOdxX
uVCegEsoubkDRfooOHPUq7tJy3LzytDChOe+GC2hhZZS5ENCVKkqIhKCNvCa0eU4xt9WahTjkUiF
mpz0j8t+4XRsIVvS4SkLAvd9Xzhv4wpdeVDx3bw7QsA85mcyPn8G2K4EU08F/PkC1H1bgXiUPhoh
tN41hLuq5PzVQIyNfiFmEiASOL1gEGnuR+gUtDK/YMRD+KNTjR8vV/+LuSKcKEhbB4i2AA==
More information about the bazaar
mailing list