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? val name: String fun orNull(): IStarboundFile? = if (exists) this else null operator fun get(name: String): IStarboundFile? { return children?.get(name) } fun explore(): Stream { 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 /** * @throws IllegalStateException if file is a directory * @throws FileNotFoundException if file does not exist */ fun asyncReader(): CompletableFuture { 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 { 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? get() = null override val name: String get() = "" override fun asyncRead(): CompletableFuture { 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? get() = null override val exists: Boolean get() = false override fun asyncRead(): CompletableFuture { 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? 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 { 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]" } }