Repository: aio-libs/aiohttp

Issue:
MultipartWriter does not support binary content encoding
## Long story short

There is no support of binary encoding in MultipartWriter
Some services require binary encoding. Seems that easy to implement with just returning unprocessed chunks.
## Expected behaviour

Supported binary encoding
## Actual behaviour

Not implemented encoding
https://github.com/KeepSafe/aiohttp/blob/master/aiohttp/multipart.py#L858
## Steps to reproduce

Specify header 'Content-Transfer-Encoding' to 'binary' in MultiPartWriter



Discussion:

Resolving patch:
diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
--- a/aiohttp/multipart.py
+++ b/aiohttp/multipart.py
@@ -443,7 +443,7 @@ def decode(self, data):
         Supports ``gzip``, ``deflate`` and ``identity`` encodings for
         `Content-Encoding` header.
 
-        Supports ``base64``, ``quoted-printable`` encodings for
+        Supports ``base64``, ``quoted-printable``, ``binary`` encodings for
         `Content-Transfer-Encoding` header.
 
         :param bytearray data: Data to decode.
@@ -477,6 +477,8 @@ def _decode_content_transfer(self, data):
             return base64.b64decode(data)
         elif encoding == 'quoted-printable':
             return binascii.a2b_qp(data)
+        elif encoding == 'binary':
+            return data
         else:
             raise RuntimeError('unknown content transfer encoding: {}'
                                ''.format(encoding))
@@ -854,6 +856,8 @@ def _apply_content_transfer_encoding(self, stream):
         elif encoding == 'quoted-printable':
             for chunk in stream:
                 yield binascii.b2a_qp(chunk)
+        elif encoding == 'binary':
+            yield from stream
         else:
             raise RuntimeError('unknown content transfer encoding: {}'
                                ''.format(encoding))


Test patch:
diff --git a/tests/test_multipart.py b/tests/test_multipart.py
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -339,6 +339,15 @@ def test_read_with_content_transfer_encoding_quoted_printable(self):
         self.assertEqual(b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82,'
                          b' \xd0\xbc\xd0\xb8\xd1\x80!', result)
 
+    def test_read_with_content_transfer_encoding_binary(self):
+        obj = aiohttp.multipart.BodyPartReader(
+            self.boundary, {CONTENT_TRANSFER_ENCODING: 'binary'},
+            Stream(b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82,'
+                   b' \xd0\xbc\xd0\xb8\xd1\x80!\r\n--:--'))
+        result = yield from obj.read(decode=True)
+        self.assertEqual(b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82,'
+                         b' \xd0\xbc\xd0\xb8\xd1\x80!', result)
+
     def test_read_with_content_transfer_encoding_unknown(self):
         obj = aiohttp.multipart.BodyPartReader(
             self.boundary, {CONTENT_TRANSFER_ENCODING: 'unknown'},
@@ -941,6 +950,21 @@ def test_serialize_with_content_transfer_encoding_quote_printable(self):
         self.assertEqual(b'\r\n', next(stream))
         self.assertIsNone(next(stream, None))
 
+    def test_serialize_with_content_transfer_encoding_binary(self):
+        part = aiohttp.multipart.BodyPartWriter(
+            'Привет, мир!'.encode('utf-8'),
+            {CONTENT_TRANSFER_ENCODING: 'binary'})
+        stream = part.serialize()
+        self.assertEqual(b'Content-Transfer-Encoding: binary\r\n'
+                         b'Content-Type: application/octet-stream',
+                         next(stream))
+        self.assertEqual(b'\r\n\r\n', next(stream))
+
+        self.assertEqual(b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82,'
+                         b' \xd0\xbc\xd0\xb8\xd1\x80!', next(stream))
+        self.assertEqual(b'\r\n', next(stream))
+        self.assertIsNone(next(stream, None))
+
     def test_serialize_with_content_transfer_encoding_unknown(self):
         part = aiohttp.multipart.BodyPartWriter(
             'Time to Relax!', {CONTENT_TRANSFER_ENCODING: 'unknown'})
diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py
--- a/tests/test_web_functional.py
+++ b/tests/test_web_functional.py
@@ -178,6 +178,38 @@ def handler(request):
     yield from resp.release()
 
 
+@asyncio.coroutine
+def test_multipart_content_transfer_encoding(loop, test_client):
+    """For issue #1168"""
+    with multipart.MultipartWriter() as writer:
+        writer.append(b'\x00' * 10,
+                      headers={'Content-Transfer-Encoding': 'binary'})
+
+    @asyncio.coroutine
+    def handler(request):
+        reader = yield from request.multipart()
+        assert isinstance(reader, multipart.MultipartReader)
+
+        part = yield from reader.next()
+        assert isinstance(part, multipart.BodyPartReader)
+        assert part.headers['Content-Transfer-Encoding'] == 'binary'
+        thing = yield from part.read()
+        assert thing == b'\x00' * 10
+
+        resp = web.Response()
+        resp.content_type = 'application/json'
+        resp.body = b''
+        return resp
+
+    app = web.Application(loop=loop)
+    app.router.add_post('/', handler)
+    client = yield from test_client(app)
+
+    resp = yield from client.post('/', data=writer, headers=writer.headers)
+    assert 200 == resp.status
+    yield from resp.release()
+
+
 @asyncio.coroutine
 def test_render_redirect(loop, test_client):