diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java index 8136bd761..2df3ee815 100644 --- a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java @@ -58,13 +58,35 @@ public interface ExtensionDescription { @NonNull String main(); + /** + * Gets the extension's major api version + * + * @return the extension's major api version + */ + int majorApiVersion(); + + /** + * Gets the extension's minor api version + * + * @return the extension's minor api version + */ + int minorApiVersion(); + + /** + * Gets the extension's patch api version + * + * @return the extension's patch api version + */ + int patchApiVersion(); + /** * Gets the extension's api version. * * @return the extension's api version */ - @NonNull - String apiVersion(); + default String apiVersion() { + return majorApiVersion() + "." + minorApiVersion() + "." + patchApiVersion(); + } /** * Gets the extension's description. diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java index 04f80e19d..716b763f5 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java @@ -25,58 +25,95 @@ package org.geysermc.geyser.extension; +import lombok.Getter; +import lombok.Setter; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.extension.ExtensionDescription; import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException; -import org.yaml.snakeyaml.DumperOptions; +import org.geysermc.geyser.text.GeyserLocale; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor; import java.io.Reader; import java.util.*; +import java.util.function.Supplier; +import java.util.regex.Pattern; -public record GeyserExtensionDescription(String id, String name, String main, String apiVersion, String version, List authors) implements ExtensionDescription { - @SuppressWarnings("unchecked") +public record GeyserExtensionDescription(@NonNull String id, + @NonNull String name, + @NonNull String main, + int majorApiVersion, + int minorApiVersion, + int patchApiVersion, + @NonNull String version, + @NonNull List authors) implements ExtensionDescription { + + private static final Yaml YAML = new Yaml(new CustomClassLoaderConstructor(Source.class.getClassLoader())); + + public static final Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}"); + public static final Pattern NAME_PATTERN = Pattern.compile("^[A-Za-z_.-]+$"); + public static final Pattern API_VERSION_PATTERN = Pattern.compile("^\\d+\\.\\d+\\.\\d+$"); + + @NonNull public static GeyserExtensionDescription fromYaml(Reader reader) throws InvalidDescriptionException { - DumperOptions dumperOptions = new DumperOptions(); - dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); - - Yaml yaml = new Yaml(dumperOptions); - Map yamlMap = yaml.loadAs(reader, LinkedHashMap.class); - - String id = ((String) yamlMap.get("id")).replaceAll("[^A-Za-z0-9 _.-]", ""); - if (id.isBlank()) { - throw new InvalidDescriptionException("Invalid extension id, cannot be empty"); + Source source; + try { + source = YAML.loadAs(reader, Source.class); + } catch (Exception e) { + throw new InvalidDescriptionException(e); } - String name = ((String) yamlMap.get("name")).replaceAll("[^A-Za-z0-9 _.-]", ""); - if (name.isBlank()) { - throw new InvalidDescriptionException("Invalid extension name, cannot be empty"); + String id = require(source::getId, "id"); + if (!ID_PATTERN.matcher(id).matches()) { + throw new InvalidDescriptionException("Invalid extension id, must match: " + ID_PATTERN.pattern()); } - name = name.replace(" ", "_"); - String version = String.valueOf(yamlMap.get("version")); - String main = (String) yamlMap.get("main"); - String apiVersion; - - Object api = yamlMap.get("api"); - if (api instanceof String) { - apiVersion = (String) api; - } else { - throw new InvalidDescriptionException("Invalid api version format, should be a string: major.minor.patch"); + String name = require(source::getName, "name"); + if (!NAME_PATTERN.matcher(name).matches()) { + throw new InvalidDescriptionException("Invalid extension name, must match: " + NAME_PATTERN.pattern()); } + String version = String.valueOf(source.version); + String main = require(source::getMain, "main"); + + String apiVersion = require(source::getApi, "api"); + if (!API_VERSION_PATTERN.matcher(apiVersion).matches()) { + throw new InvalidDescriptionException(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, apiVersion)); + } + String[] api = apiVersion.split("\\."); + int majorApi = Integer.parseUnsignedInt(api[0]); + int minorApi = Integer.parseUnsignedInt(api[1]); + int patchApi = Integer.parseUnsignedInt(api[2]); + List authors = new ArrayList<>(); - if (yamlMap.containsKey("author")) { - authors.add((String) yamlMap.get("author")); + if (source.author != null) { + authors.add(source.author); + } + if (source.authors != null) { + authors.addAll(source.authors); } - if (yamlMap.containsKey("authors")) { - try { - authors.addAll((Collection) yamlMap.get("authors")); - } catch (Exception e) { - throw new InvalidDescriptionException("Invalid authors format, should be a list of strings", e); - } - } + return new GeyserExtensionDescription(id, name, main, majorApi, minorApi, patchApi, version, authors); + } - return new GeyserExtensionDescription(id, name, main, apiVersion, version, Collections.unmodifiableList(authors)); + @NonNull + private static String require(Supplier supplier, String name) throws InvalidDescriptionException { + String value = supplier.get(); + if (value == null) { + throw new InvalidDescriptionException("Extension description is missing string property '" + name + "'"); + } + return value; + } + + @Getter + @Setter + public static class Source { + String id; + String name; + String main; + String api; + String version; + String author; + List authors; } } diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java index d317bc013..44558e798 100644 --- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java @@ -39,6 +39,7 @@ import org.geysermc.geyser.extension.event.GeyserExtensionEventBus; import org.geysermc.geyser.text.GeyserLocale; import java.io.IOException; +import java.io.Reader; import java.nio.file.*; import java.util.HashMap; import java.util.LinkedHashMap; @@ -48,7 +49,6 @@ import java.util.stream.Stream; @RequiredArgsConstructor public class GeyserExtensionLoader extends ExtensionLoader { - private static final Pattern API_VERSION_PATTERN = Pattern.compile("^\\d+\\.\\d+\\.\\d+$"); private static final Pattern[] EXTENSION_FILTERS = new Pattern[] { Pattern.compile("^.+\\.jar$") }; private final Object2ReferenceMap> classes = new Object2ReferenceOpenHashMap<>(); @@ -56,7 +56,7 @@ public class GeyserExtensionLoader extends ExtensionLoader { private final Map extensionContainers = new HashMap<>(); private final Path extensionsDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("extensions"); - public GeyserExtensionContainer loadExtension(Path path, GeyserExtensionDescription description) throws InvalidExtensionException, InvalidDescriptionException { + public GeyserExtensionContainer loadExtension(Path path, GeyserExtensionDescription description) throws InvalidExtensionException { if (path == null) { throw new InvalidExtensionException("Path is null"); } @@ -93,7 +93,9 @@ public class GeyserExtensionLoader extends ExtensionLoader { Map environment = new HashMap<>(); try (FileSystem fileSystem = FileSystems.newFileSystem(path, environment, null)) { Path extensionYml = fileSystem.getPath("extension.yml"); - return GeyserExtensionDescription.fromYaml(Files.newBufferedReader(extensionYml)); + try (Reader reader = Files.newBufferedReader(extensionYml)) { + return GeyserExtensionDescription.fromYaml(reader); + } } catch (IOException ex) { throw new InvalidDescriptionException("Failed to load extension description for " + path, ex); } @@ -148,9 +150,6 @@ public class GeyserExtensionLoader extends ExtensionLoader { try { GeyserExtensionDescription description = this.extensionDescription(path); - if (description == null) { - return; - } String name = description.name(); if (extensions.containsKey(name) || extensionManager.extension(name) != null) { @@ -158,30 +157,15 @@ public class GeyserExtensionLoader extends ExtensionLoader { return; } - int majorVersion = Geyser.api().majorApiVersion(); - int minorVersion = Geyser.api().minorApiVersion(); - - try { - // Check the format: majorVersion.minorVersion.patch - if (!API_VERSION_PATTERN.matcher(description.apiVersion()).matches()) { - throw new IllegalArgumentException(); - } - } catch (NullPointerException | IllegalArgumentException e) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, majorVersion + "." + minorVersion)); - return; - } - - String[] versionArray = description.apiVersion().split("\\."); - // Completely different API version - if (Integer.parseInt(versionArray[0]) != majorVersion) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, majorVersion + "." + minorVersion)); + if (description.majorApiVersion() != Geyser.api().majorApiVersion()) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion())); return; } // If the extension requires new API features, being backwards compatible - if (Integer.parseInt(versionArray[1]) > minorVersion) { - GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, majorVersion + "." + minorVersion)); + if (description.minorApiVersion() > Geyser.api().minorApiVersion()) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion())); return; }