|
| 1 | +package pl.syntaxdevteam.syntaxlauncher.core.mods |
| 2 | + |
| 3 | +/** |
| 4 | + * Conservative, bounded subset of Forge/NeoForge TOML metadata for mod inventory. |
| 5 | + * |
| 6 | + * A complete TOML implementation is deliberately NOT claimed: only simple |
| 7 | + * quoted scalar fields, booleans and array-of-tables headers are read. Unknown |
| 8 | + * constructs are ignored instead of guessed, so a dependency is reported only |
| 9 | + * when its type and client applicability are explicit. |
| 10 | + */ |
| 11 | +internal object ForgeTomlMetadataReader { |
| 12 | + private val safeId = Regex("[a-z][a-z0-9_-]{0,63}") |
| 13 | + private val arraySection = Regex("""^\s*\[\[([a-zA-Z0-9_.-]{1,160})]]\s*(?:#.*)?$""") |
| 14 | + private val property = Regex("""^\s*([a-zA-Z][a-zA-Z0-9_]*)\s*=\s*(.*?)\s*$""") |
| 15 | + private val quotedValue = Regex("""^["']([^"'\r\n]{0,180})["'](?:\s*#.*)?$""") |
| 16 | + private val boolValue = Regex("""^(true|false)(?:\s*#.*)?$""") |
| 17 | + private const val maxLength = 256 * 1024 |
| 18 | + |
| 19 | + private class Requirement { |
| 20 | + var modId: String? = null |
| 21 | + var mandatory: Boolean? = null |
| 22 | + var type: String? = null |
| 23 | + var range: String = "*" |
| 24 | + var side: String = "BOTH" |
| 25 | + } |
| 26 | + |
| 27 | + fun parse(source: String): LocalModMetadata? { |
| 28 | + require(source.length <= maxLength) { "Forge mod metadata is too large." } |
| 29 | + var inFirstMod = false |
| 30 | + var modAlreadySeen = false |
| 31 | + var id: String? = null |
| 32 | + var name: String? = null |
| 33 | + var version: String? = null |
| 34 | + var current: Requirement? = null |
| 35 | + val requirements = mutableListOf<Pair<String, Requirement>>() |
| 36 | + |
| 37 | + for (rawLine in source.lineSequence()) { |
| 38 | + val line = rawLine.trim() |
| 39 | + if (line.startsWith("[")) { |
| 40 | + val header = arraySection.matchEntire(line)?.groupValues?.get(1) |
| 41 | + inFirstMod = header == "mods" && !modAlreadySeen |
| 42 | + if (header == "mods") modAlreadySeen = true |
| 43 | + current = null |
| 44 | + if (header != null && header.startsWith("dependencies.")) { |
| 45 | + val owner = header.removePrefix("dependencies.") |
| 46 | + if (owner.matches(safeId)) { |
| 47 | + current = Requirement().also { requirements += owner to it } |
| 48 | + } |
| 49 | + } |
| 50 | + continue |
| 51 | + } |
| 52 | + val match = property.matchEntire(line) ?: continue |
| 53 | + val key = match.groupValues[1] |
| 54 | + val value = match.groupValues[2] |
| 55 | + val string = quotedValue.matchEntire(value)?.groupValues?.get(1) |
| 56 | + if (inFirstMod) { |
| 57 | + when (key) { |
| 58 | + "modId" -> if (string != null) id = string |
| 59 | + "displayName" -> if (string != null) name = string |
| 60 | + "version" -> if (string != null) version = string |
| 61 | + } |
| 62 | + } else { |
| 63 | + val dependency = current ?: continue |
| 64 | + when (key) { |
| 65 | + "modId" -> if (string != null) dependency.modId = string |
| 66 | + "mandatory" -> boolValue.matchEntire(value)?.let { |
| 67 | + dependency.mandatory = it.groupValues[1] == "true" |
| 68 | + } |
| 69 | + "type" -> if (string != null) dependency.type = string.lowercase() |
| 70 | + "versionRange" -> if (string != null) dependency.range = string |
| 71 | + "side" -> if (string != null) dependency.side = string.uppercase() |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + val modId = id?.takeIf { it.matches(safeId) } ?: return null |
| 77 | + val depends = linkedMapOf<String, String>() |
| 78 | + val breaks = linkedMapOf<String, String>() |
| 79 | + requirements.asSequence().filter { (owner, _) -> owner == modId } |
| 80 | + .map { it.second }.forEach { dependency -> |
| 81 | + val requiredId = dependency.modId?.takeIf { it.matches(safeId) } ?: return@forEach |
| 82 | + // Server-only requirements do not apply to this client launcher. |
| 83 | + if (dependency.side == "SERVER") return@forEach |
| 84 | + when { |
| 85 | + dependency.type == "required" || (dependency.type == null && |
| 86 | + dependency.mandatory == true) -> |
| 87 | + depends[requiredId] = dependency.range.take(180) |
| 88 | + dependency.type == "incompatible" -> |
| 89 | + breaks[requiredId] = dependency.range.take(180) |
| 90 | + } |
| 91 | + } |
| 92 | + return LocalModMetadata( |
| 93 | + id = modId, |
| 94 | + name = name?.takeIf { it.isNotBlank() }?.take(120) ?: modId, |
| 95 | + version = version?.takeIf { it.isNotBlank() }?.take(120) ?: "unknown", |
| 96 | + environment = null, |
| 97 | + depends = depends, |
| 98 | + breaks = breaks, |
| 99 | + ) |
| 100 | + } |
| 101 | +} |
0 commit comments