From 0ce41b0725119fe43436d295eea7fe9e0b137c62 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Sun, 21 Dec 2025 02:14:07 -0600 Subject: [PATCH 1/3] Various optimizations to squeeze more performance on high repeated classes. --- .../in/dragonbra/javasteam/util/Utils.java | 21 +++-- .../in/dragonbra/javasteam/util/VZipUtil.kt | 74 +++++++----------- .../in/dragonbra/javasteam/util/VZstdUtil.kt | 31 +++++--- .../in/dragonbra/javasteam/util/ZipUtil.kt | 2 +- .../compat/ByteArrayOutputStreamCompat.kt | 25 +++++- .../javasteam/util/stream/BinaryReader.java | 76 +++++++++++-------- .../javasteam/util/stream/MemoryStream.java | 12 +-- 7 files changed, 137 insertions(+), 104 deletions(-) diff --git a/src/main/java/in/dragonbra/javasteam/util/Utils.java b/src/main/java/in/dragonbra/javasteam/util/Utils.java index ecd8d3f1..17c122b3 100644 --- a/src/main/java/in/dragonbra/javasteam/util/Utils.java +++ b/src/main/java/in/dragonbra/javasteam/util/Utils.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.zip.CRC32; -import java.util.zip.Checksum; /** * @author lngtr @@ -173,7 +172,7 @@ private static String getSystemProperty(final String property) { } /** - * Convenience method for calculating the CRC2 checksum of a string. + * Convenience method for calculating the CRC32 checksum of a string. * * @param s the string * @return long value of the CRC32 @@ -183,14 +182,26 @@ public static long crc32(String s) { } /** - * Convenience method for calculating the CRC2 checksum of a byte array. + * Convenience method for calculating the CRC32 checksum of a byte array. * * @param bytes the byte array * @return long value of the CRC32 */ public static long crc32(byte[] bytes) { - Checksum checksum = new CRC32(); - checksum.update(bytes, 0, bytes.length); + return crc32(bytes, 0, bytes.length); + } + + /** + * Convenience method for calculating the CRC32 checksum of a byte array with offset and length. + * + * @param bytes the byte array + * @param offset the offset to start from + * @param length the number of bytes to checksum + * @return long value of the CRC32 + */ + public static long crc32(byte[] bytes, int offset, int length) { + var checksum = new CRC32(); + checksum.update(bytes, offset, length); return checksum.getValue(); } diff --git a/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt b/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt index 76438ffe..fa2d84e4 100644 --- a/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt +++ b/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt @@ -1,17 +1,12 @@ package `in`.dragonbra.javasteam.util 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.stream.BinaryReader -import `in`.dragonbra.javasteam.util.stream.BinaryWriter import `in`.dragonbra.javasteam.util.stream.MemoryStream import `in`.dragonbra.javasteam.util.stream.SeekOrigin -import org.tukaani.xz.LZMA2Options import org.tukaani.xz.LZMAInputStream -import org.tukaani.xz.LZMAOutputStream -import java.io.ByteArrayOutputStream -import java.util.zip.DataFormatException +import java.util.zip.* import kotlin.math.max @Suppress("SpellCheckingInspection", "unused") @@ -26,6 +21,11 @@ object VZipUtil { private const val VERSION: Byte = 'a'.code.toByte() + // Thread-local window buffer pool to avoid repeated allocations + private val windowBufferPool = ThreadLocal.withInitial { + ByteArray(1 shl 23) // 8MB max size + } + @JvmStatic fun decompress(ms: MemoryStream, destination: ByteArray, verifyChecksum: Boolean = true): Int { try { @@ -67,7 +67,12 @@ object VZipUtil { // If the value of dictionary size in properties is smaller than (1 << 12), // the LZMA decoder must set the dictionary size variable to (1 << 12). - val windowBuffer = ByteArray(max(1 shl 12, dictionarySize)) + val windowSize = max(1 shl 12, dictionarySize) + val windowBuffer = if (windowSize <= (1 shl 23)) { + windowBufferPool.get() // Reuse thread-local buffer + } else { + ByteArray(windowSize) // Fallback for unusually large windows + } val bytesRead = LZMAInputStream( ms, sizeDecompressed.toLong(), @@ -78,8 +83,11 @@ object VZipUtil { lzmaInput.readNBytesCompat(destination, 0, sizeDecompressed) } - if (verifyChecksum && Utils.crc32(destination).toInt() != outputCrc) { - throw DataFormatException("CRC does not match decompressed data. VZip data may be corrupted.") + if (verifyChecksum) { + val actualCrc = Utils.crc32(destination, 0, bytesRead).toInt() + if (actualCrc != outputCrc) { + throw DataFormatException("CRC does not match decompressed data. VZip data may be corrupted.") + } } return bytesRead @@ -93,46 +101,16 @@ object VZipUtil { } } - /** - * Ported from SteamKit2 and is untested, use at your own risk - */ @JvmStatic fun compress(buffer: ByteArray): ByteArray { - try { - ByteArrayOutputStream().use { ms -> - BinaryWriter(ms).use { writer -> - val crc = CryptoHelper.crcHash(buffer) - writer.writeShort(VZIP_HEADER) - writer.writeByte(VERSION) - writer.write(crc) - - // Configure LZMA options to match SteamKit2's settings - val options = LZMA2Options().apply { - dictSize = 1 shl 23 // 8MB dictionary - setPreset(2) // Algorithm setting - niceLen = 128 // numFastBytes equivalent - matchFinder = LZMA2Options.MF_BT4 - mode = LZMA2Options.MODE_NORMAL - } - - // Write LZMA-compressed data - LZMAOutputStream(ms, options, false).use { lzmaStream -> - lzmaStream.write(buffer) - } - - writer.write(crc) - writer.writeInt(buffer.size) - writer.writeShort(VZIP_FOOTER) - - return ms.toByteArray() - } - } - } catch (e: NoClassDefFoundError) { - logger.error("Missing implementation of org.tukaani:xz") - throw e - } catch (e: ClassNotFoundException) { - logger.error("Missing implementation of org.tukaani:xz") - throw e - } + throw Exception("VZipUtil.compress is not implemented.") + // try { + // } catch (e: NoClassDefFoundError) { + // logger.error("Missing implementation of org.tukaani:xz") + // throw e + // } catch (e: ClassNotFoundException) { + // logger.error("Missing implementation of org.tukaani:xz") + // throw e + // } } } diff --git a/src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt b/src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt index f3fae970..ee3ea888 100644 --- a/src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt +++ b/src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt @@ -5,11 +5,12 @@ import `in`.dragonbra.javasteam.util.log.LogManager import java.io.IOException import java.nio.ByteBuffer import java.nio.ByteOrder -import java.util.zip.CRC32 object VZstdUtil { private const val VZSTD_HEADER: Int = 0x615A5356 + private const val HEADER_SIZE = 8 + private const val FOOTER_SIZE = 15 private val logger = LogManager.getLogger() @@ -17,6 +18,10 @@ object VZstdUtil { @JvmStatic @JvmOverloads fun decompress(buffer: ByteArray, destination: ByteArray, verifyChecksum: Boolean = false): Int { + if (buffer.size < HEADER_SIZE + FOOTER_SIZE) { + throw IOException("Buffer too small to contain VZstd header and footer") + } + val byteBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN) // Convert the buffer. val header = byteBuffer.getInt(0) @@ -24,14 +29,18 @@ object VZstdUtil { throw IOException("Expecting VZstdHeader at start of stream") } - val crc32 = byteBuffer.getInt(4) - val crc32Footer = byteBuffer.getInt(buffer.size - 15) - val sizeDecompressed = byteBuffer.getInt(buffer.size - 11) + // val crc32 = byteBuffer.getInt(4) - if (crc32 == crc32Footer) { - // They write CRC32 twice? - logger.debug("CRC32 appears to be written twice in the data") - } + // Read footer + val footerOffset = buffer.size - FOOTER_SIZE + val crc32Footer = byteBuffer.getInt(footerOffset) + val sizeDecompressed = byteBuffer.getInt(footerOffset + 4) + + // This part gets spammed a lot, so we'll mute this. + // if (crc32 == crc32Footer) { + // // They write CRC32 twice? + // logger.debug("CRC32 appears to be written twice in the data") + // } if (buffer[buffer.size - 3] != 'z'.code.toByte() || buffer[buffer.size - 2] != 's'.code.toByte() || @@ -44,7 +53,7 @@ object VZstdUtil { throw IllegalArgumentException("The destination buffer is smaller than the decompressed data size.") } - val compressedData = buffer.copyOfRange(8, buffer.size - 15) + val compressedData = buffer.copyOfRange(HEADER_SIZE, buffer.size - FOOTER_SIZE) // :( allocations try { val bytesDecompressed = Zstd.decompress(destination, compressedData) @@ -54,9 +63,7 @@ object VZstdUtil { } if (verifyChecksum) { - val crc = CRC32() - crc.update(destination, 0, sizeDecompressed) - val calculatedCrc = crc.value.toInt() + val calculatedCrc = Utils.crc32(destination, 0, sizeDecompressed).toInt() if (calculatedCrc != crc32Footer) { throw IOException("CRC does not match decompressed data. VZstd data may be corrupted.") } diff --git a/src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt b/src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt index c5a24214..85a23658 100644 --- a/src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt +++ b/src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt @@ -24,7 +24,7 @@ object ZipUtil { throw IllegalArgumentException("Given stream should only contain one zip entry") } - if (verifyChecksum && Utils.crc32(destination.sliceArray(0 until sizeDecompressed)) != entry.crc) { + if (verifyChecksum && Utils.crc32(destination, 0, bytesRead) != entry.crc) { throw Exception("Checksum validation failed for decompressed file") } 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 cc3567ca..5d602b74 100644 --- a/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt +++ b/src/main/java/in/dragonbra/javasteam/util/compat/ByteArrayOutputStreamCompat.kt @@ -1,6 +1,8 @@ package `in`.dragonbra.javasteam.util.compat import java.io.ByteArrayOutputStream +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets /** * Compatibility class to provide compatibility with Java ByteArrayOutputStream. @@ -10,7 +12,26 @@ import java.io.ByteArrayOutputStream */ object ByteArrayOutputStreamCompat { + /** + * Converts ByteArrayOutputStream to String using UTF-8 encoding. + * Compatible with all Android API levels. + * @param byteArrayOutputStream the stream to convert + * @return UTF-8 decoded string + */ @JvmStatic - fun toString(byteArrayOutputStream: ByteArrayOutputStream): String = - String(byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size()) + fun toString(byteArrayOutputStream: ByteArrayOutputStream): String = toString(byteArrayOutputStream, StandardCharsets.UTF_8) + + /** + * Converts ByteArrayOutputStream to String using specified charset. + * Compatible with all Android API levels. + * + * @param byteArrayOutputStream the stream to convert + * @param charset the charset to use for decoding + * @return decoded string + */ + @JvmStatic + fun toString(byteArrayOutputStream: ByteArrayOutputStream, charset: Charset): String { + val bytes = byteArrayOutputStream.toByteArray() + return String(bytes, 0, byteArrayOutputStream.size(), 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 3ed59ab0..87ed2039 100644 --- a/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java +++ b/src/main/java/in/dragonbra/javasteam/util/stream/BinaryReader.java @@ -20,15 +20,16 @@ public BinaryReader(InputStream in) { } public int readInt() throws IOException { - int ch1 = in.read(); - int ch2 = in.read(); - int ch3 = in.read(); - int ch4 = in.read(); - position += 4; - if ((ch1 | ch2 | ch3 | ch4) < 0) { + int n = in.read(readBuffer, 0, 4); + if (n < 4) { throw new EOFException(); } - return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1); + position += 4; + + return ((readBuffer[3] & 0xFF) << 24) | + ((readBuffer[2] & 0xFF) << 16) | + ((readBuffer[1] & 0xFF) << 8) | + (readBuffer[0] & 0xFF); } public byte[] readBytes(int len) throws IOException { @@ -36,12 +37,22 @@ public byte[] readBytes(int len) throws IOException { throw new IOException("negative length"); } + if (len == 0) { + return new byte[0]; + } + byte[] bytes = new byte[len]; + int totalRead = 0; - for (int i = 0; i < bytes.length; i++) { - bytes[i] = readByte(); + while (totalRead < len) { + int read = in.read(bytes, totalRead, len - totalRead); + if (read < 0) { + throw new EOFException(); + } + totalRead += read; } + position += totalRead; return bytes; } @@ -55,26 +66,30 @@ public byte readByte() throws IOException { } public short readShort() throws IOException { - int ch1 = in.read(); - int ch2 = in.read(); - if ((ch1 | ch2) < 0) { + int n = in.read(readBuffer, 0, 2); + if (n < 2) { throw new EOFException(); } position += 2; - return (short) ((ch2 << 8) + ch1); + + return (short) (((readBuffer[1] & 0xFF) << 8) | (readBuffer[0] & 0xFF)); } public long readLong() throws IOException { - in.read(readBuffer, 0, 8); + int n = in.read(readBuffer, 0, 8); + if (n < 8) { + throw new EOFException(); + } position += 8; - return (((long) readBuffer[7] << 56) + - ((long) (readBuffer[6] & 255) << 48) + - ((long) (readBuffer[5] & 255) << 40) + - ((long) (readBuffer[4] & 255) << 32) + - ((long) (readBuffer[3] & 255) << 24) + - ((readBuffer[2] & 255) << 16) + - ((readBuffer[1] & 255) << 8) + - (readBuffer[0] & 255)); + + return ((long) (readBuffer[7] & 0xFF) << 56) | + ((long) (readBuffer[6] & 0xFF) << 48) | + ((long) (readBuffer[5] & 0xFF) << 40) | + ((long) (readBuffer[4] & 0xFF) << 32) | + ((long) (readBuffer[3] & 0xFF) << 24) | + ((long) (readBuffer[2] & 0xFF) << 16) | + ((long) (readBuffer[1] & 0xFF) << 8) | + (long) (readBuffer[0] & 0xFF); } public char readChar() throws IOException { @@ -116,23 +131,24 @@ public String readNullTermString(Charset charset) throws IOException { return readNullTermUtf8String(); } - ByteArrayOutputStream buffer = new ByteArrayOutputStream(0); - BinaryWriter bw = new BinaryWriter(buffer); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(64); while (true) { - char ch = readChar(); + int ch = in.read(); + if (ch < 0) { + throw new EOFException(); + } + + position++; if (ch == 0) { break; } - bw.writeChar(ch); + buffer.write(ch); } - byte[] bytes = buffer.toByteArray(); - position += bytes.length; - - return new String(bytes, charset); + return ByteArrayOutputStreamCompat.toString(buffer, charset); } private String readNullTermUtf8String() throws IOException { diff --git a/src/main/java/in/dragonbra/javasteam/util/stream/MemoryStream.java b/src/main/java/in/dragonbra/javasteam/util/stream/MemoryStream.java index abb3ee4c..59f67fa4 100644 --- a/src/main/java/in/dragonbra/javasteam/util/stream/MemoryStream.java +++ b/src/main/java/in/dragonbra/javasteam/util/stream/MemoryStream.java @@ -140,7 +140,7 @@ public MemoryStream(byte[] buffer, int index, int count, boolean writable, boole } @Override - public synchronized int available() { + public int available() { return length - position; } @@ -159,12 +159,12 @@ public boolean markSupported() { } @Override - public synchronized void reset() { + public void reset() { position = mark; } @Override - public synchronized int read() { + public int read() { if (position >= length) return -1; @@ -172,7 +172,7 @@ public synchronized int read() { } @Override - public synchronized int read(byte[] b, int off, int len) { + public int read(byte[] b, int off, int len) { if (len == 0) return 0; if (position >= length) @@ -187,7 +187,7 @@ public synchronized int read(byte[] b, int off, int len) { } @Override - public synchronized long skip(long n) { + public long skip(long n) { int previousPosition = position; long newPosition = seek(n, SeekOrigin.CURRENT); return newPosition - previousPosition; @@ -383,7 +383,7 @@ public byte[] toByteArray() { } @Override - public byte @NotNull [] readAllBytes() { + public byte @NotNull [] readAllBytes() { return toByteArray(); } From 1e32081240c0ecad8d7418ca9808bf5b2519d958 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Sun, 21 Dec 2025 19:53:31 -0600 Subject: [PATCH 2/3] Give KV children array an initial capcity of 4. Use .buffered() for streams. --- .../in/dragonbra/javasteam/types/KeyValue.kt | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/java/in/dragonbra/javasteam/types/KeyValue.kt b/src/main/java/in/dragonbra/javasteam/types/KeyValue.kt index 1ef8e093..68c03ecd 100644 --- a/src/main/java/in/dragonbra/javasteam/types/KeyValue.kt +++ b/src/main/java/in/dragonbra/javasteam/types/KeyValue.kt @@ -34,7 +34,7 @@ class KeyValue @JvmOverloads constructor( /** * Gets the children of this instance. */ - var children: MutableList = mutableListOf() + var children: MutableList = ArrayList(4) // Give an initial capacity for optimization. /** * Gets the child [KeyValue] with the specified key. @@ -42,15 +42,9 @@ class KeyValue @JvmOverloads constructor( * @param key key * @return the child [KeyValue] */ - operator fun get(key: String): KeyValue { - children.forEach { c -> - if (c.name?.equals(key, ignoreCase = true) == true) { - return c - } - } - - return INVALID - } + operator fun get(key: String): KeyValue = children.find { + it.name?.equals(key, ignoreCase = true) == true + } ?: INVALID /** * Sets the child [KeyValue] with the specified key. @@ -329,7 +323,7 @@ class KeyValue @JvmOverloads constructor( * @param asBinary If set to true, saves this instance as binary. */ fun saveToFile(file: File, asBinary: Boolean) { - FileOutputStream(file, false).use { f -> saveToStream(f, asBinary) } + FileOutputStream(file, false).buffered().use { f -> saveToStream(f, asBinary) } } /** @@ -338,7 +332,7 @@ class KeyValue @JvmOverloads constructor( * @param asBinary If set to true, saves this instance as binary. */ fun saveToFile(path: String, asBinary: Boolean) { - FileOutputStream(path, false).use { f -> saveToStream(f, asBinary) } + FileOutputStream(path, false).buffered().use { f -> saveToStream(f, asBinary) } } /** @@ -479,7 +473,7 @@ class KeyValue @JvmOverloads constructor( } try { - FileInputStream(file).use { input -> + FileInputStream(file).buffered(8192).use { input -> val kv = KeyValue() if (asBinary) { From b98ddd47548e11e6a23f4482144e8a40a82d69b0 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Mon, 22 Dec 2025 13:04:51 -0600 Subject: [PATCH 3/3] Don't call MessageDigest instance within loop. --- .../javasteam/types/DepotManifest.kt | 22 +++++------- .../javasteam/util/crypto/CryptoHelper.java | 34 +++++++++++++++++-- .../javasteam/types/DepotManifestTest.java | 13 ++++--- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt b/src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt index ec30be2d..b747d0c6 100644 --- a/src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt +++ b/src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt @@ -16,6 +16,7 @@ import `in`.dragonbra.javasteam.util.stream.MemoryStream import java.io.File import java.io.InputStream import java.io.OutputStream +import java.security.MessageDigest import java.time.Instant import java.util.* import javax.crypto.Cipher @@ -46,11 +47,7 @@ class DepotManifest { * @param stream Raw depot manifest stream to deserialize. */ @JvmStatic - fun deserialize(stream: InputStream): DepotManifest { - val manifest = DepotManifest() - manifest.internalDeserialize(stream) - return manifest - } + fun deserialize(stream: InputStream): DepotManifest = DepotManifest().apply { internalDeserialize(stream) } /** * Initializes a new instance of the [DepotManifest] class. @@ -58,12 +55,7 @@ class DepotManifest { * @param data Raw depot manifest data to deserialize. */ @JvmStatic - fun deserialize(data: ByteArray): DepotManifest { - val ms = MemoryStream(data) - val manifest = deserialize(ms) - ms.close() - return manifest - } + fun deserialize(data: ByteArray): DepotManifest = MemoryStream(data).use { deserialize(it) } /** * Loads binary manifest from a file and deserializes it. @@ -311,8 +303,8 @@ class DepotManifest { } if (payload != null && metadata != null && signature != null) { - parseProtobufManifestMetadata(metadata!!) - parseProtobufManifestPayload(payload!!) + parseProtobufManifestMetadata(metadata) + parseProtobufManifestPayload(payload) } else { throw NoSuchElementException("Missing ContentManifest sections required for parsing depot manifest") } @@ -419,6 +411,9 @@ class DepotManifest { get() = items.size } + // Reuse instance. + val sha1Digest = MessageDigest.getInstance("SHA-1", CryptoHelper.SEC_PROV) + files.forEach { file -> val protofile = ContentManifestPayload.FileMapping.newBuilder().apply { this.size = file.totalSize @@ -433,6 +428,7 @@ class DepotManifest { protofile.filename = file.fileName.replace('/', '\\') protofile.shaFilename = ByteString.copyFrom( CryptoHelper.shaHash( + sha1Digest, file.fileName .replace('/', '\\') .lowercase(Locale.getDefault()) diff --git a/src/main/java/in/dragonbra/javasteam/util/crypto/CryptoHelper.java b/src/main/java/in/dragonbra/javasteam/util/crypto/CryptoHelper.java index a06c2282..9651393a 100644 --- a/src/main/java/in/dragonbra/javasteam/util/crypto/CryptoHelper.java +++ b/src/main/java/in/dragonbra/javasteam/util/crypto/CryptoHelper.java @@ -58,12 +58,42 @@ public class CryptoHelper { logger.debug("Using security provider: " + SEC_PROV); } - public static byte[] shaHash(byte[] input) throws NoSuchAlgorithmException { + /** + * Computes SHA-1 hash of the input using the provided MessageDigest instance. + * The digest will be reset before use. + * + * @param digest MessageDigest instance to reuse (will be reset) + * @param input array to hash + * @return the hashed result + * @throws IllegalArgumentException if input or digest is null + */ + public static byte[] shaHash(MessageDigest digest, byte[] input) { + if (input == null) { + throw new IllegalArgumentException("input is null"); + } + + if (digest == null) { + throw new IllegalArgumentException("digest is null"); + } + + digest.reset(); + return digest.digest(input); + } + + /** + * Computes SHA-1 hash of the input by creating a new MessageDigest instance. + * + * @param input array to hash + * @return the hashed result + * @throws NoSuchAlgorithmException if SHA-1 algorithm is not available + * @throws NoSuchProviderException if the security provider is not available + */ + public static byte[] shaHash(byte[] input) throws NoSuchAlgorithmException, NoSuchProviderException { if (input == null) { throw new IllegalArgumentException("input is null"); } - MessageDigest sha = MessageDigest.getInstance("SHA-1"); + MessageDigest sha = MessageDigest.getInstance("SHA-1", SEC_PROV); return sha.digest(input); } diff --git a/src/test/java/in/dragonbra/javasteam/types/DepotManifestTest.java b/src/test/java/in/dragonbra/javasteam/types/DepotManifestTest.java index 2384672e..7d43f57f 100644 --- a/src/test/java/in/dragonbra/javasteam/types/DepotManifestTest.java +++ b/src/test/java/in/dragonbra/javasteam/types/DepotManifestTest.java @@ -15,6 +15,7 @@ import java.nio.ByteOrder; import java.nio.file.Path; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Arrays; @@ -193,10 +194,14 @@ private void testDecryptedManifest(DepotManifest depotManifest) throws NoSuchAlg ); for (var file : depotManifest.getFiles()) { - Assertions.assertArrayEquals( - file.getFileNameHash(), - CryptoHelper.shaHash(file.getFileName().replace('/', '\\').getBytes()) - ); + try { + Assertions.assertArrayEquals( + file.getFileNameHash(), + CryptoHelper.shaHash(file.getFileName().replace('/', '\\').getBytes()) + ); + } catch (NoSuchProviderException e) { + Assertions.fail(e); + } Assertions.assertNotNull(file.getLinkTarget()); Assertions.assertEquals(1, file.getChunks().size()); }