From fe695941802f5a37514e5fb35f2220b24e589bef Mon Sep 17 00:00:00 2001 From: Graham Date: Sun, 17 Apr 2022 09:42:57 +0100 Subject: [PATCH] Add command for extracting caches found with Edward's cache finder Useful for running directly on my server, which is headless Linux. (The current extract tool is a GUI Windows app.) Signed-off-by: Graham --- .../org/openrs2/archive/cache/CacheCommand.kt | 2 + .../cache/finder/CacheFinderExtractor.kt | 149 ++++++++++++++++++ .../archive/cache/finder/ExtractCommand.kt | 25 +++ 3 files changed, 176 insertions(+) create mode 100644 archive/src/main/kotlin/org/openrs2/archive/cache/finder/CacheFinderExtractor.kt create mode 100644 archive/src/main/kotlin/org/openrs2/archive/cache/finder/ExtractCommand.kt diff --git a/archive/src/main/kotlin/org/openrs2/archive/cache/CacheCommand.kt b/archive/src/main/kotlin/org/openrs2/archive/cache/CacheCommand.kt index 7786e419..196b9e49 100644 --- a/archive/src/main/kotlin/org/openrs2/archive/cache/CacheCommand.kt +++ b/archive/src/main/kotlin/org/openrs2/archive/cache/CacheCommand.kt @@ -2,11 +2,13 @@ package org.openrs2.archive.cache import com.github.ajalt.clikt.core.NoOpCliktCommand import com.github.ajalt.clikt.core.subcommands +import org.openrs2.archive.cache.finder.ExtractCommand public class CacheCommand : NoOpCliktCommand(name = "cache") { init { subcommands( DownloadCommand(), + ExtractCommand(), ImportCommand(), ImportMasterIndexCommand(), ExportCommand(), diff --git a/archive/src/main/kotlin/org/openrs2/archive/cache/finder/CacheFinderExtractor.kt b/archive/src/main/kotlin/org/openrs2/archive/cache/finder/CacheFinderExtractor.kt new file mode 100644 index 00000000..0c4b17f5 --- /dev/null +++ b/archive/src/main/kotlin/org/openrs2/archive/cache/finder/CacheFinderExtractor.kt @@ -0,0 +1,149 @@ +package org.openrs2.archive.cache.finder + +import com.github.michaelbull.logging.InlineLogger +import com.google.common.io.ByteStreams +import com.google.common.io.LittleEndianDataInputStream +import org.openrs2.util.charset.Cp1252Charset +import java.io.Closeable +import java.io.EOFException +import java.io.IOException +import java.io.InputStream +import java.io.PushbackInputStream +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributeView +import java.nio.file.attribute.FileTime +import java.time.Instant + +public class CacheFinderExtractor( + input: InputStream +) : Closeable { + private val pushbackInput = PushbackInputStream(input) + private val input = LittleEndianDataInputStream(pushbackInput) + + private fun readTimestamp(): FileTime { + val lo = input.readInt().toLong() + val hi = input.readInt().toLong() and 0xFFFFFFFF + + val seconds = (((hi shl 32) or lo) / 10_000_000) - FILETIME_TO_UNIX_EPOCH + + return FileTime.from(Instant.ofEpochSecond(seconds, lo)) + } + + private fun readName(): String { + val bytes = ByteArray(MAX_PATH) + input.readFully(bytes) + + var len = bytes.size + for ((i, b) in bytes.withIndex()) { + if (b.toInt() == 0) { + len = i + break + } + } + + return String(bytes, 0, len, Cp1252Charset) + } + + private fun peekUnsignedByte(): Int { + val n = pushbackInput.read() + pushbackInput.unread(n) + return n + } + + public fun extract(destination: Path) { + val newVersion = peekUnsignedByte() == 0xFE + if (newVersion) { + val signature = input.readInt() + if (signature != 0x435352FE) { + throw IOException("Invalid signature") + } + } + + var readDirectoryPath = true + var number = 0 + var directorySuffix: String? = null + + while (true) { + if (newVersion && readDirectoryPath) { + val len = try { + input.readInt() + } catch (ex: EOFException) { + break + } + + val bytes = ByteArray(len) + input.readFully(bytes) + + val path = String(bytes, Cp1252Charset) + logger.info { "Extracting $path" } + + readDirectoryPath = false + directorySuffix = path.substring(path.lastIndexOf('\\') + 1) + .replace(INVALID_CHARS, "_") + + continue + } + + if (peekUnsignedByte() == 0xFF) { + input.skipBytes(1) + readDirectoryPath = true + number++ + continue + } + + val attributes = try { + input.readInt() + } catch (ex: EOFException) { + break + } + + val btime = readTimestamp() + val atime = readTimestamp() + val mtime = readTimestamp() + + val sizeHi = input.readInt().toLong() + val sizeLo = input.readInt().toLong() and 0xFFFFFFFF + val size = (sizeHi shl 32) or sizeLo + + input.skipBytes(8) // reserved + + val name = readName() + + input.skipBytes(14) // alternate name + input.skipBytes(2) // padding + + val dir = if (directorySuffix != null) { + destination.resolve("cache${number}_$directorySuffix") + } else { + destination.resolve("cache$number") + } + + Files.createDirectories(dir) + + if ((attributes and FILE_ATTRIBUTE_DIRECTORY) == 0) { + val file = dir.resolve(name) + + Files.newOutputStream(file).use { output -> + ByteStreams.copy(ByteStreams.limit(input, size), output) + } + + val view = Files.getFileAttributeView(file, BasicFileAttributeView::class.java) + view.setTimes(mtime, atime, btime) + } + } + } + + override fun close() { + input.close() + } + + private companion object { + private const val FILETIME_TO_UNIX_EPOCH: Long = 11644473600 + private const val MAX_PATH = 260 + private const val FILE_ATTRIBUTE_DIRECTORY = 0x10 + private val INVALID_CHARS = Regex("[^A-Za-z0-9-]") + + private val logger = InlineLogger() + } +} diff --git a/archive/src/main/kotlin/org/openrs2/archive/cache/finder/ExtractCommand.kt b/archive/src/main/kotlin/org/openrs2/archive/cache/finder/ExtractCommand.kt new file mode 100644 index 00000000..bddd5a26 --- /dev/null +++ b/archive/src/main/kotlin/org/openrs2/archive/cache/finder/ExtractCommand.kt @@ -0,0 +1,25 @@ +package org.openrs2.archive.cache.finder + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.default +import com.github.ajalt.clikt.parameters.types.inputStream +import com.github.ajalt.clikt.parameters.types.path +import java.nio.file.Path + +public class ExtractCommand : CliktCommand(name = "extract") { + private val input by argument().inputStream() + private val output by argument().path( + mustExist = false, + canBeFile = false, + canBeDir = true, + mustBeReadable = true, + mustBeWritable = true + ).default(Path.of(".")) + + override fun run() { + CacheFinderExtractor(input).use { extractor -> + extractor.extract(output) + } + } +}