Merge lp:~cjwatson/lazr.restful/text-field-normalize-crlf into lp:lazr.restful

Proposed by Colin Watson
Status: Merged
Merged at revision: 276
Proposed branch: lp:~cjwatson/lazr.restful/text-field-normalize-crlf
Merge into: lp:lazr.restful
Diff against target: 158 lines (+61/-5)
6 files modified
NEWS.rst (+3/-0)
src/lazr/restful/docs/webservice-marshallers.rst (+10/-0)
src/lazr/restful/example/base/interfaces.py (+5/-0)
src/lazr/restful/example/base/root.py (+10/-4)
src/lazr/restful/example/base/tests/collection.txt (+27/-0)
src/lazr/restful/marshallers.py (+6/-1)
To merge this branch: bzr merge lp:~cjwatson/lazr.restful/text-field-normalize-crlf
Reviewer Review Type Date Requested Status
Cristian Gonzalez (community) Approve
Review via email: mp+396428@code.launchpad.net

Commit message

Normalize line breaks in text fields marshalled from a request.

Description of the change

multipart/form-data encoding (now used by lazr.restful.testing.webservice.WebServiceCaller for named POST requests) requires line breaks to be encoded as CRLF. This caused a discrepancy in Launchpad's lib/lp/code/stories/webservice/xx-branchmergeproposal.txt test, and it seems reasonable to normalize this in the specific case of text fields. Convert any line breaks to Unix-style LF there.

To post a comment you must log in.
Revision history for this message
Cristian Gonzalez (cristiangsp) wrote :

Loos good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS.rst'
2--- NEWS.rst 2021-01-04 11:24:14 +0000
3+++ NEWS.rst 2021-01-18 11:37:39 +0000
4@@ -11,6 +11,9 @@
5 allowing robust use of binary arguments on both Python 2 and 3
6 (bug 1116954).
7
8+Normalize line breaks in text fields marshalled from a request to Unix-style
9+LF, since ``multipart/form-data`` encoding requires CRLF.
10+
11 0.23.0 (2020-09-28)
12 ===================
13
14
15=== modified file 'src/lazr/restful/docs/webservice-marshallers.rst'
16--- src/lazr/restful/docs/webservice-marshallers.rst 2020-09-07 09:30:30 +0000
17+++ src/lazr/restful/docs/webservice-marshallers.rst 2021-01-18 11:37:39 +0000
18@@ -26,6 +26,7 @@
19 >>> def pformat_value(value):
20 ... """Pretty-format a single value."""
21 ... if isinstance(value, six.text_type):
22+ ... value = value.encode('unicode_escape').decode('ASCII')
23 ... if "'" in value and '"' not in value:
24 ... return '"%s"' % value
25 ... else:
26@@ -450,6 +451,15 @@
27 >>> print(marshaller.marshall_from_request('null'))
28 None
29
30+Line breaks coming from the request are normalized to LF.
31+
32+ >>> pprint_value(marshaller.marshall_from_request('abc\r\n\r\ndef\r\n'))
33+ 'abc\n\ndef\n'
34+ >>> pprint_value(marshaller.marshall_from_request('abc\n\ndef\n'))
35+ 'abc\n\ndef\n'
36+ >>> pprint_value(marshaller.marshall_from_request('abc\r\rdef\r'))
37+ 'abc\n\ndef\n'
38+
39 Bytes
40 -----
41
42
43=== modified file 'src/lazr/restful/example/base/interfaces.py'
44--- src/lazr/restful/example/base/interfaces.py 2020-09-02 22:35:45 +0000
45+++ src/lazr/restful/example/base/interfaces.py 2021-01-18 11:37:39 +0000
46@@ -238,6 +238,11 @@
47 def getRecipes():
48 """Return the list of recipes."""
49
50+ @export_factory_operation(
51+ IRecipe, ['id', 'cookbook', 'dish', 'instructions', 'private'])
52+ def createRecipe(id, cookbook, dish, instructions, private=False):
53+ """Create a new recipe."""
54+
55 def removeRecipe(recipe):
56 """Remove a recipe from the list."""
57
58
59=== modified file 'src/lazr/restful/example/base/root.py'
60--- src/lazr/restful/example/base/root.py 2020-02-04 11:52:59 +0000
61+++ src/lazr/restful/example/base/root.py 2021-01-18 11:37:39 +0000
62@@ -19,6 +19,7 @@
63 from zope.location.interfaces import ILocation
64 from zope.component import getMultiAdapter, getUtility
65 from zope.schema.interfaces import IBytes
66+from zope.security.proxy import removeSecurityProxy
67
68 from lazr.restful import directives, ServiceRootResource
69
70@@ -180,9 +181,9 @@
71 def __init__(self, id, cookbook, dish, instructions, private=False):
72 self.id = id
73 self.dish = dish
74- self.dish.recipes.append(self)
75+ removeSecurityProxy(self.dish.recipes).append(self)
76 self.cookbook = cookbook
77- self.cookbook.recipes.append(self)
78+ removeSecurityProxy(self.cookbook.recipes).append(self)
79 self.instructions = instructions
80 self.private = private
81 self.prepared_image = None
82@@ -321,10 +322,15 @@
83 return match[0]
84 return None
85
86+ def createRecipe(self, id, cookbook, dish, instructions, private=False):
87+ recipe = Recipe(id, cookbook, dish, instructions, private=private)
88+ self.recipes.append(recipe)
89+ return recipe
90+
91 def removeRecipe(self, recipe):
92 self.recipes.remove(recipe)
93- recipe.cookbook.removeRecipe(recipe)
94- recipe.dish.removeRecipe(recipe)
95+ removeSecurityProxy(recipe.cookbook).removeRecipe(recipe)
96+ removeSecurityProxy(recipe.dish).removeRecipe(recipe)
97
98
99 # Define some globally accessible sample data.
100
101=== modified file 'src/lazr/restful/example/base/tests/collection.txt'
102--- src/lazr/restful/example/base/tests/collection.txt 2020-09-07 09:54:10 +0000
103+++ src/lazr/restful/example/base/tests/collection.txt 2021-01-18 11:37:39 +0000
104@@ -320,3 +320,30 @@
105 HTTP/1.1 400 Bad Request
106 ...
107 No such operation: nosuchop
108+
109+POST operations may involve text fields. These are marshalled via a
110+multipart/form-data request, which requires line breaks to be represented as
111+CRLF. The server turns these into Unix-style LF.
112+
113+ >>> import six
114+
115+ >>> def create_recipe(id, cookbook_link, dish_link, instructions,
116+ ... private=False):
117+ ... return webservice.named_post(
118+ ... "/recipes", "createRecipe", {},
119+ ... id=id, cookbook=cookbook_link, dish=dish_link,
120+ ... instructions=instructions, private=private)
121+
122+ >>> cookbook_link = webservice.get(
123+ ... quote("/cookbooks/The Joy of Cooking")).jsonBody()["self_link"]
124+ >>> dish_link = webservice.get(
125+ ... quote("/dishes/Roast chicken")).jsonBody()["self_link"]
126+ >>> response = create_recipe(
127+ ... 20, cookbook_link, dish_link,
128+ ... "Recipe\r\ncontaining\rsome\nline\r\n\r\nbreaks")
129+ >>> response.status
130+ 201
131+ >>> recipe = webservice.get(response.getHeader("Location")).jsonBody()
132+ >>> six.ensure_str(recipe["instructions"])
133+ 'Recipe\ncontaining\nsome\nline\n\nbreaks'
134+ >>> _ = webservice.delete(recipe["self_link"])
135
136=== modified file 'src/lazr/restful/marshallers.py'
137--- src/lazr/restful/marshallers.py 2020-09-01 13:20:54 +0000
138+++ src/lazr/restful/marshallers.py 2021-01-18 11:37:39 +0000
139@@ -26,8 +26,9 @@
140
141 from datetime import datetime
142 from io import BytesIO
143+import re
144+
145 import pytz
146-
147 import simplejson
148 import six
149 from six.moves.urllib.parse import unquote
150@@ -329,6 +330,10 @@
151 Converts the value to unicode.
152 """
153 value = six.text_type(value)
154+ # multipart/form-data encoding of text fields is required (RFC 2046
155+ # section 4.1.1) to use CRLF for line breaks. Normalize to
156+ # Unix-style LF.
157+ value = re.sub(r'\r\n?', '\n', value)
158 return super(TextFieldMarshaller, self)._marshall_from_request(value)
159
160

Subscribers

People subscribed via source and target branches