From 86246af64a6e342a939c814acf269dd687044ebc Mon Sep 17 00:00:00 2001 From: Kamil Berdychowski Date: Thu, 20 Aug 2026 14:45:38 +0200 Subject: [PATCH] fix: avoid null chunk when file size is a multiple of part size iterateChunks treated hasNext as true until EOF, so an exact-size last part was followed by a null stream and an NPE in uploadBigFile. Closes #1957 --- .../sdkgen/internal/utils/UtilsManager.java | 46 +++++++++------ .../internal/utils/UtilsManagerTest.java | 57 +++++++++++++++++++ 2 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 src/test/java/com/box/sdkgen/internal/utils/UtilsManagerTest.java diff --git a/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java b/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java index 665c23722..7832c8dcd 100644 --- a/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java +++ b/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -311,15 +312,14 @@ public static String hexToBase64(String hex) { public static Iterator iterateChunks( InputStream stream, long chunkSize, long fileSize) { return new Iterator() { - private boolean streamIsFinished = false; + private InputStream nextChunk; + private boolean isNextChunkPrepared = false; - @Override - public boolean hasNext() { - return !streamIsFinished; - } - - @Override - public InputStream next() { + private void prepareNext() { + if (isNextChunkPrepared) { + return; + } + isNextChunkPrepared = true; try { byte[] buffer = new byte[(int) chunkSize]; int bytesRead = 0; @@ -327,25 +327,39 @@ public InputStream next() { while (bytesRead < chunkSize) { int read = stream.read(buffer, bytesRead, (int) (chunkSize - bytesRead)); if (read == -1) { - // End of stream - streamIsFinished = true; break; } bytesRead += read; } if (bytesRead == 0) { - // No more data to yield - streamIsFinished = true; - return null; + nextChunk = null; + return; } - // Return the chunk as a ByteArrayInputStream - return new ByteArrayInputStream(buffer, 0, bytesRead); - } catch (Exception e) { + nextChunk = new ByteArrayInputStream(buffer, 0, bytesRead); + } catch (IOException e) { throw new RuntimeException("Error reading from stream", e); } } + + @Override + public boolean hasNext() { + prepareNext(); + return nextChunk != null; + } + + @Override + public InputStream next() { + prepareNext(); + if (nextChunk == null) { + throw new NoSuchElementException(); + } + InputStream result = nextChunk; + nextChunk = null; + isNextChunkPrepared = false; + return result; + } }; } diff --git a/src/test/java/com/box/sdkgen/internal/utils/UtilsManagerTest.java b/src/test/java/com/box/sdkgen/internal/utils/UtilsManagerTest.java new file mode 100644 index 000000000..6d9923c66 --- /dev/null +++ b/src/test/java/com/box/sdkgen/internal/utils/UtilsManagerTest.java @@ -0,0 +1,57 @@ +package com.box.sdkgen.internal.utils; + +import static com.box.sdkgen.internal.utils.UtilsManager.generateByteStream; +import static com.box.sdkgen.internal.utils.UtilsManager.iterateChunks; +import static com.box.sdkgen.internal.utils.UtilsManager.readByteStream; +import static com.box.sdkgen.internal.utils.UtilsManager.reduceIterator; + +import java.io.InputStream; +import java.util.Iterator; +import org.junit.jupiter.api.Test; + +public class UtilsManagerTest { + + @Test + public void testIterateChunksWhenFileSizeIsExactMultipleOfChunkSize() { + int chunkSize = 8; + int fileSize = 16; + drainAndAssertChunks(chunkSize, fileSize, fileSize, 2); + } + + @Test + public void testIterateChunksWhenFileSizeHasRemainder() { + int chunkSize = 8; + int fileSize = 20; + drainAndAssertChunks(chunkSize, fileSize, fileSize, 3); + } + + @Test + public void testIterateChunksReadsUntilEofWhenReportedFileSizeDoesNotMatch() { + int chunkSize = 8; + int streamSize = 16; + drainAndAssertChunks(chunkSize, streamSize, -1, 2); + } + + private static void drainAndAssertChunks( + int chunkSize, int streamSize, long reportedFileSize, int expectedChunkCount) { + Iterator chunks = + iterateChunks(generateByteStream(streamSize), chunkSize, reportedFileSize); + ChunkDrainResult result = + reduceIterator(chunks, UtilsManagerTest::reduceChunks, new ChunkDrainResult()); + assert result.chunkCount == expectedChunkCount; + assert result.totalBytes == streamSize; + } + + private static ChunkDrainResult reduceChunks(ChunkDrainResult acc, InputStream chunk) { + assert chunk != null; + byte[] bytes = readByteStream(chunk); + acc.chunkCount += 1; + acc.totalBytes += bytes.length; + return acc; + } + + private static final class ChunkDrainResult { + private int chunkCount; + private int totalBytes; + } +}