Skip to content

Commit cb05f80

Browse files
authored
Merge pull request #375 from LossyDragon/optimizations
Optimizations
2 parents 8f86fec + b98ddd4 commit cb05f80

11 files changed

Lines changed: 194 additions & 136 deletions

File tree

src/main/java/in/dragonbra/javasteam/types/DepotManifest.kt

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import `in`.dragonbra.javasteam.util.stream.MemoryStream
1616
import java.io.File
1717
import java.io.InputStream
1818
import java.io.OutputStream
19+
import java.security.MessageDigest
1920
import java.time.Instant
2021
import java.util.*
2122
import javax.crypto.Cipher
@@ -46,24 +47,15 @@ class DepotManifest {
4647
* @param stream Raw depot manifest stream to deserialize.
4748
*/
4849
@JvmStatic
49-
fun deserialize(stream: InputStream): DepotManifest {
50-
val manifest = DepotManifest()
51-
manifest.internalDeserialize(stream)
52-
return manifest
53-
}
50+
fun deserialize(stream: InputStream): DepotManifest = DepotManifest().apply { internalDeserialize(stream) }
5451

5552
/**
5653
* Initializes a new instance of the [DepotManifest] class.
5754
* Depot manifests may come from the Steam CDN or from Steam/depotcache/ manifest files.
5855
* @param data Raw depot manifest data to deserialize.
5956
*/
6057
@JvmStatic
61-
fun deserialize(data: ByteArray): DepotManifest {
62-
val ms = MemoryStream(data)
63-
val manifest = deserialize(ms)
64-
ms.close()
65-
return manifest
66-
}
58+
fun deserialize(data: ByteArray): DepotManifest = MemoryStream(data).use { deserialize(it) }
6759

6860
/**
6961
* Loads binary manifest from a file and deserializes it.
@@ -311,8 +303,8 @@ class DepotManifest {
311303
}
312304

313305
if (payload != null && metadata != null && signature != null) {
314-
parseProtobufManifestMetadata(metadata!!)
315-
parseProtobufManifestPayload(payload!!)
306+
parseProtobufManifestMetadata(metadata)
307+
parseProtobufManifestPayload(payload)
316308
} else {
317309
throw NoSuchElementException("Missing ContentManifest sections required for parsing depot manifest")
318310
}
@@ -419,6 +411,9 @@ class DepotManifest {
419411
get() = items.size
420412
}
421413

414+
// Reuse instance.
415+
val sha1Digest = MessageDigest.getInstance("SHA-1", CryptoHelper.SEC_PROV)
416+
422417
files.forEach { file ->
423418
val protofile = ContentManifestPayload.FileMapping.newBuilder().apply {
424419
this.size = file.totalSize
@@ -433,6 +428,7 @@ class DepotManifest {
433428
protofile.filename = file.fileName.replace('/', '\\')
434429
protofile.shaFilename = ByteString.copyFrom(
435430
CryptoHelper.shaHash(
431+
sha1Digest,
436432
file.fileName
437433
.replace('/', '\\')
438434
.lowercase(Locale.getDefault())

src/main/java/in/dragonbra/javasteam/types/KeyValue.kt

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,17 @@ class KeyValue @JvmOverloads constructor(
3434
/**
3535
* Gets the children of this instance.
3636
*/
37-
var children: MutableList<KeyValue> = mutableListOf()
37+
var children: MutableList<KeyValue> = ArrayList(4) // Give an initial capacity for optimization.
3838

3939
/**
4040
* Gets the child [KeyValue] with the specified key.
4141
* If no child with the given key exists, [KeyValue.INVALID] is returned.
4242
* @param key key
4343
* @return the child [KeyValue]
4444
*/
45-
operator fun get(key: String): KeyValue {
46-
children.forEach { c ->
47-
if (c.name?.equals(key, ignoreCase = true) == true) {
48-
return c
49-
}
50-
}
51-
52-
return INVALID
53-
}
45+
operator fun get(key: String): KeyValue = children.find {
46+
it.name?.equals(key, ignoreCase = true) == true
47+
} ?: INVALID
5448

5549
/**
5650
* Sets the child [KeyValue] with the specified key.
@@ -329,7 +323,7 @@ class KeyValue @JvmOverloads constructor(
329323
* @param asBinary If set to <c>true</c>, saves this instance as binary.
330324
*/
331325
fun saveToFile(file: File, asBinary: Boolean) {
332-
FileOutputStream(file, false).use { f -> saveToStream(f, asBinary) }
326+
FileOutputStream(file, false).buffered().use { f -> saveToStream(f, asBinary) }
333327
}
334328

335329
/**
@@ -338,7 +332,7 @@ class KeyValue @JvmOverloads constructor(
338332
* @param asBinary If set to <c>true</c>, saves this instance as binary.
339333
*/
340334
fun saveToFile(path: String, asBinary: Boolean) {
341-
FileOutputStream(path, false).use { f -> saveToStream(f, asBinary) }
335+
FileOutputStream(path, false).buffered().use { f -> saveToStream(f, asBinary) }
342336
}
343337

344338
/**
@@ -479,7 +473,7 @@ class KeyValue @JvmOverloads constructor(
479473
}
480474

481475
try {
482-
FileInputStream(file).use { input ->
476+
FileInputStream(file).buffered(8192).use { input ->
483477
val kv = KeyValue()
484478

485479
if (asBinary) {

src/main/java/in/dragonbra/javasteam/util/Utils.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import java.util.ArrayList;
1414
import java.util.Arrays;
1515
import java.util.zip.CRC32;
16-
import java.util.zip.Checksum;
1716

1817
/**
1918
* @author lngtr
@@ -173,7 +172,7 @@ private static String getSystemProperty(final String property) {
173172
}
174173

175174
/**
176-
* Convenience method for calculating the CRC2 checksum of a string.
175+
* Convenience method for calculating the CRC32 checksum of a string.
177176
*
178177
* @param s the string
179178
* @return long value of the CRC32
@@ -183,14 +182,26 @@ public static long crc32(String s) {
183182
}
184183

185184
/**
186-
* Convenience method for calculating the CRC2 checksum of a byte array.
185+
* Convenience method for calculating the CRC32 checksum of a byte array.
187186
*
188187
* @param bytes the byte array
189188
* @return long value of the CRC32
190189
*/
191190
public static long crc32(byte[] bytes) {
192-
Checksum checksum = new CRC32();
193-
checksum.update(bytes, 0, bytes.length);
191+
return crc32(bytes, 0, bytes.length);
192+
}
193+
194+
/**
195+
* Convenience method for calculating the CRC32 checksum of a byte array with offset and length.
196+
*
197+
* @param bytes the byte array
198+
* @param offset the offset to start from
199+
* @param length the number of bytes to checksum
200+
* @return long value of the CRC32
201+
*/
202+
public static long crc32(byte[] bytes, int offset, int length) {
203+
var checksum = new CRC32();
204+
checksum.update(bytes, offset, length);
194205
return checksum.getValue();
195206
}
196207

src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt

Lines changed: 26 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
package `in`.dragonbra.javasteam.util
22

33
import `in`.dragonbra.javasteam.util.compat.readNBytesCompat
4-
import `in`.dragonbra.javasteam.util.crypto.CryptoHelper
54
import `in`.dragonbra.javasteam.util.log.LogManager
65
import `in`.dragonbra.javasteam.util.stream.BinaryReader
7-
import `in`.dragonbra.javasteam.util.stream.BinaryWriter
86
import `in`.dragonbra.javasteam.util.stream.MemoryStream
97
import `in`.dragonbra.javasteam.util.stream.SeekOrigin
10-
import org.tukaani.xz.LZMA2Options
118
import org.tukaani.xz.LZMAInputStream
12-
import org.tukaani.xz.LZMAOutputStream
13-
import java.io.ByteArrayOutputStream
14-
import java.util.zip.DataFormatException
9+
import java.util.zip.*
1510
import kotlin.math.max
1611

1712
@Suppress("SpellCheckingInspection", "unused")
@@ -26,6 +21,11 @@ object VZipUtil {
2621

2722
private const val VERSION: Byte = 'a'.code.toByte()
2823

24+
// Thread-local window buffer pool to avoid repeated allocations
25+
private val windowBufferPool = ThreadLocal.withInitial {
26+
ByteArray(1 shl 23) // 8MB max size
27+
}
28+
2929
@JvmStatic
3030
fun decompress(ms: MemoryStream, destination: ByteArray, verifyChecksum: Boolean = true): Int {
3131
try {
@@ -67,7 +67,12 @@ object VZipUtil {
6767

6868
// If the value of dictionary size in properties is smaller than (1 << 12),
6969
// the LZMA decoder must set the dictionary size variable to (1 << 12).
70-
val windowBuffer = ByteArray(max(1 shl 12, dictionarySize))
70+
val windowSize = max(1 shl 12, dictionarySize)
71+
val windowBuffer = if (windowSize <= (1 shl 23)) {
72+
windowBufferPool.get() // Reuse thread-local buffer
73+
} else {
74+
ByteArray(windowSize) // Fallback for unusually large windows
75+
}
7176
val bytesRead = LZMAInputStream(
7277
ms,
7378
sizeDecompressed.toLong(),
@@ -78,8 +83,11 @@ object VZipUtil {
7883
lzmaInput.readNBytesCompat(destination, 0, sizeDecompressed)
7984
}
8085

81-
if (verifyChecksum && Utils.crc32(destination).toInt() != outputCrc) {
82-
throw DataFormatException("CRC does not match decompressed data. VZip data may be corrupted.")
86+
if (verifyChecksum) {
87+
val actualCrc = Utils.crc32(destination, 0, bytesRead).toInt()
88+
if (actualCrc != outputCrc) {
89+
throw DataFormatException("CRC does not match decompressed data. VZip data may be corrupted.")
90+
}
8391
}
8492

8593
return bytesRead
@@ -93,46 +101,16 @@ object VZipUtil {
93101
}
94102
}
95103

96-
/**
97-
* Ported from SteamKit2 and is untested, use at your own risk
98-
*/
99104
@JvmStatic
100105
fun compress(buffer: ByteArray): ByteArray {
101-
try {
102-
ByteArrayOutputStream().use { ms ->
103-
BinaryWriter(ms).use { writer ->
104-
val crc = CryptoHelper.crcHash(buffer)
105-
writer.writeShort(VZIP_HEADER)
106-
writer.writeByte(VERSION)
107-
writer.write(crc)
108-
109-
// Configure LZMA options to match SteamKit2's settings
110-
val options = LZMA2Options().apply {
111-
dictSize = 1 shl 23 // 8MB dictionary
112-
setPreset(2) // Algorithm setting
113-
niceLen = 128 // numFastBytes equivalent
114-
matchFinder = LZMA2Options.MF_BT4
115-
mode = LZMA2Options.MODE_NORMAL
116-
}
117-
118-
// Write LZMA-compressed data
119-
LZMAOutputStream(ms, options, false).use { lzmaStream ->
120-
lzmaStream.write(buffer)
121-
}
122-
123-
writer.write(crc)
124-
writer.writeInt(buffer.size)
125-
writer.writeShort(VZIP_FOOTER)
126-
127-
return ms.toByteArray()
128-
}
129-
}
130-
} catch (e: NoClassDefFoundError) {
131-
logger.error("Missing implementation of org.tukaani:xz")
132-
throw e
133-
} catch (e: ClassNotFoundException) {
134-
logger.error("Missing implementation of org.tukaani:xz")
135-
throw e
136-
}
106+
throw Exception("VZipUtil.compress is not implemented.")
107+
// try {
108+
// } catch (e: NoClassDefFoundError) {
109+
// logger.error("Missing implementation of org.tukaani:xz")
110+
// throw e
111+
// } catch (e: ClassNotFoundException) {
112+
// logger.error("Missing implementation of org.tukaani:xz")
113+
// throw e
114+
// }
137115
}
138116
}

src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,42 @@ import `in`.dragonbra.javasteam.util.log.LogManager
55
import java.io.IOException
66
import java.nio.ByteBuffer
77
import java.nio.ByteOrder
8-
import java.util.zip.CRC32
98

109
object VZstdUtil {
1110

1211
private const val VZSTD_HEADER: Int = 0x615A5356
12+
private const val HEADER_SIZE = 8
13+
private const val FOOTER_SIZE = 15
1314

1415
private val logger = LogManager.getLogger<VZstdUtil>()
1516

1617
@Throws(IOException::class, IllegalArgumentException::class)
1718
@JvmStatic
1819
@JvmOverloads
1920
fun decompress(buffer: ByteArray, destination: ByteArray, verifyChecksum: Boolean = false): Int {
21+
if (buffer.size < HEADER_SIZE + FOOTER_SIZE) {
22+
throw IOException("Buffer too small to contain VZstd header and footer")
23+
}
24+
2025
val byteBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN) // Convert the buffer.
2126

2227
val header = byteBuffer.getInt(0)
2328
if (header != VZSTD_HEADER) {
2429
throw IOException("Expecting VZstdHeader at start of stream")
2530
}
2631

27-
val crc32 = byteBuffer.getInt(4)
28-
val crc32Footer = byteBuffer.getInt(buffer.size - 15)
29-
val sizeDecompressed = byteBuffer.getInt(buffer.size - 11)
32+
// val crc32 = byteBuffer.getInt(4)
3033

31-
if (crc32 == crc32Footer) {
32-
// They write CRC32 twice?
33-
logger.debug("CRC32 appears to be written twice in the data")
34-
}
34+
// Read footer
35+
val footerOffset = buffer.size - FOOTER_SIZE
36+
val crc32Footer = byteBuffer.getInt(footerOffset)
37+
val sizeDecompressed = byteBuffer.getInt(footerOffset + 4)
38+
39+
// This part gets spammed a lot, so we'll mute this.
40+
// if (crc32 == crc32Footer) {
41+
// // They write CRC32 twice?
42+
// logger.debug("CRC32 appears to be written twice in the data")
43+
// }
3544

3645
if (buffer[buffer.size - 3] != 'z'.code.toByte() ||
3746
buffer[buffer.size - 2] != 's'.code.toByte() ||
@@ -44,7 +53,7 @@ object VZstdUtil {
4453
throw IllegalArgumentException("The destination buffer is smaller than the decompressed data size.")
4554
}
4655

47-
val compressedData = buffer.copyOfRange(8, buffer.size - 15)
56+
val compressedData = buffer.copyOfRange(HEADER_SIZE, buffer.size - FOOTER_SIZE) // :( allocations
4857

4958
try {
5059
val bytesDecompressed = Zstd.decompress(destination, compressedData)
@@ -54,9 +63,7 @@ object VZstdUtil {
5463
}
5564

5665
if (verifyChecksum) {
57-
val crc = CRC32()
58-
crc.update(destination, 0, sizeDecompressed)
59-
val calculatedCrc = crc.value.toInt()
66+
val calculatedCrc = Utils.crc32(destination, 0, sizeDecompressed).toInt()
6067
if (calculatedCrc != crc32Footer) {
6168
throw IOException("CRC does not match decompressed data. VZstd data may be corrupted.")
6269
}

src/main/java/in/dragonbra/javasteam/util/ZipUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ object ZipUtil {
2424
throw IllegalArgumentException("Given stream should only contain one zip entry")
2525
}
2626

27-
if (verifyChecksum && Utils.crc32(destination.sliceArray(0 until sizeDecompressed)) != entry.crc) {
27+
if (verifyChecksum && Utils.crc32(destination, 0, bytesRead) != entry.crc) {
2828
throw Exception("Checksum validation failed for decompressed file")
2929
}
3030

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package `in`.dragonbra.javasteam.util.compat
22

33
import java.io.ByteArrayOutputStream
4+
import java.nio.charset.Charset
5+
import java.nio.charset.StandardCharsets
46

57
/**
68
* Compatibility class to provide compatibility with Java ByteArrayOutputStream.
@@ -10,7 +12,26 @@ import java.io.ByteArrayOutputStream
1012
*/
1113
object ByteArrayOutputStreamCompat {
1214

15+
/**
16+
* Converts ByteArrayOutputStream to String using UTF-8 encoding.
17+
* Compatible with all Android API levels.
18+
* @param byteArrayOutputStream the stream to convert
19+
* @return UTF-8 decoded string
20+
*/
1321
@JvmStatic
14-
fun toString(byteArrayOutputStream: ByteArrayOutputStream): String =
15-
String(byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size())
22+
fun toString(byteArrayOutputStream: ByteArrayOutputStream): String = toString(byteArrayOutputStream, StandardCharsets.UTF_8)
23+
24+
/**
25+
* Converts ByteArrayOutputStream to String using specified charset.
26+
* Compatible with all Android API levels.
27+
*
28+
* @param byteArrayOutputStream the stream to convert
29+
* @param charset the charset to use for decoding
30+
* @return decoded string
31+
*/
32+
@JvmStatic
33+
fun toString(byteArrayOutputStream: ByteArrayOutputStream, charset: Charset): String {
34+
val bytes = byteArrayOutputStream.toByteArray()
35+
return String(bytes, 0, byteArrayOutputStream.size(), charset)
36+
}
1637
}

0 commit comments

Comments
 (0)