Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a56f12f1 authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #1931 from k9mail/GH-1919_fix_ImapResponseParser_bug

Properly handle exceptions thrown by ImapResponseCallback
parents 406fd6b9 55c87fca
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -69,4 +69,10 @@ public class FixedLengthInputStream extends InputStream {
    public String toString() {
        return String.format(Locale.US, "FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
    }

    public void skipRemaining() throws IOException {
        while (available() > 0) {
            skip(available());
        }
    }
}
+16 −10
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ class ImapResponseParser {
            }

            if (exception != null) {
                throw new RuntimeException("readResponse(): Exception in callback method", exception);
                throw new ImapResponseParserException("readResponse(): Exception in callback method", exception);
            }

            return response;
@@ -354,24 +354,30 @@ class ImapResponseParser {
        if (response.getCallback() != null) {
            FixedLengthInputStream fixed = new FixedLengthInputStream(inputStream, size);

            Exception callbackException = null;
            Object result = null;
            try {
                result = response.getCallback().foundLiteral(response, fixed);
            } catch (IOException e) {
                // Pass IOExceptions through
                throw e;
            } catch (Exception e) {
                // Catch everything else and save it for later.
                exception = e;
                callbackException = e;
            }

            // Check if only some of the literal data was read
            int available = fixed.available();
            if (available > 0 && available != size) {
                // If so, skip the rest
                while (fixed.available() > 0) {
                    fixed.skip(fixed.available());
            boolean someDataWasRead = fixed.available() != size;
            if (someDataWasRead) {
                if (result == null && callbackException == null) {
                    throw new AssertionError("Callback consumed some data but returned no result");
                }

                fixed.skipRemaining();
            }

            if (callbackException != null) {
                if (exception == null) {
                    exception = callbackException;
                }
                return "EXCEPTION";
            }
            
            if (result != null) {
+8 −0
Original line number Diff line number Diff line
package com.fsck.k9.mail.store.imap;


public class ImapResponseParserException extends RuntimeException {
    public ImapResponseParserException(String message, Throwable cause) {
        super(message, cause);
    }
}
+98 −19
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import static org.junit.Assert.fail;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 21)
public class ImapResponseParserTest {
    private PeekableInputStream peekableInputStream;


    @Test
    public void testSimpleOkResponse() throws IOException {
@@ -231,7 +233,7 @@ public class ImapResponseParserTest {
    @Test
    public void testParseLiteralWithConsumingCallbackReturningNull() throws Exception {
        ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
        TestImapResponseCallback callback = new TestImapResponseCallback(4, "cheeseburger");
        TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(4, "cheeseburger");

        ImapResponse response = parser.readResponse(callback);

@@ -242,37 +244,88 @@ public class ImapResponseParserTest {
    @Test
    public void testParseLiteralWithNonConsumingCallbackReturningNull() throws Exception {
        ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
        TestImapResponseCallback callback = new TestImapResponseCallback(0, null);
        TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(0, null);

        ImapResponse response = parser.readResponse(callback);

        assertEquals(1, response.size());
        assertEquals("test", response.getString(0));
        assertTrue(callback.foundLiteralCalled);
        assertAllInputConsumed();
    }

    @Test
    public void readResponse_withPartlyConsumingCallbackReturningNull_shouldThrow() throws Exception {
        ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
        TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(2, null);

        try {
            parser.readResponse(callback);
            fail();
        } catch (AssertionError e) {
            assertEquals("Callback consumed some data but returned no result", e.getMessage());
        }
    }

    @Test
    public void readResponse_withPartlyConsumingCallbackThatThrows_shouldReadAllDataAndThrow() throws Exception {
        ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
        TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(2);

        try {
            parser.readResponse(callback);
            fail();
        } catch (ImapResponseParserException e) {
            assertEquals("readResponse(): Exception in callback method", e.getMessage());
            assertEquals(ImapResponseParserTestException.class, e.getCause().getClass());
        }

        assertAllInputConsumed();
    }

    @Test
    public void readResponse_withCallbackThatThrowsRepeatedly_shouldConsumeAllInputAndThrowFirstException()
            throws Exception {
        ImapResponseParser parser = createParser("* {3}\r\none {3}\r\ntwo\r\n");
        TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(3);

        try {
            parser.readResponse(callback);
            fail();
        } catch (ImapResponseParserException e) {
            assertEquals("readResponse(): Exception in callback method", e.getMessage());
            assertEquals(ImapResponseParserTestException.class, e.getCause().getClass());
            assertEquals(0, ((ImapResponseParserTestException) e.getCause()).instanceNumber);
        }

        assertAllInputConsumed();
    }

    @Test
    public void testParseLiteralWithIncompleteConsumingCallbackReturningString() throws Exception {
        ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
        TestImapResponseCallback callback = new TestImapResponseCallback(2, "ninja");
        TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(2, "ninja");

        ImapResponse response = parser.readResponse(callback);

        assertEquals(1, response.size());
        assertEquals("ninja", response.getString(0));
        assertAllInputConsumed();
    }

    @Test(expected = RuntimeException.class)
    @Test
    public void testParseLiteralWithThrowingCallback() throws Exception {
        ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
        ImapResponseCallback callback = new ImapResponseCallback() {
            @Override
            public Object foundLiteral(ImapResponse response, FixedLengthInputStream literal) throws Exception {
                throw new RuntimeException();
            }
        };
        ImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(0);

        try {
            parser.readResponse(callback);
            fail();
        } catch (ImapResponseParserException e) {
            assertEquals("readResponse(): Exception in callback method", e.getMessage());
        }
        
        assertAllInputConsumed();
    }

    @Test(expected = IOException.class)
@@ -415,19 +468,34 @@ public class ImapResponseParserTest {

    private ImapResponseParser createParser(String response) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(response.getBytes());
        PeekableInputStream peekableInputStream = new PeekableInputStream(byteArrayInputStream);
        peekableInputStream = new PeekableInputStream(byteArrayInputStream);
        return new ImapResponseParser(peekableInputStream);
    }

    private void assertAllInputConsumed() throws IOException {
        assertEquals(0, peekableInputStream.available());
    }


    private class TestImapResponseCallback implements ImapResponseCallback {
    static class TestImapResponseCallback implements ImapResponseCallback {
        private final int readNumberOfBytes;
        private final Object returnValue;
        private final boolean throwException;
        private int exceptionCount = 0;
        public boolean foundLiteralCalled = false;

        TestImapResponseCallback(int readNumberOfBytes, Object returnValue) {
        public static TestImapResponseCallback readBytesAndReturn(int readNumberOfBytes, Object returnValue) {
            return new TestImapResponseCallback(readNumberOfBytes, returnValue, false);
        }

        public static TestImapResponseCallback readBytesAndThrow(int readNumberOfBytes) {
            return new TestImapResponseCallback(readNumberOfBytes, null, true);
        }

        private TestImapResponseCallback(int readNumberOfBytes, Object returnValue, boolean throwException) {
            this.readNumberOfBytes = readNumberOfBytes;
            this.returnValue = returnValue;
            this.throwException = throwException;
        }

        @Override
@@ -435,17 +503,28 @@ public class ImapResponseParserTest {
            foundLiteralCalled = true;

            int skipBytes = readNumberOfBytes;
            long skippedBytes;
            do {
                skippedBytes = literal.skip(skipBytes);
            while (skipBytes > 0) {
                long skippedBytes = literal.skip(skipBytes);
                skipBytes -= skippedBytes;
            } while (skippedBytes > 0);
            }
            
            if (throwException) {
                throw new ImapResponseParserTestException(exceptionCount++);
            }

            return returnValue;
        }
    }

    private class TestUntaggedHandler implements UntaggedHandler {
    static class ImapResponseParserTestException extends RuntimeException {
        public final int instanceNumber;

        public ImapResponseParserTestException(int instanceNumber) {
            this.instanceNumber = instanceNumber;
        }
    } 
    
    static class TestUntaggedHandler implements UntaggedHandler {
        public final List<ImapResponse> responses = new ArrayList<ImapResponse>();

        @Override