Repository: netty/netty (Java)

Issue:
Parsing multipart/form-data requests with form-data content-type including optional parameters uses last parameter as content-type
### Expected behavior
When passed a content type header with optional parameter, content type should be correctly set and optional parameters should be attributes.

e.g. 

contnet-type: audio/ogg; codecs=opus should have content type of audio/ogg and a codecs attribute of opus

### Actual behavior

content type is currently set to codecs=opus

### Steps to reproduce

Create a request with a content type containing an optional parameter on the content type header

### Minimal yet complete reproducer code (or URL to code)

The following test added to HttpPostMultiPartRequestDecoderTest will currently fail.

```
    @Test
    public void testDecodeFullHttpRequestWithOptionalParameters() {
        String content = "\n--861fbeab-cd20-470c-9609-d40a0f704466\r\n" +
                "content-disposition: form-data; " +
                "name=\"file\"; filename=\"myfile.ogg\"\r\n" +
                "content-type: audio/ogg; codecs=opus; charset=UTF8\r\ncontent-transfer-encoding: binary\r\n" +
                "\r\n\u0001\u0002\u0003\u0004\r\n--861fbeab-cd20-470c-9609-d40a0f704466--\r\n\",\n";

        FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload",
                Unpooled.copiedBuffer(content, CharsetUtil.US_ASCII));
        req.headers().set("content-type", "multipart/form-data; boundary=861fbeab-cd20-470c-9609-d40a0f704466");
        req.headers().set("content-length", content.length());

        HttpPostMultipartRequestDecoder test = new HttpPostMultipartRequestDecoder(req);
        assertEquals("audio/ogg", ((MixedFileUpload)test.getBodyHttpDatas("file").get(0)).getContentType());
    }

```

### Netty version
4.1.74

### JVM version (e.g. `java -version`)
openjdk version "1.8.0_302"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_302-b08)      
OpenJDK 64-Bit Server VM (Temurin)(build 25.302-b08, mixed mode)

### OS version (e.g. `uname -a`)
Windows 10, Ubuntu 18.04


Hints:

Patch:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
@@ -771,6 +771,18 @@ private InterfaceHttpData findMultipartDisposition() {
                                 throw new ErrorDataDecoderException(e);
                             }
                             currentFieldAttributes.put(HttpHeaderValues.CHARSET, attribute);
+                        } else if (contents[i].contains("=")) {
+                            String name = StringUtil.substringBefore(contents[i], '=');
+                            String values = StringUtil.substringAfter(contents[i], '=');
+                            Attribute attribute;
+                            try {
+                                attribute = factory.createAttribute(request, cleanString(name), values);
+                            } catch (NullPointerException e) {
+                                throw new ErrorDataDecoderException(e);
+                            } catch (IllegalArgumentException e) {
+                                throw new ErrorDataDecoderException(e);
+                            }
+                            currentFieldAttributes.put(name, attribute);
                         } else {
                             Attribute attribute;
                             try {
diff --git a/common/src/main/java/io/netty/util/internal/StringUtil.java b/common/src/main/java/io/netty/util/internal/StringUtil.java
--- a/common/src/main/java/io/netty/util/internal/StringUtil.java
+++ b/common/src/main/java/io/netty/util/internal/StringUtil.java
@@ -102,6 +102,19 @@ public static String substringAfter(String value, char delim) {
         return null;
     }
 
+    /**
+     * Get the item before one char delim if the delim is found (else null).
+     * This operation is a simplified and optimized
+     * version of {@link String#split(String, int)}.
+     */
+    public static String substringBefore(String value, char delim) {
+        int pos = value.indexOf(delim);
+        if (pos >= 0) {
+            return value.substring(0, pos);
+        }
+        return null;
+    }
+
     /**
      * Checks if two strings have the same suffix of specified length
      *


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostMultiPartRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostMultiPartRequestDecoderTest.java
--- a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostMultiPartRequestDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostMultiPartRequestDecoderTest.java
@@ -300,6 +300,25 @@ public void testNotBadReleaseBuffersDuringDecodingMixedFactory() throws IOExcept
         commonNotBadReleaseBuffersDuringDecoding(factory, false);
     }
 
+    @Test
+    public void testDecodeFullHttpRequestWithOptionalParameters() {
+        String content = "\n--861fbeab-cd20-470c-9609-d40a0f704466\r\n" +
+                "content-disposition: form-data; " +
+                "name=\"file\"; filename=\"myfile.ogg\"\r\n" +
+                "content-type: audio/ogg; codecs=opus; charset=UTF8\r\ncontent-transfer-encoding: binary\r\n" +
+                "\r\n\u0001\u0002\u0003\u0004\r\n--861fbeab-cd20-470c-9609-d40a0f704466--\r\n\",\n";
+
+        FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload",
+                Unpooled.copiedBuffer(content, CharsetUtil.US_ASCII));
+        req.headers().set("content-type", "multipart/form-data; boundary=861fbeab-cd20-470c-9609-d40a0f704466");
+        req.headers().set("content-length", content.length());
+
+        HttpPostMultipartRequestDecoder test = new HttpPostMultipartRequestDecoder(req);
+        FileUpload httpData = (FileUpload) test.getBodyHttpDatas("file").get(0);
+        assertEquals("audio/ogg", httpData.getContentType());
+        test.destroy();
+    }
+
     private static void commonNotBadReleaseBuffersDuringDecoding(HttpDataFactory factory, boolean inMemory)
             throws IOException {
         int nbItems = 20;