From d3ca21df3678b030b50a09bee5626e0a4e3a0256 Mon Sep 17 00:00:00 2001 From: mohammed arib Date: Mon, 13 Jul 2026 11:24:18 +0530 Subject: [PATCH] avoid emitting a phantom byte on truncated input in QPDecoderStream Signed-off-by: mohammed arib --- .../angus/mail/util/QPDecoderStream.java | 12 +++- .../angus/mail/util/QPDecoderStreamTest.java | 71 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/QPDecoderStreamTest.java diff --git a/core/src/main/java/org/eclipse/angus/mail/util/QPDecoderStream.java b/core/src/main/java/org/eclipse/angus/mail/util/QPDecoderStream.java index c29c8e4c..ed5b81da 100644 --- a/core/src/main/java/org/eclipse/angus/mail/util/QPDecoderStream.java +++ b/core/src/main/java/org/eclipse/angus/mail/util/QPDecoderStream.java @@ -95,7 +95,7 @@ public int read() throws IOException { } else if (a == '\r') { // Expecting LF. This forms a soft linebreak to be ignored. int b = in.read(); - if (b != '\n') + if (b != '\n' && b != -1) /* Not really confirming QP encoding, but * lets allow this as well. */ @@ -106,7 +106,15 @@ public int read() throws IOException { return -1; } else { ba[0] = (byte) a; - ba[1] = (byte) in.read(); + int b = in.read(); + if (b == -1) { + // '=' followed by a single character at EOF; not a valid + // encoded atom. Push back the real character and return + // '=' literally, without inventing a second byte. + ((PushbackInputStream) in).unread(a); + return c; + } + ba[1] = (byte) b; try { return ASCIIUtility.parseInt(ba, 0, 2, 16); } catch (NumberFormatException nex) { diff --git a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/QPDecoderStreamTest.java b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/QPDecoderStreamTest.java new file mode 100644 index 00000000..141e4767 --- /dev/null +++ b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/util/QPDecoderStreamTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.eclipse.angus.mail.util; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import static org.junit.Assert.assertArrayEquals; + +/** + * Test quoted-printable decoder. + */ + +public class QPDecoderStreamTest { + + private static byte[] decode(byte[] in) throws Exception { + InputStream qp = new QPDecoderStream(new ByteArrayInputStream(in)); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int c; + while ((c = qp.read()) != -1) + bos.write(c); + qp.close(); + return bos.toByteArray(); + } + + /** + * A valid encoded atom decodes to the corresponding byte. + */ + @Test + public void testDecodeAtom() throws Exception { + assertArrayEquals(new byte[]{'A'}, decode(new byte[]{'=', '4', '1'})); + } + + /** + * An '=' followed by a single hex digit at EOF must not add a byte that + * wasn't in the input. The truncated atom is passed through literally. + */ + @Test + public void testTruncatedAtomAtEOF() throws Exception { + assertArrayEquals(new byte[]{'=', '4'}, decode(new byte[]{'=', '4'})); + assertArrayEquals(new byte[]{'h', 'i', '=', '4'}, + decode(new byte[]{'h', 'i', '=', '4'})); + } + + /** + * An '=' followed by CR at EOF (a truncated soft line break) must not add + * a byte that wasn't in the input. + */ + @Test + public void testTruncatedSoftBreakAtEOF() throws Exception { + assertArrayEquals(new byte[0], decode(new byte[]{'=', '\r'})); + assertArrayEquals(new byte[]{'A'}, decode(new byte[]{'A', '=', '\r'})); + } +}