This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-mime4j.git
commit d8aa672222b1752e6fd1ad9485eccc5b09e329a8 Author: Benoit TELLIER <[email protected]> AuthorDate: Thu Nov 5 05:26:45 2026 +0100 [FIX] RawField#getBody: delete CR and LF before decoding, as RawFieldParser getBody decoded the bytes as UTF-8 and then unfolded the text, whereas RawFieldParser leaves line breaks out of the bytes it decodes. A bare CR inside a multi-byte sequence therefore split it for the strict parsers only: filename="caf<C3><CR><A9>.txt" read caf<U+FFFD><U+FFFD>.txt strict and café.txt lenient. getBody now deletes the line breaks from the bytes first, so that both paths agree. --- .../org/apache/james/mime4j/stream/RawField.java | 28 +++++++++++++++++++++- .../field/BareLineBreakStrictLenientTest.java | 28 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/james/mime4j/stream/RawField.java b/core/src/main/java/org/apache/james/mime4j/stream/RawField.java index 879278bc..a508f69f 100644 --- a/core/src/main/java/org/apache/james/mime4j/stream/RawField.java +++ b/core/src/main/java/org/apache/james/mime4j/stream/RawField.java @@ -22,6 +22,7 @@ package org.apache.james.mime4j.stream; import java.nio.charset.StandardCharsets; import java.util.Locale; +import org.apache.james.mime4j.util.ByteArrayBuffer; import org.apache.james.mime4j.util.ByteSequence; import org.apache.james.mime4j.util.CharsetUtil; import org.apache.james.mime4j.util.ContentUtil; @@ -161,11 +162,36 @@ public final class RawField implements Field { if (len > off && (CharsetUtil.isWhitespace((char) (raw.byteAt(off) & 0xff)))) { off++; } - return MimeUtil.unfold(ContentUtil.decode(raw, off, len - off, StandardCharsets.UTF_8)); + return decodeUnfolded(raw, off, len); } return null; } + /** + * Deletes CR and LF from the bytes before decoding, as RawFieldParser does: a line break inside + * a multi-byte sequence must not split it. + */ + private static String decodeUnfolded(ByteSequence raw, int off, int len) { + ByteArrayBuffer unfolded = null; + for (int i = off; i < len; i++) { + byte b = raw.byteAt(i); + if (b == '\r' || b == '\n') { + if (unfolded == null) { + unfolded = new ByteArrayBuffer(len - off); + for (int j = off; j < i; j++) { + unfolded.append(raw.byteAt(j)); + } + } + } else if (unfolded != null) { + unfolded.append(b); + } + } + if (unfolded == null) { + return ContentUtil.decode(raw, off, len - off, StandardCharsets.UTF_8); + } + return ContentUtil.decode(unfolded, 0, unfolded.length(), StandardCharsets.UTF_8); + } + public int getDelimiterIdx() { return delimiterIdx; } diff --git a/dom/src/test/java/org/apache/james/mime4j/field/BareLineBreakStrictLenientTest.java b/dom/src/test/java/org/apache/james/mime4j/field/BareLineBreakStrictLenientTest.java index 5fc1bd06..b88cfb3c 100644 --- a/dom/src/test/java/org/apache/james/mime4j/field/BareLineBreakStrictLenientTest.java +++ b/dom/src/test/java/org/apache/james/mime4j/field/BareLineBreakStrictLenientTest.java @@ -19,14 +19,20 @@ package org.apache.james.mime4j.field; +import org.apache.james.mime4j.codec.DecodeMonitor; import org.apache.james.mime4j.dom.field.ContentDispositionField; import org.apache.james.mime4j.dom.field.ContentLanguageField; import org.apache.james.mime4j.dom.field.ContentLocationField; import org.apache.james.mime4j.dom.field.ContentTypeField; import org.apache.james.mime4j.dom.field.MimeVersionField; +import org.apache.james.mime4j.stream.RawField; +import org.apache.james.mime4j.stream.RawFieldParser; +import org.apache.james.mime4j.util.ByteArrayBuffer; import org.junit.Assert; import org.junit.Test; +import java.nio.charset.StandardCharsets; + /** * A CR or LF that is not part of a fold is deleted by MimeUtil#unfold, which the strict parsers * read: the lenient parsers must not take it as a word separator either. @@ -89,4 +95,26 @@ public class BareLineBreakStrictLenientTest { Assert.assertEquals(raw, strict.getDispositionType(), lenient.getDispositionType()); Assert.assertEquals(raw, strict.getParameter(parameter), lenient.getParameter(parameter)); } + + @Test + public void bareCarriageReturnInsideAUtf8SequenceShouldBeReadTheSameWayByBothParsers() throws Exception { + assertFilename("caf\u00e9.txt", "Content-Disposition: a; filename=\"caf\u00c3\r\u00a9.txt\""); + assertFilename("caf\u00e9.txt", "Content-Disposition: a; filename=caf\u00c3\r\u00a9.txt"); + } + + @Test + public void bareLineFeedInsideAUtf8SequenceShouldBeReadTheSameWayByBothParsers() throws Exception { + assertFilename("caf\u00e9.txt", "Content-Disposition: a; filename=\"caf\u00c3\n\u00a9.txt\""); + } + + private static void assertFilename(String expected, String latin1) throws Exception { + RawField field = RawFieldParser.DEFAULT.parseField( + new ByteArrayBuffer(latin1.getBytes(StandardCharsets.ISO_8859_1), true)); + + ContentDispositionField strict = (ContentDispositionField) DefaultFieldParser.getParser().parse(field, DecodeMonitor.SILENT); + ContentDispositionField lenient = (ContentDispositionField) LenientFieldParser.getParser().parse(field, DecodeMonitor.SILENT); + + Assert.assertEquals(expected, lenient.getFilename()); + Assert.assertEquals(expected, strict.getFilename()); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
