Rewrite FlowedMessageUtils.deflow()

This new version should use a lot less allocations.
This commit is contained in:
cketti 2022-04-21 23:40:27 +02:00
parent f7b6b8371f
commit ef8d9abed3
2 changed files with 99 additions and 122 deletions

View file

@ -1,120 +1,83 @@
package com.fsck.k9.mail.internet;
package com.fsck.k9.mail.internet
/**
* Adapted from the Apache James project, see
* https://james.apache.org/mailet/base/apidocs/org/apache/mailet/base/FlowedMessageUtils.html
*
* <p>Manages texts encoded as <code>text/plain; format=flowed</code>.</p>
* <p>As a reference see:</p>
* <ul>
* <li><a href='http://www.rfc-editor.org/rfc/rfc2646.txt'>RFC2646</a></li>
* <li><a href='http://www.rfc-editor.org/rfc/rfc3676.txt'>RFC3676</a> (new method with DelSP support).
* </ul>
* <h4>Note</h4>
* <ul>
* <li>In order to decode, the input text must belong to a mail with headers similar to:
* Content-Type: text/plain; charset="CHARSET"; [delsp="yes|no"; ]format="flowed"
* (the quotes around CHARSET are not mandatory).
* Furthermore the header Content-Transfer-Encoding MUST NOT BE Quoted-Printable
* (see RFC3676 paragraph 4.2).(In fact this happens often for non 7bit messages).
* </li>
* </ul>
* Decodes text encoded as `text/plain; format=flowed` (RFC 3676).
*/
public final class FlowedMessageUtils {
private static final char RFC2646_SPACE = ' ';
private static final char RFC2646_QUOTE = '>';
private static final String RFC2646_SIGNATURE = "-- ";
private static final String RFC2646_CRLF = "\r\n";
object FlowedMessageUtils {
private const val QUOTE = '>'
private const val SPACE = ' '
private const val CR = '\r'
private const val LF = '\n'
private const val SIGNATURE = "-- "
private const val CRLF = "\r\n"
private FlowedMessageUtils() {
// this class cannot be instantiated
}
@JvmStatic
fun deflow(text: String, delSp: Boolean): String {
var lineStartIndex = 0
var lastLineQuoteDepth = 0
var lastLineFlowed = false
/**
* Decodes a text previously wrapped using "format=flowed".
*/
public static String deflow(String text, boolean delSp) {
String[] lines = text.split("\r\n|\n", -1);
StringBuffer result = null;
StringBuffer resultLine = new StringBuffer();
int resultLineQuoteDepth = 0;
boolean resultLineFlowed = false;
// One more cycle, to close the last line
for (int i = 0; i <= lines.length; i++) {
String line = i < lines.length ? lines[i] : null;
int actualQuoteDepth = 0;
return buildString {
while (lineStartIndex <= text.lastIndex) {
var quoteDepth = 0
while (lineStartIndex <= text.lastIndex && text[lineStartIndex] == QUOTE) {
quoteDepth++
lineStartIndex++
}
if (line != null) {
if (line.equals(RFC2646_SIGNATURE)) {
// signature handling (the previous line is not flowed)
resultLineFlowed = false;
} else if (line.length() > 0 && line.charAt(0) == RFC2646_QUOTE) {
// Quote
actualQuoteDepth = 1;
while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) {
actualQuoteDepth++;
// Remove space stuffing
if (lineStartIndex <= text.lastIndex && text[lineStartIndex] == SPACE) {
lineStartIndex++
}
// We support both LF and CRLF line endings. To cover both cases we search for LF.
val lineFeedIndex = text.indexOf(LF, lineStartIndex)
val lineBreakFound = lineFeedIndex != -1
var lineEndIndex = if (lineBreakFound) lineFeedIndex else text.length
if (lineEndIndex > 0 && text[lineEndIndex - 1] == CR) {
lineEndIndex--
}
if (lastLineFlowed && quoteDepth != lastLineQuoteDepth) {
append(CRLF)
lastLineFlowed = false
}
val lineIsSignatureMarker = lineEndIndex - lineStartIndex == SIGNATURE.length &&
text.regionMatches(lineStartIndex, SIGNATURE, 0, SIGNATURE.length)
var lineFlowed = false
if (lineIsSignatureMarker) {
if (lastLineFlowed) {
append(CRLF)
lastLineFlowed = false
}
// if quote-depth changes wrt the previous line then this is not flowed
if (resultLineQuoteDepth != actualQuoteDepth) {
resultLineFlowed = false;
}
line = line.substring(actualQuoteDepth);
} else {
// if quote-depth changes wrt the first line then this is not flowed
if (resultLineQuoteDepth > 0) {
resultLineFlowed = false;
}
}
if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE) {
// Line space-stuffed
line = line.substring(1);
}
// if the previous was the last then it was not flowed
} else {
resultLineFlowed = false;
}
// Add the PREVIOUS line.
// This often will find the flow looking for a space as the last char of the line.
// With quote changes or signatures it could be the following line to void the flow.
if (!resultLineFlowed && i > 0) {
if (resultLineQuoteDepth > 0) {
resultLine.insert(0, RFC2646_SPACE);
}
for (int j = 0; j < resultLineQuoteDepth; j++) {
resultLine.insert(0, RFC2646_QUOTE);
}
if (result == null) {
result = new StringBuffer();
} else {
result.append(RFC2646_CRLF);
}
result.append(resultLine.toString());
resultLine = new StringBuffer();
resultLineFlowed = false;
}
resultLineQuoteDepth = actualQuoteDepth;
if (line != null) {
if (!line.equals(RFC2646_SIGNATURE) && line.endsWith("" + RFC2646_SPACE) && i < lines.length - 1) {
// Line flowed (NOTE: for the split operation the line having i == lines.length is the last that
// does not end with RFC2646_CRLF)
} else if (lineEndIndex > lineStartIndex && text[lineEndIndex - 1] == SPACE) {
lineFlowed = true
if (delSp) {
line = line.substring(0, line.length() - 1);
lineEndIndex--
}
resultLineFlowed = true;
} else {
resultLineFlowed = false;
}
resultLine.append(line);
if (!lastLineFlowed && quoteDepth > 0) {
// This is not a continuation line, so prefix the text with quote characters.
repeat(quoteDepth) {
append(QUOTE)
}
append(SPACE)
}
append(text, lineStartIndex, lineEndIndex)
if (!lineFlowed && lineBreakFound) {
append(CRLF)
}
lineStartIndex = if (lineBreakFound) lineFeedIndex + 1 else text.length
lastLineQuoteDepth = quoteDepth
lastLineFlowed = lineFlowed
}
}
return result.toString();
}
}

View file

@ -4,16 +4,13 @@ import com.fsck.k9.mail.crlf
import com.google.common.truth.Truth.assertThat
import org.junit.Test
private const val DEL_SP_NO = false
private const val DEL_SP_YES = true
class FlowedMessageUtilsTest {
@Test
fun `deflow() with simple text`() {
val input = "Text that should be \r\n" +
"displayed on one line"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("Text that should be displayed on one line")
}
@ -27,7 +24,7 @@ class FlowedMessageUtilsTest {
"Text that should retain\r\n" +
"its line break."
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@ -42,7 +39,7 @@ class FlowedMessageUtilsTest {
fun `deflow() with nothing to do`() {
val input = "Line one\r\nLine two\r\n"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(input)
}
@ -56,7 +53,7 @@ class FlowedMessageUtilsTest {
"Some more text that should be \r\n" +
"displayed on one line.\r\n"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@ -74,7 +71,7 @@ class FlowedMessageUtilsTest {
val input = "> Quoted text \r\n" +
"Some other text"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("> Quoted text \r\nSome other text")
}
@ -86,7 +83,7 @@ class FlowedMessageUtilsTest {
"> is here\r\n" +
"Some other text"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@ -103,7 +100,7 @@ class FlowedMessageUtilsTest {
"\r\n" +
"Text"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(input)
}
@ -112,7 +109,7 @@ class FlowedMessageUtilsTest {
fun `deflow() with delSp=true`() {
val input = "Text that is wrapped mid wo \r\nrd"
val result = FlowedMessageUtils.deflow(input, DEL_SP_YES)
val result = FlowedMessageUtils.deflow(input, delSp = true)
assertThat(result).isEqualTo("Text that is wrapped mid word")
}
@ -122,7 +119,7 @@ class FlowedMessageUtilsTest {
val input = "> Quoted te \r\n" +
"> xt"
val result = FlowedMessageUtils.deflow(input, DEL_SP_YES)
val result = FlowedMessageUtils.deflow(input, delSp = true)
assertThat(result).isEqualTo("> Quoted text")
}
@ -132,7 +129,7 @@ class FlowedMessageUtilsTest {
val input = "Text that should be \r\n" +
" displayed on one line"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("Text that should be displayed on one line")
}
@ -143,7 +140,7 @@ class FlowedMessageUtilsTest {
" Line 2\r\n" +
" Line 3\r\n"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("Line 1\r\nLine 2\r\nLine 3\r\n")
}
@ -153,7 +150,7 @@ class FlowedMessageUtilsTest {
val input = "> Text that should be \r\n" +
"> displayed on one line"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo("> Text that should be displayed on one line")
}
@ -167,7 +164,7 @@ class FlowedMessageUtilsTest {
"Signature \r\n" +
"text"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@ -188,7 +185,7 @@ class FlowedMessageUtilsTest {
"> Signature \r\n" +
"> text"
val result = FlowedMessageUtils.deflow(input, DEL_SP_NO)
val result = FlowedMessageUtils.deflow(input, delSp = false)
assertThat(result).isEqualTo(
"""
@ -199,4 +196,21 @@ class FlowedMessageUtilsTest {
""".trimIndent().crlf()
)
}
@Test
fun `deflow() with flowed line followed by signature separator`() {
val input = "Fake flowed line \r\n" +
"-- \r\n" +
"Signature"
val result = FlowedMessageUtils.deflow(input, delSp = true)
assertThat(result).isEqualTo(
"""
Fake flowed line
--${" "}
Signature
""".trimIndent().crlf()
)
}
}