diff --git a/src/main/java/net/fabricmc/loader/api/metadata/version/VersionComparisonOperator.java b/src/main/java/net/fabricmc/loader/api/metadata/version/VersionComparisonOperator.java index ea20e4e12..87a62a006 100644 --- a/src/main/java/net/fabricmc/loader/api/metadata/version/VersionComparisonOperator.java +++ b/src/main/java/net/fabricmc/loader/api/metadata/version/VersionComparisonOperator.java @@ -18,6 +18,7 @@ import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.impl.util.version.CommitHashVersion; import net.fabricmc.loader.impl.util.version.SemanticVersionImpl; public enum VersionComparisonOperator { @@ -28,6 +29,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) { return a.compareTo((Version) b) >= 0; } + @Override + public boolean test(CommitHashVersion a, CommitHashVersion b) { + return a.compareTo(b) >= 0; + } + @Override public SemanticVersion minVersion(SemanticVersion version) { return version; @@ -39,6 +45,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) { return a.compareTo((Version) b) <= 0; } + @Override + public boolean test(CommitHashVersion a, CommitHashVersion b) { + return a.compareTo(b) <= 0; + } + @Override public SemanticVersion maxVersion(SemanticVersion version) { return version; @@ -50,6 +61,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) { return a.compareTo((Version) b) > 0; } + @Override + public boolean test(CommitHashVersion a, CommitHashVersion b) { + return a.compareTo(b) > 0; + } + @Override public SemanticVersion minVersion(SemanticVersion version) { return version; @@ -61,6 +77,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) { return a.compareTo((Version) b) < 0; } + @Override + public boolean test(CommitHashVersion a, CommitHashVersion b) { + return a.compareTo(b) < 0; + } + @Override public SemanticVersion maxVersion(SemanticVersion version) { return version; @@ -72,6 +93,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) { return a.compareTo((Version) b) == 0; } + @Override + public boolean test(CommitHashVersion a, CommitHashVersion b) { + return a.compareTo(b) == 0; + } + @Override public SemanticVersion minVersion(SemanticVersion version) { return version; @@ -90,6 +116,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) { && a.getVersionComponent(1) == b.getVersionComponent(1); } + @Override + public boolean test(CommitHashVersion a, CommitHashVersion b) { + throw new UnsupportedOperationException("This operator is not supported for Git hash string versions"); + } + @Override public SemanticVersion minVersion(SemanticVersion version) { return version; @@ -107,6 +138,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) { && a.getVersionComponent(0) == b.getVersionComponent(0); } + @Override + public boolean test(CommitHashVersion a, CommitHashVersion b) { + throw new UnsupportedOperationException("This operator is not supported for Git hash string versions"); + } + @Override public SemanticVersion minVersion(SemanticVersion version) { return version; @@ -143,6 +179,8 @@ public final boolean isMaxInclusive() { public final boolean test(Version a, Version b) { if (a instanceof SemanticVersion && b instanceof SemanticVersion) { return test((SemanticVersion) a, (SemanticVersion) b); + } else if (a instanceof CommitHashVersion && b instanceof CommitHashVersion) { + return test((CommitHashVersion) a, (CommitHashVersion) b); } else if (minInclusive || maxInclusive) { return a.getFriendlyString().equals(b.getFriendlyString()); } else { @@ -152,6 +190,8 @@ public final boolean test(Version a, Version b) { public abstract boolean test(SemanticVersion a, SemanticVersion b); + public abstract boolean test(CommitHashVersion a, CommitHashVersion b); + public SemanticVersion minVersion(SemanticVersion version) { return null; } diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java b/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java index 890255f60..e61f6e690 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java @@ -115,7 +115,7 @@ static LoaderModMetadata parse(JsonReader reader) throws IOException, ParseMetad } try { - version = VersionParser.parse(reader.nextString(), false); + version = VersionParser.parse(reader.nextString(), false, contact); } catch (VersionParsingException e) { throw new ParseMetadataException("Failed to parse version", e); } diff --git a/src/main/java/net/fabricmc/loader/impl/util/version/CommitHashVersion.java b/src/main/java/net/fabricmc/loader/impl/util/version/CommitHashVersion.java new file mode 100644 index 000000000..b53704423 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/util/version/CommitHashVersion.java @@ -0,0 +1,105 @@ +package net.fabricmc.loader.impl.util.version; + +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.impl.lib.gson.JsonReader; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CommitHashVersion implements Version { + private static final Pattern GIT_COMMIT_HASH_PATTERN = Pattern.compile("^[0-9a-fA-F]{40}$"); + private static final Pattern SOURCES_PATTERN = Pattern.compile("^(?:https?://)?github\\.com/([\\w-]+)/([\\w-]+)/?$"); + + private final String commitHash; + private final Instant commitDate; + + public CommitHashVersion(String commitHash, String sources) throws VersionParsingException { + if (!GIT_COMMIT_HASH_PATTERN.matcher(commitHash).matches()) { + throw new VersionParsingException("Invalid Git commit hash"); + } + + this.commitHash = commitHash; + + Matcher matcher = SOURCES_PATTERN.matcher(sources); + if (!matcher.matches()) { + throw new VersionParsingException("Unsupported or invalid sources link"); + } + + String apiUrlString = String.format("https://api.github.com/repos/%s/%s/git/commits/%s", matcher.group(1), matcher.group(2), this.commitHash); + URL apiUrl; + try { + apiUrl = new URL(apiUrlString); + } catch (MalformedURLException e) { + throw new VersionParsingException("Sources URL is malformed"); + } + + String dateString; + try (JsonReader reader = new JsonReader(new InputStreamReader(apiUrl.openStream()))) { + reader.beginObject(); + + // skip sha, node id, url, HTML url, author + for (int i = 0; i < 5; i++) { + reader.nextName(); + reader.skipValue(); + } + + reader.nextName(); + reader.beginObject(); + // skip name, email + for (int i = 0; i < 2; i++) { + reader.nextName(); + reader.skipValue(); + } + reader.nextName(); + dateString = reader.nextString(); + } catch (IOException e) { + throw new VersionParsingException("Could not connect to GitHub's API"); + } + + Instant date; + try { + date = Instant.parse(dateString); + } catch (DateTimeParseException e) { + throw new VersionParsingException("Date could not be parsed"); + } + + this.commitDate = date; + } + + @Override + public String getFriendlyString() { + return String.format("%s (%s)", this.commitHash.substring(0, 7), this.commitDate.toString()); + } + + @Override + public int compareTo(@NotNull Version other) { + if (!(other instanceof CommitHashVersion)) { + return this.getFriendlyString().compareTo(other.getFriendlyString()); + } + + return this.commitDate.compareTo(((CommitHashVersion) other).commitDate); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof CommitHashVersion) { + return this.commitHash.equals(((CommitHashVersion) obj).commitHash); + } + + return false; + } + + @Override + public String toString() { + return this.getFriendlyString(); + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/util/version/VersionParser.java b/src/main/java/net/fabricmc/loader/impl/util/version/VersionParser.java index 55a6ac273..7ffc8f5c9 100644 --- a/src/main/java/net/fabricmc/loader/impl/util/version/VersionParser.java +++ b/src/main/java/net/fabricmc/loader/impl/util/version/VersionParser.java @@ -19,9 +19,14 @@ import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.Version; import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.ContactInformation; public final class VersionParser { public static Version parse(String s, boolean storeX) throws VersionParsingException { + return parse(s, storeX, null); + } + + public static Version parse(String s, boolean storeX, ContactInformation contact) throws VersionParsingException { if (s == null || s.isEmpty()) { throw new VersionParsingException("Version must be a non-empty string!"); } @@ -31,7 +36,16 @@ public static Version parse(String s, boolean storeX) throws VersionParsingExcep try { version = new SemanticVersionImpl(s, storeX); } catch (VersionParsingException e) { - version = new StringVersion(s); + String sources; + if (contact != null && (sources = contact.get("sources").orElse(null)) != null) { + try { + version = new CommitHashVersion(s, sources); + } catch (VersionParsingException ex) { + version = new StringVersion(s); + } + } else { + version = new StringVersion(s); + } } return version; diff --git a/src/main/java/net/fabricmc/loader/impl/util/version/VersionPredicateParser.java b/src/main/java/net/fabricmc/loader/impl/util/version/VersionPredicateParser.java index cd315897d..dbd902268 100644 --- a/src/main/java/net/fabricmc/loader/impl/util/version/VersionPredicateParser.java +++ b/src/main/java/net/fabricmc/loader/impl/util/version/VersionPredicateParser.java @@ -27,6 +27,7 @@ import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.Version; import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.ContactInformation; import net.fabricmc.loader.api.metadata.version.VersionComparisonOperator; import net.fabricmc.loader.api.metadata.version.VersionInterval; import net.fabricmc.loader.api.metadata.version.VersionPredicate; @@ -36,6 +37,10 @@ public final class VersionPredicateParser { private static final VersionComparisonOperator[] OPERATORS = VersionComparisonOperator.values(); public static VersionPredicate parse(String predicate) throws VersionParsingException { + return parse(predicate, null); + } + + public static VersionPredicate parse(String predicate, ContactInformation contact) throws VersionParsingException { List predicateList = new ArrayList<>(); for (String s : predicate.split(" ")) { @@ -55,7 +60,7 @@ public static VersionPredicate parse(String predicate) throws VersionParsingExce } } - Version version = VersionParser.parse(s, true); + Version version = VersionParser.parse(s, true, contact); if (version instanceof SemanticVersion) { SemanticVersion semVer = (SemanticVersion) version; @@ -80,10 +85,12 @@ public static VersionPredicate parse(String predicate) throws VersionParsingExce version = new SemanticVersionImpl(newComponents, "", semVer.getBuildKey().orElse(null)); } - } else if (!operator.isMinInclusive() && !operator.isMaxInclusive()) { // non-semver without inclusive bound - throw new VersionParsingException("Invalid predicate: "+predicate+", version ranges need to be semantic version compatible to use operators that exclude the bound!"); - } else { // non-semver with inclusive bound - operator = VersionComparisonOperator.EQUAL; + } else if (!(version instanceof CommitHashVersion)) { + if (!operator.isMinInclusive() && !operator.isMaxInclusive()) { // unknown format without inclusive bound + throw new VersionParsingException("Invalid predicate: "+predicate+", version ranges need to be comparable to use operators that exclude the bound!"); + } else { // unknown format with inclusive bound + operator = VersionComparisonOperator.EQUAL; + } } predicateList.add(new SingleVersionPredicate(operator, version)); diff --git a/src/test/java/net/fabricmc/test/VersionParsingTests.java b/src/test/java/net/fabricmc/test/VersionParsingTests.java index 887d2502e..a0ba84a05 100644 --- a/src/test/java/net/fabricmc/test/VersionParsingTests.java +++ b/src/test/java/net/fabricmc/test/VersionParsingTests.java @@ -16,8 +16,14 @@ package net.fabricmc.test; +import java.util.HashMap; +import java.util.Map; import java.util.function.Predicate; +import net.fabricmc.loader.api.metadata.ContactInformation; +import net.fabricmc.loader.impl.metadata.ContactInformationImpl; +import net.fabricmc.loader.impl.util.version.CommitHashVersion; + import org.jetbrains.annotations.Nullable; import net.fabricmc.loader.api.Version; @@ -35,6 +41,15 @@ private static Exception tryParseSemantic(String s, boolean storeX) { } } + private static Exception tryParseCommitHash(String s, String sources) { + try { + new CommitHashVersion(s, sources); + return null; + } catch (VersionParsingException e) { + return e; + } + } + private static void testTrue(@Nullable Exception b) { if (b != null) { throw new RuntimeException("Test failed!", b); @@ -318,5 +333,41 @@ public static void main(String[] args) throws Exception { testFalse(predicate.test(new SemanticVersionImpl("2.0.0", false))); testFalse(predicate.test(new SemanticVersionImpl("2.0.0-beta.2", false))); } + + // Test: Commit Hash versions + testTrue(tryParseCommitHash("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader/")); + testTrue(tryParseCommitHash("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader")); + testTrue(tryParseCommitHash("d9a000670180e73569a6983c423dd4299278afda", "http://github.com/fabricMC/fabric-loader/")); + testTrue(tryParseCommitHash("d9a000670180e73569a6983c423dd4299278afda", "github.com/fabricMC/fabric-loader/")); + + // Test: Comparing Commit Hash versions. + Map contactMap = new HashMap() {{ + put("sources", "https://github.com/fabricMC/fabric-loader/"); + }}; + ContactInformation contact = new ContactInformationImpl(contactMap); + { + Predicate predicate = VersionPredicateParser.parse("d9a000670180e73569a6983c423dd4299278afda", contact); + testTrue(predicate.test(new CommitHashVersion("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader/"))); + } + { + Predicate predicate = VersionPredicateParser.parse(">=4dbcb72dcce4e4034f58ed3839da41aae097c901", contact); + testTrue(predicate.test(new CommitHashVersion("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader/"))); + } + { + Predicate predicate = VersionPredicateParser.parse("<4dbcb72dcce4e4034f58ed3839da41aae097c901", contact); + testFalse(predicate.test(new CommitHashVersion("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader/"))); + } + + // Test: Unsupported Commit Hash version predicates + try { + VersionPredicateParser.parse("~4dbcb72dcce4e4034f58ed3839da41aae097c901", contact); + } catch (UnsupportedOperationException e) { + testFalse(e); + } + try { + VersionPredicateParser.parse("^4dbcb72dcce4e4034f58ed3839da41aae097c901", contact); + } catch (UnsupportedOperationException e) { + testFalse(e); + } } }