diff --git a/changes.md b/changes.md index d625a433ac..e5d8d3bdce 100644 --- a/changes.md +++ b/changes.md @@ -229,6 +229,8 @@ Changes log entity stream only, and is ignored after the head of a response announcing an empty body. The failures caused by stopping the client report "Client stopped". Issue #1601. - The `Max-Age` and `Path` attributes of a cookie setting are no longer quoted. Issue #1334. + - `MultiPartRepresentation.setBoundary` kept only the boundary parameter when replacing an + existing boundary, dropping the media type's other parameters. Issue #1600. - `Restlet.handle()` no longer fails with a `NullPointerException` when the Restlet has no context and cannot be started. - `CookieAuthenticator` no longer lets the next restlet overwrite the redirection sent after a login. Issue #554. diff --git a/org.restlet/src/main/java/org/restlet/representation/MultiPartRepresentation.java b/org.restlet/src/main/java/org/restlet/representation/MultiPartRepresentation.java index bd93fdbde1..8fa7448647 100644 --- a/org.restlet/src/main/java/org/restlet/representation/MultiPartRepresentation.java +++ b/org.restlet/src/main/java/org/restlet/representation/MultiPartRepresentation.java @@ -201,8 +201,8 @@ public static String getBoundary(MediaType mediaType) { } /** - * Sets a boundary to an existing media type. If the original media-type already has a - * "boundary" parameter, it will be erased. * + * Sets a boundary to an existing media type. If the original media type already has a + * "boundary" parameter, its value is replaced. The other parameters are kept. * * @param mediaType The media type to update. * @param boundary The boundary to add as a parameter. @@ -212,13 +212,57 @@ public static MediaType setBoundary(MediaType mediaType, String boundary) { MediaType result = null; if (mediaType != null) { - if (mediaType.getParameters().getFirst(PARAMETER_BOUNDARY) != null) { - result = new MediaType(mediaType.getParent(), PARAMETER_BOUNDARY, boundary); - } else { - result = new MediaType(mediaType, PARAMETER_BOUNDARY, boundary); + result = + new MediaType( + new MediaType( + withoutBoundary(mediaType.getName()), + mediaType.getDescription()), + PARAMETER_BOUNDARY, + boundary); + } + + return result; + } + + /** + * Removes the boundary parameters from a media type name, which holds all the parameters, + * keeping the others as written. A null name, accepted by {@link MediaType}, is returned as is. + */ + private static String withoutBoundary(String name) { + int index = (name == null) ? -1 : name.indexOf(';'); + if (index < 0) { + return name; + } + + StringBuilder result = new StringBuilder(name.substring(0, index)); + for (String parameter : splitParameters(name.substring(index + 1))) { + String trimmed = parameter.trim(); + int equals = trimmed.indexOf('='); + String parameterName = (equals < 0) ? trimmed : trimmed.substring(0, equals).trim(); + if (!trimmed.isEmpty() && !PARAMETER_BOUNDARY.equalsIgnoreCase(parameterName)) { + result.append("; ").append(trimmed); } } + return result.toString(); + } + /** Splits parameters on ';', except inside quoted strings. */ + private static List splitParameters(String parameters) { + List result = new ArrayList<>(); + boolean quoted = false; + int start = 0; + for (int i = 0; i < parameters.length(); i++) { + char c = parameters.charAt(i); + if (quoted && c == '\\') { + i++; + } else if (c == '"') { + quoted = !quoted; + } else if (c == ';' && !quoted) { + result.add(parameters.substring(start, i)); + start = i + 1; + } + } + result.add(parameters.substring(start)); return result; } diff --git a/org.restlet/src/test/java/org/restlet/engine/connector/MultiPartRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/engine/connector/MultiPartRepresentationTestCase.java index 866d4a67c8..6c5c8df2a7 100644 --- a/org.restlet/src/test/java/org/restlet/engine/connector/MultiPartRepresentationTestCase.java +++ b/org.restlet/src/test/java/org/restlet/engine/connector/MultiPartRepresentationTestCase.java @@ -20,10 +20,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.restlet.data.MediaType; +import org.restlet.data.Parameter; import org.restlet.representation.FileRepresentation; import org.restlet.representation.MultiPartRepresentation; import org.restlet.representation.MultiPartRepresentation.Part; import org.restlet.representation.StringRepresentation; +import org.restlet.util.Series; /** * Test case for the {@link MultiPartRepresentation} class in multipart mode. @@ -77,6 +79,58 @@ void testContentType() { "multipart/form-data; boundary=myActualBoundary", rep.getMediaType().toString()); } + @Test + void setBoundaryKeepsTheOtherParameters() { + assertEquals( + "multipart/mixed; deferSpec=20220824; boundary=b", + MultiPartRepresentation.setBoundary( + MediaType.valueOf("multipart/mixed; deferSpec=20220824"), "b") + .toString()); + assertEquals( + "multipart/mixed; deferSpec=20220824; boundary=b", + MultiPartRepresentation.setBoundary( + MediaType.valueOf( + "multipart/mixed; boundary=a; deferSpec=20220824"), + "b") + .toString()); + assertEquals( + "multipart/related; type=\"a;boundary=x\"; boundary=b", + MultiPartRepresentation.setBoundary( + MediaType.valueOf( + "multipart/related; Boundary=a; type=\"a;boundary=x\""), + "b") + .toString()); + // As parsed from a Content-Type header + Series parameters = new Series<>(Parameter.class); + parameters.add("type", "application/xml"); + parameters.add("BOUNDARY", "a"); + assertEquals( + "multipart/related; type=\"application/xml\"; boundary=b", + MultiPartRepresentation.setBoundary( + new MediaType("multipart/related", parameters), "b") + .toString()); + assertNull(MultiPartRepresentation.setBoundary(null, "b")); + assertEquals( + MediaType.MULTIPART_FORM_DATA.getDescription(), + MultiPartRepresentation.setBoundary(MediaType.MULTIPART_FORM_DATA, "b") + .getDescription()); + assertEquals( + "b", + MultiPartRepresentation.getBoundary( + MultiPartRepresentation.setBoundary(new MediaType((String) null), "b"))); + + MultiPartRepresentation rep = + new MultiPartRepresentation( + MediaType.valueOf("multipart/mixed; deferSpec=20220824"), + "b", + List.of(new Part("a", null, new StringRepresentation("b")))); + assertEquals( + "multipart/mixed; deferSpec=20220824; boundary=b", rep.getMediaType().toString()); + rep.setBoundary("c"); + assertEquals( + "multipart/mixed; deferSpec=20220824; boundary=c", rep.getMediaType().toString()); + } + @Test void testRandomBoundary() { MultiPartRepresentation rep =