mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-11-07 05:04:31 +01:00
Significant rewrite to use 4koma, OkHttp, PackwizPath; fixing several issues
Github release plugin, Kotlin, Okio and OkHttp updated Toml4j removed and replaced with 4koma - improves null safety, immutability, TOML compliance kotlin-result removed (I don't think it was used anyway) SpaceSafeURI replaced with PackwizPath which handles special characters much better (fixes #5) Fixed directory traversal issues Hashing system rewritten for better type safety and cleaner code Download mode changed to use an enum Request system completely rewritten; now uses OkHttp for HTTP requests (fixes #36, fixes #5) Added request interceptor which retries requests (fixes #4) Removed: support for extracting packs from zip files (and Github zipballs) Cleaner exceptions; more improvements pending (in particular showing request failure causes) Improved speed of cancelling in-progress downloads Better support for installing from local files (no file: URI required; though it is still supported) Various code cleanup changes
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import cc.ekblad.toml.model.TomlValue
|
||||
import cc.ekblad.toml.tomlMapper
|
||||
|
||||
enum class DownloadMode {
|
||||
URL,
|
||||
CURSEFORGE;
|
||||
|
||||
companion object {
|
||||
fun mapper() = tomlMapper {
|
||||
decoder { it: TomlValue.String -> when (it.value) {
|
||||
"", "url" -> URL
|
||||
"metadata:curseforge" -> CURSEFORGE
|
||||
else -> throw Exception("Unsupported download mode ${it.value}")
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class EfficientBooleanAdapter : TypeAdapter<Boolean?>() {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): Boolean? {
|
||||
override fun read(reader: JsonReader): Boolean {
|
||||
if (reader.peek() == JsonToken.NULL) {
|
||||
reader.nextNull()
|
||||
return false
|
||||
|
||||
@@ -1,100 +1,97 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.moandjiezana.toml.Toml
|
||||
import cc.ekblad.toml.decode
|
||||
import cc.ekblad.toml.tomlMapper
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||
import link.infra.packwiz.installer.util.delegateTransitive
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.file.Paths
|
||||
|
||||
class IndexFile {
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String = "sha-256"
|
||||
var files: MutableList<File> = ArrayList()
|
||||
|
||||
class File {
|
||||
var file: SpaceSafeURI? = null
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
var alias: SpaceSafeURI? = null
|
||||
var metafile = false
|
||||
var preserve = false
|
||||
|
||||
@Transient
|
||||
data class IndexFile(
|
||||
val hashFormat: HashFormat<*>,
|
||||
val files: List<File> = listOf()
|
||||
) {
|
||||
data class File(
|
||||
val file: PackwizPath<*>,
|
||||
private val hashFormat: HashFormat<*>? = null,
|
||||
val hash: String,
|
||||
val alias: PackwizPath<*>?,
|
||||
val metafile: Boolean = false,
|
||||
val preserve: Boolean = false,
|
||||
) {
|
||||
var linkedFile: ModFile? = null
|
||||
@Transient
|
||||
var linkedFileURI: SpaceSafeURI? = null
|
||||
|
||||
fun hashFormat(index: IndexFile) = hashFormat ?: index.hashFormat
|
||||
@Throws(Exception::class)
|
||||
fun getHashObj(index: IndexFile): Hash<*> {
|
||||
// TODO: more specific exceptions?
|
||||
return hashFormat(index).fromString(hash)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun downloadMeta(parentIndexFile: IndexFile, indexUri: SpaceSafeURI?) {
|
||||
fun downloadMeta(index: IndexFile, clientHolder: ClientHolder) {
|
||||
if (!metafile) {
|
||||
return
|
||||
}
|
||||
if (hashFormat?.length ?: 0 == 0) {
|
||||
hashFormat = parentIndexFile.hashFormat
|
||||
}
|
||||
// TODO: throw a proper exception instead of allowing NPE?
|
||||
val fileHash = getHash(hashFormat!!, hash!!)
|
||||
linkedFileURI = getNewLoc(indexUri, file)
|
||||
val src = getFileSource(linkedFileURI!!)
|
||||
val fileStream = getHasher(hashFormat!!).getHashingSource(src)
|
||||
linkedFile = Toml().read(InputStreamReader(fileStream.buffer().inputStream(), "UTF-8")).to(ModFile::class.java)
|
||||
if (!fileStream.hashIsEqual(fileHash)) {
|
||||
val fileHash = getHashObj(index)
|
||||
val src = file.source(clientHolder)
|
||||
val fileStream = hashFormat(index).source(src)
|
||||
linkedFile = ModFile.mapper(file).decode<ModFile>(fileStream.buffer().inputStream())
|
||||
if (fileHash != fileStream.hash) {
|
||||
// TODO: propagate details about hash, and show better error!
|
||||
throw Exception("Invalid mod file hash")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getSource(indexUri: SpaceSafeURI?): Source {
|
||||
fun getSource(clientHolder: ClientHolder): Source {
|
||||
return if (metafile) {
|
||||
if (linkedFile == null) {
|
||||
throw Exception("Linked file doesn't exist!")
|
||||
}
|
||||
linkedFile!!.getSource(linkedFileURI)
|
||||
linkedFile!!.getSource(clientHolder)
|
||||
} else {
|
||||
val newLoc = getNewLoc(indexUri, file) ?: throw Exception("Index file URI is invalid")
|
||||
getFileSource(newLoc)
|
||||
file.source(clientHolder)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getHashObj(): Hash {
|
||||
if (hash == null) { // TODO: should these be more specific exceptions (e.g. IndexFileException?!)
|
||||
throw Exception("Index file doesn't have a hash")
|
||||
}
|
||||
if (hashFormat == null) {
|
||||
throw Exception("Index file doesn't have a hash format")
|
||||
}
|
||||
return getHash(hashFormat!!, hash!!)
|
||||
}
|
||||
|
||||
// TODO: throw some kind of exception?
|
||||
val name: String
|
||||
get() {
|
||||
if (metafile) {
|
||||
return linkedFile?.name ?: linkedFile?.filename ?:
|
||||
file?.run { Paths.get(path ?: return "Invalid file").fileName.toString() } ?: "Invalid file"
|
||||
return linkedFile?.name ?: file.filename
|
||||
}
|
||||
return file?.run { Paths.get(path ?: return "Invalid file").fileName.toString() } ?: "Invalid file"
|
||||
return file.filename
|
||||
}
|
||||
|
||||
// TODO: URIs are bad
|
||||
val destURI: SpaceSafeURI?
|
||||
val destURI: PackwizPath<*>
|
||||
get() {
|
||||
if (alias != null) {
|
||||
return alias
|
||||
}
|
||||
return if (metafile && linkedFile != null) {
|
||||
linkedFile?.filename?.let { file?.resolve(it) }
|
||||
return if (metafile) {
|
||||
linkedFile!!.filename
|
||||
} else {
|
||||
file
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
||||
mapping<File>("hash-format" to "hashFormat")
|
||||
delegateTransitive<HashFormat<*>>(HashFormat.mapper())
|
||||
delegateTransitive<PackwizPath<*>>(PackwizPath.mapperRelativeTo(base))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
||||
mapping<IndexFile>("hash-format" to "hashFormat")
|
||||
delegateTransitive<HashFormat<*>>(HashFormat.mapper())
|
||||
delegateTransitive<File>(File.mapper(base))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,23 @@ package link.infra.packwiz.installer.metadata
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.target.Side
|
||||
import link.infra.packwiz.installer.target.path.PackwizFilePath
|
||||
|
||||
class ManifestFile {
|
||||
var packFileHash: Hash? = null
|
||||
var indexFileHash: Hash? = null
|
||||
var cachedFiles: MutableMap<SpaceSafeURI, File> = HashMap()
|
||||
var packFileHash: Hash<*>? = null
|
||||
var indexFileHash: Hash<*>? = null
|
||||
var cachedFiles: MutableMap<PackwizFilePath, File> = HashMap()
|
||||
// If the side changes, EVERYTHING invalidates. FUN!!!
|
||||
var cachedSide = Side.CLIENT
|
||||
|
||||
// TODO: switch to Kotlin-friendly JSON/TOML libs?
|
||||
class File {
|
||||
@Transient
|
||||
var revert: File? = null
|
||||
private set
|
||||
|
||||
var hash: Hash? = null
|
||||
var linkedFileHash: Hash? = null
|
||||
var cachedLocation: String? = null
|
||||
var hash: Hash<*>? = null
|
||||
var linkedFileHash: Hash<*>? = null
|
||||
var cachedLocation: PackwizFilePath? = null
|
||||
|
||||
@JsonAdapter(EfficientBooleanAdapter::class)
|
||||
var isOptional = false
|
||||
|
||||
@@ -1,74 +1,96 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import cc.ekblad.toml.delegate
|
||||
import cc.ekblad.toml.model.TomlValue
|
||||
import cc.ekblad.toml.tomlMapper
|
||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateData
|
||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateDeserializer
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import link.infra.packwiz.installer.target.Side
|
||||
import link.infra.packwiz.installer.target.path.HttpUrlPath
|
||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||
import link.infra.packwiz.installer.util.delegateTransitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okio.Source
|
||||
import kotlin.reflect.KType
|
||||
|
||||
class ModFile {
|
||||
var name: String? = null
|
||||
var filename: String? = null
|
||||
var side: Side? = null
|
||||
var download: Download? = null
|
||||
data class ModFile(
|
||||
val name: String,
|
||||
val filename: PackwizPath<*>,
|
||||
val side: Side = Side.BOTH,
|
||||
val download: Download,
|
||||
val update: Map<String, UpdateData> = mapOf(),
|
||||
val option: Option = Option(false)
|
||||
) {
|
||||
data class Download(
|
||||
val url: PackwizPath<*>?,
|
||||
val hashFormat: HashFormat<*>,
|
||||
val hash: String,
|
||||
val mode: DownloadMode = DownloadMode.URL
|
||||
) {
|
||||
companion object {
|
||||
fun mapper() = tomlMapper {
|
||||
decoder<TomlValue.String, PackwizPath<*>> { it -> HttpUrlPath(it.value.toHttpUrl()) }
|
||||
mapping<Download>("hash-format" to "hashFormat")
|
||||
|
||||
class Download {
|
||||
var url: SpaceSafeURI? = null
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
var mode: String? = null
|
||||
delegateTransitive<HashFormat<*>>(HashFormat.mapper())
|
||||
delegate<DownloadMode>(DownloadMode.mapper())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonAdapter(UpdateDeserializer::class)
|
||||
var update: Map<String, UpdateData>? = null
|
||||
var option: Option? = null
|
||||
|
||||
@Transient
|
||||
val resolvedUpdateData = mutableMapOf<String, SpaceSafeURI>()
|
||||
val resolvedUpdateData = mutableMapOf<String, PackwizPath<*>>()
|
||||
|
||||
class Option {
|
||||
var optional = false
|
||||
var description: String? = null
|
||||
@SerializedName("default")
|
||||
var defaultValue = false
|
||||
data class Option(
|
||||
val optional: Boolean,
|
||||
val description: String = "",
|
||||
val defaultValue: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
fun mapper() = tomlMapper {
|
||||
mapping<Option>("default" to "defaultValue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getSource(baseLoc: SpaceSafeURI?): Source {
|
||||
download?.let {
|
||||
if (it.mode == null || it.mode == "" || it.mode == "url") {
|
||||
if (it.url == null) {
|
||||
throw Exception("Metadata file doesn't have a download URI")
|
||||
}
|
||||
val newLoc = getNewLoc(baseLoc, it.url) ?: throw Exception("Metadata file URI is invalid")
|
||||
return getFileSource(newLoc)
|
||||
} else if (it.mode == "metadata:curseforge") {
|
||||
fun getSource(clientHolder: ClientHolder): Source {
|
||||
return when (download.mode) {
|
||||
DownloadMode.URL -> {
|
||||
(download.url ?: throw Exception("No download URL provided")).source(clientHolder)
|
||||
}
|
||||
DownloadMode.CURSEFORGE -> {
|
||||
if (!resolvedUpdateData.contains("curseforge")) {
|
||||
throw Exception("Metadata file specifies CurseForge mode, but is missing metadata")
|
||||
}
|
||||
return getFileSource(resolvedUpdateData["curseforge"]!!)
|
||||
} else {
|
||||
throw Exception("Unsupported download mode " + it.mode)
|
||||
return resolvedUpdateData["curseforge"]!!.source(clientHolder)
|
||||
}
|
||||
} ?: throw Exception("Metadata file doesn't have download")
|
||||
}
|
||||
}
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
val hash: Hash
|
||||
get() {
|
||||
download?.let {
|
||||
return getHash(
|
||||
it.hashFormat ?: throw Exception("Metadata file doesn't have a hash format"),
|
||||
it.hash ?: throw Exception("Metadata file doesn't have a hash")
|
||||
)
|
||||
} ?: throw Exception("Metadata file doesn't have download")
|
||||
}
|
||||
val hash: Hash<*>
|
||||
get() = download.hashFormat.fromString(download.hash)
|
||||
|
||||
val isOptional: Boolean get() = option?.optional ?: false
|
||||
companion object {
|
||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
||||
delegateTransitive<PackwizPath<*>>(PackwizPath.mapperRelativeTo(base))
|
||||
|
||||
delegateTransitive<Option>(Option.mapper())
|
||||
delegateTransitive<Download>(Download.mapper())
|
||||
|
||||
delegateTransitive<Side>(Side.mapper())
|
||||
|
||||
val updateDataMapper = UpdateData.mapper()
|
||||
decoder { type: KType, it: TomlValue.Map ->
|
||||
if (type.arguments[1].type?.classifier == UpdateData::class) {
|
||||
updateDataMapper.decode<Map<String, UpdateData>>(it)
|
||||
} else {
|
||||
pass()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,37 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import cc.ekblad.toml.model.TomlValue
|
||||
import cc.ekblad.toml.tomlMapper
|
||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||
import link.infra.packwiz.installer.util.delegateTransitive
|
||||
|
||||
class PackFile {
|
||||
var name: String? = null
|
||||
var index: IndexFileLoc? = null
|
||||
|
||||
class IndexFileLoc {
|
||||
var file: SpaceSafeURI? = null
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
data class PackFile(
|
||||
val name: String,
|
||||
val packFormat: PackFormat = PackFormat.DEFAULT,
|
||||
val index: IndexFileLoc,
|
||||
val versions: Map<String, String> = mapOf()
|
||||
) {
|
||||
data class IndexFileLoc(
|
||||
val file: PackwizPath<*>,
|
||||
val hashFormat: HashFormat<*>,
|
||||
val hash: String,
|
||||
) {
|
||||
companion object {
|
||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
||||
mapping<IndexFileLoc>("hash-format" to "hashFormat")
|
||||
delegateTransitive<PackwizPath<*>>(PackwizPath.mapperRelativeTo(base))
|
||||
delegateTransitive<HashFormat<*>>(HashFormat.mapper())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var versions: Map<String, String>? = null
|
||||
companion object {
|
||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
||||
mapping<PackFile>("pack-format" to "packFormat")
|
||||
decoder { it: TomlValue.String -> PackFormat(it.value) }
|
||||
encoder { it: PackFormat -> TomlValue.String(it.format) }
|
||||
delegateTransitive<IndexFileLoc>(IndexFileLoc.mapper(base))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
@JvmInline
|
||||
value class PackFormat(val format: String) {
|
||||
companion object {
|
||||
val DEFAULT = PackFormat("packwiz:1.0.0")
|
||||
}
|
||||
|
||||
// TODO: implement validation, errors for too new / invalid versions
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import java.io.Serializable
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URL
|
||||
|
||||
// The world's worst URI wrapper
|
||||
@JsonAdapter(SpaceSafeURIParser::class)
|
||||
class SpaceSafeURI : Comparable<SpaceSafeURI>, Serializable {
|
||||
private val u: URI
|
||||
|
||||
@Throws(URISyntaxException::class)
|
||||
constructor(str: String) {
|
||||
u = URI(str.replace(" ", "%20"))
|
||||
}
|
||||
|
||||
constructor(uri: URI) {
|
||||
u = uri
|
||||
}
|
||||
|
||||
@Throws(URISyntaxException::class)
|
||||
constructor(scheme: String?, authority: String?, path: String?, query: String?, fragment: String?) { // TODO: do all components need to be replaced?
|
||||
u = URI(
|
||||
scheme?.replace(" ", "%20"),
|
||||
authority?.replace(" ", "%20"),
|
||||
path?.replace(" ", "%20"),
|
||||
query?.replace(" ", "%20"),
|
||||
fragment?.replace(" ", "%20")
|
||||
)
|
||||
}
|
||||
|
||||
val path: String? get() = u.path?.replace("%20", " ")
|
||||
|
||||
override fun toString(): String = u.toString().replace("%20", " ")
|
||||
|
||||
fun resolve(path: String): SpaceSafeURI = SpaceSafeURI(u.resolve(path.replace(" ", "%20")))
|
||||
|
||||
fun resolve(loc: SpaceSafeURI): SpaceSafeURI = SpaceSafeURI(u.resolve(loc.u))
|
||||
|
||||
fun relativize(loc: SpaceSafeURI): SpaceSafeURI = SpaceSafeURI(u.relativize(loc.u))
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is SpaceSafeURI) {
|
||||
u == other.u
|
||||
} else false
|
||||
}
|
||||
|
||||
override fun hashCode() = u.hashCode()
|
||||
|
||||
override fun compareTo(other: SpaceSafeURI): Int = u.compareTo(other.u)
|
||||
|
||||
val scheme: String? get() = u.scheme
|
||||
val authority: String? get() = u.authority
|
||||
val host: String? get() = u.host
|
||||
|
||||
@Throws(MalformedURLException::class)
|
||||
fun toURL(): URL = u.toURL()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import java.lang.reflect.Type
|
||||
import java.net.URISyntaxException
|
||||
|
||||
/**
|
||||
* This class encodes spaces before parsing the URI, so the URI can actually be
|
||||
* parsed.
|
||||
*/
|
||||
internal class SpaceSafeURIParser : JsonDeserializer<SpaceSafeURI> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): SpaceSafeURI {
|
||||
return try {
|
||||
SpaceSafeURI(json.asString)
|
||||
} catch (e: URISyntaxException) {
|
||||
throw JsonParseException("Failed to parse URI", e)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace this with a better solution?
|
||||
}
|
||||
@@ -4,15 +4,18 @@ import com.google.gson.Gson
|
||||
import com.google.gson.JsonIOException
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import link.infra.packwiz.installer.metadata.IndexFile
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import link.infra.packwiz.installer.target.path.HttpUrlPath
|
||||
import link.infra.packwiz.installer.target.path.PackwizFilePath
|
||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.io.path.absolute
|
||||
|
||||
private class GetFilesRequest(val fileIds: List<Int>)
|
||||
private class GetModsRequest(val modIds: List<Int>)
|
||||
@@ -21,7 +24,7 @@ private class GetFilesResponse {
|
||||
class CfFile {
|
||||
var id = 0
|
||||
var modId = 0
|
||||
var downloadUrl: SpaceSafeURI? = null
|
||||
var downloadUrl: String? = null
|
||||
}
|
||||
val data = mutableListOf<CfFile>()
|
||||
}
|
||||
@@ -43,25 +46,17 @@ private const val APIServer = "api.curseforge.com"
|
||||
private val APIKey = "JDJhJDEwJHNBWVhqblU1N0EzSmpzcmJYM3JVdk92UWk2NHBLS3BnQ2VpbGc1TUM1UGNKL0RYTmlGWWxh".decodeBase64()!!
|
||||
.string(StandardCharsets.UTF_8)
|
||||
|
||||
private val clientHolder = ClientHolder()
|
||||
|
||||
// TODO: switch to PackwizPath stuff and OkHttp in old code
|
||||
|
||||
@Throws(JsonSyntaxException::class, JsonIOException::class)
|
||||
fun resolveCfMetadata(mods: List<IndexFile.File>): List<ExceptionDetails> {
|
||||
fun resolveCfMetadata(mods: List<IndexFile.File>, packFolder: PackwizFilePath, clientHolder: ClientHolder): List<ExceptionDetails> {
|
||||
val failures = mutableListOf<ExceptionDetails>()
|
||||
val fileIdMap = mutableMapOf<Int, IndexFile.File>()
|
||||
|
||||
for (mod in mods) {
|
||||
if (mod.linkedFile!!.update == null) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name ?: mod.linkedFile!!.filename!!, Exception("Failed to resolve CurseForge metadata: no update section")))
|
||||
if (!mod.linkedFile!!.update.contains("curseforge")) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name, Exception("Failed to resolve CurseForge metadata: no CurseForge update section")))
|
||||
continue
|
||||
}
|
||||
if (!mod.linkedFile!!.update!!.contains("curseforge")) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name ?: mod.linkedFile!!.filename!!, Exception("Failed to resolve CurseForge metadata: no CurseForge update section")))
|
||||
continue
|
||||
}
|
||||
fileIdMap[(mod.linkedFile!!.update!!["curseforge"] as CurseForgeUpdateData).fileId] = mod
|
||||
fileIdMap[(mod.linkedFile!!.update["curseforge"] as CurseForgeUpdateData).fileId] = mod
|
||||
}
|
||||
|
||||
val reqData = GetFilesRequest(fileIdMap.keys.toList())
|
||||
@@ -93,7 +88,13 @@ fun resolveCfMetadata(mods: List<IndexFile.File>): List<ExceptionDetails> {
|
||||
manualDownloadMods[file.modId] = Pair(fileIdMap[file.id]!!, file.id)
|
||||
continue
|
||||
}
|
||||
fileIdMap[file.id]!!.linkedFile!!.resolvedUpdateData["curseforge"] = file.downloadUrl!!
|
||||
try {
|
||||
fileIdMap[file.id]!!.linkedFile!!.resolvedUpdateData["curseforge"] =
|
||||
HttpUrlPath(file.downloadUrl!!.toHttpUrl())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
failures.add(ExceptionDetails(file.id.toString(),
|
||||
Exception("Failed to parse URL: ${file.downloadUrl} for ID ${file.id}, Project ID ${file.modId}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
if (manualDownloadMods.isNotEmpty()) {
|
||||
@@ -124,7 +125,7 @@ fun resolveCfMetadata(mods: List<IndexFile.File>): List<ExceptionDetails> {
|
||||
|
||||
val modFile = manualDownloadMods[mod.id]!!
|
||||
failures.add(ExceptionDetails(mod.name, Exception("This mod is excluded from the CurseForge API and must be downloaded manually.\n" +
|
||||
"Please go to ${mod.links?.websiteUrl}/files/${modFile.second} and save this file to ${modFile.first.destURI}")))
|
||||
"Please go to ${mod.links?.websiteUrl}/files/${modFile.second} and save this file to ${modFile.first.destURI.rebase(packFolder).nioPath.absolute()}")))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import cc.ekblad.toml.tomlMapper
|
||||
|
||||
class CurseForgeUpdateData: UpdateData {
|
||||
@SerializedName("file-id")
|
||||
var fileId = 0
|
||||
@SerializedName("project-id")
|
||||
var projectId = 0
|
||||
data class CurseForgeUpdateData(
|
||||
val fileId: Int,
|
||||
val projectId: Int,
|
||||
): UpdateData {
|
||||
companion object {
|
||||
fun mapper() = tomlMapper {
|
||||
mapping<CurseForgeUpdateData>("file-id" to "fileId", "project-id" to "projectId")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
interface UpdateData
|
||||
import cc.ekblad.toml.model.TomlValue
|
||||
import cc.ekblad.toml.tomlMapper
|
||||
|
||||
interface UpdateData {
|
||||
companion object {
|
||||
fun mapper() = tomlMapper {
|
||||
val cfMapper = CurseForgeUpdateData.mapper()
|
||||
decoder { it: TomlValue.Map ->
|
||||
if (it.properties.contains("curseforge")) {
|
||||
mapOf("curseforge" to cfMapper.decode<CurseForgeUpdateData>(it.properties["curseforge"]!!))
|
||||
} else { mapOf() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
|
||||
abstract class GeneralHashingSource(delegate: Source) : ForwardingSource(delegate) {
|
||||
abstract val hash: Hash
|
||||
|
||||
fun hashIsEqual(compareTo: Any) = compareTo == hash
|
||||
}
|
||||
@@ -1,20 +1,55 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import com.google.gson.*
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash.SourceProvider
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import okio.ForwardingSource
|
||||
import okio.HashingSource
|
||||
import okio.Source
|
||||
import java.lang.reflect.Type
|
||||
|
||||
abstract class Hash {
|
||||
protected abstract val stringValue: String
|
||||
protected abstract val type: String
|
||||
data class Hash<T>(val type: HashFormat<T>, val value: T) {
|
||||
interface Encoding<T> {
|
||||
fun encodeToString(value: T): String
|
||||
fun decodeFromString(str: String): T
|
||||
|
||||
class TypeHandler : JsonDeserializer<Hash>, JsonSerializer<Hash> {
|
||||
override fun serialize(src: Hash, typeOfSrc: Type, context: JsonSerializationContext): JsonElement = JsonObject().apply {
|
||||
add("type", JsonPrimitive(src.type))
|
||||
add("value", JsonPrimitive(src.stringValue))
|
||||
object Hex: Encoding<ByteString> {
|
||||
override fun encodeToString(value: ByteString) = value.hex()
|
||||
override fun decodeFromString(str: String) = str.decodeHex()
|
||||
}
|
||||
|
||||
object UInt: Encoding<kotlin.UInt> {
|
||||
override fun encodeToString(value: kotlin.UInt) = value.toString()
|
||||
override fun decodeFromString(str: String) = str.toUInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun interface SourceProvider<T> {
|
||||
fun source(type: HashFormat<T>, delegate: Source): HasherSource<T>
|
||||
|
||||
companion object {
|
||||
fun fromOkio(provider: ((Source) -> HashingSource)): SourceProvider<ByteString> {
|
||||
return SourceProvider { type, delegate ->
|
||||
val delegateHashing = provider.invoke(delegate)
|
||||
object : ForwardingSource(delegateHashing), HasherSource<ByteString> {
|
||||
override val hash: Hash<ByteString> by lazy(LazyThreadSafetyMode.NONE) { Hash(type, delegateHashing.hash) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TypeHandler : JsonDeserializer<Hash<*>>, JsonSerializer<Hash<*>> {
|
||||
override fun serialize(src: Hash<*>, typeOfSrc: Type, context: JsonSerializationContext): JsonElement = JsonObject().apply {
|
||||
add("type", JsonPrimitive(src.type.formatName))
|
||||
// Local function for generics
|
||||
fun <T> addValue(src: Hash<T>) = add("value", JsonPrimitive(src.type.encodeToString(src.value)))
|
||||
addValue(src)
|
||||
}
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Hash {
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Hash<*> {
|
||||
val obj = json.asJsonObject
|
||||
val type: String
|
||||
val value: String
|
||||
@@ -25,7 +60,7 @@ abstract class Hash {
|
||||
throw JsonParseException("Invalid hash JSON data")
|
||||
}
|
||||
return try {
|
||||
HashUtils.getHash(type, value)
|
||||
(HashFormat.fromName(type) ?: throw JsonParseException("Unknown hash type $type")).fromString(value)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Failed to create hash object", e)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import cc.ekblad.toml.model.TomlValue
|
||||
import cc.ekblad.toml.tomlMapper
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash.Encoding
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash.SourceProvider
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash.SourceProvider.Companion.fromOkio
|
||||
import okio.ByteString
|
||||
import okio.Source
|
||||
import okio.HashingSource.Companion as OkHashes
|
||||
|
||||
sealed class HashFormat<T>(val formatName: String): Encoding<T>, SourceProvider<T> {
|
||||
object SHA1: HashFormat<ByteString>("sha1"),
|
||||
Encoding<ByteString> by Encoding.Hex, SourceProvider<ByteString> by fromOkio(OkHashes::sha1)
|
||||
object SHA256: HashFormat<ByteString>("sha256"),
|
||||
Encoding<ByteString> by Encoding.Hex, SourceProvider<ByteString> by fromOkio(OkHashes::sha256)
|
||||
object SHA512: HashFormat<ByteString>("sha512"),
|
||||
Encoding<ByteString> by Encoding.Hex, SourceProvider<ByteString> by fromOkio(OkHashes::sha512)
|
||||
object MD5: HashFormat<ByteString>("md5"),
|
||||
Encoding<ByteString> by Encoding.Hex, SourceProvider<ByteString> by fromOkio(OkHashes::md5)
|
||||
object MURMUR2: HashFormat<UInt>("murmur2"),
|
||||
Encoding<UInt> by Encoding.UInt, SourceProvider<UInt> by SourceProvider(::Murmur2HasherSource)
|
||||
|
||||
fun source(delegate: Source): HasherSource<T> = source(this, delegate)
|
||||
fun fromString(str: String) = Hash(this, decodeFromString(str))
|
||||
override fun toString() = formatName
|
||||
|
||||
companion object {
|
||||
// lazy used to prevent initialisation issues!
|
||||
private val values by lazy { listOf(SHA1, SHA256, SHA512, MD5, MURMUR2) }
|
||||
fun fromName(formatName: String) = values.find { formatName == it.formatName }
|
||||
|
||||
fun mapper() = tomlMapper {
|
||||
// TODO: better exception?
|
||||
decoder { it: TomlValue.String -> fromName(it.value) ?: throw Exception("Hash format ${it.value} not supported") }
|
||||
encoder { it: HashFormat<*> -> TomlValue.String(it.formatName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
object HashUtils {
|
||||
private val hashTypeConversion: Map<String, IHasher> = mapOf(
|
||||
"sha256" to HashingSourceHasher("sha256"),
|
||||
"sha512" to HashingSourceHasher("sha512"),
|
||||
"murmur2" to Murmur2Hasher(),
|
||||
"sha1" to HashingSourceHasher("sha1")
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Exception::class)
|
||||
fun getHasher(type: String): IHasher {
|
||||
return hashTypeConversion[type] ?: throw Exception("Hash type not supported: $type")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Exception::class)
|
||||
fun getHash(type: String, value: String): Hash {
|
||||
return hashTypeConversion[type]?.getHash(value) ?: throw Exception("Hash type not supported: $type")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.Source
|
||||
|
||||
interface HasherSource<T>: Source {
|
||||
val hash: Hash<T>
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.HashingSource
|
||||
import okio.Source
|
||||
|
||||
class HashingSourceHasher internal constructor(private val type: String) : IHasher {
|
||||
// i love naming things
|
||||
private inner class HashingSourceGeneralHashingSource(val delegateHashing: HashingSource) : GeneralHashingSource(delegateHashing) {
|
||||
override val hash: Hash by lazy(LazyThreadSafetyMode.NONE) {
|
||||
HashingSourceHash(delegateHashing.hash.hex())
|
||||
}
|
||||
}
|
||||
|
||||
// this some funky inner class stuff
|
||||
// each of these classes is specific to the instance of the HasherHashingSource
|
||||
// therefore HashingSourceHashes from different parent instances will be not instanceof each other
|
||||
private inner class HashingSourceHash(val value: String) : Hash() {
|
||||
override val stringValue get() = value
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is HashingSourceHash) {
|
||||
return false
|
||||
}
|
||||
return stringValue.equals(other.stringValue, ignoreCase = true)
|
||||
}
|
||||
|
||||
override fun toString(): String = "$type: $stringValue"
|
||||
override fun hashCode(): Int = value.hashCode()
|
||||
|
||||
override val type: String get() = this@HashingSourceHasher.type
|
||||
}
|
||||
|
||||
override fun getHashingSource(delegate: Source): GeneralHashingSource {
|
||||
when (type) {
|
||||
"md5" -> return HashingSourceGeneralHashingSource(HashingSource.md5(delegate))
|
||||
"sha256" -> return HashingSourceGeneralHashingSource(HashingSource.sha256(delegate))
|
||||
"sha512" -> return HashingSourceGeneralHashingSource(HashingSource.sha512(delegate))
|
||||
"sha1" -> return HashingSourceGeneralHashingSource(HashingSource.sha1(delegate))
|
||||
}
|
||||
throw RuntimeException("Invalid hash type provided")
|
||||
}
|
||||
|
||||
override fun getHash(value: String): Hash {
|
||||
return HashingSourceHash(value)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.Source
|
||||
|
||||
interface IHasher {
|
||||
fun getHashingSource(delegate: Source): GeneralHashingSource
|
||||
fun getHash(value: String): Hash
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.Buffer
|
||||
import okio.Source
|
||||
import java.io.IOException
|
||||
|
||||
class Murmur2Hasher : IHasher {
|
||||
private inner class Murmur2GeneralHashingSource(delegate: Source) : GeneralHashingSource(delegate) {
|
||||
val internalBuffer = Buffer()
|
||||
val tempBuffer = Buffer()
|
||||
|
||||
override val hash: Hash by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val data = internalBuffer.readByteArray()
|
||||
Murmur2Hash(Murmur2Lib.hash32(data, data.size, 1))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val out = delegate.read(tempBuffer, byteCount)
|
||||
if (out > -1) {
|
||||
sink.write(tempBuffer.clone(), out)
|
||||
computeNormalizedBufferFaster(tempBuffer, internalBuffer)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Credit to https://github.com/modmuss50/CAV2/blob/master/murmur.go
|
||||
// private fun computeNormalizedArray(input: ByteArray): ByteArray {
|
||||
// val output = ByteArray(input.size)
|
||||
// var index = 0
|
||||
// for (b in input) {
|
||||
// when (b) {
|
||||
// 9.toByte(), 10.toByte(), 13.toByte(), 32.toByte() -> {}
|
||||
// else -> {
|
||||
// output[index] = b
|
||||
// index++
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// val outputTrimmed = ByteArray(index)
|
||||
// System.arraycopy(output, 0, outputTrimmed, 0, index)
|
||||
// return outputTrimmed
|
||||
// }
|
||||
|
||||
private fun computeNormalizedBufferFaster(input: Buffer, output: Buffer) {
|
||||
var index = 0
|
||||
val arr = input.readByteArray()
|
||||
for (b in arr) {
|
||||
when (b) {
|
||||
9.toByte(), 10.toByte(), 13.toByte(), 32.toByte() -> {}
|
||||
else -> {
|
||||
arr[index] = b
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
output.write(arr, 0, index)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class Murmur2Hash : Hash {
|
||||
val value: Int
|
||||
|
||||
constructor(value: String) {
|
||||
// Parsing as long then casting to int converts values gt int max value but lt uint max value
|
||||
// into negatives. I presume this is how the murmur2 code handles this.
|
||||
this.value = value.toLong().toInt()
|
||||
}
|
||||
|
||||
constructor(value: Int) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override val stringValue get() = value.toString()
|
||||
override val type get() = "murmur2"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Murmur2Hash) {
|
||||
return false
|
||||
}
|
||||
return value == other.value
|
||||
}
|
||||
|
||||
override fun toString(): String = "murmur2: $value"
|
||||
override fun hashCode(): Int = value
|
||||
}
|
||||
|
||||
override fun getHashingSource(delegate: Source): GeneralHashingSource = Murmur2GeneralHashingSource(delegate)
|
||||
override fun getHash(value: String): Hash = Murmur2Hash(value)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.Buffer
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
import java.io.IOException
|
||||
|
||||
class Murmur2HasherSource(type: HashFormat<UInt>, delegate: Source) : ForwardingSource(delegate), HasherSource<UInt> {
|
||||
private val internalBuffer = Buffer()
|
||||
private val tempBuffer = Buffer()
|
||||
|
||||
override val hash: Hash<UInt> by lazy(LazyThreadSafetyMode.NONE) {
|
||||
// TODO: remove internal buffering?
|
||||
val data = internalBuffer.readByteArray()
|
||||
Hash(type, Murmur2Lib.hash32(data, data.size, 1).toUInt())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val out = delegate.read(tempBuffer, byteCount)
|
||||
if (out > -1) {
|
||||
sink.write(tempBuffer.clone(), out)
|
||||
computeNormalizedBufferFaster(tempBuffer, internalBuffer)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Credit to https://github.com/modmuss50/CAV2/blob/master/murmur.go
|
||||
private fun computeNormalizedBufferFaster(input: Buffer, output: Buffer) {
|
||||
var index = 0
|
||||
val arr = input.readByteArray()
|
||||
for (b in arr) {
|
||||
when (b) {
|
||||
9.toByte(), 10.toByte(), 13.toByte(), 32.toByte() -> {}
|
||||
else -> {
|
||||
arr[index] = b
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
output.write(arr, 0, index)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user