Merge lp:~mwhudson/launchpad/unicod-branch-names-bug-449528 into lp:launchpad
- unicod-branch-names-bug-449528
- Merge into devel
Proposed by
Michael Hudson-Doyle
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Tim Penhey | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~mwhudson/launchpad/unicod-branch-names-bug-449528 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
396 lines (+121/-29) 8 files modified
lib/lp/code/doc/branch-xmlrpc.txt (+2/-2) lib/lp/code/model/branch.py (+1/-1) lib/lp/code/xmlrpc/branch.py (+11/-8) lib/lp/code/xmlrpc/codehosting.py (+11/-6) lib/lp/code/xmlrpc/tests/test_branch.py (+63/-6) lib/lp/code/xmlrpc/tests/test_codehosting.py (+22/-1) lib/lp/codehosting/inmemory.py (+7/-4) lib/lp/codehosting/vfs/branchfs.py (+4/-1) |
||||
To merge this branch: | bzr merge lp:~mwhudson/launchpad/unicod-branch-names-bug-449528 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Penhey (community) | Approve | ||
Review via email: mp+19572@code.launchpad.net |
Commit message
Fix some OOPSes that can result from attempts to push branches containing non-ASCII names.
Description of the change
To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote : | # |
Revision history for this message
Tim Penhey (thumper) wrote : | # |
Looks good to me. I trust you've tried pushing locally - I'm not going to.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/doc/branch-xmlrpc.txt' | |||
2 | --- lib/lp/code/doc/branch-xmlrpc.txt 2009-08-13 15:12:16 +0000 | |||
3 | +++ lib/lp/code/doc/branch-xmlrpc.txt 2010-02-22 01:48:21 +0000 | |||
4 | @@ -417,8 +417,8 @@ | |||
5 | 417 | >>> results = branchset_api.resolve_lp_path('~mark/+junk/doesntexist') | 417 | >>> results = branchset_api.resolve_lp_path('~mark/+junk/doesntexist') |
6 | 418 | >>> for url in results['urls']: | 418 | >>> for url in results['urls']: |
7 | 419 | ... print url | 419 | ... print url |
10 | 420 | bzr+ssh://bazaar.launchpad.dev/~mark/+junk/doesntexist | 420 | bzr+ssh://bazaar.launchpad.dev/~mark/%2Bjunk/doesntexist |
11 | 421 | http://bazaar.launchpad.dev/~mark/+junk/doesntexist | 421 | http://bazaar.launchpad.dev/~mark/%2Bjunk/doesntexist |
12 | 422 | 422 | ||
13 | 423 | 423 | ||
14 | 424 | But if the project or the user doesn't exist, Launchpad will return a fault: | 424 | But if the project or the user doesn't exist, Launchpad will return a fault: |
15 | 425 | 425 | ||
16 | === modified file 'lib/lp/code/model/branch.py' | |||
17 | --- lib/lp/code/model/branch.py 2010-02-16 20:36:58 +0000 | |||
18 | +++ lib/lp/code/model/branch.py 2010-02-22 01:48:21 +0000 | |||
19 | @@ -1309,7 +1309,7 @@ | |||
20 | 1309 | accepted_schemes.add('sftp') | 1309 | accepted_schemes.add('sftp') |
21 | 1310 | assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme | 1310 | assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme |
22 | 1311 | host = URI(config.codehosting.supermirror_root).host | 1311 | host = URI(config.codehosting.supermirror_root).host |
24 | 1312 | path = '/' + unique_name | 1312 | path = '/' + urlutils.escape(unique_name) |
25 | 1313 | if suffix is not None: | 1313 | if suffix is not None: |
26 | 1314 | path = os.path.join(path, suffix) | 1314 | path = os.path.join(path, suffix) |
27 | 1315 | return str(URI(scheme=scheme, host=host, path=path)) | 1315 | return str(URI(scheme=scheme, host=host, path=path)) |
28 | 1316 | 1316 | ||
29 | === modified file 'lib/lp/code/xmlrpc/branch.py' | |||
30 | --- lib/lp/code/xmlrpc/branch.py 2010-02-16 20:36:58 +0000 | |||
31 | +++ lib/lp/code/xmlrpc/branch.py 2010-02-22 01:48:21 +0000 | |||
32 | @@ -12,6 +12,8 @@ | |||
33 | 12 | 'PublicCodehostingAPI'] | 12 | 'PublicCodehostingAPI'] |
34 | 13 | 13 | ||
35 | 14 | 14 | ||
36 | 15 | from bzrlib import urlutils | ||
37 | 16 | |||
38 | 15 | from zope.component import getUtility | 17 | from zope.component import getUtility |
39 | 16 | from zope.interface import Interface, implements | 18 | from zope.interface import Interface, implements |
40 | 17 | 19 | ||
41 | @@ -100,7 +102,7 @@ | |||
42 | 100 | 102 | ||
43 | 101 | try: | 103 | try: |
44 | 102 | unicode_branch_url = branch_url.decode('utf-8') | 104 | unicode_branch_url = branch_url.decode('utf-8') |
46 | 103 | url = IBranch['url'].validate(unicode_branch_url) | 105 | IBranch['url'].validate(unicode_branch_url) |
47 | 104 | except LaunchpadValidationError, exc: | 106 | except LaunchpadValidationError, exc: |
48 | 105 | return faults.InvalidBranchUrl(branch_url, exc) | 107 | return faults.InvalidBranchUrl(branch_url, exc) |
49 | 106 | 108 | ||
50 | @@ -238,23 +240,24 @@ | |||
51 | 238 | # the model code(blech) or some automated way of reraising as faults | 240 | # the model code(blech) or some automated way of reraising as faults |
52 | 239 | # or using a narrower range of faults (e.g. only one "NoSuch" fault). | 241 | # or using a narrower range of faults (e.g. only one "NoSuch" fault). |
53 | 240 | except InvalidProductName, e: | 242 | except InvalidProductName, e: |
55 | 241 | raise faults.InvalidProductIdentifier(e.name) | 243 | raise faults.InvalidProductIdentifier(urlutils.escape(e.name)) |
56 | 242 | except NoSuchProductSeries, e: | 244 | except NoSuchProductSeries, e: |
58 | 243 | raise faults.NoSuchProductSeries(e.name, e.product) | 245 | raise faults.NoSuchProductSeries( |
59 | 246 | urlutils.escape(e.name), e.product) | ||
60 | 244 | except NoSuchPerson, e: | 247 | except NoSuchPerson, e: |
62 | 245 | raise faults.NoSuchPersonWithName(e.name) | 248 | raise faults.NoSuchPersonWithName(urlutils.escape(e.name)) |
63 | 246 | except NoSuchProduct, e: | 249 | except NoSuchProduct, e: |
65 | 247 | raise faults.NoSuchProduct(e.name) | 250 | raise faults.NoSuchProduct(urlutils.escape(e.name)) |
66 | 248 | except NoSuchDistroSeries, e: | 251 | except NoSuchDistroSeries, e: |
68 | 249 | raise faults.NoSuchDistroSeries(e.name) | 252 | raise faults.NoSuchDistroSeries(urlutils.escape(e.name)) |
69 | 250 | except NoSuchSourcePackageName, e: | 253 | except NoSuchSourcePackageName, e: |
71 | 251 | raise faults.NoSuchSourcePackageName(e.name) | 254 | raise faults.NoSuchSourcePackageName(urlutils.escape(e.name)) |
72 | 252 | except NoLinkedBranch, e: | 255 | except NoLinkedBranch, e: |
73 | 253 | raise faults.NoLinkedBranch(e.component) | 256 | raise faults.NoLinkedBranch(e.component) |
74 | 254 | except CannotHaveLinkedBranch, e: | 257 | except CannotHaveLinkedBranch, e: |
75 | 255 | raise faults.CannotHaveLinkedBranch(e.component) | 258 | raise faults.CannotHaveLinkedBranch(e.component) |
76 | 256 | except InvalidNamespace, e: | 259 | except InvalidNamespace, e: |
78 | 257 | raise faults.InvalidBranchUniqueName(e.name) | 260 | raise faults.InvalidBranchUniqueName(urlutils.escape(e.name)) |
79 | 258 | return self._getResultDict(branch, suffix, supported_schemes) | 261 | return self._getResultDict(branch, suffix, supported_schemes) |
80 | 259 | 262 | ||
81 | 260 | def resolve_lp_path(self, path): | 263 | def resolve_lp_path(self, path): |
82 | 261 | 264 | ||
83 | === modified file 'lib/lp/code/xmlrpc/codehosting.py' | |||
84 | --- lib/lp/code/xmlrpc/codehosting.py 2009-10-19 05:40:52 +0000 | |||
85 | +++ lib/lp/code/xmlrpc/codehosting.py 2010-02-22 01:48:21 +0000 | |||
86 | @@ -12,6 +12,7 @@ | |||
87 | 12 | 12 | ||
88 | 13 | 13 | ||
89 | 14 | import datetime | 14 | import datetime |
90 | 15 | import urllib | ||
91 | 15 | 16 | ||
92 | 16 | import pytz | 17 | import pytz |
93 | 17 | 18 | ||
94 | @@ -232,7 +233,7 @@ | |||
95 | 232 | def create_branch(requester): | 233 | def create_branch(requester): |
96 | 233 | if not branch_path.startswith('/'): | 234 | if not branch_path.startswith('/'): |
97 | 234 | return faults.InvalidPath(branch_path) | 235 | return faults.InvalidPath(branch_path) |
99 | 235 | escaped_path = unescape(branch_path.strip('/')).encode('utf-8') | 236 | escaped_path = unescape(branch_path.strip('/')) |
100 | 236 | try: | 237 | try: |
101 | 237 | namespace_name, branch_name = split_unique_name(escaped_path) | 238 | namespace_name, branch_name = split_unique_name(escaped_path) |
102 | 238 | except ValueError: | 239 | except ValueError: |
103 | @@ -254,7 +255,12 @@ | |||
104 | 254 | try: | 255 | try: |
105 | 255 | branch = namespace.createBranch( | 256 | branch = namespace.createBranch( |
106 | 256 | BranchType.HOSTED, branch_name, requester) | 257 | BranchType.HOSTED, branch_name, requester) |
108 | 257 | except (BranchCreationException, LaunchpadValidationError), e: | 258 | except LaunchpadValidationError, e: |
109 | 259 | msg = e.args[0] | ||
110 | 260 | if isinstance(msg, unicode): | ||
111 | 261 | msg = msg.encode('utf-8') | ||
112 | 262 | return faults.PermissionDenied(msg) | ||
113 | 263 | except BranchCreationException, e: | ||
114 | 258 | return faults.PermissionDenied(str(e)) | 264 | return faults.PermissionDenied(str(e)) |
115 | 259 | else: | 265 | else: |
116 | 260 | return branch.id | 266 | return branch.id |
117 | @@ -294,8 +300,7 @@ | |||
118 | 294 | def _serializeControlDirectory(self, requester, product_path, | 300 | def _serializeControlDirectory(self, requester, product_path, |
119 | 295 | trailing_path): | 301 | trailing_path): |
120 | 296 | try: | 302 | try: |
123 | 297 | namespace = lookup_branch_namespace( | 303 | namespace = lookup_branch_namespace(product_path) |
122 | 298 | unescape(product_path).encode('utf-8')) | ||
124 | 299 | except (InvalidNamespace, NotFoundError): | 304 | except (InvalidNamespace, NotFoundError): |
125 | 300 | return | 305 | return |
126 | 301 | if not ('.bzr' == trailing_path or trailing_path.startswith('.bzr/')): | 306 | if not ('.bzr' == trailing_path or trailing_path.startswith('.bzr/')): |
127 | @@ -321,9 +326,9 @@ | |||
128 | 321 | return faults.InvalidPath(path) | 326 | return faults.InvalidPath(path) |
129 | 322 | stripped_path = path.strip('/') | 327 | stripped_path = path.strip('/') |
130 | 323 | for first, second in iter_split(stripped_path, '/'): | 328 | for first, second in iter_split(stripped_path, '/'): |
131 | 329 | first = unescape(first) | ||
132 | 324 | # Is it a branch? | 330 | # Is it a branch? |
135 | 325 | branch = getUtility(IBranchLookup).getByUniqueName( | 331 | branch = getUtility(IBranchLookup).getByUniqueName(first) |
134 | 326 | unescape(first).encode('utf-8')) | ||
136 | 327 | if branch is not None: | 332 | if branch is not None: |
137 | 328 | branch = self._serializeBranch(requester, branch, second) | 333 | branch = self._serializeBranch(requester, branch, second) |
138 | 329 | if branch is None: | 334 | if branch is None: |
139 | 330 | 335 | ||
140 | === modified file 'lib/lp/code/xmlrpc/tests/test_branch.py' | |||
141 | --- lib/lp/code/xmlrpc/tests/test_branch.py 2009-11-12 04:28:28 +0000 | |||
142 | +++ lib/lp/code/xmlrpc/tests/test_branch.py 2010-02-22 01:48:21 +0000 | |||
143 | @@ -11,6 +11,8 @@ | |||
144 | 11 | import unittest | 11 | import unittest |
145 | 12 | import xmlrpclib | 12 | import xmlrpclib |
146 | 13 | 13 | ||
147 | 14 | from bzrlib import urlutils | ||
148 | 15 | |||
149 | 14 | from zope.security.proxy import removeSecurityProxy | 16 | from zope.security.proxy import removeSecurityProxy |
150 | 15 | 17 | ||
151 | 16 | from canonical.config import config | 18 | from canonical.config import config |
152 | @@ -23,6 +25,9 @@ | |||
153 | 23 | from canonical.testing import DatabaseFunctionalLayer | 25 | from canonical.testing import DatabaseFunctionalLayer |
154 | 24 | 26 | ||
155 | 25 | 27 | ||
156 | 28 | NON_ASCII_NAME = u'nam\N{LATIN SMALL LETTER E WITH ACUTE}' | ||
157 | 29 | |||
158 | 30 | |||
159 | 26 | class TestExpandURL(TestCaseWithFactory): | 31 | class TestExpandURL(TestCaseWithFactory): |
160 | 27 | """Test the way that URLs are expanded.""" | 32 | """Test the way that URLs are expanded.""" |
161 | 28 | 33 | ||
162 | @@ -54,9 +59,7 @@ | |||
163 | 54 | return branch | 59 | return branch |
164 | 55 | 60 | ||
165 | 56 | def assertResolves(self, lp_url_path, unique_name): | 61 | def assertResolves(self, lp_url_path, unique_name): |
169 | 57 | """Assert that the given lp URL path expands to the unique name of | 62 | """Assert that `lp_url_path` path expands to `unique_name`.""" |
167 | 58 | 'branch'. | ||
168 | 59 | """ | ||
170 | 60 | results = self.api.resolve_lp_path(lp_url_path) | 63 | results = self.api.resolve_lp_path(lp_url_path) |
171 | 61 | # This improves the error message if results happens to be a fault. | 64 | # This improves the error message if results happens to be a fault. |
172 | 62 | if isinstance(results, faults.LaunchpadFault): | 65 | if isinstance(results, faults.LaunchpadFault): |
173 | @@ -110,7 +113,7 @@ | |||
174 | 110 | self.assertResolves( | 113 | self.assertResolves( |
175 | 111 | self.product.name, trunk_series.branch.unique_name) | 114 | self.product.name, trunk_series.branch.unique_name) |
176 | 112 | 115 | ||
178 | 113 | def test_productDoesntExist(self): | 116 | def test_product_doesnt_exist(self): |
179 | 114 | # Return a NoSuchProduct fault if the product doesn't exist. | 117 | # Return a NoSuchProduct fault if the product doesn't exist. |
180 | 115 | self.assertFault( | 118 | self.assertFault( |
181 | 116 | 'doesntexist', faults.NoSuchProduct('doesntexist')) | 119 | 'doesntexist', faults.NoSuchProduct('doesntexist')) |
182 | @@ -141,11 +144,19 @@ | |||
183 | 141 | def test_invalid_product_name(self): | 144 | def test_invalid_product_name(self): |
184 | 142 | # If we get a string that cannot be a name for a product where we | 145 | # If we get a string that cannot be a name for a product where we |
185 | 143 | # expect the name of a product, we should error appropriately. | 146 | # expect the name of a product, we should error appropriately. |
187 | 144 | invalid_name = '+' + self.factory.getUniqueString() | 147 | invalid_name = '_' + self.factory.getUniqueString() |
188 | 145 | self.assertFault( | 148 | self.assertFault( |
189 | 146 | invalid_name, | 149 | invalid_name, |
190 | 147 | faults.InvalidProductIdentifier(invalid_name)) | 150 | faults.InvalidProductIdentifier(invalid_name)) |
191 | 148 | 151 | ||
192 | 152 | def test_invalid_product_name_non_ascii(self): | ||
193 | 153 | # lp:<non-ascii-string> returns InvalidProductIdentifier with the name | ||
194 | 154 | # escaped. | ||
195 | 155 | invalid_name = '_' + NON_ASCII_NAME | ||
196 | 156 | self.assertFault( | ||
197 | 157 | invalid_name, | ||
198 | 158 | faults.InvalidProductIdentifier(urlutils.escape(invalid_name))) | ||
199 | 159 | |||
200 | 149 | def test_product_and_series(self): | 160 | def test_product_and_series(self): |
201 | 150 | # lp:product/series expands to the branch associated with the product | 161 | # lp:product/series expands to the branch associated with the product |
202 | 151 | # series 'series' on 'product'. | 162 | # series 'series' on 'product'. |
203 | @@ -184,6 +195,14 @@ | |||
204 | 184 | '%s/%s' % (self.product.name, "doesntexist"), | 195 | '%s/%s' % (self.product.name, "doesntexist"), |
205 | 185 | faults.NoSuchProductSeries("doesntexist", self.product)) | 196 | faults.NoSuchProductSeries("doesntexist", self.product)) |
206 | 186 | 197 | ||
207 | 198 | def test_no_such_product_series_non_ascii(self): | ||
208 | 199 | # lp:product/<non-ascii-string> returns NoSuchProductSeries with the | ||
209 | 200 | # name escaped. | ||
210 | 201 | self.assertFault( | ||
211 | 202 | '%s/%s' % (self.product.name, NON_ASCII_NAME), | ||
212 | 203 | faults.NoSuchProductSeries( | ||
213 | 204 | urlutils.escape(NON_ASCII_NAME), self.product)) | ||
214 | 205 | |||
215 | 187 | def test_no_such_distro_series(self): | 206 | def test_no_such_distro_series(self): |
216 | 188 | # Return a NoSuchDistroSeries fault if there is no series of the given | 207 | # Return a NoSuchDistroSeries fault if there is no series of the given |
217 | 189 | # name on that distribution. | 208 | # name on that distribution. |
218 | @@ -192,6 +211,14 @@ | |||
219 | 192 | '%s/doesntexist/whocares' % distro.name, | 211 | '%s/doesntexist/whocares' % distro.name, |
220 | 193 | faults.NoSuchDistroSeries("doesntexist")) | 212 | faults.NoSuchDistroSeries("doesntexist")) |
221 | 194 | 213 | ||
222 | 214 | def test_no_such_distro_series_non_ascii(self): | ||
223 | 215 | # lp:distro/<non-ascii-string>/whatever returns NoSuchDistroSeries | ||
224 | 216 | # with the name escaped. | ||
225 | 217 | distro = self.factory.makeDistribution() | ||
226 | 218 | self.assertFault( | ||
227 | 219 | '%s/%s/whocares' % (distro.name, NON_ASCII_NAME), | ||
228 | 220 | faults.NoSuchDistroSeries(urlutils.escape(NON_ASCII_NAME))) | ||
229 | 221 | |||
230 | 195 | def test_no_such_source_package(self): | 222 | def test_no_such_source_package(self): |
231 | 196 | # Return a NoSuchSourcePackageName fault if there is no source package | 223 | # Return a NoSuchSourcePackageName fault if there is no source package |
232 | 197 | # of the given name. | 224 | # of the given name. |
233 | @@ -201,6 +228,16 @@ | |||
234 | 201 | '%s/%s/doesntexist' % (distribution.name, distroseries.name), | 228 | '%s/%s/doesntexist' % (distribution.name, distroseries.name), |
235 | 202 | faults.NoSuchSourcePackageName('doesntexist')) | 229 | faults.NoSuchSourcePackageName('doesntexist')) |
236 | 203 | 230 | ||
237 | 231 | def test_no_such_source_package_non_ascii(self): | ||
238 | 232 | # lp:distro/series/<non-ascii-name> returns NoSuchSourcePackageName | ||
239 | 233 | # with the name escaped. | ||
240 | 234 | distroseries = self.factory.makeDistroRelease() | ||
241 | 235 | distribution = distroseries.distribution | ||
242 | 236 | self.assertFault( | ||
243 | 237 | '%s/%s/%s' % ( | ||
244 | 238 | distribution.name, distroseries.name, NON_ASCII_NAME), | ||
245 | 239 | faults.NoSuchSourcePackageName(urlutils.escape(NON_ASCII_NAME))) | ||
246 | 240 | |||
247 | 204 | def test_no_linked_branch_for_source_package(self): | 241 | def test_no_linked_branch_for_source_package(self): |
248 | 205 | # Return a NoLinkedBranch fault if there's no linked branch for the | 242 | # Return a NoLinkedBranch fault if there's no linked branch for the |
249 | 206 | # sourcepackage. | 243 | # sourcepackage. |
250 | @@ -234,13 +271,23 @@ | |||
251 | 234 | owner.name, self.product.name) | 271 | owner.name, self.product.name) |
252 | 235 | self.assertResolves(nonexistent_branch, nonexistent_branch) | 272 | self.assertResolves(nonexistent_branch, nonexistent_branch) |
253 | 236 | 273 | ||
254 | 274 | def test_no_such_branch_product_non_ascii(self): | ||
255 | 275 | # A path to a branch that contains non ascii characters will never | ||
256 | 276 | # find a branch, but it still resolves rather than erroring. | ||
257 | 277 | owner = self.factory.makePerson() | ||
258 | 278 | nonexistent_branch = u'~%s/%s/%s' % ( | ||
259 | 279 | owner.name, self.product.name, NON_ASCII_NAME) | ||
260 | 280 | self.assertResolves( | ||
261 | 281 | nonexistent_branch, urlutils.escape(nonexistent_branch)) | ||
262 | 282 | |||
263 | 237 | def test_no_such_branch_personal(self): | 283 | def test_no_such_branch_personal(self): |
264 | 238 | # Resolve paths to junk branches. | 284 | # Resolve paths to junk branches. |
265 | 239 | # This test added to make sure we don't raise a fault when looking for | 285 | # This test added to make sure we don't raise a fault when looking for |
266 | 240 | # the '+junk' project, which doesn't actually exist. | 286 | # the '+junk' project, which doesn't actually exist. |
267 | 241 | owner = self.factory.makePerson() | 287 | owner = self.factory.makePerson() |
268 | 242 | nonexistent_branch = '~%s/+junk/doesntexist' % owner.name | 288 | nonexistent_branch = '~%s/+junk/doesntexist' % owner.name |
270 | 243 | self.assertResolves(nonexistent_branch, nonexistent_branch) | 289 | self.assertResolves( |
271 | 290 | nonexistent_branch, urlutils.escape(nonexistent_branch)) | ||
272 | 244 | 291 | ||
273 | 245 | def test_no_such_branch_package(self): | 292 | def test_no_such_branch_package(self): |
274 | 246 | # Resolve paths to package branches even if there's no branch of that | 293 | # Resolve paths to package branches even if there's no branch of that |
275 | @@ -269,6 +316,16 @@ | |||
276 | 269 | nonexistent_owner_branch, | 316 | nonexistent_owner_branch, |
277 | 270 | faults.NoSuchPersonWithName('doesntexist')) | 317 | faults.NoSuchPersonWithName('doesntexist')) |
278 | 271 | 318 | ||
279 | 319 | def test_resolve_branch_with_no_such_owner_non_ascii(self): | ||
280 | 320 | # lp:~<non-ascii-string>/product/name returns NoSuchPersonWithName | ||
281 | 321 | # with the name escaped. | ||
282 | 322 | nonexistent_owner_branch = u"~%s/%s/%s" % ( | ||
283 | 323 | NON_ASCII_NAME, self.factory.getUniqueString(), | ||
284 | 324 | self.factory.getUniqueString()) | ||
285 | 325 | self.assertFault( | ||
286 | 326 | nonexistent_owner_branch, | ||
287 | 327 | faults.NoSuchPersonWithName(urlutils.escape(NON_ASCII_NAME))) | ||
288 | 328 | |||
289 | 272 | def test_too_many_segments(self): | 329 | def test_too_many_segments(self): |
290 | 273 | # If we have more segments than are necessary to refer to a branch, | 330 | # If we have more segments than are necessary to refer to a branch, |
291 | 274 | # then attach these segments to the resolved url. | 331 | # then attach these segments to the resolved url. |
292 | 275 | 332 | ||
293 | === modified file 'lib/lp/code/xmlrpc/tests/test_codehosting.py' | |||
294 | --- lib/lp/code/xmlrpc/tests/test_codehosting.py 2009-10-19 05:40:52 +0000 | |||
295 | +++ lib/lp/code/xmlrpc/tests/test_codehosting.py 2010-02-22 01:48:21 +0000 | |||
296 | @@ -676,13 +676,27 @@ | |||
297 | 676 | owner = self.factory.makePerson() | 676 | owner = self.factory.makePerson() |
298 | 677 | product = self.factory.makeProduct() | 677 | product = self.factory.makeProduct() |
299 | 678 | invalid_name = 'invalid name!' | 678 | invalid_name = 'invalid name!' |
301 | 679 | message = ("Invalid branch name %r. %s" | 679 | message = ("Invalid branch name '%s'. %s" |
302 | 680 | % (invalid_name, BRANCH_NAME_VALIDATION_ERROR_MESSAGE)) | 680 | % (invalid_name, BRANCH_NAME_VALIDATION_ERROR_MESSAGE)) |
303 | 681 | fault = self.branchfs.createBranch( | 681 | fault = self.branchfs.createBranch( |
304 | 682 | owner.id, escape( | 682 | owner.id, escape( |
305 | 683 | '/~%s/%s/%s' % (owner.name, product.name, invalid_name))) | 683 | '/~%s/%s/%s' % (owner.name, product.name, invalid_name))) |
306 | 684 | self.assertFaultEqual(faults.PermissionDenied(message), fault) | 684 | self.assertFaultEqual(faults.PermissionDenied(message), fault) |
307 | 685 | 685 | ||
308 | 686 | def test_createBranch_unicode_name(self): | ||
309 | 687 | # Creating a branch with an invalid name fails. | ||
310 | 688 | owner = self.factory.makePerson() | ||
311 | 689 | product = self.factory.makeProduct() | ||
312 | 690 | invalid_name = u'invalid\N{LATIN SMALL LETTER E WITH ACUTE}' | ||
313 | 691 | message = ("Invalid branch name '%s'. %s" | ||
314 | 692 | % (invalid_name.encode('utf-8'), | ||
315 | 693 | str(BRANCH_NAME_VALIDATION_ERROR_MESSAGE))) | ||
316 | 694 | fault = self.branchfs.createBranch( | ||
317 | 695 | owner.id, escape( | ||
318 | 696 | '/~%s/%s/%s' % (owner.name, product.name, invalid_name))) | ||
319 | 697 | self.assertFaultEqual( | ||
320 | 698 | faults.PermissionDenied(message), fault) | ||
321 | 699 | |||
322 | 686 | def test_createBranch_bad_user(self): | 700 | def test_createBranch_bad_user(self): |
323 | 687 | # Creating a branch under a non-existent user fails. | 701 | # Creating a branch under a non-existent user fails. |
324 | 688 | owner = self.factory.makePerson() | 702 | owner = self.factory.makePerson() |
325 | @@ -920,6 +934,13 @@ | |||
326 | 920 | path = '/~%s/%s/no-such-branch' % (requester.name, product.name) | 934 | path = '/~%s/%s/no-such-branch' % (requester.name, product.name) |
327 | 921 | self.assertNotFound(requester, path) | 935 | self.assertNotFound(requester, path) |
328 | 922 | 936 | ||
329 | 937 | def test_translatePath_no_such_branch_non_ascii(self): | ||
330 | 938 | requester = self.factory.makePerson() | ||
331 | 939 | product = self.factory.makeProduct() | ||
332 | 940 | path = u'/~%s/%s/non-asci\N{LATIN SMALL LETTER I WITH DIAERESIS}' % ( | ||
333 | 941 | requester.name, product.name) | ||
334 | 942 | self.assertNotFound(requester, escape(path)) | ||
335 | 943 | |||
336 | 923 | def test_translatePath_private_branch(self): | 944 | def test_translatePath_private_branch(self): |
337 | 924 | requester = self.factory.makePerson() | 945 | requester = self.factory.makePerson() |
338 | 925 | branch = removeSecurityProxy( | 946 | branch = removeSecurityProxy( |
339 | 926 | 947 | ||
340 | === modified file 'lib/lp/codehosting/inmemory.py' | |||
341 | --- lib/lp/codehosting/inmemory.py 2010-01-06 12:09:46 +0000 | |||
342 | +++ lib/lp/codehosting/inmemory.py 2010-02-22 01:48:21 +0000 | |||
343 | @@ -555,7 +555,7 @@ | |||
344 | 555 | def createBranch(self, requester_id, branch_path): | 555 | def createBranch(self, requester_id, branch_path): |
345 | 556 | if not branch_path.startswith('/'): | 556 | if not branch_path.startswith('/'): |
346 | 557 | return faults.InvalidPath(branch_path) | 557 | return faults.InvalidPath(branch_path) |
348 | 558 | escaped_path = unescape(branch_path.strip('/')).encode('utf-8') | 558 | escaped_path = unescape(branch_path.strip('/')) |
349 | 559 | try: | 559 | try: |
350 | 560 | namespace_path, branch_name = escaped_path.rsplit('/', 1) | 560 | namespace_path, branch_name = escaped_path.rsplit('/', 1) |
351 | 561 | except ValueError: | 561 | except ValueError: |
352 | @@ -565,7 +565,7 @@ | |||
353 | 565 | owner = self._person_set.getByName(data['person']) | 565 | owner = self._person_set.getByName(data['person']) |
354 | 566 | if owner is None: | 566 | if owner is None: |
355 | 567 | return faults.NotFound( | 567 | return faults.NotFound( |
357 | 568 | "User/team %r does not exist." % (data['person'],)) | 568 | "User/team '%s' does not exist." % (data['person'],)) |
358 | 569 | registrant = self._person_set.get(requester_id) | 569 | registrant = self._person_set.get(requester_id) |
359 | 570 | # The real code consults the branch creation policy of the product. We | 570 | # The real code consults the branch creation policy of the product. We |
360 | 571 | # don't need to do so here, since the tests above this layer never | 571 | # don't need to do so here, since the tests above this layer never |
361 | @@ -583,7 +583,7 @@ | |||
362 | 583 | product = self._product_set.getByName(data['product']) | 583 | product = self._product_set.getByName(data['product']) |
363 | 584 | if product is None: | 584 | if product is None: |
364 | 585 | return faults.NotFound( | 585 | return faults.NotFound( |
366 | 586 | "Project %r does not exist." % (data['product'],)) | 586 | "Project '%s' does not exist." % (data['product'],)) |
367 | 587 | elif data['distribution'] is not None: | 587 | elif data['distribution'] is not None: |
368 | 588 | distro = self._distribution_set.getByName(data['distribution']) | 588 | distro = self._distribution_set.getByName(data['distribution']) |
369 | 589 | if distro is None: | 589 | if distro is None: |
370 | @@ -612,7 +612,10 @@ | |||
371 | 612 | sourcepackage=sourcepackage, registrant=registrant, | 612 | sourcepackage=sourcepackage, registrant=registrant, |
372 | 613 | branch_type=BranchType.HOSTED).id | 613 | branch_type=BranchType.HOSTED).id |
373 | 614 | except LaunchpadValidationError, e: | 614 | except LaunchpadValidationError, e: |
375 | 615 | return faults.PermissionDenied(str(e)) | 615 | msg = e.args[0] |
376 | 616 | if isinstance(msg, unicode): | ||
377 | 617 | msg = msg.encode('utf-8') | ||
378 | 618 | return faults.PermissionDenied(msg) | ||
379 | 616 | 619 | ||
380 | 617 | def requestMirror(self, requester_id, branch_id): | 620 | def requestMirror(self, requester_id, branch_id): |
381 | 618 | self._branch_set.get(branch_id).requestMirror() | 621 | self._branch_set.get(branch_id).requestMirror() |
382 | 619 | 622 | ||
383 | === modified file 'lib/lp/codehosting/vfs/branchfs.py' | |||
384 | --- lib/lp/codehosting/vfs/branchfs.py 2010-01-20 02:15:27 +0000 | |||
385 | +++ lib/lp/codehosting/vfs/branchfs.py 2010-02-22 01:48:21 +0000 | |||
386 | @@ -612,7 +612,10 @@ | |||
387 | 612 | # parent directories", which is just misleading. | 612 | # parent directories", which is just misleading. |
388 | 613 | fault = trap_fault( | 613 | fault = trap_fault( |
389 | 614 | failure, faults.NotFound, faults.PermissionDenied) | 614 | failure, faults.NotFound, faults.PermissionDenied) |
391 | 615 | raise PermissionDenied(virtual_url_fragment, fault.faultString) | 615 | faultString = fault.faultString |
392 | 616 | if isinstance(faultString, unicode): | ||
393 | 617 | faultString = faultString.encode('utf-8') | ||
394 | 618 | raise PermissionDenied(virtual_url_fragment, faultString) | ||
395 | 616 | 619 | ||
396 | 617 | return deferred.addErrback(translate_fault) | 620 | return deferred.addErrback(translate_fault) |
397 | 618 | 621 |
Hi there,
This branch fixes various explosions you can get when trying to use non-ascii characters in branch names. We don't allow this, but we should fail gracefully and this branch tries to get us there.
There are various things that conspire to making this annoying:
* xmlrpclib papers over the difference between str and unicode to the extent that if you send a utf-8 encoded str containing non-ascii characters, the other side will get a unicode string.
* in particular this applied to the 'faultString' attribute of Faults.
* Python exceptions containing non-ascii unicode strings are a bit like dodgy hand grenades, at least in Python 2.5: they tend to blow up if you touch them.
* There turned out to be bugs in bzr in this area, now fixed in the version we are using.
Anyway, this branch fixes most problems I found. I recommend running make run_all and trying to push branches named things like "bzr+ssh://<email address hidden> /~m%c2% a9rk/gnome- trminal/ dd".
This branch has been sitting around in my repo for ages. I'll be happy to get it done!
Cheers,
mwh