From 44a0a9196101494c63bfbbe3338e80cf745b6818 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Fri, 27 Dec 2024 23:29:53 -0600 Subject: [PATCH 1/6] Replace ByteArrayOutputStream#toString() with alternative. --- .../in/dragonbra/javasteam/util/stream/BinaryReader.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java index 960561e1..15315c61 100644 --- a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java +++ b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java @@ -129,9 +129,11 @@ public String readNullTermString(Charset charset) throws IOException { byte[] bytes = buffer.toByteArray(); position += bytes.length; + return new String(bytes, charset); } + @SuppressWarnings("StringOperationCanBeSimplified") private String readNullTermUtf8String() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int b; @@ -146,7 +148,8 @@ private String readNullTermUtf8String() throws IOException { position++; // Increment for the null terminator - return baos.toString(StandardCharsets.UTF_8); + // Suppressed. Fixes NoSuchMethodError in Java 11 on the Google Pixel 3 XL (Android 12) + return new String(baos.toByteArray(), StandardCharsets.UTF_8); } public int getPosition() { From 840b2f0edbbb960ee67811a3a7f07b26d058f254 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Sun, 29 Dec 2024 01:00:01 -0600 Subject: [PATCH 2/6] Fix crashes with readNBytes by adding compat extension functions. --- .../dragonbra/javasteam/steam/cdn/Client.kt | 5 +- .../contentdownloader/ContentDownloader.kt | 5 +- .../contentdownloader/FileManifestProvider.kt | 5 +- .../javasteam/types/DepotManifest.kt | 7 +- .../javasteam/types/Steam3Manifest.kt | 7 +- .../in/dragonbra/javasteam/util/VZipUtil.kt | 3 +- .../in/dragonbra/javasteam/util/ZipUtil.kt | 3 +- .../javasteam/util/compat/InputStream.kt | 92 +++++++++++++++++++ 8 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 src/main/java/in/dragonbra/javasteam/util/compat/InputStream.kt diff --git a/src/main/java/in/dragonbra/javasteam/steam/cdn/Client.kt b/src/main/java/in/dragonbra/javasteam/steam/cdn/Client.kt index c1c5ff03..0fbe584f 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/cdn/Client.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/cdn/Client.kt @@ -6,6 +6,7 @@ import `in`.dragonbra.javasteam.types.ChunkData import `in`.dragonbra.javasteam.types.DepotManifest import `in`.dragonbra.javasteam.util.SteamKitWebRequestException import `in`.dragonbra.javasteam.util.Strings +import `in`.dragonbra.javasteam.util.compat.readNBytesCompat import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.log.Logger import `in`.dragonbra.javasteam.util.stream.MemoryStream @@ -257,7 +258,7 @@ class Client(steamClient: SteamClient) : Closeable { if (depotKey == null) { val bytesRead = withTimeout(responseBodyTimeout) { response.body.byteStream().use { input -> - input.readNBytes(destination, 0, contentLength) + input.readNBytesCompat(destination, 0, contentLength) } } @@ -274,7 +275,7 @@ class Client(steamClient: SteamClient) : Closeable { try { val bytesRead = withTimeout(responseBodyTimeout) { response.body.byteStream().use { input -> - input.readNBytes(buffer, 0, contentLength) + input.readNBytesCompat(buffer, 0, contentLength) } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/ContentDownloader.kt b/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/ContentDownloader.kt index 0793ec6d..043f7ef4 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/ContentDownloader.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/ContentDownloader.kt @@ -17,6 +17,7 @@ import `in`.dragonbra.javasteam.types.KeyValue import `in`.dragonbra.javasteam.util.SteamKitWebRequestException import `in`.dragonbra.javasteam.util.Strings import `in`.dragonbra.javasteam.util.Utils +import `in`.dragonbra.javasteam.util.compat.readNBytesCompat import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.log.Logger import kotlinx.coroutines.CancellationException @@ -419,7 +420,7 @@ class ContentDownloader(val steamClient: SteamClient) { fsOld.channel.position(match.oldChunk.offset) val tmp = ByteArray(match.oldChunk.uncompressedLength) - fsOld.readNBytes(tmp, 0, tmp.size) + fsOld.readNBytesCompat(tmp, 0, tmp.size) val adler = Utils.adlerHash(tmp) if (adler != match.oldChunk.checksum) { @@ -441,7 +442,7 @@ class ContentDownloader(val steamClient: SteamClient) { fsOld.channel.position(match.oldChunk.offset) val tmp = ByteArray(match.oldChunk.uncompressedLength) - fsOld.readNBytes(tmp, 0, tmp.size) + fsOld.readNBytesCompat(tmp, 0, tmp.size) fs.channel.position(match.newChunk.offset) fs.write(tmp) diff --git a/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/FileManifestProvider.kt b/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/FileManifestProvider.kt index 65614989..0c454da7 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/FileManifestProvider.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/FileManifestProvider.kt @@ -1,6 +1,7 @@ package `in`.dragonbra.javasteam.steam.contentdownloader import `in`.dragonbra.javasteam.types.DepotManifest +import `in`.dragonbra.javasteam.util.compat.readNBytesCompat import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.log.Logger import java.io.ByteArrayOutputStream @@ -70,7 +71,7 @@ class FileManifestProvider(private val file: Path) : IManifestProvider { if (!entry.isDirectory) { val entryBytes = ByteArray(entry.size.toInt()) - from.readNBytes(entryBytes, 0, entryBytes.size) + from.readNBytesCompat(entryBytes, 0, entryBytes.size) to.write(entryBytes) } @@ -126,7 +127,7 @@ class FileManifestProvider(private val file: Path) : IManifestProvider { ZipInputStream(fis).use { zip -> seekToEntry(zip, getLatestEntryName(depotID))?.let { idEntry -> if (idEntry.size > 0) { - ByteBuffer.wrap(zip.readNBytes(idEntry.size.toInt())).getLong() + ByteBuffer.wrap(zip.readNBytesCompat(idEntry.size.toInt())).getLong() } else { null } diff --git a/src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt b/src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt index 63543378..877d1f54 100644 --- a/src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt +++ b/src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt @@ -6,6 +6,7 @@ import `in`.dragonbra.javasteam.protobufs.steamclient.ContentManifest.ContentMan import `in`.dragonbra.javasteam.protobufs.steamclient.ContentManifest.ContentManifestPayload import `in`.dragonbra.javasteam.protobufs.steamclient.ContentManifest.ContentManifestSignature import `in`.dragonbra.javasteam.util.Utils +import `in`.dragonbra.javasteam.util.compat.readNBytesCompat import `in`.dragonbra.javasteam.util.crypto.CryptoHelper import `in`.dragonbra.javasteam.util.log.LogManager import `in`.dragonbra.javasteam.util.log.Logger @@ -249,17 +250,17 @@ class DepotManifest { PROTOBUF_PAYLOAD_MAGIC -> { val payloadLength = br.readInt() - payload = ContentManifestPayload.parseFrom(stream.readNBytes(payloadLength)) + payload = ContentManifestPayload.parseFrom(stream.readNBytesCompat(payloadLength)) } PROTOBUF_METADATA_MAGIC -> { val metadataLength = br.readInt() - metadata = ContentManifestMetadata.parseFrom(stream.readNBytes(metadataLength)) + metadata = ContentManifestMetadata.parseFrom(stream.readNBytesCompat(metadataLength)) } PROTOBUF_SIGNATURE_MAGIC -> { val signatureLength = br.readInt() - signature = ContentManifestSignature.parseFrom(stream.readNBytes(signatureLength)) + signature = ContentManifestSignature.parseFrom(stream.readNBytesCompat(signatureLength)) } else -> throw NoSuchElementException("Unrecognized magic value ${magic.toHexString(HexFormat.Default)} in depot manifest.") diff --git a/src/main/java/in/dragonbra/javasteam/types/Steam3Manifest.kt b/src/main/java/in/dragonbra/javasteam/types/Steam3Manifest.kt index 4f22b5d9..745ea5b3 100644 --- a/src/main/java/in/dragonbra/javasteam/types/Steam3Manifest.kt +++ b/src/main/java/in/dragonbra/javasteam/types/Steam3Manifest.kt @@ -3,6 +3,7 @@ package `in`.dragonbra.javasteam.types import `in`.dragonbra.javasteam.enums.EDepotFileFlag +import `in`.dragonbra.javasteam.util.compat.readNBytesCompat import `in`.dragonbra.javasteam.util.stream.BinaryReader import java.time.Instant import java.util.Date @@ -48,7 +49,7 @@ class Steam3Manifest( ) { companion object { internal fun deserialize(ds: BinaryReader): Chunk = Chunk( - chunkGID = ds.readNBytes(20), + chunkGID = ds.readNBytesCompat(20), checksum = ds.readInt(), offset = ds.readLong(), decompressedSize = ds.readInt(), @@ -62,8 +63,8 @@ class Steam3Manifest( val fileName = ds.readNullTermString(Charsets.UTF_8) val totalSize = ds.readLong() val flags = EDepotFileFlag.from(ds.readInt()) - val hashContent = ds.readNBytes(20) - val hashFileName = ds.readNBytes(20) + val hashContent = ds.readNBytesCompat(20) + val hashFileName = ds.readNBytesCompat(20) val numChunks = ds.readInt() return FileMapping( diff --git a/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt b/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt index f321f8e3..f626b8c3 100644 --- a/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt +++ b/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt @@ -1,5 +1,6 @@ package `in`.dragonbra.javasteam.util +import `in`.dragonbra.javasteam.util.compat.readNBytesCompat import `in`.dragonbra.javasteam.util.crypto.CryptoHelper import `in`.dragonbra.javasteam.util.stream.BinaryReader import `in`.dragonbra.javasteam.util.stream.BinaryWriter @@ -69,7 +70,7 @@ object VZipUtil { dictionarySize, windowBuffer ).use { lzmaInput -> - lzmaInput.readNBytes(destination, 0, sizeDecompressed) + lzmaInput.readNBytesCompat(destination, 0, sizeDecompressed) } if (verifyChecksum && Utils.crc32(destination).toInt() != outputCrc) { diff --git a/src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt b/src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt index ec6d8a6f..b581ca20 100644 --- a/src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt +++ b/src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt @@ -1,5 +1,6 @@ package `in`.dragonbra.javasteam.util +import `in`.dragonbra.javasteam.util.compat.readNBytesCompat import `in`.dragonbra.javasteam.util.stream.MemoryStream import java.util.zip.ZipInputStream @@ -16,7 +17,7 @@ object ZipUtil { throw IllegalArgumentException("The destination buffer is smaller than the decompressed data size.") } - val bytesRead = zip.readNBytes(destination, 0, sizeDecompressed) + val bytesRead = zip.readNBytesCompat(destination, 0, sizeDecompressed) if (zip.nextEntry != null) { throw IllegalArgumentException("Given stream should only contain one zip entry") diff --git a/src/main/java/in/dragonbra/javasteam/util/compat/InputStream.kt b/src/main/java/in/dragonbra/javasteam/util/compat/InputStream.kt new file mode 100644 index 00000000..5f9ff54a --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/util/compat/InputStream.kt @@ -0,0 +1,92 @@ +package `in`.dragonbra.javasteam.util.compat + +import java.io.IOException +import java.io.InputStream +import java.util.Objects +import kotlin.jvm.Throws +import kotlin.math.min + +/** + * Compatibility (extension) functions for [InputStream.readNBytes]. + * These are basically the same from InputStream. + */ + +@Throws(IOException::class) +fun InputStream.readNBytesCompat(b: ByteArray, off: Int, len: Int): Int { + Objects.checkFromIndexSize(off, len, b.size) + + var n = 0 + + while (n < len) { + val count = read(b, off + n, len - n) + if (count < 0) { + break + } + n += count + } + + return n +} + +@Suppress("RedundantExplicitType") +@Throws(IOException::class) +fun InputStream.readNBytesCompat(len: Int): ByteArray { + if (len < 0) { + throw IllegalArgumentException("len < 0") + } + + var bufs: MutableList? = null + var result: ByteArray? = null + var total: Int = 0 + var remaining: Int = len + var n: Int + + do { + var buf = ByteArray(min(remaining, 8192)) + var nread = 0 + + // read to EOF which may read more or less than buffer size + while (read(buf, nread, minOf(buf.size - nread, remaining)).also { n = it } > 0) { + nread += n + remaining -= n + } + + if (nread > 0) { + if ((Integer.MAX_VALUE - 8) - total < nread) { + throw OutOfMemoryError("Required array size too large") + } + total += nread + if (result == null) { + result = buf + } else { + if (bufs == null) { + bufs = arrayListOf() + bufs.add(result) + } + bufs.add(buf) + } + } + // if the last call to read returned -1 or the number of bytes + // requested have been read then break + } while (n >= 0 && remaining > 0) + + if (bufs == null) { + if (result == null) { + return ByteArray(0) + } + return if (result.size == total) result else result.copyOf(total) + } + + result = ByteArray(total) + var offset = 0 + remaining = total + + bufs.forEach { b -> + var count = min(b.size, remaining) + System.arraycopy(b, 0, result, offset, count) + offset += count + remaining -= count + } + + return result +} From ea7b2bab406afb09ce7691dd66e935c642e465b2 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Sun, 29 Dec 2024 01:01:37 -0600 Subject: [PATCH 3/6] Rename file. --- .../util/compat/{InputStream.kt => InputStreamCompat.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/in/dragonbra/javasteam/util/compat/{InputStream.kt => InputStreamCompat.kt} (100%) diff --git a/src/main/java/in/dragonbra/javasteam/util/compat/InputStream.kt b/src/main/java/in/dragonbra/javasteam/util/compat/InputStreamCompat.kt similarity index 100% rename from src/main/java/in/dragonbra/javasteam/util/compat/InputStream.kt rename to src/main/java/in/dragonbra/javasteam/util/compat/InputStreamCompat.kt From 4ef516497d04fcd453c8d9764a31827e50e474ca Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Sun, 29 Dec 2024 01:10:05 -0600 Subject: [PATCH 4/6] Rework ByteArrayOutputStream compatibility. --- .../util/compat/ByteArrayOutputStreamCompat.kt | 13 +++++++++++++ .../javasteam/util/stream/BinaryReader.java | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt diff --git a/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt b/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt new file mode 100644 index 00000000..317f81c2 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt @@ -0,0 +1,13 @@ +package `in`.dragonbra.javasteam.util.compat + +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset + +/** + * Compatibility class to provide compatibility with Java ByteArrayOutputStream. + */ +object ByteArrayOutputStreamCompat { + + @JvmStatic + fun toString(baos: ByteArrayOutputStream, charset: Charset): String = String(baos.toByteArray(), charset) +} diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java index 15315c61..fd2f7035 100644 --- a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java +++ b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java @@ -1,5 +1,7 @@ package in.dragonbra.javasteam.util.stream; +import in.dragonbra.javasteam.util.compat.ByteArrayOutputStreamCompat; + import java.io.*; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -133,7 +135,6 @@ public String readNullTermString(Charset charset) throws IOException { return new String(bytes, charset); } - @SuppressWarnings("StringOperationCanBeSimplified") private String readNullTermUtf8String() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int b; @@ -148,8 +149,7 @@ private String readNullTermUtf8String() throws IOException { position++; // Increment for the null terminator - // Suppressed. Fixes NoSuchMethodError in Java 11 on the Google Pixel 3 XL (Android 12) - return new String(baos.toByteArray(), StandardCharsets.UTF_8); + return ByteArrayOutputStreamCompat.toString(baos, StandardCharsets.UTF_8); } public int getPosition() { From fb51550815af415146bde00ef695c87ad93d9644 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Mon, 30 Dec 2024 18:00:56 -0600 Subject: [PATCH 5/6] Further refine ByteArrayOutputStreamCompat --- .../javasteam/util/compat/ByteArrayOutputStreamCompat.kt | 4 ++-- .../java/in/dragonbra/javasteam/util/stream/BinaryReader.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt b/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt index 317f81c2..ca6d3b8e 100644 --- a/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt +++ b/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt @@ -1,7 +1,6 @@ package `in`.dragonbra.javasteam.util.compat import java.io.ByteArrayOutputStream -import java.nio.charset.Charset /** * Compatibility class to provide compatibility with Java ByteArrayOutputStream. @@ -9,5 +8,6 @@ import java.nio.charset.Charset object ByteArrayOutputStreamCompat { @JvmStatic - fun toString(baos: ByteArrayOutputStream, charset: Charset): String = String(baos.toByteArray(), charset) + fun toString(byteArrayOutputStream: ByteArrayOutputStream): String = + String(byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size()) } diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java index fd2f7035..3ed59ab0 100644 --- a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java +++ b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java @@ -149,7 +149,7 @@ private String readNullTermUtf8String() throws IOException { position++; // Increment for the null terminator - return ByteArrayOutputStreamCompat.toString(baos, StandardCharsets.UTF_8); + return ByteArrayOutputStreamCompat.toString(baos); } public int getPosition() { From 11a33cbb59bdc826d63595f225200d963c409871 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Mon, 30 Dec 2024 18:01:38 -0600 Subject: [PATCH 6/6] Add tests for compat functions, ensuring parity with the methods they replace. --- .../ByteArrayOutputStreamCompatTest.java | 93 +++++++++++ .../util/compat/InputStreamCompatTest.kt | 154 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 src/test/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompatTest.java create mode 100644 src/test/java/in/dragonbra/javasteam/util/compat/InputStreamCompatTest.kt diff --git a/src/test/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompatTest.java b/src/test/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompatTest.java new file mode 100644 index 00000000..73e83a7d --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompatTest.java @@ -0,0 +1,93 @@ +package in.dragonbra.javasteam.util.compat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +public class ByteArrayOutputStreamCompatTest { + + @Test + public void testEmptyStream() { + var baos = new ByteArrayOutputStream(); + + var compatResult = ByteArrayOutputStreamCompat.toString(baos); + var standardResult = baos.toString(); + + Assertions.assertEquals("", compatResult); + Assertions.assertEquals("", standardResult); + Assertions.assertEquals(standardResult, compatResult); + } + + @Test + public void testAsciiContent() { + var baos = new ByteArrayOutputStream(); + var testString = "Hello, World!"; + + baos.write(testString.getBytes(StandardCharsets.UTF_8), 0, testString.length()); + + var compatResult = ByteArrayOutputStreamCompat.toString(baos); + var standardResult = baos.toString(); + + Assertions.assertEquals(testString, compatResult); + Assertions.assertEquals(testString, standardResult); + Assertions.assertEquals(standardResult, compatResult); + } + + @Test + public void testUnicodeContent() { + var baos = new ByteArrayOutputStream(); + var testString = "Hello, δΈ–η•Œ! πŸ‘‹"; + var bytes = testString.getBytes(StandardCharsets.UTF_8); + + baos.write(bytes, 0, bytes.length); + + var compatResult = ByteArrayOutputStreamCompat.toString(baos); + var standardResult = baos.toString(); + + Assertions.assertEquals(testString, compatResult); + Assertions.assertEquals(testString, standardResult); + Assertions.assertEquals(standardResult, compatResult); + } + + @Test + public void testLargeContent() { + var baos = new ByteArrayOutputStream(); + var largeString = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + largeString.append("Line ").append(i).append("\n"); + } + var testString = largeString.toString(); + var bytes = testString.getBytes(StandardCharsets.UTF_8); + baos.write(bytes, 0, bytes.length); + + var compatResult = ByteArrayOutputStreamCompat.toString(baos); + var standardResult = baos.toString(); + + Assertions.assertEquals(testString, compatResult); + Assertions.assertEquals(testString, standardResult); + Assertions.assertEquals(standardResult, compatResult); + } + + @Test + public void testPartialWrites() { + var baos = new ByteArrayOutputStream(); + var part1 = "Hello"; + var part2 = ", "; + var part3 = "World!"; + + baos.write(part1.getBytes(StandardCharsets.UTF_8), 0, part1.length()); + baos.write(part2.getBytes(StandardCharsets.UTF_8), 0, part2.length()); + baos.write(part3.getBytes(StandardCharsets.UTF_8), 0, part3.length()); + + var expected = part1 + part2 + part3; + var compatResult = ByteArrayOutputStreamCompat.toString(baos); + var standardResult = baos.toString(); + + Assertions.assertEquals(expected, compatResult); + Assertions.assertEquals(expected, standardResult); + Assertions.assertEquals(standardResult, compatResult); + } + +} diff --git a/src/test/java/in/dragonbra/javasteam/util/compat/InputStreamCompatTest.kt b/src/test/java/in/dragonbra/javasteam/util/compat/InputStreamCompatTest.kt new file mode 100644 index 00000000..60852a0d --- /dev/null +++ b/src/test/java/in/dragonbra/javasteam/util/compat/InputStreamCompatTest.kt @@ -0,0 +1,154 @@ +package `in`.dragonbra.javasteam.util.compat + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import kotlin.random.Random + +/** + * [readNBytesCompat] methods are kotlin only extension functions for [InputStream]. + * This test is in Kotlin to ensure functionality for the extension functions + */ +class InputStreamCompatTest { + + private lateinit var testData: ByteArray + private lateinit var emptyStream: InputStream + private lateinit var normalStream: InputStream + private lateinit var largeStream: InputStream + + @BeforeEach + fun setup() { + testData = Random.nextBytes(16384) + emptyStream = ByteArrayInputStream(ByteArray(0)) + normalStream = ByteArrayInputStream(testData) + largeStream = ByteArrayInputStream(Random.nextBytes(1024 * 1024)) + } + + @Nested + inner class ReadNBytesCompatArrayTest { + + @Test + fun `empty stream returns zero bytes`() { + val buffer = ByteArray(100) + val bytesRead = emptyStream.readNBytesCompat(buffer, 0, buffer.size) + Assertions.assertEquals(0, bytesRead) + } + + @Test + fun `reading zero bytes returns zero`() { + val buffer = ByteArray(100) + val bytesRead = normalStream.readNBytesCompat(buffer, 0, 0) + Assertions.assertEquals(0, bytesRead) + } + + @ParameterizedTest + @ValueSource(ints = [1, 100, 1000, 8192]) + fun `reading n bytes matches JDK implementation`(n: Int) { + val compatBuffer = ByteArray(n) + val jdkBuffer = ByteArray(n) + + val stream1 = ByteArrayInputStream(testData) + val stream2 = ByteArrayInputStream(testData) + + val compatBytesRead = stream1.readNBytesCompat(compatBuffer, 0, n) + val jdkBytesRead = stream2.readNBytes(jdkBuffer, 0, n) + + Assertions.assertArrayEquals(jdkBuffer, compatBuffer) + Assertions.assertEquals(jdkBytesRead, compatBytesRead) + } + + @Test + fun `throws exception on negative length`() { + val buffer = ByteArray(100) + Assertions.assertThrows(IndexOutOfBoundsException::class.java) { + normalStream.readNBytesCompat(buffer, 0, -1) + } + } + + @Test + fun `throws exception on invalid offset`() { + val buffer = ByteArray(100) + Assertions.assertThrows(IndexOutOfBoundsException::class.java) { + normalStream.readNBytesCompat(buffer, -1, 50) + } + Assertions.assertThrows(IndexOutOfBoundsException::class.java) { + normalStream.readNBytesCompat(buffer, 101, 50) + } + } + } + + @Nested + inner class ReadNBytesCompatLengthTest { + + @Test + fun `empty stream returns empty array`() { + val result = emptyStream.readNBytesCompat(100) + Assertions.assertEquals(0, result.size) + } + + @Test + fun `reading zero bytes returns empty array`() { + val result = normalStream.readNBytesCompat(0) + Assertions.assertEquals(0, result.size) + } + + @ParameterizedTest + @ValueSource(ints = [1, 100, 1000, 8192, 16384]) + fun `reading n bytes matches JDK implementation`(n: Int) { + val stream1 = ByteArrayInputStream(testData) + val stream2 = ByteArrayInputStream(testData) + + val compatResult = stream1.readNBytesCompat(n) + val jdkResult = stream2.readNBytes(n) + + Assertions.assertArrayEquals(jdkResult, compatResult) + } + + @Test + fun `throws exception on negative length`() { + Assertions.assertThrows(IllegalArgumentException::class.java) { + normalStream.readNBytesCompat(-1) + } + } + + @Test + fun `handles large reads correctly`() { + val lengthToRead = 1024 * 1024 + val result = largeStream.readNBytesCompat(lengthToRead) + Assertions.assertEquals(lengthToRead, result.size) + } + + @Test + fun `partial read when EOF reached`() { + val result = normalStream.readNBytesCompat(testData.size * 2) + Assertions.assertEquals(testData.size, result.size) + Assertions.assertArrayEquals(testData, result) + } + } + + @Nested + inner class ErrorConditionsTest { + + @Test + fun `handles IOException from underlying stream`() { + val failingStream = object : InputStream() { + override fun read(): Int = throw IOException("Simulated failure") + override fun read(b: ByteArray, off: Int, len: Int): Int = throw IOException("Simulated failure") + } + + Assertions.assertThrows(IOException::class.java) { + failingStream.readNBytesCompat(100) + } + + Assertions.assertThrows(IOException::class.java) { + failingStream.readNBytesCompat(ByteArray(100), 0, 100) + } + } + } +}