Skip to content

Commit 65a0ad7

Browse files
committed
feat(mods): conservatively read required and incompatible Forge/NeoForge TOML dependencies
1 parent 8ca4c0e commit 65a0ad7

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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

Comments
 (0)