diff --git a/lib/src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java b/lib/src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java index 37e49bf3..b6358300 100644 --- a/lib/src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java +++ b/lib/src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java @@ -16,6 +16,8 @@ public class ParcelFileDescriptorUtil { + private static final int BUFFER_SIZE = 1024; + private ParcelFileDescriptorUtil() { } public static ParcelFileDescriptor pipeFrom(InputStream inputStream, IThreadListener listener) @@ -61,7 +63,7 @@ static class TransferThread extends Thread { @Override public void run() { - byte[] buf = new byte[1024]; + byte[] buf = new byte[BUFFER_SIZE]; int len; try { diff --git a/lib/src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java b/lib/src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java index 1b31c989..65efe674 100644 --- a/lib/src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java +++ b/lib/src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java @@ -23,6 +23,7 @@ import com.nextcloud.android.sso.exceptions.SSOException; import com.nextcloud.android.sso.model.SingleSignOnAccount; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -37,8 +38,8 @@ public class NextcloudAPI implements AutoCloseable { + private static final int PEEK_LIMIT = 64; private static final String TAG = NextcloudAPI.class.getCanonicalName(); - private static final EmptyResponse EMPTY_RESPONSE = new EmptyResponse(); private final NetworkRequest networkRequest; @@ -127,27 +128,55 @@ public T convertStreamToTargetEntity(InputStream inputStream, Type targetEnt ensureTypeNotVoid(targetEntity); final T result; - try (InputStream os = inputStream; - Reader targetReader = new InputStreamReader(os)) { - if (targetEntity == EmptyResponse.class) { + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + reader.mark(PEEK_LIMIT); + boolean empty = isReaderContainsEmptyResponse(reader); + reader.reset(); + + if (targetEntity == EmptyResponse.class || empty) { //noinspection unchecked - result = (T) EMPTY_RESPONSE; - } else { - result = gson.fromJson(targetReader, targetEntity); - if (result == null) { - if (targetEntity == Object.class) { - //noinspection unchecked - return (T) EMPTY_RESPONSE; - } else { - throw new IllegalStateException("Could not instantiate \"" + - targetEntity + "\", because response was null."); - } + return (T) EMPTY_RESPONSE; + } + + result = gson.fromJson(reader, targetEntity); + + if (result == null) { + if (targetEntity == Object.class) { + //noinspection unchecked + return (T) EMPTY_RESPONSE; + } else { + throw new IllegalStateException("Could not instantiate \"" + + targetEntity + "\", because response was null."); } } } + return result; } + public boolean isReaderContainsEmptyResponse(Reader reader) throws IOException { + reader.mark(PEEK_LIMIT); + try { + int c; + int count = 0; + while ((c = reader.read()) != -1 && count < PEEK_LIMIT) { + if (c != '\u0000' && !Character.isWhitespace(c)) { + // Found meaningful character + return false; + } + count++; + } + return true; + } finally { + try { + reader.reset(); + } catch (Exception e) { + // Ignored + } + } + } + /** * The InputStreams needs to be closed after reading from it * diff --git a/lib/src/test/java/com/nextcloud/android/sso/api/TestRetrofitAPI.java b/lib/src/test/java/com/nextcloud/android/sso/api/TestRetrofitAPI.java index 295371eb..25924937 100644 --- a/lib/src/test/java/com/nextcloud/android/sso/api/TestRetrofitAPI.java +++ b/lib/src/test/java/com/nextcloud/android/sso/api/TestRetrofitAPI.java @@ -11,9 +11,10 @@ */ package com.nextcloud.android.sso.api; -import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; @@ -34,6 +35,8 @@ import org.mockito.junit.MockitoRule; import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; @@ -75,6 +78,8 @@ public void setUp() throws Exception { lenient().when(nextcloudApiMock.getGson()).thenReturn(new GsonBuilder().create()); lenient().when(nextcloudApiMock.performRequestObservableV2(any(), any())).thenReturn(Observable.empty()); lenient().when(nextcloudApiMock.performNetworkRequestV2(any())).thenReturn(new com.nextcloud.android.sso.api.Response(null, null)); + lenient().when(nextcloudApiMock.isReaderContainsEmptyResponse(any())) + .thenCallRealMethod(); mApi = new NextcloudRetrofitApiBuilder(nextcloudApiMock, mApiEndpoint).create(API.class); } @@ -538,4 +543,31 @@ public void testEnqueue() throws InterruptedException { assertTrue(successful); } + @Test + public void testValidJson() throws IOException { + String json = "{\"name\": \"value\", \"type\": \"value_2\"}"; + Reader reader = new StringReader(json); + assertFalse(nextcloudApiMock.isReaderContainsEmptyResponse(reader)); + } + + @Test + public void testValidXml() throws IOException { + String xml = "UserUser2"; + Reader reader = new StringReader(xml); + assertFalse(nextcloudApiMock.isReaderContainsEmptyResponse(reader)); + } + + @Test + public void testOnlyNulls() throws IOException { + String nulls = "\u0000\u0000\u0000\u0000\u0000 \n\t"; // nulls + whitespace + Reader reader = new StringReader(nulls); + assertTrue(nextcloudApiMock.isReaderContainsEmptyResponse(reader)); + } + + @Test + public void testWhitespaceOnly() throws IOException { + String whitespace = " \n\t "; + Reader reader = new StringReader(whitespace); + assertTrue(nextcloudApiMock.isReaderContainsEmptyResponse(reader)); + } }