diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index e05b5169..f0e7ca61 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -24,4 +24,5 @@ object Versions { const val openrs2Natives = "2.0.1" const val shadowPlugin = "6.0.0" const val versionsPlugin = "0.29.0" + const val xz = "1.8" } diff --git a/compress-cli/build.gradle.kts b/compress-cli/build.gradle.kts index 31e8e2af..3dd3c31c 100644 --- a/compress-cli/build.gradle.kts +++ b/compress-cli/build.gradle.kts @@ -24,7 +24,7 @@ publishing { description.set( """ Tools for compressing and uncompressing headerless bzip2, - DEFLATE and gzip files. + DEFLATE, gzip and LZMA files. """.trimIndent() ) } diff --git a/compress-cli/src/main/java/dev/openrs2/compress/cli/CompressCommand.kt b/compress-cli/src/main/java/dev/openrs2/compress/cli/CompressCommand.kt index ba4bc3b4..104781f9 100644 --- a/compress-cli/src/main/java/dev/openrs2/compress/cli/CompressCommand.kt +++ b/compress-cli/src/main/java/dev/openrs2/compress/cli/CompressCommand.kt @@ -8,6 +8,8 @@ import dev.openrs2.compress.cli.deflate.DeflateCommand import dev.openrs2.compress.cli.deflate.InflateCommand import dev.openrs2.compress.cli.gzip.GunzipCommand import dev.openrs2.compress.cli.gzip.GzipCommand +import dev.openrs2.compress.cli.lzma.LzmaCommand +import dev.openrs2.compress.cli.lzma.UnlzmaCommand fun main(args: Array) = CompressCommand().main(args) @@ -19,7 +21,9 @@ class CompressCommand : NoOpCliktCommand(name = "compress") { DeflateCommand(), InflateCommand(), GzipCommand(), - GunzipCommand() + GunzipCommand(), + LzmaCommand(), + UnlzmaCommand() ) } } diff --git a/compress-cli/src/main/java/dev/openrs2/compress/cli/lzma/LzmaCommand.kt b/compress-cli/src/main/java/dev/openrs2/compress/cli/lzma/LzmaCommand.kt new file mode 100644 index 00000000..de2f04ed --- /dev/null +++ b/compress-cli/src/main/java/dev/openrs2/compress/cli/lzma/LzmaCommand.kt @@ -0,0 +1,31 @@ +package dev.openrs2.compress.cli.lzma + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.validate +import com.github.ajalt.clikt.parameters.types.defaultStdin +import com.github.ajalt.clikt.parameters.types.defaultStdout +import com.github.ajalt.clikt.parameters.types.inputStream +import com.github.ajalt.clikt.parameters.types.int +import com.github.ajalt.clikt.parameters.types.outputStream +import dev.openrs2.compress.lzma.Lzma +import org.tukaani.xz.LZMA2Options + +class LzmaCommand : CliktCommand(name = "lzma") { + private val input by option().inputStream().defaultStdin() + private val output by option().outputStream().defaultStdout() + private val level by option().int().default(LZMA2Options.PRESET_MAX).validate { + require(it >= LZMA2Options.PRESET_MIN && it <= LZMA2Options.PRESET_MAX) { + "--level must be between ${LZMA2Options.PRESET_MIN} and ${LZMA2Options.PRESET_MAX} inclusive" + } + } + + override fun run() { + input.use { input -> + Lzma.createHeaderlessOutputStream(output, LZMA2Options(level)).use { output -> + System.err.println(input.copyTo(output)) + } + } + } +} diff --git a/compress-cli/src/main/java/dev/openrs2/compress/cli/lzma/UnlzmaCommand.kt b/compress-cli/src/main/java/dev/openrs2/compress/cli/lzma/UnlzmaCommand.kt new file mode 100644 index 00000000..4150c586 --- /dev/null +++ b/compress-cli/src/main/java/dev/openrs2/compress/cli/lzma/UnlzmaCommand.kt @@ -0,0 +1,25 @@ +package dev.openrs2.compress.cli.lzma + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.defaultStdin +import com.github.ajalt.clikt.parameters.types.defaultStdout +import com.github.ajalt.clikt.parameters.types.inputStream +import com.github.ajalt.clikt.parameters.types.long +import com.github.ajalt.clikt.parameters.types.outputStream +import dev.openrs2.compress.lzma.Lzma + +class UnlzmaCommand : CliktCommand(name = "unlzma") { + private val input by option().inputStream().defaultStdin() + private val length by option().long().required() + private val output by option().outputStream().defaultStdout() + + override fun run() { + Lzma.createHeaderlessInputStream(input, length).use { input -> + output.use { output -> + input.copyTo(output) + } + } + } +} diff --git a/compress/build.gradle.kts b/compress/build.gradle.kts index 827324a3..abdbab6d 100644 --- a/compress/build.gradle.kts +++ b/compress/build.gradle.kts @@ -4,7 +4,10 @@ plugins { } dependencies { + api("org.tukaani:xz:${Versions.xz}") + implementation(project(":util")) + implementation("com.google.guava:guava:${Versions.guava}") implementation("org.apache.commons:commons-compress:${Versions.commonsCompress}") } @@ -17,7 +20,7 @@ publishing { name.set("OpenRS2 Compression") description.set( """ - Provides headerless implementations of bzip2 and gzip. + Provides headerless implementations of bzip2, gzip and LZMA. """.trimIndent() ) } diff --git a/compress/src/main/java/dev/openrs2/compress/lzma/Lzma.kt b/compress/src/main/java/dev/openrs2/compress/lzma/Lzma.kt new file mode 100644 index 00000000..7c723e19 --- /dev/null +++ b/compress/src/main/java/dev/openrs2/compress/lzma/Lzma.kt @@ -0,0 +1,28 @@ +package dev.openrs2.compress.lzma + +import com.google.common.io.LittleEndianDataInputStream +import com.google.common.io.LittleEndianDataOutputStream +import org.tukaani.xz.LZMA2Options +import org.tukaani.xz.LZMAInputStream +import org.tukaani.xz.LZMAOutputStream +import java.io.InputStream +import java.io.OutputStream + +object Lzma { + fun createHeaderlessInputStream(input: InputStream, length: Long): InputStream { + val headerInput = LittleEndianDataInputStream(input) + + val properties = headerInput.readByte() + val dictionarySize = headerInput.readInt() + + return LZMAInputStream(input, length, properties, dictionarySize) + } + + fun createHeaderlessOutputStream(output: OutputStream, options: LZMA2Options): OutputStream { + val headerOutput = LittleEndianDataOutputStream(output) + headerOutput.writeByte((options.pb * 5 + options.lp) * 9 + options.lc) + headerOutput.writeInt(options.dictSize) + + return LZMAOutputStream(output, options, false) + } +}