Skip to content

Commit c867116

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 ae69a06 commit c867116

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
@@ -215,7 +215,7 @@ def _set_os_environ(value):
215215

216216
return _application(environ, start_response, _set_environ=_set_os_environ, _wrap_wsgi_errors=wrap_wsgi_errors)
217217

218-
def _application(environ, start_response, _set_environ, _format_output=format_output, _retrieve_handler=_import_backend, _wrap_wsgi_errors=True, _config_file=None, _skip_log=False):
218+
def _application(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):
219219

220220
# NOTE: pass app environ including apache and query args on to sub handlers
221221
# through the usual 'os.environ' channel expected in functionality
@@ -326,7 +326,7 @@ def _application(environ, start_response, _set_environ, _format_output=format_ou
326326
# (backend, script_name))
327327
fieldstorage = cgi.FieldStorage(fp=environ['wsgi.input'],
328328
environ=environ)
329-
user_arguments_dict = fieldstorage_to_dict(fieldstorage)
329+
user_arguments_dict = _fieldstorage_to_dict(fieldstorage)
330330
if 'output_format' in user_arguments_dict:
331331
output_format = user_arguments_dict['output_format'][0]
332332

@@ -444,7 +444,10 @@ def _application(environ, start_response, _set_environ, _format_output=format_ou
444444
# (backend, i+1, chunk_parts))
445445
# end index may be after end of content - but no problem
446446
part = output[i*download_block_size:(i+1)*download_block_size]
447-
yield _ensure_encoded_string(part)
447+
if output_format == 'file':
448+
yield part
449+
else:
450+
yield _ensure_encoded_string(part)
448451
if chunk_parts > 1:
449452
_logger.info("WSGI %s finished yielding all %d output parts" %
450453
(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
@@ -41,7 +41,7 @@
4141
from unittest import TestCase, main as testmain
4242

4343
from tests.support.suppconst import MIG_BASE, TEST_BASE, TEST_FIXTURE_DIR, \
44-
TEST_OUTPUT_DIR
44+
TEST_OUTPUT_DIR, TEST_DATA_DIR
4545

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

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

@@ -90,14 +91,35 @@ def _is_return_value(return_value):
9091
return return_value in defined_return_values
9192

9293

93-
def _trigger_and_unpack_result(application_result):
94+
def _trigger_and_unpack_result(application_result, result_kind='textual'):
95+
assert result_kind in ('textual', 'binary')
96+
9497
chunks = list(application_result)
9598
assert len(chunks) > 0, "invocation returned no output"
9699
complete_value = b''.join(chunks)
97-
decoded_value = codecs.decode(complete_value, 'utf8')
100+
if result_kind == 'binary':
101+
decoded_value = complete_value
102+
else:
103+
decoded_value = codecs.decode(complete_value, 'utf8')
98104
return decoded_value
99105

100106

107+
def create_instrumented_fieldstorage_to_dict():
108+
def _instrumented_fieldstorage_to_dict(fieldstorage):
109+
return _instrumented_fieldstorage_to_dict._result
110+
111+
_instrumented_fieldstorage_to_dict._result = {
112+
'output_format': ('html',)
113+
}
114+
115+
def set_result(result):
116+
_instrumented_fieldstorage_to_dict._result = result
117+
118+
_instrumented_fieldstorage_to_dict.set_result = set_result
119+
120+
return _instrumented_fieldstorage_to_dict
121+
122+
101123
def create_instrumented_format_output():
102124
def _instrumented_format_output(
103125
configuration,
@@ -112,6 +134,16 @@ def _instrumented_format_output(
112134
call_args = (configuration, backend, ret_val, ret_msg, call_args_out_obj, outputformat,)
113135
_instrumented_format_output.calls.append({ 'args': call_args })
114136

137+
if _instrumented_format_output._file:
138+
return format_output(
139+
configuration,
140+
backend,
141+
ret_val,
142+
ret_msg,
143+
out_obj,
144+
outputformat,
145+
)
146+
115147
# FIXME: the following is a workaround for a bug that exists between the WSGI wrapper
116148
# and the output formatter - specifically, the latter adds default header and
117149
# title if start does not exist, but the former ensures that start always exists
@@ -146,11 +178,18 @@ def _instrumented_format_output(
146178
outputformat,
147179
)
148180
_instrumented_format_output.calls = []
181+
_instrumented_format_output._file = False
149182
_instrumented_format_output.values = dict(
150183
title_text='',
151184
header_text='',
152185
)
153186

187+
188+
def _set_file(is_enabled):
189+
_instrumented_format_output._file = is_enabled
190+
191+
setattr(_instrumented_format_output, 'set_file', _set_file)
192+
154193
def _program_values(**kwargs):
155194
_instrumented_format_output.values.update(kwargs)
156195

@@ -209,6 +248,7 @@ def before_each(self):
209248
self.fake_start_response = create_wsgi_start_response()
210249

211250
# MiG WSGI wrapper specific setup
251+
self.instrumented_fieldstorage_to_dict = create_instrumented_fieldstorage_to_dict()
212252
self.instrumented_format_output = create_instrumented_format_output()
213253
self.instrumented_retrieve_handler = create_instrumented_retrieve_handler()
214254

@@ -218,6 +258,7 @@ def before_each(self):
218258
_config_file=_TEST_CONF_FILE,
219259
_skip_log=True,
220260
_format_output=self.instrumented_format_output,
261+
_fieldstorage_to_dict=self.instrumented_fieldstorage_to_dict,
221262
_retrieve_handler=self.instrumented_retrieve_handler,
222263
_set_environ=noop,
223264
)
@@ -262,6 +303,29 @@ def test_return_value_ok_returns_expected_title(self):
262303
self.assertInstrumentation()
263304
self.assertHtmlElementTextContent(output, 'title', 'TEST', trim_newlines=True)
264305

306+
def test_xxxx(self):
307+
test_binary_file = os.path.join(TEST_DATA_DIR, 'loading.gif')
308+
with open(test_binary_file, 'rb') as f:
309+
test_binary_data = f.read()
310+
311+
self.instrumented_fieldstorage_to_dict.set_result({
312+
'output_format': ('file',)
313+
})
314+
self.instrumented_format_output.set_file(True)
315+
316+
file_obj = { 'object_type': 'binary', 'data': test_binary_data }
317+
self.instrumented_retrieve_handler.program([file_obj], returnvalues.OK)
318+
319+
application_result = migwsgi._application(
320+
*self.application_args,
321+
**self.application_kwargs
322+
)
323+
324+
output = _trigger_and_unpack_result(application_result, 'binary')
325+
326+
self.assertInstrumentation()
327+
self.assertEqual(output, test_binary_data)
328+
265329

266330
if __name__ == '__main__':
267331
testmain()

0 commit comments

Comments
 (0)