Skip to content

Commit e9444d4

Browse files
committed
Make responding with binary data work under PY3.
The code as previously written would unconditionally encode parts as though they contained text - this went unnoticed on PY2 because strings and bytes are one and the same thing but blew up on PY3 where a string is explicitly of type unicode while a binary file would be raw bytes. Explicitly check the output_format and if instructed to serve a file do so without touching the chunks of file content bytes being yielded.
1 parent f9859bd commit e9444d4

File tree

5 files changed

+76
-7
lines changed

5 files changed

+76
-7
lines changed

mig/wsgi-bin/migwsgi.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def _set_os_environ(value):
216216
return _application(None, environ, start_response, _set_environ=_set_os_environ, _wrap_wsgi_errors=wrap_wsgi_errors)
217217

218218

219-
def _application(configuration, environ, start_response, _set_environ, _format_output=format_output, _retrieve_handler=_import_backend, _wrap_wsgi_errors=True, _config_file=None, _skip_log=False):
219+
def _application(configuration, environ, start_response, _set_environ, _fieldstorage_to_dict=fieldstorage_to_dict, _format_output=format_output, _retrieve_handler=_import_backend, _wrap_wsgi_errors=True, _config_file=None, _skip_log=False):
220220

221221
# NOTE: pass app environ including apache and query args on to sub handlers
222222
# through the usual 'os.environ' channel expected in functionality
@@ -329,7 +329,7 @@ def _application(configuration, environ, start_response, _set_environ, _format_o
329329
# (backend, script_name))
330330
fieldstorage = cgi.FieldStorage(fp=environ['wsgi.input'],
331331
environ=environ)
332-
user_arguments_dict = fieldstorage_to_dict(fieldstorage)
332+
user_arguments_dict = _fieldstorage_to_dict(fieldstorage)
333333
if 'output_format' in user_arguments_dict:
334334
output_format = user_arguments_dict['output_format'][0]
335335

@@ -447,7 +447,10 @@ def _application(configuration, environ, start_response, _set_environ, _format_o
447447
# (backend, i+1, chunk_parts))
448448
# end index may be after end of content - but no problem
449449
part = output[i*download_block_size:(i+1)*download_block_size]
450-
yield _ensure_encoded_string(part)
450+
if output_format == 'file':
451+
yield part
452+
else:
453+
yield _ensure_encoded_string(part)
451454
if chunk_parts > 1:
452455
_logger.info("WSGI %s finished yielding all %d output parts" %
453456
(backend, chunk_parts))

tests/data/loading.gif

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../mig/images/loading.gif

tests/support/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242

4343
from tests.support.configsupp import FakeConfiguration
4444
from tests.support.suppconst import MIG_BASE, TEST_BASE, TEST_FIXTURE_DIR, \
45-
TEST_OUTPUT_DIR
45+
TEST_OUTPUT_DIR, TEST_DATA_DIR
4646

4747
PY2 = (sys.version_info[0] == 2)
4848

tests/support/suppconst.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
# Use abspath for __file__ on Py2
3232
_SUPPORT_DIR = os.path.dirname(os.path.abspath(__file__))
3333
TEST_BASE = os.path.normpath(os.path.join(_SUPPORT_DIR, ".."))
34+
TEST_DATA_DIR = os.path.join(TEST_BASE, "data")
3435
TEST_FIXTURE_DIR = os.path.join(TEST_BASE, "fixture")
3536
TEST_OUTPUT_DIR = os.path.join(TEST_BASE, "output")
3637
MIG_BASE = os.path.realpath(os.path.join(TEST_BASE, ".."))

tests/test_mig_wsgi-bin_migwsgi.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@
3232
import os
3333
import stat
3434
import sys
35+
import unittest
3536

36-
from tests.support import MIG_BASE, MigTestCase, testmain
37+
from tests.support import MIG_BASE, TEST_BASE, TEST_DATA_DIR, MigTestCase, testmain
3738
from mig.shared.output import format_output
3839
import mig.shared.returnvalues as returnvalues
3940

@@ -66,14 +67,35 @@ def _is_return_value(return_value):
6667
return return_value in defined_return_values
6768

6869

69-
def _trigger_and_unpack_result(application_result):
70+
def _trigger_and_unpack_result(application_result, result_kind='textual'):
71+
assert result_kind in ('textual', 'binary')
72+
7073
chunks = list(application_result)
7174
assert len(chunks) > 0, "invocation returned no output"
7275
complete_value = b''.join(chunks)
73-
decoded_value = codecs.decode(complete_value, 'utf8')
76+
if result_kind == 'binary':
77+
decoded_value = complete_value
78+
else:
79+
decoded_value = codecs.decode(complete_value, 'utf8')
7480
return decoded_value
7581

7682

83+
def create_instrumented_fieldstorage_to_dict():
84+
def _instrumented_fieldstorage_to_dict(fieldstorage):
85+
return _instrumented_fieldstorage_to_dict._result
86+
87+
_instrumented_fieldstorage_to_dict._result = {
88+
'output_format': ('html',)
89+
}
90+
91+
def set_result(result):
92+
_instrumented_fieldstorage_to_dict._result = result
93+
94+
_instrumented_fieldstorage_to_dict.set_result = set_result
95+
96+
return _instrumented_fieldstorage_to_dict
97+
98+
7799
def create_instrumented_format_output():
78100
def _instrumented_format_output(
79101
configuration,
@@ -88,6 +110,16 @@ def _instrumented_format_output(
88110
call_args = (configuration, backend, ret_val, ret_msg, call_args_out_obj, outputformat,)
89111
_instrumented_format_output.calls.append({ 'args': call_args })
90112

113+
if _instrumented_format_output._file:
114+
return format_output(
115+
configuration,
116+
backend,
117+
ret_val,
118+
ret_msg,
119+
out_obj,
120+
outputformat,
121+
)
122+
91123
# FIXME: the following is a workaround for a bug that exists between the WSGI wrapper
92124
# and the output formatter - specifically, the latter adds default header and
93125
# title if start does not exist, but the former ensures that start always exists
@@ -122,11 +154,18 @@ def _instrumented_format_output(
122154
outputformat,
123155
)
124156
_instrumented_format_output.calls = []
157+
_instrumented_format_output._file = False
125158
_instrumented_format_output.values = dict(
126159
title_text='',
127160
header_text='',
128161
)
129162

163+
164+
def _set_file(is_enabled):
165+
_instrumented_format_output._file = is_enabled
166+
167+
setattr(_instrumented_format_output, 'set_file', _set_file)
168+
130169
def _program_values(**kwargs):
131170
_instrumented_format_output.values.update(kwargs)
132171

@@ -185,13 +224,15 @@ def before_each(self):
185224
self.fake_start_response = create_wsgi_start_response()
186225

187226
# MiG WSGI wrapper specific setup
227+
self.instrumented_fieldstorage_to_dict = create_instrumented_fieldstorage_to_dict()
188228
self.instrumented_format_output = create_instrumented_format_output()
189229
self.instrumented_retrieve_handler = create_instrumented_retrieve_handler()
190230

191231
self.application_args = (self.configuration, self.fake_wsgi_environ, self.fake_start_response,)
192232
self.application_kwargs = dict(
193233
_wrap_wsgi_errors=noop,
194234
_format_output=self.instrumented_format_output,
235+
_fieldstorage_to_dict=self.instrumented_fieldstorage_to_dict,
195236
_retrieve_handler=self.instrumented_retrieve_handler,
196237
_set_environ=noop,
197238
)
@@ -236,6 +277,29 @@ def test_return_value_ok_returns_expected_title(self):
236277
self.assertInstrumentation()
237278
self.assertHtmlElementTextContent(output, 'title', 'TEST', trim_newlines=True)
238279

280+
def test_return_value_ok_serving_a_binary_file(self):
281+
test_binary_file = os.path.join(TEST_DATA_DIR, 'loading.gif')
282+
with open(test_binary_file, 'rb') as f:
283+
test_binary_data = f.read()
284+
285+
self.instrumented_fieldstorage_to_dict.set_result({
286+
'output_format': ('file',)
287+
})
288+
self.instrumented_format_output.set_file(True)
289+
290+
file_obj = { 'object_type': 'binary', 'data': test_binary_data }
291+
self.instrumented_retrieve_handler.program([file_obj], returnvalues.OK)
292+
293+
application_result = migwsgi._application(
294+
*self.application_args,
295+
**self.application_kwargs
296+
)
297+
298+
output = _trigger_and_unpack_result(application_result, 'binary')
299+
300+
self.assertInstrumentation()
301+
self.assertEqual(output, test_binary_data)
302+
239303

240304
if __name__ == '__main__':
241305
testmain()

0 commit comments

Comments
 (0)