diff --git a/README.md b/README.md
index 66d9f939..04449d60 100644
--- a/README.md
+++ b/README.md
@@ -72,10 +72,8 @@ Maven
* Note: To eliminate any errors or warnings, you should try and match the same version JavaSteam uses.
* Content Downloading:
- * If you plan on working with Content Downloading, Depot files may be compressed with Zstd *(Zstandard)*.
- * You will need to implement the correct type
- of [ztd implementation](https://mvnrepository.com/artifact/com.github.luben/zstd-jni) if using JVM or Android.
- * Android uses `aar` for the library type.
+ * Add the following dependencies to your project: [XZ For Java](https://mvnrepository.com/artifact/org.tukaani/xz) and [ZSTD JNI](https://mvnrepository.com/artifact/com.github.luben/zstd-jni).
+ * ZSTD for android uses `aar` for the libray type.
You can find the latest version of these dependencies JavaSteam
supports [here](https://github.com/Longi94/JavaSteam/blob/master/gradle/libs.versions.toml).
diff --git a/build.gradle.kts b/build.gradle.kts
index ae7af00c..8ab101ac 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -107,6 +107,13 @@ sourceSets.main {
)
}
+/* Basic Java 9 JPMS support */
+tasks.jar {
+ manifest {
+ attributes["Automatic-Module-Name"] = "in.dragonbra.javasteam"
+ }
+}
+
/* Dependencies */
tasks["lintKotlinMain"].dependsOn("formatKotlin")
tasks["check"].dependsOn("jacocoTestReport")
@@ -126,18 +133,17 @@ tasks.withType {
dependencies {
implementation(libs.bundles.ktor)
- implementation(libs.commons.io)
implementation(libs.commons.lang3)
- implementation(libs.commons.validator)
- implementation(libs.gson)
implementation(libs.kotlin.coroutines)
implementation(libs.kotlin.stdib)
implementation(libs.okHttp)
implementation(libs.protobuf.java)
- implementation(libs.xz)
+ compileOnly(libs.xz)
compileOnly(libs.zstd)
+ testImplementation(platform(libs.tests.junit.bom))
testImplementation(libs.bundles.testing)
+ testRuntimeOnly(libs.tests.junit.platform)
}
/* Artifact publishing */
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 1f8d7fd3..bb62a8fa 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -13,9 +13,9 @@ dependencies {
implementation(gradleApi())
// https://mvnrepository.com/artifact/commons-io/commons-io
- implementation("commons-io:commons-io:2.18.0")
+ implementation("commons-io:commons-io:2.20.0")
// https://mvnrepository.com/artifact/com.squareup/kotlinpoet
- implementation("com.squareup:kotlinpoet:2.0.0")
+ implementation("com.squareup:kotlinpoet:2.2.0")
}
gradlePlugin {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3236a389..d42f0aef 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,41 +4,37 @@
# ****
[versions]
-# Java / Kotlin versions
java = "11"
-kotlin = "2.1.20" # https://kotlinlang.org/docs/releases.html#release-details
+kotlin = "2.2.0" # https://kotlinlang.org/docs/releases.html#release-details
dokka = "2.0.0" # https://mvnrepository.com/artifact/org.jetbrains.dokka/dokka-gradle-plugin
-kotlinter = "5.0.2" # https://plugins.gradle.org/plugin/org.jmailen.kotlinter
+kotlinter = "5.1.1" # https://plugins.gradle.org/plugin/org.jmailen.kotlinter
+jacoco = "0.8.13" # https://www.eclemma.org/jacoco
# Standard Library versions
-bouncyCastle = "1.80" # https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on
-commons-io = "2.19.0" # https://mvnrepository.com/artifact/commons-io/commons-io
-commons-lang3 = "3.17.0" # https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
-commons-validator = "1.9.0" # https://mvnrepository.com/artifact/commons-validator/commons-validator
-gson = "2.13.1" # https://mvnrepository.com/artifact/com.google.code.gson/gson
-jacoco = "0.8.13" # https://www.eclemma.org/jacoco
+commons-lang3 = "3.18.0" # https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
kotlin-coroutines = "1.10.2" # https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
-ktor = "3.2.1" # https://mvnrepository.com/artifact/io.ktor/ktor-client-cio
-okHttp = "5.0.0-alpha.14" # https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
-protobuf = "4.30.2" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java
+ktor = "3.2.2" # https://mvnrepository.com/artifact/io.ktor/ktor-client-cio
+okHttp = "5.1.0" # https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
+protobuf = "4.31.1" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java
protobuf-gradle = "0.9.5" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-gradle-plugin
publishPlugin = "2.0.0" # https://mvnrepository.com/artifact/io.github.gradle-nexus/publish-plugin
-qrCode = "1.0.1" # https://mvnrepository.com/artifact/pro.leaco.qrcode/console-qrcode
xz = "1.10" # https://mvnrepository.com/artifact/org.tukaani/xz
-zstd = "1.5.7-3" # https://search.maven.org/artifact/com.github.luben/zstd-jni
+zstd = "1.5.7-4" # https://search.maven.org/artifact/com.github.luben/zstd-jni
# Testing Lib versions
-commonsCodec = "1.18.0" # https://mvnrepository.com/artifact/commons-codec/commons-codec
-junit5 = "5.11.4" # https://mvnrepository.com/artifact/org.junit/junit-bom
-mockWebServer = "5.0.0-alpha.14" # https://mvnrepository.com/artifact/com.squareup.okhttp3/mockwebserver3-junit5
-mockitoVersion = "5.15.2" # https://mvnrepository.com/artifact/org.mockito/mockito-core
+commons-io = "2.20.0" # https://mvnrepository.com/artifact/commons-io/commons-io
+commonsCodec = "1.19.0" # https://mvnrepository.com/artifact/commons-codec/commons-codec
+junit5 = "5.13.4" # https://mvnrepository.com/artifact/org.junit/junit-bom
+mockWebServer = "5.1.0" # https://mvnrepository.com/artifact/com.squareup.okhttp3/mockwebserver3-junit5
+mockitoVersion = "5.18.0" # https://mvnrepository.com/artifact/org.mockito/mockito-core
+
+# Samples
+bouncyCastle = "1.81" # https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on
+gson = "2.13.1" # https://mvnrepository.com/artifact/com.google.code.gson/gson
+qrCode = "1.0.1" # https://mvnrepository.com/artifact/pro.leaco.qrcode/console-qrcode
[libraries]
-bouncyCastle = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle" }
-commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang3" }
-commons-validator = { module = "commons-validator:commons-validator", version.ref = "commons-validator" }
-gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
kotlin-stdib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
@@ -47,17 +43,23 @@ ktor-client-websocket = { module = "io.ktor:ktor-client-websockets", version.ref
okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" }
protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
-qrCode = { module = "pro.leaco.qrcode:console-qrcode", version.ref = "qrCode" }
xz = { module = "org.tukaani:xz", version.ref = "xz" }
zstd = { module = "com.github.luben:zstd-jni", version.ref = "zstd" }
+# Tests
test-commons-codec = { module = "commons-codec:commons-codec", version.ref = "commonsCodec" }
-test-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" }
-test-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" }
-test-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" }
+test-commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
+test-mock-core = { module = "org.mockito:mockito-core", version.ref = "mockitoVersion" }
+test-mock-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockitoVersion" }
test-mock-webserver3 = { module = "com.squareup.okhttp3:mockwebserver3-junit5", version.ref = "mockWebServer" }
-test-mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoVersion" }
-test-mockito-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockitoVersion" }
+tests-junit-bom = { module = "org.junit:junit-bom", version.ref = "junit5" }
+tests-junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
+tests-junit-platform = { module = "org.junit.platform:junit-platform-launcher" }
+
+# Samples
+bouncyCastle = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
+qrCode = { module = "pro.leaco.qrcode:console-qrcode", version.ref = "qrCode" }
[plugins]
kotlin-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
@@ -69,14 +71,14 @@ protobuf-gradle = { id = "com.google.protobuf", version.ref = "protobuf-gradle"
[bundles]
testing = [
"bouncyCastle",
+ "zstd",
+ "xz",
"test-commons-codec",
- "test-jupiter-api",
- "test-jupiter-engine",
- "test-jupiter-params",
+ "test-commons-io",
+ "test-mock-core",
+ "test-mock-jupiter",
"test-mock-webserver3",
- "test-mockito-core",
- "test-mockito-jupiter",
- "zstd"
+ "tests-junit-jupiter",
]
ktor = [
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 92e5d764..804cabc1 100644
--- a/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/ContentDownloader.kt
+++ b/src/main/java/in/dragonbra/javasteam/steam/contentdownloader/ContentDownloader.kt
@@ -7,7 +7,6 @@ import `in`.dragonbra.javasteam.steam.cdn.Server
import `in`.dragonbra.javasteam.steam.handlers.steamapps.PICSProductInfo
import `in`.dragonbra.javasteam.steam.handlers.steamapps.PICSRequest
import `in`.dragonbra.javasteam.steam.handlers.steamapps.SteamApps
-import `in`.dragonbra.javasteam.steam.handlers.steamapps.callback.PICSProductInfoCallback
import `in`.dragonbra.javasteam.steam.handlers.steamcontent.SteamContent
import `in`.dragonbra.javasteam.steam.steamclient.SteamClient
import `in`.dragonbra.javasteam.types.ChunkData
@@ -104,7 +103,7 @@ class ContentDownloader(val steamClient: SteamClient) {
private fun getAppDirName(app: PICSProductInfo): String {
val installDirKeyValue = app.keyValues["config"]["installdir"]
- return if (installDirKeyValue != KeyValue.INVALID) installDirKeyValue.value else app.id.toString()
+ return if (installDirKeyValue != KeyValue.INVALID) installDirKeyValue.value!! else app.id.toString()
}
private fun getAppInfo(
@@ -113,7 +112,7 @@ class ContentDownloader(val steamClient: SteamClient) {
): Deferred = parentScope.async {
val steamApps = steamClient.getHandler(SteamApps::class.java)
val callback = steamApps?.picsGetProductInfo(PICSRequest(appId))?.await()
- val apps = callback?.results?.flatMap { (it as PICSProductInfoCallback).apps.values }
+ val apps = callback?.results?.flatMap { it.apps.values }
if (apps.isNullOrEmpty()) {
logger.error("Received empty apps list in PICSProductInfo response for $appId")
diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt
index 7b950dbb..b2b6cb33 100644
--- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt
+++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt
@@ -79,7 +79,7 @@ class Lobby(
return metadata
}
- metadata[value.name] = value.value
+ metadata[value.name!!] = value.value!!
}
return metadata.toMap()
diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt
index 722d2c53..da33691d 100644
--- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt
+++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt
@@ -151,6 +151,7 @@ class SteamUserStats : ClientMsgHandler() {
}
// JavaSteam addition.
+
/**
* Gets the Stats-Schema for the specified app. This schema includes Global Achievements and Stats,
* @param appId The appID of the game.
diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt
index a873f743..1872ff77 100644
--- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt
+++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt
@@ -96,6 +96,7 @@ class SteamClient @JvmOverloads constructor(
}
//region Handlers
+
/**
* Adds a new handler to the internal list of message handlers.
* @param handler The handler to add.
@@ -150,6 +151,7 @@ class SteamClient @JvmOverloads constructor(
//endregion
//region Callbacks
+
/**
* Gets the next callback object in the queue, and removes it.
* @return The next callback in the queue, or null if no callback is waiting.
@@ -196,6 +198,7 @@ class SteamClient @JvmOverloads constructor(
//endregion
//region Jobs
+
/**
* Returns the next available JobID for job based messages.
* @return The next available JobID.
diff --git a/src/main/java/in/dragonbra/javasteam/types/KeyValue.java b/src/main/java/in/dragonbra/javasteam/types/KeyValue.java
deleted file mode 100644
index 7ced87fb..00000000
--- a/src/main/java/in/dragonbra/javasteam/types/KeyValue.java
+++ /dev/null
@@ -1,714 +0,0 @@
-package in.dragonbra.javasteam.types;
-
-import in.dragonbra.javasteam.util.Passable;
-import in.dragonbra.javasteam.util.Strings;
-import in.dragonbra.javasteam.util.log.LogManager;
-import in.dragonbra.javasteam.util.log.Logger;
-import in.dragonbra.javasteam.util.stream.BinaryReader;
-import in.dragonbra.javasteam.util.stream.MemoryStream;
-import org.apache.commons.io.IOUtils;
-
-import java.io.*;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Represents a recursive string key to arbitrary value container.
- */
-@SuppressWarnings("unchecked")
-public class KeyValue {
-
- private static final Logger logger = LogManager.getLogger(KeyValue.class);
-
- /**
- * Represents an invalid {@link KeyValue} given when a searched for child does not exist.
- */
- public static final KeyValue INVALID = new KeyValue();
-
- private String name;
-
- private String value;
-
- private List children = new ArrayList<>();
-
- /**
- * Initializes a new instance of the {@link KeyValue} class.
- */
- public KeyValue() {
- this(null);
- }
-
- /**
- * Initializes a new instance of the {@link KeyValue} class.
- *
- * @param name The optional name of the root key.
- */
- public KeyValue(String name) {
- this(name, null);
- }
-
- /**
- * Initializes a new instance of the {@link KeyValue} class.
- *
- * @param name The optional name of the root key.
- * @param value The optional value assigned to the root key.
- */
- public KeyValue(String name, String value) {
- this.name = name;
- this.value = value;
- }
-
- /**
- * Gets the child {@link KeyValue} with the specified key.
- * If no child with the given key exists, {@link KeyValue#INVALID} is returned.
- *
- * @param key key
- * @return the child {@link KeyValue}
- */
- public KeyValue get(String key) {
- if (key == null) {
- throw new IllegalArgumentException("key is null");
- }
-
- for (KeyValue c : children) {
- if (key.equalsIgnoreCase(c.name)) {
- return c;
- }
- }
- return INVALID;
- }
-
- /**
- * Sets the child {@link KeyValue} with the specified key.
- *
- * @param key key
- * @param value the child {@link KeyValue}
- */
- public void set(String key, KeyValue value) {
- if (key == null) {
- throw new IllegalArgumentException("key is null");
- }
-
- children.removeIf(keyValue -> key.equalsIgnoreCase(keyValue.name));
-
- value.setName(key);
- children.add(value);
- }
-
- /**
- * Returns the value of this instance as a string.
- *
- * @return The value of this instance as a string.
- */
- public String asString() {
- return value;
- }
-
- /**
- * Attempts to convert and return the value of this instance as a byte.
- * If the conversion is invalid, the default value is returned.
- *
- * @param defaultValue The default value to return if the conversion is invalid.
- * @return The value of this instance as an unsigned byte.
- */
- public byte asByte(byte defaultValue) {
- try {
- return Byte.parseByte(value);
- } catch (NullPointerException | NumberFormatException nfe) {
- return defaultValue;
- }
- }
-
- /**
- * Attempts to convert and return the value of this instance as a byte.
- * If the conversion is invalid, the default value is returned.
- *
- * @return The value of this instance as an unsigned byte.
- */
- public byte asByte() {
- return asByte((byte) 0);
- }
-
- /**
- * Attempts to convert and return the value of this instance as a short.
- * If the conversion is invalid, the default value is returned.
- *
- * @param defaultValue The default value to return if the conversion is invalid.
- * @return The value of this instance as an unsigned byte.
- */
- public short asShort(short defaultValue) {
- try {
- return Short.parseShort(value);
- } catch (NullPointerException | NumberFormatException nfe) {
- return defaultValue;
- }
- }
-
- /**
- * Attempts to convert and return the value of this instance as a short.
- * If the conversion is invalid, the default value is returned.
- *
- * @return The value of this instance as an unsigned byte.
- */
- public short asShort() {
- return asShort((short) 0);
- }
-
- /**
- * Attempts to convert and return the value of this instance as an integer.
- * If the conversion is invalid, the default value is returned.
- *
- * @param defaultValue The default value to return if the conversion is invalid.
- * @return The value of this instance as an unsigned byte.
- */
- public int asInteger(int defaultValue) {
- try {
- return Integer.parseInt(value);
- } catch (NullPointerException | NumberFormatException nfe) {
- return defaultValue;
- }
- }
-
- /**
- * Attempts to convert and return the value of this instance as an integer.
- * If the conversion is invalid, the default value is returned.
- *
- * @return The value of this instance as an unsigned byte.
- */
- public int asInteger() {
- return asInteger(0);
- }
-
- /**
- * Attempts to convert and return the value of this instance as a long.
- * If the conversion is invalid, the default value is returned.
- *
- * @param defaultValue The default value to return if the conversion is invalid.
- * @return The value of this instance as an unsigned byte.
- */
- public long asLong(long defaultValue) {
- try {
- return Long.parseLong(value);
- } catch (NullPointerException | NumberFormatException nfe) {
- return defaultValue;
- }
- }
-
- /**
- * Attempts to convert and return the value of this instance as a long.
- * If the conversion is invalid, the default value is returned.
- *
- * @return The value of this instance as an unsigned byte.
- */
- public long asLong() {
- return asLong(0L);
- }
-
- /**
- * Attempts to convert and return the value of this instance as a float.
- * If the conversion is invalid, the default value is returned.
- *
- * @param defaultValue The default value to return if the conversion is invalid.
- * @return The value of this instance as an unsigned byte.
- */
- public float asFloat(float defaultValue) {
- try {
- return Float.parseFloat(value);
- } catch (NullPointerException | NumberFormatException nfe) {
- return defaultValue;
- }
- }
-
- /**
- * Attempts to convert and return the value of this instance as a float.
- * If the conversion is invalid, the default value is returned.
- *
- * @return The value of this instance as an unsigned byte.
- */
- public float asFloat() {
- return asFloat(0.0f);
- }
-
- /**
- * Attempts to convert and return the value of this instance as a boolean.
- * If the conversion is invalid, the default value is returned.
- *
- * @param defaultValue The default value to return if the conversion is invalid.
- * @return The value of this instance as an unsigned byte.
- */
- public boolean asBoolean(boolean defaultValue) {
- try {
- return Integer.parseInt(value) != 0;
- } catch (NullPointerException | NumberFormatException e) {
- try {
- return Boolean.parseBoolean(value);
- } catch (NullPointerException | NumberFormatException e1) {
- return defaultValue;
- }
- }
- }
-
- /**
- * Attempts to convert and return the value of this instance as a boolean.
- * If the conversion is invalid, the default value is returned.
- *
- * @return The value of this instance as an unsigned byte.
- */
- public boolean asBoolean() {
- return asBoolean(false);
- }
-
- /**
- * Attempts to convert and return the value of this instance as an enum.
- * If the conversion is invalid, the default value is returned.
- *
- * @param the type of the enum to convert to
- * @param enumClass the type of the enum to convert to
- * @param defaultValue The default value to return if the conversion is invalid.
- * @return The value of this instance as an unsigned byte.
- */
- public > EnumSet asEnum(Class enumClass, T defaultValue) {
- return asEnum(enumClass, EnumSet.of(defaultValue));
- }
-
- /**
- * Attempts to convert and return the value of this instance as an enum.
- * If the conversion is invalid, the default value is returned.
- *
- * @param the type of the enum to convert to
- * @param enumClass the type of the enum to convert to
- * @param defaultValue The default value to return if the conversion is invalid.
- * @return The value of this instance as an unsigned byte.
- */
- public > EnumSet asEnum(Class enumClass, EnumSet defaultValue) {
- // this is ugly af, but it comes with handling bit flags as enumsets
- try {
- // see if it's a number first
- int code = Integer.parseInt(value);
-
- Field codeField = enumClass.getDeclaredField("code");
- Method from = enumClass.getMethod("from", codeField.getType());
- Object res = from.invoke(null, code);
-
- if (res instanceof EnumSet) {
- return (EnumSet) res;
- } else {
- return EnumSet.of(enumClass.cast(res));
- }
- } catch (NullPointerException | NumberFormatException ignored) {
- } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
- return null;
- }
-
- try {
- // see if it exists as an enum
- return EnumSet.of(T.valueOf(enumClass, value));
- } catch (NullPointerException | IllegalArgumentException ignored) {
- }
-
- // check for static enumset fields
- try {
- for (Field field : enumClass.getDeclaredFields()) {
- if (Modifier.isStatic(field.getModifiers()) && field.getName().equals(value) && field.getType().isAssignableFrom(EnumSet.class)) {
- return (EnumSet) field.get(null);
- }
- }
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
-
- return defaultValue;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public String getValue() {
- return value;
- }
-
- public void setValue(String value) {
- this.value = value;
- }
-
- public List getChildren() {
- return children;
- }
-
- public boolean readAsText(InputStream is) throws IOException {
- if (is == null) {
- throw new IllegalArgumentException("input stream is null");
- }
-
- children = new ArrayList<>();
-
- new KVTextReader(this, is);
-
- return true;
- }
-
- /**
- * Opens and reads the given filename as text.
- *
- * @param filename The file to open and read.
- * @return true if the read was successful; otherwise, false.
- * @throws IOException exception while reading from the file
- */
- public boolean readFileAsText(String filename) throws IOException {
- try (var fis = new FileInputStream(filename)) {
- return readAsText(fis);
- }
- }
-
- void recursiveLoadFromBuffer(KVTextReader kvr) throws IOException {
- Passable wasQuoted = new Passable<>(false);
- Passable wasConditional = new Passable<>(false);
-
- while (true) {
- // get the key name
- String name = kvr.readToken(wasQuoted, wasConditional);
-
- if (Strings.isNullOrEmpty(name)) {
- throw new IllegalStateException("RecursiveLoadFromBuffer: got EOF or empty keyname");
- }
-
- if (name.startsWith("}") && !wasQuoted.getValue()) {
- break;
- }
-
- KeyValue dat = new KeyValue(name);
- dat.children = new ArrayList<>();
- children.add(dat);
-
- String value = kvr.readToken(wasQuoted, wasConditional);
-
- if (value == null) {
- throw new IllegalStateException("RecursiveLoadFromBuffer: got NULL key");
- }
-
- if (value.startsWith("}") && !wasQuoted.getValue()) {
- throw new IllegalStateException("RecursiveLoadFromBuffer: got } in key");
- }
-
- if (value.startsWith("{") && !wasQuoted.getValue()) {
- dat.recursiveLoadFromBuffer(kvr);
- } else {
- if (wasConditional.getValue()) {
- throw new IllegalStateException("RecursiveLoadFromBuffer: got conditional between key and value");
- }
-
- dat.setValue(value);
- }
- }
- }
-
- /**
- * Attempts to load the given filename as a text {@link KeyValue}.
- *
- * @param path The path to the file to load.
- * @return a {@link KeyValue} instance if the load was successful, or null on failure.
- */
- public static KeyValue loadAsText(String path) {
- return loadFromFile(path, false);
- }
-
- /**
- * Attempts to load the given filename as a binary {@link KeyValue}.
- *
- * @param path The path to the file to load.
- * @return a {@link KeyValue} instance if the load was successful, or null on failure.
- */
- public static KeyValue tryLoadAsBinary(String path) {
- return loadFromFile(path, true);
- }
-
- private static KeyValue loadFromFile(String path, boolean asBinary) {
- File file = new File(path);
-
- if (!file.exists() || file.isDirectory()) {
- return null;
- }
-
- // TODO charsets?
- try (var fis = new FileInputStream(file)) {
- // Massage the incoming file to be encoded as UTF-8.
- String fisString = IOUtils.toString(fis, Charset.defaultCharset());
- byte[] fisStringToBytes = fisString.getBytes(StandardCharsets.UTF_8);
-
- try (var ms = new MemoryStream(fisStringToBytes, 0, fisStringToBytes.length - 1)) {
- KeyValue kv = new KeyValue();
-
- if (asBinary) {
- if (!kv.tryReadAsBinary(ms)) {
- return null;
- }
- } else {
- if (!kv.readAsText(ms)) {
- return null;
- }
- }
-
- return kv;
- }
- } catch (Exception e) {
- logger.error(e);
- return null;
- }
- }
-
- /**
- * Attempts to create an instance of {@link KeyValue} from the given input text.
- *
- * @param input The input text to load.
- * @return a {@link KeyValue} instance if the load was successful, or null on failure.
- */
- public static KeyValue loadFromString(String input) {
- if (input == null) {
- throw new IllegalArgumentException("input is null");
- }
-
- byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
-
- try (var bais = new ByteArrayInputStream(bytes)) {
- KeyValue kv = new KeyValue();
-
- if (!kv.readAsText(bais)) {
- return null;
- }
-
- return kv;
- } catch (IOException e) {
- logger.error(e);
- return null;
- }
- }
-
- /**
- * Saves this instance to file.
- *
- * @param path The file path to save to.
- * @param binary If set to true, saves this instance as binary.
- * @throws IOException exception while writing to the file
- */
- public void saveToFile(File path, boolean binary) throws IOException {
- try (var fos = new FileOutputStream(path, false)) {
- saveToStream(fos, binary);
- }
- }
-
- public void saveToStream(OutputStream os, boolean binary) throws IOException {
- if (os == null) {
- throw new IllegalArgumentException("output stream is null");
- }
-
- if (binary) {
- recursiveSaveBinaryToStream(os);
- } else {
- recursiveSaveTextToFile(os);
- }
- }
-
- private void recursiveSaveBinaryToStream(OutputStream os) throws IOException {
- recursiveSaveBinaryToStreamCore(os);
- os.write(Type.END.code());
- }
-
- private void recursiveSaveBinaryToStreamCore(OutputStream os) throws IOException {
- // Only supported types ATM:
- // 1. KeyValue with children (no value itself)
- // 2. String KeyValue
- if (value == null) {
- os.write(Type.NONE.code());
- os.write(name.getBytes(StandardCharsets.UTF_8));
- os.write(0);
- for (KeyValue child : children) {
- child.recursiveSaveBinaryToStreamCore(os);
- }
- os.write(Type.END.code());
- } else {
- os.write(Type.STRING.code());
- os.write(name.getBytes(StandardCharsets.UTF_8));
- os.write(0);
- os.write(value.getBytes(StandardCharsets.UTF_8));
- os.write(0);
- }
- }
-
- private void recursiveSaveTextToFile(OutputStream os) throws IOException {
- recursiveSaveTextToFile(os, 0);
- }
-
- private void recursiveSaveTextToFile(OutputStream os, int indentLevel) throws IOException {
- // write header
- writeIndents(os, indentLevel);
- writeString(os, name, true);
- writeString(os, "\n");
- writeIndents(os, indentLevel);
- writeString(os, "{\n");
-
- // loop through all our keys writing them to disk
- for (KeyValue child : children) {
- if (child.getValue() == null) {
- child.recursiveSaveTextToFile(os, indentLevel + 1);
- } else {
- writeIndents(os, indentLevel + 1);
- writeString(os, child.getName(), true);
- writeString(os, "\t\t");
- writeString(os, escapeText(child.asString()), true);
- writeString(os, "\n");
- }
- }
-
- writeIndents(os, indentLevel);
- writeString(os, "}\n");
- }
-
- private static String escapeText(String value) {
- for (Map.Entry entry : KVTextReader.ESCAPED_MAPPING.entrySet()) {
- String textToReplace = String.valueOf(entry.getValue());
- String escapedReplacement = "\\" + entry.getKey();
- value = value.replace(textToReplace, escapedReplacement);
- }
-
- return value;
- }
-
- private void writeIndents(OutputStream os, int indentLevel) throws IOException {
- writeString(os, new String(new char[indentLevel]).replace('\0', '\t'));
- }
-
- private static void writeString(OutputStream os, String str) throws IOException {
- writeString(os, str, false);
- }
-
- private static void writeString(OutputStream os, String str, boolean quote) throws IOException {
- str = str.replaceAll("\"", "\\\"");
- if (quote) {
- str = "\"" + str + "\"";
- }
- byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
- os.write(bytes);
- }
-
- /**
- * Populate this instance from the given {@link InputStream} as a binary {@link KeyValue}.
- *
- * @param is The input {@link InputStream} to read from.
- * @return true if the read was successful; otherwise, false.
- * @throws IOException exception while reading from the stream
- * @throws IllegalArgumentException exception while reading from the stream
- */
- public boolean tryReadAsBinary(InputStream is) throws IllegalArgumentException, IOException {
- if (is == null) {
- throw new IllegalArgumentException("input stream is null");
- }
-
- return tryReadAsBinaryCore(is, this, null);
- }
-
- @SuppressWarnings("DuplicateBranchesInSwitch")
- private static boolean tryReadAsBinaryCore(InputStream is, KeyValue current, KeyValue parent) throws IOException {
- current.children = new ArrayList<>();
-
- try (var br = new BinaryReader(is)) {
- while (true) {
- Type type = Type.from(br.readByte());
-
- if (type == Type.END || type == Type.ALTERNATEEND) {
- break;
- }
-
- current.setName(br.readNullTermString(StandardCharsets.UTF_8));
- switch (type) {
- case NONE:
- KeyValue child = new KeyValue();
- boolean didReadChild = tryReadAsBinaryCore(is, child, current);
- if (!didReadChild) {
- return false;
- }
- break;
- case STRING:
- current.setValue(br.readNullTermString(StandardCharsets.UTF_8));
- break;
- case WIDESTRING:
- logger.debug("Encountered WideString type when parsing binary KeyValue, which is unsupported. Returning false.");
- return false;
- case INT32:
- case COLOR:
- case POINTER:
- current.setValue(String.valueOf(br.readInt()));
- break;
- case UINT64:
- current.setValue(String.valueOf(br.readLong()));
- break;
- case FLOAT32:
- current.setValue(String.valueOf(br.readFloat()));
- break;
- case INT64:
- current.setValue(String.valueOf(br.readLong()));
- break;
- default:
- return false;
- }
-
- if (parent != null) {
- parent.getChildren().add(current);
- }
- current = new KeyValue();
- }
- }
-
- return true;
- }
-
- @Override
- public String toString() {
- return String.format("%s = %s", name, value);
- }
-
- public enum Type {
- NONE((byte) 0),
- STRING((byte) 1),
- INT32((byte) 2),
- FLOAT32((byte) 3),
- POINTER((byte) 4),
- WIDESTRING((byte) 5),
- COLOR((byte) 6),
- UINT64((byte) 7),
- END((byte) 8),
- INT64((byte) 10),
- ALTERNATEEND((byte) 11);
-
- private final byte code;
-
- Type(byte code) {
- this.code = code;
- }
-
- public byte code() {
- return this.code;
- }
-
- public static Type from(byte code) {
- for (Type e : Type.values()) {
- if (e.code == code) {
- return e;
- }
- }
- return null;
- }
- }
-}
diff --git a/src/main/java/in/dragonbra/javasteam/types/KeyValue.kt b/src/main/java/in/dragonbra/javasteam/types/KeyValue.kt
new file mode 100644
index 00000000..573ddd28
--- /dev/null
+++ b/src/main/java/in/dragonbra/javasteam/types/KeyValue.kt
@@ -0,0 +1,583 @@
+package `in`.dragonbra.javasteam.types
+
+import `in`.dragonbra.javasteam.util.Passable
+import `in`.dragonbra.javasteam.util.log.LogManager
+import `in`.dragonbra.javasteam.util.log.Logger
+import `in`.dragonbra.javasteam.util.stream.BinaryReader
+import java.io.ByteArrayInputStream
+import java.io.EOFException
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.Modifier
+import java.nio.charset.StandardCharsets
+import java.util.*
+
+/**
+ * Represents a recursive string key to arbitrary value container.
+ * @constructor Initializes a new instance of the class.
+ * @param name The optional name of the root key.
+ * @param value The optional value assigned to the root key.
+ * @property name Gets or sets the name of this instance.
+ * @property value Gets or sets the value of this instance.
+ */
+@Suppress("unused")
+class KeyValue @JvmOverloads constructor(
+ var name: String? = null,
+ var value: String? = null,
+) {
+
+ /**
+ * Gets the children of this instance.
+ */
+ var children: MutableList = mutableListOf()
+
+ /**
+ * Gets the child [KeyValue] with the specified key.
+ * If no child with the given key exists, [KeyValue.INVALID] is returned.
+ * @param key key
+ * @return the child [KeyValue]
+ */
+ operator fun get(key: String): KeyValue = children.firstOrNull {
+ it.name?.equals(key, ignoreCase = true) == true
+ } ?: INVALID
+
+ operator fun set(key: String, value: KeyValue) {
+ // Remove existing key if it exists
+ children.removeAll { it.name?.equals(key, ignoreCase = true) == true }
+
+ // Ensure the given KV has the correct key assigned
+ value.name = key
+
+ children.add(value)
+ }
+
+ /**
+ * Returns the value of this instance as a string.
+ */
+ fun asString(): String? = this.value
+
+ /**
+ * Attempts to convert and return the value of this instance as a byte.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as a byte.
+ */
+ @JvmOverloads
+ fun asByte(defaultValue: Byte = 0): Byte = value?.toByteOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as an unsigned byte.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as an unsigned byte.
+ */
+ @JvmOverloads
+ fun asUnsignedByte(defaultValue: UByte = 0u): UByte = value?.toUByteOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as a short.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as a short.
+ */
+ @JvmOverloads
+ fun asShort(defaultValue: Short = 0): Short = value?.toShortOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as an unsigned short.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as an unsigned short.
+ */
+ @JvmOverloads
+ fun asUnsignedShort(defaultValue: UShort = 0u): UShort = value?.toUShortOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as an integer.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as an integer.
+ */
+ @JvmOverloads
+ fun asInteger(defaultValue: Int = 0): Int = value?.toIntOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as an unsigned integer.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as an unsigned integer.
+ */
+ @JvmOverloads
+ fun asUnsignedInteger(defaultValue: UInt = 0u): UInt = value?.toUIntOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as a long.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as a long.
+ */
+ @JvmOverloads
+ fun asLong(defaultValue: Long = 0L): Long = value?.toLongOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as an unsigned long.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as an unsigned long.
+ */
+ @JvmOverloads
+ fun asUnsignedLong(defaultValue: ULong = 0uL): ULong = value?.toULongOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as a float.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as a float.
+ */
+ @JvmOverloads
+ fun asFloat(defaultValue: Float = 0f): Float = value?.toFloatOrNull() ?: defaultValue
+
+ /**
+ * Attempts to convert and return the value of this instance as a boolean.
+ * If the conversion is invalid, the default value is returned.
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as a boolean.
+ */
+ @JvmOverloads
+ fun asBoolean(defaultValue: Boolean = false): Boolean = try {
+ value!!.toInt() != 0
+ } catch (e: Exception) {
+ when (value?.lowercase()) {
+ "true" -> true
+ "false" -> false
+ else -> defaultValue
+ }
+ }
+
+ /**
+ * Attempts to convert and return the value of this instance as an enum.
+ * If the conversion is invalid, the default value is returned.
+ * @param T The type of the enum to convert to
+ * @param enumClass The type of the enum to convert to
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as an unsigned byte.
+ */
+ fun > asEnum(enumClass: Class, defaultValue: T): EnumSet =
+ asEnum(enumClass, EnumSet.of(defaultValue))
+
+ /**
+ * Attempts to convert and return the value of this instance as an enum.
+ * If the conversion is invalid, the default value is returned.
+ * @param T The type of the enum to convert to
+ * @param enumClass The type of the enum to convert to
+ * @param defaultValue The default value to return if the conversion is invalid.
+ * @return The value of this instance as an unsigned byte.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun > asEnum(enumClass: Class, defaultValue: EnumSet): EnumSet {
+ // this is ugly af, but it comes with handling bit flags as enumsets
+ try {
+ // see if it's a number first
+ val code = value?.toInt() ?: return defaultValue
+
+ val codeField = enumClass.getDeclaredField("code")
+ val fromMethod = enumClass.getMethod("from", codeField.type)
+
+ @Suppress("MoveVariableDeclarationIntoWhen")
+ val result = fromMethod.invoke(null, code)
+
+ return when (result) {
+ is EnumSet<*> -> result as EnumSet
+ else -> EnumSet.of(enumClass.cast(result))
+ }
+ } catch (e: NumberFormatException) {
+ // ignore and try next approach
+ } catch (e: NoSuchFieldException) {
+ return defaultValue
+ } catch (e: NoSuchMethodException) {
+ return defaultValue
+ } catch (e: IllegalAccessException) {
+ return defaultValue
+ } catch (e: InvocationTargetException) {
+ return defaultValue
+ }
+
+ try {
+ // see if it exists as an enum
+ val enumValue = java.lang.Enum.valueOf(enumClass, value ?: return defaultValue)
+ return EnumSet.of(enumValue)
+ } catch (e: IllegalArgumentException) {
+ // ignore and try next approach
+ }
+
+ // check for static enumset fields
+ try {
+ for (field in enumClass.declaredFields) {
+ if (Modifier.isStatic(field.modifiers) &&
+ field.name == value &&
+ EnumSet::class.java.isAssignableFrom(field.type)
+ ) {
+ @Suppress("UNCHECKED_CAST")
+ return field.get(null) as EnumSet
+ }
+ }
+ } catch (e: IllegalAccessException) {
+ e.printStackTrace()
+ }
+
+ return defaultValue
+ }
+
+ /**
+ * Returns a [String] that represents this instance.
+ */
+ override fun toString(): String = "$name = $value"
+
+ /**
+ * Populate this instance from the given [InputStream] as a text [KeyValue].
+ * @param input The input [InputStream] to read from.
+ * @return true if the read was successful otherwise, false.
+ */
+ fun readAsText(input: InputStream): Boolean {
+ children.clear()
+
+ KVTextReader(this, input).use { _ -> }
+
+ return true
+ }
+
+ /**
+ * Opens and reads the given filename as text.
+ * @see [readAsText]
+ * @param filename The file to open and read.
+ * @return true if the read was successful otherwise, false.
+ */
+ fun readFileAsText(filename: String): Boolean = FileInputStream(filename).use(::readAsText)
+
+ internal fun recursiveLoadFromBuffer(kvr: KVTextReader) {
+ val wasQuoted = Passable(false)
+ val wasConditional = Passable(false)
+
+ while (true) {
+ // val bAccepted = true
+
+ // get the key name
+ val name = kvr.readToken(wasQuoted = wasQuoted, wasConditional = wasConditional)
+
+ if (name.isNullOrEmpty()) {
+ throw IllegalStateException("RecursiveLoadFromBuffer: got EOF or empty keyname")
+ }
+
+ if (name.startsWith('}') && wasQuoted.value == false) {
+ // top level closed, stop reading
+ break
+ }
+
+ val dat = KeyValue(name)
+ dat.children.clear()
+ this.children.add(dat)
+
+ // get the value
+ var value = kvr.readToken(wasQuoted, wasConditional)
+
+ if (wasConditional.value == true && value != null) {
+ // bAccepted = ( value == "[$WIN32]" )
+ value = kvr.readToken(wasQuoted, wasConditional)
+ }
+
+ if (value == null) {
+ throw IllegalStateException("RecursiveLoadFromBuffer: got NULL key")
+ }
+
+ if (value.startsWith('}') && wasQuoted.value == false) {
+ throw IllegalStateException("RecursiveLoadFromBuffer: got } in key")
+ }
+
+ if (value.startsWith('{') && wasQuoted.value == false) {
+ dat.recursiveLoadFromBuffer(kvr)
+ } else {
+ if (wasConditional.value == true) {
+ throw IllegalStateException("RecursiveLoadFromBuffer: got conditional between key and value")
+ }
+
+ dat.value = value
+ // blahconditionalsdontcare
+ }
+ }
+ }
+
+ /**
+ * Saves this instance to file.
+ * @param file The file to save to.
+ * @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) }
+ }
+
+ /**
+ * Saves this instance to file.
+ * @param path The file path to save to.
+ * @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) }
+ }
+
+ /**
+ * Saves this instance to a given [OutputStream]
+ * @param stream The [OutputStream] to save to.
+ * @param asBinary If set to true, saves this instance as binary.
+ */
+ @Throws(IOException::class)
+ fun saveToStream(stream: OutputStream, asBinary: Boolean) {
+ if (asBinary) {
+ recursiveSaveBinaryToStream(stream)
+ } else {
+ recursiveSaveTextToFile(stream)
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun recursiveSaveBinaryToStream(f: OutputStream) {
+ recursiveSaveBinaryToStreamCore(f)
+ f.write(Type.END.code.toInt())
+ }
+
+ @Throws(IOException::class)
+ private fun recursiveSaveBinaryToStreamCore(f: OutputStream) {
+ // Only supported types ATM:
+ // 1. KeyValue with children (no value itself)
+ // 2. String KeyValue
+ if (value == null) {
+ f.write(Type.NONE.code.toInt())
+ f.write(getNameForSerialization().toByteArray(StandardCharsets.UTF_8))
+ f.write(0)
+ children.forEach { child ->
+ child.recursiveSaveBinaryToStreamCore(f)
+ }
+ f.write(Type.END.code.toInt())
+ } else {
+ f.write(Type.STRING.code.toInt())
+ f.write(getNameForSerialization().toByteArray(StandardCharsets.UTF_8))
+ f.write(0)
+ f.write(value?.toByteArray(StandardCharsets.UTF_8))
+ f.write(0)
+ }
+ }
+
+ private fun recursiveSaveTextToFile(os: OutputStream, indentLevel: Int = 0) {
+ // write header
+ writeIndents(os, indentLevel)
+ writeString(os, getNameForSerialization(), true)
+ writeString(os, "\n")
+ writeIndents(os, indentLevel)
+ writeString(os, "{\n")
+
+ // loop through all our keys writing them to disk
+ children.forEach { child ->
+ if (child.value == null) {
+ child.recursiveSaveTextToFile(os, indentLevel + 1)
+ } else {
+ writeIndents(os, indentLevel + 1)
+ writeString(os, child.getNameForSerialization(), true)
+ writeString(os, "\t\t")
+ writeString(os, escapeText(child.asString()!!), true)
+ writeString(os, "\n")
+ }
+ }
+
+ writeIndents(os, indentLevel)
+ writeString(os, "}\n")
+ }
+
+ /**
+ * Populate this instance from the given [InputStream] as a binary [KeyValue].
+ * @param input The input [InputStream] to read from.
+ * @return true if the read was successful otherwise, false.
+ */
+ @Throws(IOException::class, EOFException::class)
+ fun tryReadAsBinary(input: InputStream): Boolean = BinaryReader(input).use { br ->
+ tryReadAsBinaryCore(br, this, null)
+ }
+
+ private fun getNameForSerialization(): String = requireNotNull(name) {
+ "Cannot serialise a KeyValue object with a null name!"
+ }
+
+ companion object {
+ enum class Type(val code: Byte) {
+ NONE(0),
+ STRING(1),
+ INT32(2),
+ FLOAT32(3),
+ POINTER(4),
+ WIDESTRING(5),
+ COLOR(6),
+ UINT64(7),
+ END(8),
+ INT64(10),
+ ALTERNATEEND(11),
+ ;
+
+ companion object {
+ private val codeMap = entries.associateBy { it.code }
+
+ fun from(code: Byte): Type? = codeMap[code]
+ }
+ }
+
+ private val logger: Logger = LogManager.getLogger(KeyValue::class.java)
+
+ /**
+ * Represents an invalid [KeyValue] given when a searched for child does not exist.
+ */
+ @JvmField
+ val INVALID = KeyValue()
+
+ /**
+ * Attempts to load the given filename as a text [KeyValue].
+ * This method will swallow any exceptions that occur when reading, use [readAsText] if you wish to handle exceptions.
+ * @param path The path to the file to load.
+ * @return a [KeyValue] instance if the load was successful, or null on failure.
+ */
+ @JvmStatic
+ fun loadAsText(path: String): KeyValue? = loadFromFile(path, false)
+
+ /**
+ * Attempts to load the given filename as a binary .
+ * @param path The path to the file to load.
+ * @return The resulting [KeyValue] object if the load was successful, or null if unsuccessful.
+ */
+ @JvmStatic
+ fun tryLoadAsBinary(path: String): KeyValue? = loadFromFile(path, true)
+
+ private fun loadFromFile(path: String, asBinary: Boolean): KeyValue? {
+ val file = File(path)
+
+ if (!file.exists() || file.isDirectory()) {
+ return null
+ }
+
+ try {
+ FileInputStream(file).use { input ->
+ val kv = KeyValue()
+
+ if (asBinary) {
+ if (!kv.tryReadAsBinary(input)) {
+ return null
+ }
+ } else {
+ if (!kv.readAsText(input)) {
+ return null
+ }
+ }
+
+ return kv
+ }
+ } catch (e: Exception) {
+ logger.error(e.message, e)
+ return null
+ }
+ }
+
+ /**
+ * Attempts to create an instance of [KeyValue] from the given input text.
+ * This method will swallow any exceptions that occur when reading, use [readAsText] if you wish to handle exceptions.
+ * @param input The input text to load.
+ * @return a [KeyValue] instance if the load was successful, or null on failure.
+ */
+ @JvmStatic
+ fun loadFromString(input: String): KeyValue? {
+ val bytes = input.toByteArray(StandardCharsets.UTF_8)
+
+ try {
+ ByteArrayInputStream(bytes).use { stream ->
+ val kv = KeyValue()
+
+ if (!kv.readAsText(stream)) {
+ return null
+ }
+ return kv
+ }
+ } catch (e: Exception) {
+ logger.error(e.message, e)
+ return null
+ }
+ }
+
+ private fun escapeText(value: String): String {
+ var localValue = value
+ KVTextReader.ESCAPED_MAPPING.forEach { kvp ->
+ val textToReplace = kvp.value.toString()
+ val escapedReplacement = "\\" + kvp.key
+ localValue = localValue.replace(textToReplace, escapedReplacement)
+ }
+ return localValue
+ }
+
+ private fun writeIndents(stream: OutputStream, indentLevel: Int) {
+ writeString(stream, "\t".repeat(indentLevel))
+ }
+
+ private fun writeString(stream: OutputStream, str: String, quote: Boolean = false) {
+ val processedStr = str.replace("\"", "\\\"")
+ val finalStr = if (quote) "\"$processedStr\"" else processedStr
+ val bytes = finalStr.toByteArray(Charsets.UTF_8)
+ stream.write(bytes)
+ }
+
+ private fun tryReadAsBinaryCore(input: BinaryReader, current: KeyValue, parent: KeyValue?): Boolean {
+ var localCurrent = current
+
+ localCurrent.children.clear()
+
+ while (true) {
+ val type = Type.from(input.readByte())
+
+ if (type == Type.END || type == Type.ALTERNATEEND) {
+ break
+ }
+
+ localCurrent.name = input.readNullTermString(StandardCharsets.UTF_8)
+
+ when (type) {
+ Type.NONE -> {
+ val child = KeyValue()
+ val didReadChild = tryReadAsBinaryCore(input, child, localCurrent)
+ if (!didReadChild) {
+ return false
+ }
+ }
+
+ Type.STRING -> localCurrent.value = input.readNullTermString(StandardCharsets.UTF_8)
+ Type.WIDESTRING -> {
+ logger.debug("Encountered WideString type when parsing binary KeyValue, which is unsupported. Returning false.")
+ return false
+ }
+
+ Type.INT32,
+ Type.COLOR,
+ Type.POINTER,
+ -> localCurrent.value = input.readInt().toString()
+
+ Type.UINT64 -> localCurrent.value = input.readLong().toString()
+ Type.FLOAT32 -> localCurrent.value = input.readFloat().toString()
+ Type.INT64 -> localCurrent.value = input.readLong().toString()
+ else -> return false
+ }
+
+ parent?.children?.add(localCurrent)
+
+ localCurrent = KeyValue()
+ }
+
+ return true
+ }
+ }
+}
diff --git a/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt b/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt
index 021007f1..642faa2e 100644
--- a/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt
+++ b/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt
@@ -2,6 +2,7 @@ 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
@@ -16,6 +17,8 @@ import kotlin.math.max
@Suppress("SpellCheckingInspection", "unused")
object VZipUtil {
+ private val logger = LogManager.getLogger(VZipUtil::class.java)
+
private const val VZIP_HEADER: Short = 0x5A56 // "VZ" in hex
private const val VZIP_FOOTER: Short = 0x767A // "vz" in hex
private const val HEADER_LENGTH = 7 // magic + version + timestamp/crc
@@ -25,60 +28,68 @@ object VZipUtil {
@JvmStatic
fun decompress(ms: MemoryStream, destination: ByteArray, verifyChecksum: Boolean = true): Int {
- BinaryReader(ms).use { reader ->
- if (reader.readShort() != VZIP_HEADER) {
- throw IllegalArgumentException("Expecting VZipHeader at start of stream")
- }
+ try {
+ BinaryReader(ms).use { reader ->
+ if (reader.readShort() != VZIP_HEADER) {
+ throw IllegalArgumentException("Expecting VZipHeader at start of stream")
+ }
- if (reader.readByte() != VERSION) {
- throw IllegalArgumentException("Expecting VZip version 'a'")
- }
+ if (reader.readByte() != VERSION) {
+ throw IllegalArgumentException("Expecting VZip version 'a'")
+ }
- // Sometimes this is a creation timestamp (e.g. for Steam Client VZips).
- // Sometimes this is a CRC32 (e.g. for depot chunks).
- /* val creationTimestampOrSecondaryCRC: UInt = */
- reader.readInt()
+ // Sometimes this is a creation timestamp (e.g. for Steam Client VZips).
+ // Sometimes this is a CRC32 (e.g. for depot chunks).
+ /* val creationTimestampOrSecondaryCRC: UInt = */
+ reader.readInt()
- // this is 5 bytes of LZMA properties
- val propertyBits = reader.readByte()
- val dictionarySize = reader.readInt()
- val compressedBytesOffset = ms.position
+ // this is 5 bytes of LZMA properties
+ val propertyBits = reader.readByte()
+ val dictionarySize = reader.readInt()
+ val compressedBytesOffset = ms.position
- // jump to the end of the buffer to read the footer
- ms.seek((-FOOTER_LENGTH).toLong(), SeekOrigin.END)
+ // jump to the end of the buffer to read the footer
+ ms.seek((-FOOTER_LENGTH).toLong(), SeekOrigin.END)
- val outputCrc = reader.readInt()
- val sizeDecompressed = reader.readInt()
+ val outputCrc = reader.readInt()
+ val sizeDecompressed = reader.readInt()
- if (reader.readShort() != VZIP_FOOTER) {
- throw IllegalArgumentException("Expecting VZipFooter at end of stream")
- }
+ if (reader.readShort() != VZIP_FOOTER) {
+ throw IllegalArgumentException("Expecting VZipFooter at end of stream")
+ }
- if (destination.size < sizeDecompressed) {
- throw IllegalArgumentException("The destination buffer is smaller than the decompressed data size.")
- }
+ if (destination.size < sizeDecompressed) {
+ throw IllegalArgumentException("The destination buffer is smaller than the decompressed data size.")
+ }
- // jump back to the beginning of the compressed data
- ms.position = compressedBytesOffset
-
- // 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 bytesRead = LZMAInputStream(
- ms,
- sizeDecompressed.toLong(),
- propertyBits,
- dictionarySize,
- windowBuffer
- ).use { lzmaInput ->
- lzmaInput.readNBytesCompat(destination, 0, sizeDecompressed)
- }
+ // jump back to the beginning of the compressed data
+ ms.position = compressedBytesOffset
+
+ // 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 bytesRead = LZMAInputStream(
+ ms,
+ sizeDecompressed.toLong(),
+ propertyBits,
+ dictionarySize,
+ windowBuffer
+ ).use { lzmaInput ->
+ 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 && Utils.crc32(destination).toInt() != outputCrc) {
+ throw DataFormatException("CRC does not match decompressed data. VZip data may be corrupted.")
+ }
- return bytesRead
+ return bytesRead
+ }
+ } 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
}
}
@@ -87,33 +98,41 @@ object VZipUtil {
*/
@JvmStatic
fun compress(buffer: ByteArray): ByteArray {
- 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
+ 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()
}
-
- // 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
}
}
}
diff --git a/src/test/java/in/dragonbra/javasteam/steam/cdn/DepotChunkTest.java b/src/test/java/in/dragonbra/javasteam/steam/cdn/DepotChunkTest.java
index d14eb1f5..cf65a2fc 100644
--- a/src/test/java/in/dragonbra/javasteam/steam/cdn/DepotChunkTest.java
+++ b/src/test/java/in/dragonbra/javasteam/steam/cdn/DepotChunkTest.java
@@ -1,5 +1,6 @@
package in.dragonbra.javasteam.steam.cdn;
+import in.dragonbra.javasteam.TestBase;
import in.dragonbra.javasteam.types.ChunkData;
import in.dragonbra.javasteam.util.stream.MemoryStream;
import org.apache.commons.io.IOUtils;
@@ -11,7 +12,7 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-public class DepotChunkTest {
+public class DepotChunkTest extends TestBase {
@Test
public void decryptsAndDecompressesDepotChunkPKZip() throws IOException, NoSuchAlgorithmException {
diff --git a/src/test/java/in/dragonbra/javasteam/steam/webapi/SteamDirectoryTest.java b/src/test/java/in/dragonbra/javasteam/steam/webapi/SteamDirectoryTest.java
index 07912113..d5984349 100644
--- a/src/test/java/in/dragonbra/javasteam/steam/webapi/SteamDirectoryTest.java
+++ b/src/test/java/in/dragonbra/javasteam/steam/webapi/SteamDirectoryTest.java
@@ -35,7 +35,7 @@ public void load() {
String resource = IOUtils.toString(vdf, StandardCharsets.UTF_8);
- MockResponse resp = new MockResponse().newBuilder().body(resource).build();
+ MockResponse resp = new MockResponse.Builder().body(resource).build();
server.enqueue(resp);
server.start();
@@ -49,10 +49,11 @@ public void load() {
assertEquals(80, servers.size());
RecordedRequest request = server.takeRequest();
- assertEquals("/ISteamDirectory/GetCMListForConnect/v1?format=vdf&cellid=0", request.getPath());
- assertEquals("GET", request.getMethod());
- server.shutdown();
+ // "/ISteamDirectory/GetCMListForConnect/v1?format=vdf&cellid=0"
+ assertEquals("/ISteamDirectory/GetCMListForConnect/v1", request.getUrl().encodedPath());
+ assertEquals("format=vdf&cellid=0", request.getUrl().encodedQuery());
+ assertEquals("GET", request.getMethod());
} catch (Exception e) {
fail(e);
}
diff --git a/src/test/java/in/dragonbra/javasteam/steam/webapi/WebAPITest.java b/src/test/java/in/dragonbra/javasteam/steam/webapi/WebAPITest.java
index 889cf37d..067827e4 100644
--- a/src/test/java/in/dragonbra/javasteam/steam/webapi/WebAPITest.java
+++ b/src/test/java/in/dragonbra/javasteam/steam/webapi/WebAPITest.java
@@ -41,7 +41,7 @@ public void setUp() throws IOException {
lock = new CountDownLatch(1);
server = new MockWebServer();
- MockResponse resp = new MockResponse().newBuilder().body("" +
+ MockResponse resp = new MockResponse.Builder().body("" +
"\"root\"" +
"{" +
" \"name\" \"stringvalue\"" +
@@ -58,8 +58,8 @@ public void setUp() throws IOException {
}
@AfterEach
- public void tearDown() throws IOException {
- server.shutdown();
+ public void tearDown() {
+ server.close();
}
@Test
@@ -96,7 +96,9 @@ public void testSyncCall() throws IOException, InterruptedException {
assertEquals("stringvalue", result.get("name").getValue());
RecordedRequest request = server.takeRequest();
- assertEquals("/TestInterface/TestFunction/v1?format=vdf", request.getPath());
+ // "/TestInterface/TestFunction/v1?format=vdf"
+ assertEquals("/TestInterface/TestFunction/v1", request.getUrl().encodedPath());
+ assertEquals("format=vdf", request.getUrl().encodedQuery());
assertEquals("GET", request.getMethod());
}
@@ -111,7 +113,9 @@ public void testAsyncCall() throws IOException, InterruptedException {
}, null);
RecordedRequest request = server.takeRequest();
- assertEquals("/TestInterface/TestFunction/v1?format=vdf", request.getPath());
+ // "/TestInterface/TestFunction/v1?format=vdf"
+ assertEquals("/TestInterface/TestFunction/v1", request.getUrl().encodedPath());
+ assertEquals("format=vdf", request.getUrl().encodedQuery());
assertEquals("GET", request.getMethod());
//noinspection ResultOfMethodCallIgnored
@@ -128,9 +132,9 @@ public void testPostCall() throws IOException, InterruptedException {
assertEquals("stringvalue", result.get("name").getValue());
RecordedRequest request = server.takeRequest();
- assertEquals("/TestInterface/TestFunction/v1", request.getPath());
+ assertEquals("/TestInterface/TestFunction/v1", request.getUrl().encodedPath());
assertEquals("POST", request.getMethod());
- assertEquals("format=vdf", request.getBody().readString(StandardCharsets.UTF_8));
+ assertEquals("format=vdf", request.getBody().utf8());
}
@Test
@@ -143,7 +147,9 @@ public void testVersionCall() throws IOException, InterruptedException {
assertEquals("stringvalue", result.get("name").getValue());
RecordedRequest request = server.takeRequest();
- assertEquals("/TestInterface/TestFunction/v69?format=vdf", request.getPath());
+ // "/TestInterface/TestFunction/v69?format=vdf"
+ assertEquals("/TestInterface/TestFunction/v69", request.getUrl().encodedPath());
+ assertEquals("format=vdf", request.getUrl().encodedQuery());
assertEquals("GET", request.getMethod());
}
@@ -161,7 +167,9 @@ public void testParametersCall() throws IOException, InterruptedException {
assertEquals("stringvalue", result.get("name").getValue());
RecordedRequest request = server.takeRequest();
- assertEquals("/TestInterface/TestFunction/v1?key1=value1&key2=value2&format=vdf", request.getPath());
+ // "/TestInterface/TestFunction/v1?key1=value1&key2=value2&format=vdf"
+ assertEquals("/TestInterface/TestFunction/v1", request.getUrl().encodedPath());
+ assertEquals("key1=value1&key2=value2&format=vdf", request.getUrl().encodedQuery());
assertEquals("GET", request.getMethod());
}
@@ -179,9 +187,9 @@ public void testParametersPostCall() throws IOException, InterruptedException {
assertEquals("stringvalue", result.get("name").getValue());
RecordedRequest request = server.takeRequest();
- assertEquals("/TestInterface/TestFunction/v1", request.getPath());
+ assertEquals("/TestInterface/TestFunction/v1", request.getUrl().encodedPath());
assertEquals("POST", request.getMethod());
- assertEquals("key1=value1&key2=value2&format=vdf", request.getBody().readString(StandardCharsets.UTF_8));
+ assertEquals("key1=value1&key2=value2&format=vdf", request.getBody().utf8());
}
@Test
diff --git a/src/test/java/in/dragonbra/javasteam/types/KeyValueTest.java b/src/test/java/in/dragonbra/javasteam/types/KeyValueTest.java
index 416bd603..3918cfed 100644
--- a/src/test/java/in/dragonbra/javasteam/types/KeyValueTest.java
+++ b/src/test/java/in/dragonbra/javasteam/types/KeyValueTest.java
@@ -13,7 +13,6 @@
import java.io.EOFException;
import java.io.IOException;
-import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -149,6 +148,25 @@ public void keyValuesHandlesBool() {
Assertions.assertFalse(kv.get("name").asBoolean(), "values that cannot be converted to integers are falsey");
}
+ @Test
+ public void keyValuesHandlesBoolAdditional() {
+ var kvFalse = new KeyValue("key", "false");
+ Assertions.assertFalse(kvFalse.asBoolean());
+
+ var kvTrue = new KeyValue("key", "true");
+ Assertions.assertTrue(kvTrue.asBoolean());
+
+ var kv0 = new KeyValue("key", "0");
+ Assertions.assertFalse(kv0.asBoolean());
+
+ var kv1 = new KeyValue("key", "1");
+ Assertions.assertTrue(kv1.asBoolean());
+
+ var kvDefault = new KeyValue("key", "invalid value");
+ Assertions.assertFalse(kvDefault.asBoolean());
+ Assertions.assertTrue(kvDefault.asBoolean(true));
+ }
+
@Test
public void keyValuesHandlesFloat() {
KeyValue kv = KeyValue.loadFromString("" +
@@ -441,6 +459,23 @@ public void canLoadUnicodeTextStream() {
// @Test
// public void canReadAndIgnoreConditionals() {
+ // String text = ("\"Repro\"\n" +
+ // "{" + "\n" +
+ // "\"Conditional\" \"You're not running Windows.\" [$!WIN32] // DEPRECATED" + "\n" +
+ // "\"EmptyThing\"\t\"\"" + "\n" +
+ // "}").trim();
+ //
+ // var kv = new KeyValue();
+ // try (var ms = new MemoryStream(text.getBytes(StandardCharsets.UTF_8))) {
+ // kv.readAsText(ms);
+ // }
+ //
+ // Assertions.assertEquals( "Repro", kv.getName() );
+ // Assertions.assertEquals( 2, kv.getChildren().size() );
+ // Assertions.assertEquals( "Conditional", kv.getChildren().get(0).getName() );
+ // Assertions.assertEquals( "You're not running Windows.", kv.getChildren().get(0).getValue() );
+ // Assertions.assertEquals( "EmptyThing", kv.getChildren().get(1).getName() );
+ // Assertions.assertEquals( "", kv.getChildren().get(1).getValue() );
// }
@Test
@@ -595,18 +630,6 @@ public void keyValuesHandlesEnum() {
Assertions.assertEquals(EChatPermission.OwnerDefault, kv.get("name").asEnum(EChatPermission.class, EChatPermission.EveryoneDefault));
}
- @Test
- public void keyValues_loadAsText_should_read_successfully() {
- URL file = this.getClass().getClassLoader().getResource("textkeyvalues/appinfo_utf8.txt");
-
- Assertions.assertNotNull(file, "Resource file was null");
-
- KeyValue kv = KeyValue.loadAsText(file.getPath());
-
- Assertions.assertEquals("1234567", kv.get("appid").getValue(), "appid should be 1234567");
- Assertions.assertEquals(2, kv.getChildren().size(), "Children should be 2");
- }
-
private static String saveToText(KeyValue kv) {
String text = null;
@@ -620,4 +643,37 @@ private static String saveToText(KeyValue kv) {
return text;
}
+
+// @Test
+// public void tryReadAsBinary() throws IOException {
+// var nis = Files.newInputStream(Path.of("C:\\Program Files (x86)\\Steam\\appcache\\stats\\UserGameStatsSchema_550.bin"));
+// var kv = new KeyValue();
+// var result = kv.tryReadAsBinary(nis);
+// System.out.println(result);
+// }
+//
+// @Test
+// public void tryLoadAsBinary_UserGameStatsSchema() throws IOException {
+// var kv = KeyValue.tryLoadAsBinary("C:\\Program Files (x86)\\Steam\\appcache\\stats\\UserGameStatsSchema_550.bin");
+// printKeyValue(kv, 0);
+// }
+//
+// @Test
+// public void tryLoadAsBinary_UserGameStats() throws IOException {
+// var kv = KeyValue.tryLoadAsBinary("C:\\Program Files (x86)\\Steam\\appcache\\stats\\UserGameStats_43540078_3527290.bin");
+// printKeyValue(kv, 0);
+// }
+//
+// private void printKeyValue(KeyValue keyValue, int depth) {
+// String spacePadding = String.join("", Collections.nCopies(depth, " "));
+//
+// if (keyValue.getChildren().isEmpty()) {
+// System.out.println(spacePadding + keyValue.getName() + ": " + keyValue.getValue());
+// } else {
+// System.out.println(spacePadding + keyValue.getName() + ":");
+// for (KeyValue child : keyValue.getChildren()) {
+// printKeyValue(child, depth + 1);
+// }
+// }
+// }
}
diff --git a/src/test/resources/textkeyvalues/appinfo_utf8.txt b/src/test/resources/textkeyvalues/appinfo_utf8.txt
deleted file mode 100644
index 1fd546be..00000000
Binary files a/src/test/resources/textkeyvalues/appinfo_utf8.txt and /dev/null differ