Validate mod files #9

Merged
owlsys merged 1 commit from TheKodeToad/better-mod-file-validation into main 2024-06-12 13:31:13 -04:00

View file

@ -7,6 +7,7 @@ import java.nio.file.FileSystem;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.electronwill.nightconfig.core.CommentedConfig; import com.electronwill.nightconfig.core.CommentedConfig;
@ -16,7 +17,10 @@ import com.electronwill.nightconfig.toml.TomlParser;
import dev.frogmc.frogloader.api.mod.ModDependencies; import dev.frogmc.frogloader.api.mod.ModDependencies;
import dev.frogmc.frogloader.api.mod.ModExtensions; import dev.frogmc.frogloader.api.mod.ModExtensions;
import dev.frogmc.frogloader.api.mod.ModProperties; import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.mod.SemVer;
import dev.frogmc.frogloader.impl.SemVerParseException;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -32,7 +36,7 @@ public class ModPropertiesReader {
CommentedConfig props = PARSER.parse(fs.getPath(PROPERTIES_FILE_NAME), FileNotFoundAction.THROW_ERROR); CommentedConfig props = PARSER.parse(fs.getPath(PROPERTIES_FILE_NAME), FileNotFoundAction.THROW_ERROR);
return Optional.of(readProperties(props, Collections.singleton(mod))); return Optional.of(readProperties(props, Collections.singleton(mod)));
} catch (IOException e) { } catch (IOException | InvalidModPropertiesException e) {
LOGGER.warn("Failed to read mod properties: ", e); LOGGER.warn("Failed to read mod properties: ", e);
} }
@ -60,7 +64,7 @@ public class ModPropertiesReader {
return Optional.empty(); return Optional.empty();
} }
private static ModProperties readProperties(UnmodifiableConfig config, Collection<Path> sources) { private static ModProperties readProperties(UnmodifiableConfig config, Collection<Path> sources) throws InvalidModPropertiesException {
String manifestVer = config.get("frog.format_version"); String manifestVer = config.get("frog.format_version");
try { try {
@ -73,53 +77,91 @@ public class ModPropertiesReader {
@AllArgsConstructor @AllArgsConstructor
private enum FormatVersion { private enum FormatVersion {
V1_0_0("1.0.0", (config, sources) -> { V1_0_0("1.0.0", (config, sources) -> {
List<String> badProperties = new ArrayList<>();
String id = config.get("frog.mod.id"); String id = config.get("frog.mod.id");
String name = config.get("frog.mod.name"); String name = config.get("frog.mod.name");
String icon = config.get("frog.mod.icon"); String icon = config.get("frog.mod.icon");
String version = config.get("frog.mod.version"); String version = config.get("frog.mod.version");
String license = config.get("frog.mod.license"); String license = Objects.requireNonNullElse(config.get("frog.mod.license"), "");
if (license == null) { if (id == null || id.isEmpty())
license = ""; badProperties.add("frog.mod.id");
if (name == null || name.isEmpty())
badProperties.add("frog.mod.name");
SemVer semVer = null;
if (version == null || version.isEmpty())
badProperties.add("frog.mod.version");
else {
try {
semVer = SemVerImpl.parse(version);
} catch (SemVerParseException e) {
badProperties.add("frog.mod.version");
}
} }
List<UnmodifiableConfig> creditsList = config.get("frog.mod.credits"); List<UnmodifiableConfig> creditsList = config.get("frog.mod.credits");
Map<String, Collection<String>> credits = new HashMap<>(); Map<String, Collection<String>> credits = new HashMap<>();
if (creditsList != null) { if (creditsList != null) {
creditsList.forEach(c -> credits.put(c.get("name"), c.get("roles"))); int i = 0;
for (UnmodifiableConfig entry : creditsList) {
String entryName = entry.get("name");
Collection<String> entryRoles = entry.get("roles");
if (entryName == null || entryName.isEmpty())
badProperties.add("frog.mod.credits." + i + ".name");
if (entryRoles == null || entryRoles.isEmpty())
badProperties.add("frog.mod.credits." + i + ".name");
credits.put(entryName, entryRoles);
++i;
}
} }
Collection<ModDependencies.Entry> depends = new HashSet<>(); Function<String, Collection<ModDependencies.Entry>> parseDependencies = key -> {
List<UnmodifiableConfig> dependsConfig = config.get("frog.dependencies.depends"); Collection<ModDependencies.Entry> result = new HashSet<>();
if (dependsConfig != null) {
dependsConfig.forEach(entry -> depends.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name")))); List<UnmodifiableConfig> dependenciesConfig = config.get("frog.dependencies." + key);
if (dependenciesConfig != null) {
int i = 0;
for (UnmodifiableConfig entry : dependenciesConfig) {
String entryId = entry.get("id");
String entryVersions = entry.get("versions");
if (entryId == null || entryId.isEmpty())
badProperties.add("frog.dependencies." + key + '.' + i + ".id");
result.add(new ModDependencies.Entry(entryId, entryVersions, entry.get("link"), entry.get("name")));
++i;
}
} }
Collection<ModDependencies.Entry> breaks = new HashSet<>(); return result;
List<UnmodifiableConfig> breaksConfig = config.get("frog.dependencies.breaks"); };
if (breaksConfig != null) {
breaksConfig.forEach(entry -> breaks.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name"))));
}
Collection<ModDependencies.Entry> suggests = new HashSet<>();
List<UnmodifiableConfig> suggestsConfig = config.get("frog.dependencies.suggests");
if (suggestsConfig != null) {
suggestsConfig.forEach(entry -> suggests.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name"))));
}
Collection<ModDependencies.Entry> provides = new HashSet<>(); var depends = parseDependencies.apply("depends");
List<UnmodifiableConfig> providesConfig = config.get("frog.dependencies.provides"); var breaks = parseDependencies.apply("breaks");
if (providesConfig != null) { var suggests = parseDependencies.apply("suggests");
providesConfig.forEach(entry -> provides.add(new ModDependencies.Entry(entry.get("id"), entry.get("version"), entry.get("link"), entry.get("name")))); var provides = parseDependencies.apply("provides");
}
if (!badProperties.isEmpty())
throw new InvalidModPropertiesException(id, sources, badProperties);
UnmodifiableConfig extensionsConfig = config.get("frog.extensions"); UnmodifiableConfig extensionsConfig = config.get("frog.extensions");
Map<String, Object> extensions = new HashMap<>(); Map<String, Object> extensions = new HashMap<>();
if (extensionsConfig != null) { if (extensionsConfig != null)
extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue())); extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue()));
}
return new ModPropertiesImpl(id, name, icon, SemVerImpl.parse(version), license, Collections.unmodifiableMap(credits), return new ModPropertiesImpl(id, name, icon, semVer, license, Collections.unmodifiableMap(credits),
new ModDependencies(depends, breaks, suggests, provides), ModExtensions.of(extensions), sources); new ModDependencies(depends, breaks, suggests, provides), ModExtensions.of(extensions), sources);
}); });
@ -133,8 +175,26 @@ public class ModPropertiesReader {
@FunctionalInterface @FunctionalInterface
public interface Parser { public interface Parser {
ModProperties parse(UnmodifiableConfig config, Collection<Path> sources) throws IOException; ModProperties parse(UnmodifiableConfig config, Collection<Path> sources) throws IOException, InvalidModPropertiesException;
} }
}
@Getter
public static class InvalidModPropertiesException extends Exception {
private final Collection<String> invalid;
public InvalidModPropertiesException(String id, Collection<Path> sources, Collection<String> invalid) {
super(
"Invalid properties for %s (%s) - invalid or missing values for: %s".formatted(
Objects.requireNonNullElse(id, "<unknown>"),
sources.stream().map(Path::toString).collect(Collectors.joining(", ")),
String.join(", ", invalid)
)
);
this.invalid = invalid;
}
} }
} }