mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 17:19:15 -10:00
port-pack: mods.toml read tolerantly (unfilled templates, line by line)
A Forge jar may carry the other loader's metadata as an unfilled template
(Drip Sounds: [[dependencies.${id}]]): the first file that parses and names
a real mod id is the jar's; when none parses, the first [[mods]] is read
line by line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4956321f71
commit
1884ed60db
1 file changed
+99
-4
@@ -42,10 +42,12 @@ pub fn read(path: &Path) -> Result<JarInfo, String> {
|
||||
zip.by_name(name).ok()?.read_to_string(&mut s).ok()?;
|
||||
Some(s)
|
||||
};
|
||||
let mut info = if let Some(t) = text(&mut zip, "META-INF/neoforge.mods.toml")
|
||||
.or_else(|| text(&mut zip, "META-INF/mods.toml"))
|
||||
{
|
||||
mods_toml(&t)?
|
||||
let tomls: Vec<String> = ["META-INF/neoforge.mods.toml", "META-INF/mods.toml"]
|
||||
.iter()
|
||||
.filter_map(|n| text(&mut zip, n))
|
||||
.collect();
|
||||
let mut info = if !tomls.is_empty() {
|
||||
metadata(&tomls)?
|
||||
} else if let Some(t) = text(&mut zip, "fabric.mod.json") {
|
||||
fabric(&t)?
|
||||
} else if let Some(t) = text(&mut zip, "mcmod.info") {
|
||||
@@ -84,6 +86,71 @@ fn manifest_value(manifest: &str, key: &str) -> Option<String> {
|
||||
.filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
/// The jar's mod from its metadata files (`neoforge.mods.toml` first, then
|
||||
/// `mods.toml`). A jar built for one loader may carry the other loader's file
|
||||
/// as an unfilled template (`modId = "${id}"`, `[[dependencies.${id}]]`), which
|
||||
/// its own loader never reads: the first file that parses and names a real mod
|
||||
/// id is the one. When none parses, the first file is read line by line
|
||||
/// ([`mods_toml_lines`]), as a loader that reads past what it does not need.
|
||||
pub fn metadata(tomls: &[String]) -> Result<JarInfo, String> {
|
||||
let real = |i: &JarInfo| !i.mod_id.is_empty() && !i.mod_id.contains("${");
|
||||
let mut first_error = None;
|
||||
for t in tomls {
|
||||
match mods_toml(t) {
|
||||
Ok(i) if real(&i) => return Ok(i),
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
first_error.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
tomls
|
||||
.iter()
|
||||
.map(|t| mods_toml_lines(t))
|
||||
.find(real)
|
||||
.ok_or_else(|| first_error.unwrap_or_else(|| "mods.toml: no [[mods]] modId".into()))
|
||||
}
|
||||
|
||||
/// The first `[[mods]]` table's `modId`, `displayName` and `version` and the
|
||||
/// top-level `license`, read line by line (`key = "value"`, `#` comments), for
|
||||
/// a metadata file a TOML parser refuses. Dependencies are not read.
|
||||
pub fn mods_toml_lines(text: &str) -> JarInfo {
|
||||
let mut info = JarInfo::default();
|
||||
// 0: top level, 1: the first [[mods]], 2: past it.
|
||||
let mut section = 0;
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with('[') {
|
||||
let name = line.trim_matches(['[', ']']).trim();
|
||||
section = match (section, name) {
|
||||
(0, "mods") => 1,
|
||||
_ => 2,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let value = value.trim();
|
||||
let Some(value) = value
|
||||
.strip_prefix('"')
|
||||
.and_then(|v| v.split_once('"'))
|
||||
.or_else(|| value.strip_prefix('\'').and_then(|v| v.split_once('\'')))
|
||||
.map(|(v, _)| v.to_owned())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
match (section, key.trim()) {
|
||||
(0, "license") => info.license = value,
|
||||
(1, "modId") if info.mod_id.is_empty() => info.mod_id = value,
|
||||
(1, "displayName") => info.name = value,
|
||||
(1, "version") => info.version = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
/// `[[mods]]` (the first one is the jar's mod), the top-level `license`, and
|
||||
/// `[[dependencies.<id>]]` with `type = "required"` (or the older
|
||||
/// `mandatory = true`).
|
||||
@@ -295,6 +362,34 @@ mandatory = true
|
||||
assert_eq!(info.depends, ["geckolib", "curios"]);
|
||||
}
|
||||
|
||||
/// Drip Sounds' Forge jar carries an unfilled NeoForge template beside its
|
||||
/// `mods.toml`; a file no parser reads still gives its id line by line.
|
||||
#[test]
|
||||
fn templates_and_broken_files_are_read_tolerantly() {
|
||||
let template = "license = \"LGPL\"\n[[mods]]\nmodId = \"${id}\"\nversion = \"${version}\"\n\n[[dependencies.${id}]]\nmodId = \"minecraft\"\n";
|
||||
let forge = "license = \"LGPL-3.0-only\"\n[[mods]]\nmodId = \"waterdripsound\"\ndisplayName = \"Drip Sounds\"\nversion = \"0.6.3\"\n[[dependencies.waterdripsound]]\nmodId = \"cloth_config\"\nmandatory = true\n";
|
||||
assert!(mods_toml(template).is_err());
|
||||
let info = metadata(&[template.to_owned(), forge.to_owned()]).unwrap();
|
||||
assert_eq!(
|
||||
(info.mod_id.as_str(), info.version.as_str()),
|
||||
("waterdripsound", "0.6.3")
|
||||
);
|
||||
assert_eq!(info.depends, ["cloth_config"]);
|
||||
// Nothing parses: the lines still name the mod (a duplicate key is a TOML error).
|
||||
let broken = "license = 'MIT' # x\n[[mods]]\nmodId = \"ruby\"\nmodId = \"ruby\"\nversion = \"2\"\ndisplayName = \"Ruby\"\n[[mods]]\nmodId = \"other\"\n";
|
||||
assert!(mods_toml(broken).is_err());
|
||||
let info = metadata(&[template.to_owned(), broken.to_owned()]).unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
info.mod_id.as_str(),
|
||||
info.name.as_str(),
|
||||
info.license.as_str()
|
||||
),
|
||||
("ruby", "Ruby", "MIT")
|
||||
);
|
||||
assert!(metadata(&[template.to_owned()]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_fabric_metadata() {
|
||||
let info = fabric(
|
||||
|
||||
Reference in new issue
Block a user