package ru.dbotthepony.kstarbound import com.google.gson.stream.JsonReader import ru.dbotthepony.kstarbound.io.StarboundPak import ru.dbotthepony.kstarbound.util.sbIntern 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.stream.Stream fun interface ISBFileLocator { fun locate(path: String): IStarboundFile fun exists(path: String): Boolean = locate(path).exists /** * @throws IllegalStateException if file is a directory * @throws FileNotFoundException if file does not exist */ fun read(path: String) = locate(path).read() /** * @throws IllegalStateException if file is a directory * @throws FileNotFoundException if file does not exist */ fun readDirect(path: String) = locate(path).readDirect() /** * @throws IllegalStateException if file is a directory * @throws FileNotFoundException if file does not exist */ fun open(path: String) = locate(path).open() /** * @throws IllegalStateException if file is a directory * @throws FileNotFoundException if file does not exist */ fun reader(path: String) = locate(path).reader() /** * @throws IllegalStateException if file is a directory * @throws FileNotFoundException if file does not exist */ @Deprecated("This does not reflect json patches") fun jsonReader(path: String) = locate(path).jsonReader() } /** * 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 read(): ByteBuffer { val stream = open() val read = stream.readAllBytes() stream.close() return ByteBuffer.wrap(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.capacity()) read.position(0) for (i in 0 until read.capacity()) { buf.put(read[i]) } 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 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 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 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]" } }