From 484da0a695532a4f325578e6dccb262553f769fa Mon Sep 17 00:00:00 2001 From: lsocha Date: Thu, 21 Nov 2024 14:39:48 +0100 Subject: [PATCH 1/2] feat: Enforce exact byte reading from Content-Length header for BoxFile representation --- .../java/com/box/sdk/BinaryBodyUtils.java | 71 ++++++++++++++++++- src/main/java/com/box/sdk/BoxFile.java | 3 +- src/test/java/com/box/sdk/BoxFileTest.java | 2 + 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/box/sdk/BinaryBodyUtils.java b/src/main/java/com/box/sdk/BinaryBodyUtils.java index 58c4bcb24..002b3b5a6 100644 --- a/src/main/java/com/box/sdk/BinaryBodyUtils.java +++ b/src/main/java/com/box/sdk/BinaryBodyUtils.java @@ -47,7 +47,40 @@ static void writeStream(BoxAPIResponse response, OutputStream output, ProgressLi } /** - * Writes content of input stream to provided output. Method is NOT closing input stream. + * Writes response body bytes to output stream. After all closes the input stream. + * + * @param response Response that is going to be written. + * @param output Output stream. + */ + + static void writeStreamWithContentLength(BoxAPIResponse response, OutputStream output) { + writeStreamWithContentLength(response, output, null); + } + + /** + * Writes response body bytes to output stream. After all closes the input stream. + * + * @param response Response that is going to be written. + * @param output Output stream. + * @param listener Listener that will be notified on writing response. Can be null. + */ + + static void writeStreamWithContentLength(BoxAPIResponse response, OutputStream output, ProgressListener listener) { + try { + InputStream input; + if (listener != null) { + input = response.getBody(listener); + } else { + input = response.getBody(); + } + writeStreamTo(input, output, response.getContentLength()); + } finally { + response.close(); + } + } + + /** + * Writes content of input stream to provided output. * * @param input Input that will be read. * @param output Output stream. @@ -71,4 +104,40 @@ static void writeStreamTo(InputStream input, OutputStream output) { } } } + + /** + * Writes the content of the input stream to the provided output stream, ensuring the exact number of bytes specified + * by the expected length is written. If the stream ends prematurely an exception is thrown. + * + * @param input The input stream to be read. + * @param output The output stream where data will be written. + * @param expectedLength The expected number of bytes to be transferred. + */ + + static void writeStreamTo(InputStream input, OutputStream output, long expectedLength) { + long totalBytesRead = 0; + if (expectedLength < 0) { + throw new RuntimeException("Expected content length should not be negative: " + expectedLength); + } + try { + byte[] buffer = new byte[BUFFER_SIZE]; + for (int n = input.read(buffer); n != -1; n = input.read(buffer)) { + output.write(buffer, 0, n); + totalBytesRead += n; // Track the total bytes read + } + if (totalBytesRead != expectedLength) { + throw new IOException("Stream ended prematurely. Expected " + expectedLength + + " bytes, but read " + totalBytesRead + " bytes."); + } + } catch (IOException e) { + throw new RuntimeException("Error during streaming: " + e.getMessage(), e); + } finally { + try { + input.close(); + output.close(); + } catch (IOException closeException) { + throw new RuntimeException("IOException during stream close", closeException); + } + } + } } diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index bcb62cf15..ca3d9883d 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -1,6 +1,7 @@ package com.box.sdk; import static com.box.sdk.BinaryBodyUtils.writeStream; +import static com.box.sdk.BinaryBodyUtils.writeStreamWithContentLength; import static com.box.sdk.http.ContentType.APPLICATION_JSON; import static com.box.sdk.http.ContentType.APPLICATION_JSON_PATCH; import static com.eclipsesource.json.Json.NULL; @@ -602,7 +603,7 @@ private void makeRepresentationContentRequest( URL repURL = new URL(representationURLTemplate.replace("{+asset_path}", assetPath)); BoxAPIRequest repContentReq = new BoxAPIRequest(this.getAPI(), repURL, HttpMethod.GET); BoxAPIResponse response = repContentReq.send(); - writeStream(response, output); + writeStreamWithContentLength(response, output); } catch (MalformedURLException ex) { throw new BoxAPIException("Could not generate representation content URL"); diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 6d642c638..62f7479bb 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -433,6 +433,7 @@ public void testGetThumbnailSucceeds() { )) .willReturn(WireMock.aResponse() .withHeader("Content-Type", "image/jpg") + .withHeader("Content-Length", String.valueOf(13)) .withBody("This is a JPG") .withStatus(200)) ); @@ -517,6 +518,7 @@ public void testGetRepresentationContentSuccess() { )) .willReturn(WireMock.aResponse() .withHeader("Content-Type", "image/jpg") + .withHeader("Content-Length", String.valueOf(13)) .withBody("This is a JPG") .withStatus(200)) ); From 4f6cb13bdaf6c02947fb11c5c7e78db13d7f8fcc Mon Sep 17 00:00:00 2001 From: lsocha Date: Thu, 21 Nov 2024 14:57:41 +0100 Subject: [PATCH 2/2] improve user test --- src/intTest/java/com/box/sdk/BoxUserIT.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/intTest/java/com/box/sdk/BoxUserIT.java b/src/intTest/java/com/box/sdk/BoxUserIT.java index eeeefe520..28b05649d 100644 --- a/src/intTest/java/com/box/sdk/BoxUserIT.java +++ b/src/intTest/java/com/box/sdk/BoxUserIT.java @@ -23,6 +23,7 @@ import java.net.URLDecoder; import java.util.Calendar; import java.util.Collection; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import okhttp3.OkHttpClient; @@ -35,8 +36,8 @@ */ public class BoxUserIT { - private static final String NEW_USER_LOGIN = "login2@boz.com"; - private static final String NEW_USER_NAME = "non-empty name"; + private static final String NEW_USER_LOGIN = UUID.randomUUID() + "@boz.com"; + private static final String NEW_USER_NAME = UUID.randomUUID().toString(); @BeforeClass public static void cleanup() {