312 lines
7.6 KiB
Kotlin
312 lines
7.6 KiB
Kotlin
package ru.dbotthepony.kstarbound
|
|
|
|
import com.google.gson.stream.JsonReader
|
|
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
|
|
import ru.dbotthepony.kstarbound.io.StarboundPak
|
|
import ru.dbotthepony.kstarbound.util.sbIntern
|
|
import ru.dbotthepony.kstarbound.util.supplyAsync
|
|
import java.io.BufferedInputStream
|
|
import java.io.File
|
|
import java.io.FileNotFoundException
|
|
import java.io.InputStream
|
|
import java.io.InputStreamReader
|
|
import java.io.Reader
|
|
import java.nio.ByteBuffer
|
|
import java.util.concurrent.CompletableFuture
|
|
import java.util.stream.Stream
|
|
|
|
fun interface ISBFileLocator {
|
|
fun locate(path: String): IStarboundFile
|
|
fun exists(path: String): Boolean = locate(path).exists
|
|
}
|
|
|
|
/**
|
|
* Two files should be considered equal if they have same absolute path
|
|
*/
|
|
interface IStarboundFile : ISBFileLocator {
|
|
val exists: Boolean
|
|
val isDirectory: Boolean
|
|
|
|
/**
|
|
* null if root
|
|
*/
|
|
val parent: IStarboundFile?
|
|
val isFile: Boolean
|
|
|
|
/**
|
|
* null if not a directory
|
|
*/
|
|
val children: Map<String, IStarboundFile>?
|
|
val name: String
|
|
|
|
fun orNull(): IStarboundFile? = if (exists) this else null
|
|
|
|
operator fun get(name: String): IStarboundFile? {
|
|
return children?.get(name)
|
|
}
|
|
|
|
fun explore(): Stream<IStarboundFile> {
|
|
val children = children ?: return Stream.of(this)
|
|
return Stream.concat(Stream.of(this), children.values.stream().flatMap { it.explore() })
|
|
}
|
|
|
|
fun explore(visitor: (IStarboundFile) -> Unit) {
|
|
visitor(this)
|
|
children?.values?.forEach { it.explore(visitor) }
|
|
}
|
|
|
|
fun computeFullPath(): String {
|
|
var path = name
|
|
var parent = parent
|
|
|
|
while (parent != null) {
|
|
path = parent.name + "/" + path
|
|
parent = parent.parent
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
fun computeDirectory(includeLastSlash: Boolean = false): String {
|
|
var path = ""
|
|
var parent = parent
|
|
|
|
while (parent != null) {
|
|
if (path == "")
|
|
path = parent.name
|
|
else
|
|
path = parent.name + "/" + path
|
|
|
|
parent = parent.parent
|
|
}
|
|
|
|
if (includeLastSlash && path.last() != '/')
|
|
return "$path/"
|
|
|
|
return path
|
|
}
|
|
|
|
override fun locate(path: String): IStarboundFile {
|
|
@Suppress("name_shadowing")
|
|
val path = path.trim()
|
|
|
|
if (path == "" || path == ".") {
|
|
return this
|
|
}
|
|
|
|
if (path == "..") {
|
|
return parent ?: NonExistingFile(computeFullPath() + "/..")
|
|
}
|
|
|
|
val split = path.lowercase().split("/")
|
|
var file = this
|
|
|
|
for (splitIndex in split.indices) {
|
|
if (split[splitIndex].isEmpty()) {
|
|
continue
|
|
}
|
|
|
|
val children = file.children ?: return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path)
|
|
val find = children[split[splitIndex]]
|
|
|
|
if (find is StarboundPak.SBDirectory) {
|
|
file = find
|
|
} else if (find is StarboundPak.SBFile) {
|
|
if (splitIndex + 1 != split.size) {
|
|
return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path)
|
|
}
|
|
|
|
return find
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path)
|
|
}
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
fun open(): InputStream
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
fun reader(): Reader = InputStreamReader(BufferedInputStream(open()))
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
@Deprecated("Careful! This does not reflect json patches")
|
|
fun jsonReader(): JsonReader = JsonReader(reader()).also { it.isLenient = true }
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
fun asyncRead(): CompletableFuture<ByteArray>
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
fun asyncReader(): CompletableFuture<Reader> {
|
|
return asyncRead().thenApply { InputStreamReader(FastByteArrayInputStream(it)) }
|
|
}
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
@Deprecated("Careful! This does not reflect json patches")
|
|
fun asyncJsonReader(): CompletableFuture<JsonReader> {
|
|
return asyncReader().thenApply { JsonReader(it).also { it.isLenient = true } }
|
|
}
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
fun read(): ByteArray {
|
|
val stream = open()
|
|
val read = stream.readAllBytes()
|
|
stream.close()
|
|
return read
|
|
}
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
fun readToString(): String {
|
|
val stream = open()
|
|
val read = stream.readAllBytes()
|
|
stream.close()
|
|
return String(read, charset = Charsets.UTF_8)
|
|
}
|
|
|
|
/**
|
|
* @throws IllegalStateException if file is a directory
|
|
* @throws FileNotFoundException if file does not exist
|
|
*/
|
|
fun readDirect(): ByteBuffer {
|
|
val read = read()
|
|
val buf = ByteBuffer.allocateDirect(read.size)
|
|
buf.put(read)
|
|
buf.position(0)
|
|
return buf
|
|
}
|
|
|
|
/**
|
|
* non existent file, without any context
|
|
*/
|
|
companion object : IStarboundFile {
|
|
override val exists: Boolean
|
|
get() = false
|
|
override val isDirectory: Boolean
|
|
get() = false
|
|
override val parent: IStarboundFile?
|
|
get() = null
|
|
override val isFile: Boolean
|
|
get() = false
|
|
override val children: Map<String, IStarboundFile>?
|
|
get() = null
|
|
override val name: String
|
|
get() = ""
|
|
|
|
override fun asyncRead(): CompletableFuture<ByteArray> {
|
|
throw FileNotFoundException()
|
|
}
|
|
|
|
override fun open(): InputStream {
|
|
throw FileNotFoundException()
|
|
}
|
|
}
|
|
}
|
|
|
|
class NonExistingFile(
|
|
override val name: String,
|
|
override val parent: IStarboundFile? = null,
|
|
val fullPath: String? = null
|
|
) : IStarboundFile {
|
|
override val isDirectory: Boolean
|
|
get() = false
|
|
override val isFile: Boolean
|
|
get() = false
|
|
override val children: Map<String, IStarboundFile>?
|
|
get() = null
|
|
|
|
override val exists: Boolean
|
|
get() = false
|
|
|
|
override fun asyncRead(): CompletableFuture<ByteArray> {
|
|
throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist")
|
|
}
|
|
|
|
override fun open(): InputStream {
|
|
throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist")
|
|
}
|
|
}
|
|
|
|
fun getPathFolder(path: String): String {
|
|
return path.substringBeforeLast('/')
|
|
}
|
|
|
|
fun getPathFilename(path: String): String {
|
|
return path.substringAfterLast('/')
|
|
}
|
|
|
|
class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) : IStarboundFile {
|
|
override val exists: Boolean
|
|
get() = real.exists()
|
|
override val isDirectory: Boolean
|
|
get() = real.isDirectory
|
|
|
|
override val isFile: Boolean
|
|
get() = real.isFile
|
|
override val children: Map<String, PhysicalFile>?
|
|
get() {
|
|
return real.list()?.associate { it to PhysicalFile(File(it), this) }
|
|
}
|
|
override val name: String
|
|
get() = real.name
|
|
|
|
private val fullPatch by lazy { super.computeFullPath().sbIntern() }
|
|
private val directory by lazy { super.computeDirectory(false).sbIntern() }
|
|
|
|
override fun computeFullPath(): String {
|
|
return fullPatch
|
|
}
|
|
|
|
override fun computeDirectory(includeLastSlash: Boolean): String {
|
|
if (includeLastSlash)
|
|
return "$directory/"
|
|
|
|
return directory
|
|
}
|
|
|
|
override fun open(): InputStream {
|
|
return BufferedInputStream(real.inputStream())
|
|
}
|
|
|
|
override fun asyncRead(): CompletableFuture<ByteArray> {
|
|
return Starbound.IO_EXECUTOR.supplyAsync { real.readBytes() }
|
|
}
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
return other is IStarboundFile && computeFullPath() == other.computeFullPath()
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
return computeFullPath().hashCode()
|
|
}
|
|
|
|
override fun toString(): String {
|
|
return "PhysicalFile[$real]"
|
|
}
|
|
}
|