diff --git a/libraries/core/src/main/java/net/ornithemc/osl/core/api/util/NamespacedIdentifier.java b/libraries/core/src/main/java/net/ornithemc/osl/core/api/util/NamespacedIdentifier.java new file mode 100644 index 00000000..598ac994 --- /dev/null +++ b/libraries/core/src/main/java/net/ornithemc/osl/core/api/util/NamespacedIdentifier.java @@ -0,0 +1,38 @@ +package net.ornithemc.osl.core.api.util; + +/** + * Namespaced identifiers are two-part strings that uniquely point to content in Minecraft. + * The two parts are the namespace and the identifier. They can be combined into a single + * string representation as namespace:identifier (the namespace, followed by the identifier, + * separated by a colon). + *

+ * A namespace is a domain for content. It is used not to point to specific content, but to + * differentiate between different content sources or publishers. The use of namespaces can + * prevent conflicts between mods, resource packs, or data packs, in cases where the same + * identifier is used. + *

+ * The identifier is a unique name for content within a namespace. It should be descriptive + * to avoid naming conflicts with other content. The preferred format is snake_case. + *

+ * Namespaces may only contain alphanumeric characters [a-zA-Z0-9] and special characters + * [-._]. Identifiers may also contain the special character [/]. + */ +public interface NamespacedIdentifier { + + /** + * The separator between the namespace and identifier in the {@code String} + * representation of a {@code NamespacedIdentifier}. + */ + static char SEPARATOR = ':'; + + /** + * @return the namespace of this {@code NamespacedIdentifier}. + */ + String namespace(); + + /** + * @return the identifier of this {@code NamespacedIdentifier}. + */ + String identifier(); + +} diff --git a/libraries/core/src/main/java/net/ornithemc/osl/core/api/util/NamespacedIdentifiers.java b/libraries/core/src/main/java/net/ornithemc/osl/core/api/util/NamespacedIdentifiers.java new file mode 100644 index 00000000..691ae091 --- /dev/null +++ b/libraries/core/src/main/java/net/ornithemc/osl/core/api/util/NamespacedIdentifiers.java @@ -0,0 +1,146 @@ +package net.ornithemc.osl.core.api.util; + +import java.util.Comparator; + +import net.ornithemc.osl.core.impl.util.NamespacedIdentifierException; +import net.ornithemc.osl.core.impl.util.NamespacedIdentifierParseException; +import net.ornithemc.osl.core.impl.util.NamespacedIdentifierImpl; + +/** + * Utility methods for creating and validating {@link NamespacedIdentifier}s. + */ +public final class NamespacedIdentifiers { + + /** + * The {@code minecraft} namespace is used for Vanilla resources and ids. + */ + public static final String MINECRAFT_NAMESPACE = "minecraft"; + /** + * The default namespace of {@code NamespacedIdentifier}s. + * It is recommended to use a custom namespace for your own identifiers. + */ + public static final String DEFAULT_NAMESPACE = MINECRAFT_NAMESPACE; + + /** + * The maximum length of a {@code NamespacedIdentifier}'s namespace string. + */ + public static final int MAX_LENGTH_NAMESPACE = Integer.MAX_VALUE; + /** + * The maximum length of a {@code NamespacedIdentifier} identifier string. + */ + public static final int MAX_LENGTH_IDENTIFIER = Integer.MAX_VALUE; + + /** + * A comparator for {@code NamespacedIdentifier}s, comparing first by identifier, then by namespace. + */ + public static final Comparator COMPARATOR = (a, b) -> { + int c = a.identifier().compareTo(b.identifier()); + if (c == 0) { + c = a.namespace().compareTo(b.namespace()); + } + + return c; + }; + + /** + * Construct and validate a {@code NamespacedIdentifier} with the default namespace and the given identifier. + * + * @return a {@code NamespacedIdentifier} with the default namespace and the given identifier. + * @throws NamespacedIdentifierException + * if the given identifier is invalid. + */ + public static NamespacedIdentifier from(String identifier) { + return from(DEFAULT_NAMESPACE, identifier); + } + + /** + * Construct and validate a {@code NamespacedIdentifier} from the given namespace and identifier. + * + * @return a {@code NamespacedIdentifier} with the given namespace and identifier. + * @throws NamespacedIdentifierException + * if the given namespace or identifier is invalid. + */ + public static NamespacedIdentifier from(String namespace, String identifier) { + return new NamespacedIdentifierImpl( + validateNamespace(namespace), + validateIdentifier(identifier) + ); + } + + /** + * Parse a {@code NamespacedIdentifier} from the given {@code String}. + * The returned identifier is always valid. If no valid identifier can + * be parsed from the given string, an exception is thrown. + * + * @return the {@code NamespacedIdentifier}} represented by the {@code String}. + * @throws NamespacedIdentifierParseException + * if no valid {@code NamespacedIdentifier} can be parsed from the given {@code String}. + */ + public static NamespacedIdentifier parse(String s) { + int i = s.indexOf(NamespacedIdentifier.SEPARATOR); + + try { + if (i < 0) { + return from(s.substring(i + 1)); + } else if (i > 0) { + return from(s.substring(0, i), s.substring(i + 1)); + } else { + throw NamespacedIdentifierParseException.invalid(s, "badly formatted"); + } + } catch (NamespacedIdentifierException e) { + throw NamespacedIdentifierParseException.invalid(s, e); + } + } + + /** + * Check whether the given {@code NamespacedIdentifier} is valid, or throw an exception. + */ + public static NamespacedIdentifier validate(NamespacedIdentifier id) { + try { + validateNamespace(id.namespace()); + validateIdentifier(id.identifier()); + + return id; + } catch (NamespacedIdentifierException e) { + throw NamespacedIdentifierException.invalid(id, e); + } + } + + /** + * Check that the given namespace is valid for a {@code NamespacedIdentifier}. + */ + public static String validateNamespace(String namespace) { + if (namespace == null || namespace.isEmpty()) { + throw NamespacedIdentifierException.invalidNamespace(namespace, "null or empty"); + } + if (namespace.length() > MAX_LENGTH_NAMESPACE) { + throw NamespacedIdentifierException.invalidNamespace(namespace, "length " + namespace.length() + " is greater than maximum allowed " + MAX_LENGTH_NAMESPACE); + } + if (!namespace.chars().allMatch(chr -> chr == '-' || chr == '.' || chr == '_' || (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9'))) { + throw NamespacedIdentifierException.invalidNamespace(namespace, "contains illegal characters - only [a-zA-Z0-9-._] are allowed"); + } + + return namespace; + } + + /** + * Check that the given identifier is valid for a {@code NamespacedIdentifier}. + */ + public static String validateIdentifier(String identifier) { + if (identifier == null || identifier.isEmpty()) { + throw NamespacedIdentifierException.invalidIdentifier(identifier, "null or empty"); + } + if (identifier.length() > MAX_LENGTH_IDENTIFIER) { + throw NamespacedIdentifierException.invalidIdentifier(identifier, "length " + identifier.length() + " is greater than maximum allowed " + MAX_LENGTH_IDENTIFIER); + } + if (!identifier.chars().allMatch(chr -> chr == '-' || chr == '.' || chr == '_' || chr == '/' || (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9'))) { + throw NamespacedIdentifierException.invalidIdentifier(identifier, "contains illegal characters - only [a-zA-Z0-9-._/] are allowed"); + } + + return identifier; + } + + public static boolean equals(NamespacedIdentifier a, NamespacedIdentifier b) { + return a.namespace().equals(b.namespace()) && a.identifier().equals(b.identifier()); + } +} diff --git a/libraries/core/src/main/java/net/ornithemc/osl/core/impl/mixin/IdentifierMixin.java b/libraries/core/src/main/java/net/ornithemc/osl/core/impl/mixin/IdentifierMixin.java new file mode 100644 index 00000000..ce6a763d --- /dev/null +++ b/libraries/core/src/main/java/net/ornithemc/osl/core/impl/mixin/IdentifierMixin.java @@ -0,0 +1,47 @@ +package net.ornithemc.osl.core.impl.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import net.minecraft.client.resource.Identifier; + +import net.ornithemc.osl.core.api.util.NamespacedIdentifier; +import net.ornithemc.osl.core.api.util.NamespacedIdentifiers; + +@Pseudo // needed because Identifier does not exist in all versions +@Mixin(Identifier.class) +public class IdentifierMixin implements NamespacedIdentifier { // TODO: interface injection + + @Shadow + private String namespace; + @Shadow + private String path; + + @Inject( + method = "equals", + remap = false, + cancellable = true, + at = @At( + value = "HEAD" + ) + ) + private void osl$core$equalsNamespacedIdentifier(Object o, CallbackInfoReturnable cir) { + if (o instanceof NamespacedIdentifier) { + cir.setReturnValue(NamespacedIdentifiers.equals(this, (NamespacedIdentifier) o)); + } + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public String identifier() { + return path; + } +} diff --git a/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierException.java b/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierException.java new file mode 100644 index 00000000..505ddc66 --- /dev/null +++ b/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierException.java @@ -0,0 +1,31 @@ +package net.ornithemc.osl.core.impl.util; + +import net.ornithemc.osl.core.api.util.NamespacedIdentifier; + +@SuppressWarnings("serial") +public class NamespacedIdentifierException extends RuntimeException { + + private NamespacedIdentifierException(String message) { + super(message); + } + + private NamespacedIdentifierException(String message, Throwable cause) { + super(message, cause); + } + + public static NamespacedIdentifierException invalid(NamespacedIdentifier id, Throwable cause) { + return new NamespacedIdentifierException("\'" + id + "\' is not a valid namespaced identifier", cause); + } + + public static NamespacedIdentifierException invalid(NamespacedIdentifier id, String reason) { + return new NamespacedIdentifierException("\'" + id + "\' is not a valid namespaced identifier: " + reason); + } + + public static NamespacedIdentifierException invalidNamespace(String namespace, String reason) { + return new NamespacedIdentifierException("\'" + namespace + "\' is not a valid namespace: " + reason); + } + + public static NamespacedIdentifierException invalidIdentifier(String identifier, String reason) { + return new NamespacedIdentifierException("\'" + identifier + "\' is not a valid identifier: " + reason); + } +} diff --git a/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierImpl.java b/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierImpl.java new file mode 100644 index 00000000..f373c436 --- /dev/null +++ b/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierImpl.java @@ -0,0 +1,57 @@ +package net.ornithemc.osl.core.impl.util; + +import net.ornithemc.osl.core.api.util.NamespacedIdentifier; +import net.ornithemc.osl.core.api.util.NamespacedIdentifiers; + +/** + * This class is a version-agnostic implementation of {@link NamespacedIdentifier}. + *

+ * This class is essentially equivalent to Vanilla's {@code Identifier}. It was added for a + * few reasons. For one, Vanilla's {@code Identifier} was only added in 13w21a, and then was + * client-only until 14w27b. Implementation details of this class also changed a few times, + * and only since 17w43a were {@code Identifiers} validated in any way. + *
This class is available for all Minecraft versions, without any version-specific + * implementation details. + */ +public final class NamespacedIdentifierImpl implements NamespacedIdentifier { + + private final String namespace; + private final String identifier; + + public NamespacedIdentifierImpl(String namespace, String identifier) { + this.namespace = namespace; + this.identifier = identifier; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof NamespacedIdentifier)) { + return false; + } + return NamespacedIdentifiers.equals(this, (NamespacedIdentifier) o); + } + + @Override + public int hashCode() { + // this impl matches Vanilla Identifier's impl + return 31 * namespace.hashCode() + identifier.hashCode(); + } + + @Override + public String toString() { + return namespace + SEPARATOR + identifier; + } + + @Override + public String namespace() { + return namespace; + } + + @Override + public String identifier() { + return identifier; + } +} diff --git a/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierParseException.java b/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierParseException.java new file mode 100644 index 00000000..caf35242 --- /dev/null +++ b/libraries/core/src/main/java/net/ornithemc/osl/core/impl/util/NamespacedIdentifierParseException.java @@ -0,0 +1,21 @@ +package net.ornithemc.osl.core.impl.util; + +@SuppressWarnings("serial") +public class NamespacedIdentifierParseException extends RuntimeException { + + private NamespacedIdentifierParseException(String message) { + super(message); + } + + private NamespacedIdentifierParseException(String message, Throwable cause) { + super(message, cause); + } + + public static NamespacedIdentifierParseException invalid(String s, Throwable cause) { + return new NamespacedIdentifierParseException("unable to parse namespaced identifier from \'" + s + "\'", cause); + } + + public static NamespacedIdentifierParseException invalid(String s, String reason) { + return new NamespacedIdentifierParseException("unable to parse namespaced identifier from \'" + s + "\': " + reason); + } +} diff --git a/libraries/core/src/main/resources/fabric.mod.json b/libraries/core/src/main/resources/fabric.mod.json index f5bf5e48..b4fd2ede 100644 --- a/libraries/core/src/main/resources/fabric.mod.json +++ b/libraries/core/src/main/resources/fabric.mod.json @@ -16,6 +16,9 @@ "license": "Apache-2.0", "icon": "assets/ornithe-standard-libraries/core/icon.png", "environment": "*", + "mixins": [ + "osl.core.mixins.json" + ], "depends": { "fabricloader": ">=0.16.0" } diff --git a/libraries/core/src/main/resources/osl.core.mixins.json b/libraries/core/src/main/resources/osl.core.mixins.json new file mode 100644 index 00000000..da16fd2f --- /dev/null +++ b/libraries/core/src/main/resources/osl.core.mixins.json @@ -0,0 +1,16 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.ornithemc.osl.core.impl.mixin", + "compatibilityLevel": "JAVA_8", + "mixins": [ + "IdentifierMixin" + ], + "client": [ + ], + "server": [ + ], + "injectors": { + "defaultRequire": 1 + } +}