Skip to content

Commit ee52793

Browse files
authored
Implement Composer version support (#291)
1 parent c2c96ed commit ee52793

4 files changed

Lines changed: 562 additions & 0 deletions

File tree

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/*
2+
* This file is part of versatile.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* Copyright (c) Niklas Düster. All Rights Reserved.
18+
*/
19+
package io.github.nscuro.versatile.version;
20+
21+
import io.github.nscuro.versatile.spi.InvalidVersionException;
22+
import io.github.nscuro.versatile.spi.Version;
23+
24+
import java.util.Locale;
25+
import java.util.Set;
26+
import java.util.regex.Matcher;
27+
import java.util.regex.Pattern;
28+
29+
import static io.github.nscuro.versatile.version.KnownVersioningSchemes.SCHEME_COMPOSER;
30+
31+
/**
32+
* @see <a href="https://github.com/composer/semver/blob/main/src/VersionParser.php">Composer VersionParser</a>
33+
* @see <a href="https://getcomposer.org/doc/articles/versions.md">Composer Versions</a>
34+
*/
35+
public class ComposerVersion extends Version {
36+
37+
public static class Provider extends AbstractBuiltinVersionProvider {
38+
39+
public Provider() {
40+
super(Set.of(SCHEME_COMPOSER), (scheme, versionStr) -> new ComposerVersion(versionStr));
41+
}
42+
43+
}
44+
45+
private enum Stability {
46+
47+
DEV("dev"),
48+
ALPHA("alpha"),
49+
BETA("beta"),
50+
RC("RC"),
51+
STABLE("stable"),
52+
PATCH("patch");
53+
54+
private final String label;
55+
56+
Stability(String label) {
57+
this.label = label;
58+
}
59+
60+
private static Stability of(String value) {
61+
return switch (value.toLowerCase(Locale.ROOT)) {
62+
case "alpha", "a" -> ALPHA;
63+
case "beta", "b" -> BETA;
64+
case "rc" -> RC;
65+
case "stable" -> STABLE;
66+
case "patch", "p", "pl" -> PATCH;
67+
default -> throw new IllegalArgumentException("Unknown stability: " + value);
68+
};
69+
}
70+
71+
}
72+
73+
private static final String MODIFIER_REGEX =
74+
"[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\\d+)*+)?)?([.-]?dev)?";
75+
private static final Pattern CLASSICAL_PATTERN = Pattern.compile(
76+
"^[vV]?(\\d{1,5}(?:\\.\\d+){0,3})" + MODIFIER_REGEX + "$",
77+
Pattern.CASE_INSENSITIVE);
78+
private static final Pattern DATE_PATTERN = Pattern.compile(
79+
"^[vV]?(\\d{4}(?:[.:-]?\\d{2}){1,6}(?:[.:-]?\\d{1,3}){0,2})" + MODIFIER_REGEX + "$",
80+
Pattern.CASE_INSENSITIVE);
81+
private static final Pattern BRANCH_NUM_PATTERN = Pattern.compile(
82+
"^[vV]?(\\d+(?:\\.\\d+)*\\.[xX])[.-]?dev$");
83+
84+
private static final Pattern ALIAS_PATTERN = Pattern.compile(
85+
"^([^,\\s]++) +as +[^,\\s]++$");
86+
private static final Pattern STABILITY_FLAG_PATTERN = Pattern.compile(
87+
"@(?:stable|RC|beta|alpha|dev)$", Pattern.CASE_INSENSITIVE);
88+
private static final Pattern METADATA_PATTERN = Pattern.compile(
89+
"^([^,\\s+]++)\\+\\S++$");
90+
91+
private final String branchName;
92+
private final String[] originalComponents;
93+
private final long[] numericComponents;
94+
private final Stability stability;
95+
private final String stabilityNumDisplay;
96+
private final int[] stabilityNumbers;
97+
private final boolean isDev;
98+
private final String normalizedString;
99+
100+
ComposerVersion(final String versionStr) {
101+
super(SCHEME_COMPOSER, versionStr);
102+
103+
if (versionStr == null || versionStr.isEmpty()) {
104+
throw new InvalidVersionException(versionStr, "Version must not be empty");
105+
}
106+
107+
String version = versionStr.trim();
108+
109+
if (version.isEmpty()) {
110+
throw new InvalidVersionException(versionStr, "Version must not be empty");
111+
}
112+
113+
final Matcher aliasMatcher = ALIAS_PATTERN.matcher(version);
114+
if (aliasMatcher.matches()) {
115+
version = aliasMatcher.group(1);
116+
}
117+
118+
final Matcher stabilityFlagMatcher = STABILITY_FLAG_PATTERN.matcher(version);
119+
if (stabilityFlagMatcher.find()) {
120+
version = version.substring(0, stabilityFlagMatcher.start());
121+
}
122+
123+
final String lower = version.toLowerCase(Locale.ROOT);
124+
if ("master".equals(lower) || "trunk".equals(lower) || "default".equals(lower)) {
125+
this.branchName = lower;
126+
this.originalComponents = new String[0];
127+
this.numericComponents = new long[0];
128+
this.stability = Stability.DEV;
129+
this.stabilityNumbers = new int[0];
130+
this.stabilityNumDisplay = null;
131+
this.isDev = false;
132+
this.normalizedString = "dev-" + this.branchName;
133+
return;
134+
}
135+
136+
if (lower.startsWith("dev-")) {
137+
final String name = version.substring(4);
138+
if (name.isEmpty()) {
139+
throw new InvalidVersionException(versionStr, "Branch name must not be empty");
140+
}
141+
this.branchName = name;
142+
this.originalComponents = new String[0];
143+
this.numericComponents = new long[0];
144+
this.stability = Stability.DEV;
145+
this.stabilityNumbers = new int[0];
146+
this.stabilityNumDisplay = null;
147+
this.isDev = false;
148+
this.normalizedString = "dev-" + this.branchName;
149+
return;
150+
}
151+
152+
this.branchName = null;
153+
154+
final Matcher metadataMatcher = METADATA_PATTERN.matcher(version);
155+
if (metadataMatcher.matches()) {
156+
version = metadataMatcher.group(1);
157+
}
158+
159+
final Matcher branchMatcher = BRANCH_NUM_PATTERN.matcher(version);
160+
if (branchMatcher.matches()) {
161+
final String[] parts = branchMatcher.group(1).split("\\.");
162+
this.originalComponents = new String[4];
163+
this.numericComponents = new long[4];
164+
for (int i = 0; i < 4; i++) {
165+
if (i < parts.length && !parts[i].equalsIgnoreCase("x")) {
166+
this.originalComponents[i] = parts[i];
167+
this.numericComponents[i] = Long.parseLong(parts[i]);
168+
} else {
169+
this.originalComponents[i] = "9999999";
170+
this.numericComponents[i] = 9999999;
171+
}
172+
}
173+
this.stability = Stability.DEV;
174+
this.stabilityNumbers = new int[0];
175+
this.stabilityNumDisplay = null;
176+
this.isDev = false;
177+
this.normalizedString = buildNormalizedString();
178+
return;
179+
}
180+
181+
Matcher matcher = CLASSICAL_PATTERN.matcher(version);
182+
boolean isDate = false;
183+
if (!matcher.matches()) {
184+
matcher = DATE_PATTERN.matcher(version);
185+
if (!matcher.matches()) {
186+
throw new InvalidVersionException(versionStr, "Invalid Composer version format");
187+
}
188+
isDate = true;
189+
}
190+
191+
String numericStr = matcher.group(1);
192+
if (isDate) {
193+
numericStr = numericStr.replaceAll("[:-]", ".");
194+
}
195+
final String[] rawParts = numericStr.split("\\.");
196+
197+
if (!isDate) {
198+
this.originalComponents = new String[4];
199+
for (int i = 0; i < 4; i++) {
200+
this.originalComponents[i] = i < rawParts.length ? rawParts[i] : "0";
201+
}
202+
} else {
203+
this.originalComponents = rawParts;
204+
}
205+
206+
this.numericComponents = new long[this.originalComponents.length];
207+
for (int i = 0; i < this.originalComponents.length; i++) {
208+
this.numericComponents[i] = Long.parseLong(this.originalComponents[i]);
209+
}
210+
211+
final String stabilityStr = matcher.group(2);
212+
final String rawStabilityNum = matcher.group(3);
213+
final String devSuffix = matcher.group(4);
214+
215+
if (stabilityStr != null) {
216+
final Stability parsedStability = Stability.of(stabilityStr);
217+
if (parsedStability == Stability.STABLE) {
218+
this.stability = Stability.STABLE;
219+
this.isDev = false;
220+
this.stabilityNumDisplay = null;
221+
this.stabilityNumbers = new int[0];
222+
} else {
223+
this.stability = parsedStability;
224+
this.isDev = devSuffix != null;
225+
if (rawStabilityNum != null && !rawStabilityNum.isEmpty()) {
226+
this.stabilityNumDisplay = rawStabilityNum.replaceAll("^[.-]+", "");
227+
final String[] numParts = this.stabilityNumDisplay.split("[.-]");
228+
this.stabilityNumbers = new int[numParts.length];
229+
for (int i = 0; i < numParts.length; i++) {
230+
this.stabilityNumbers[i] = Integer.parseInt(numParts[i]);
231+
}
232+
} else {
233+
this.stabilityNumDisplay = null;
234+
this.stabilityNumbers = new int[0];
235+
}
236+
}
237+
} else if (devSuffix != null) {
238+
this.stability = Stability.DEV;
239+
this.isDev = false;
240+
this.stabilityNumDisplay = null;
241+
this.stabilityNumbers = new int[0];
242+
} else {
243+
this.stability = Stability.STABLE;
244+
this.isDev = false;
245+
this.stabilityNumDisplay = null;
246+
this.stabilityNumbers = new int[0];
247+
}
248+
249+
this.normalizedString = buildNormalizedString();
250+
}
251+
252+
private String buildNormalizedString() {
253+
if (branchName != null) {
254+
return "dev-" + branchName;
255+
}
256+
257+
final var sb = new StringBuilder();
258+
for (int i = 0; i < originalComponents.length; i++) {
259+
if (i > 0) sb.append('.');
260+
sb.append(originalComponents[i]);
261+
}
262+
263+
if (stability != Stability.STABLE) {
264+
sb.append('-').append(stability.label);
265+
if (stabilityNumDisplay != null) {
266+
sb.append(stabilityNumDisplay);
267+
}
268+
}
269+
270+
if (isDev) {
271+
sb.append("-dev");
272+
}
273+
274+
return sb.toString();
275+
}
276+
277+
@Override
278+
public boolean isStable() {
279+
return branchName == null
280+
&& (stability == Stability.STABLE || stability == Stability.PATCH)
281+
&& !isDev;
282+
}
283+
284+
@Override
285+
public int compareTo(Version other) {
286+
if (other instanceof final ComposerVersion o) {
287+
if (this.branchName != null && o.branchName != null) {
288+
return this.branchName.compareToIgnoreCase(o.branchName);
289+
}
290+
if (this.branchName != null) {
291+
return -1;
292+
}
293+
if (o.branchName != null) {
294+
return 1;
295+
}
296+
297+
final int maxLen = Math.max(this.numericComponents.length, o.numericComponents.length);
298+
for (int i = 0; i < maxLen; i++) {
299+
final long a = i < this.numericComponents.length ? this.numericComponents[i] : 0;
300+
final long b = i < o.numericComponents.length ? o.numericComponents[i] : 0;
301+
final int cmp = Long.compare(a, b);
302+
if (cmp != 0) {
303+
return cmp;
304+
}
305+
}
306+
307+
int cmp = this.stability.compareTo(o.stability);
308+
if (cmp != 0) {
309+
return cmp;
310+
}
311+
312+
final int maxStabLen = Math.max(this.stabilityNumbers.length, o.stabilityNumbers.length);
313+
for (int i = 0; i < maxStabLen; i++) {
314+
final int a = i < this.stabilityNumbers.length ? this.stabilityNumbers[i] : 0;
315+
final int b = i < o.stabilityNumbers.length ? o.stabilityNumbers[i] : 0;
316+
cmp = Integer.compare(a, b);
317+
if (cmp != 0) {
318+
return cmp;
319+
}
320+
}
321+
322+
return Boolean.compare(o.isDev, this.isDev);
323+
}
324+
325+
throw new IllegalArgumentException(
326+
"%s can only be compared with its own type, but got %s".formatted(
327+
this.getClass().getName(), other.getClass().getName()));
328+
}
329+
330+
@Override
331+
public String toString() {
332+
return normalizedString;
333+
}
334+
335+
}

versatile-core/src/main/java/io/github/nscuro/versatile/version/KnownVersioningSchemes.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
public final class KnownVersioningSchemes {
3333

3434
public static final String SCHEME_APK = "apk";
35+
public static final String SCHEME_COMPOSER = "composer";
3536
public static final String SCHEME_CPAN = "cpan";
3637
public static final String SCHEME_DEBIAN = "deb";
3738
public static final String SCHEME_GEM = "gem";
@@ -92,6 +93,7 @@ public static Optional<String> fromPurl(final PackageURL purl) {
9293
public static Optional<String> fromPurlType(final String purlType) {
9394
return switch (purlType) {
9495
case "apk" -> Optional.of(SCHEME_APK);
96+
case "composer" -> Optional.of(SCHEME_COMPOSER);
9597
case "cpan" -> Optional.of(SCHEME_CPAN);
9698
case "clojars", "gradle", "maven" -> Optional.of(SCHEME_MAVEN);
9799
case "deb" -> Optional.of(SCHEME_DEBIAN);

versatile-core/src/main/resources/META-INF/services/io.github.nscuro.versatile.spi.VersionProvider

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
io.github.nscuro.versatile.version.ApkVersion$Provider
2+
io.github.nscuro.versatile.version.ComposerVersion$Provider
23
io.github.nscuro.versatile.version.DebianVersion$Provider
34
io.github.nscuro.versatile.version.GenericVersion$Provider
45
io.github.nscuro.versatile.version.GoVersion$Provider

0 commit comments

Comments
 (0)