forked from openrs2/openrs2
Useful for running directly on my server, which is headless Linux. (The current extract tool is a GUI Windows app.) Signed-off-by: Graham <gpe@openrs2.org>
parent
03e6c3dd81
commit
fe69594180
@ -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() |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue