KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt

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]"
}
}