2222import com .github .benmanes .caffeine .cache .Cache ;
2323import com .github .benmanes .caffeine .cache .Caffeine ;
2424import com .github .packageurl .PackageURL ;
25+ import io .github .nscuro .versatile .VersionFactory ;
26+ import io .github .nscuro .versatile .spi .InvalidVersionException ;
27+ import io .github .nscuro .versatile .spi .Version ;
28+ import io .github .nscuro .versatile .version .KnownVersioningSchemes ;
2529import jakarta .ws .rs .core .UriBuilder ;
2630import org .apache .http .HttpStatus ;
2731import org .apache .http .client .methods .CloseableHttpResponse ;
2832import org .apache .http .util .EntityUtils ;
29- import org .apache .maven .artifact .versioning .ComparableVersion ;
3033import org .dependencytrack .exception .MetaAnalyzerException ;
3134import org .dependencytrack .model .Component ;
3235import org .dependencytrack .model .RepositoryType ;
@@ -52,9 +55,9 @@ public class ComposerMetaAnalyzer extends AbstractMetaAnalyzer {
5255
5356 /**
5457 * @see <a href="https://packagist.org/apidoc#get-package-data">Packagist's API
55- * doc for
56- * "Getting package data - Using the Composer v1 metadata (DEPRECATED)"</a>
57- * Example: https://repo.packagist.org/p/monolog/monolog.json
58+ * doc for
59+ * "Getting package data - Using the Composer v1 metadata (DEPRECATED)"</a>
60+ * Example: https://repo.packagist.org/p/monolog/monolog.json
5861 */
5962 private static final String PACKAGE_META_DATA_PATH_PATTERN_V1 = "/p/%package%.json" ;
6063
@@ -66,10 +69,10 @@ public class ComposerMetaAnalyzer extends AbstractMetaAnalyzer {
6669 * Some of the properties of the root package.json are documented at
6770 * https://github.com/composer/composer/blob/main/doc/05-repositories.md
6871 * Properties to investigate / implement:
69- *
72+ * <p>
7073 * - security-advisories: very relevant, but only in a VulnerabilityAnalyzer (or
7174 * mirrored VulnerabilitySource) context
72- *
75+ * <p>
7376 * - providers-lazy-url: old v1 construct for which I haven't seen any example,
7477 * in v2 the metadata-url is used for this. seems like it's not relevant for DT
7578 * - list: returns only package names, seems like repo.packagist.org (and .com?)
@@ -273,7 +276,7 @@ private void loadIncludedPackages(final JSONObject repoRoot, final JSONObject da
273276 }
274277
275278 private MetaModel analyzeFromMetadataUrl (final MetaModel meta , final Component component ,
276- final String packageMetaDataPathPattern ) {
279+ final String packageMetaDataPathPattern ) {
277280 final String composerPackageMetadataFilename = packageMetaDataPathPattern .replaceAll ("%package%" ,
278281 getComposerPackageName (component ));
279282 final String url ;
@@ -307,7 +310,7 @@ private MetaModel analyzeFromMetadataUrl(final MetaModel meta, final Component c
307310 if (!responsePackages .has (expectedResponsePackage )) {
308311 // the package no longer exists - for v2 there's no example (yet), v1 example
309312 // https://repo.packagist.org/p/magento/adobe-ims.json
310- LOGGER .debug ("%s: Package no longer exists in repository %s." . formatted (component .getPurl (), this .repositoryId ));
313+ LOGGER .debug ("%s: Package no longer exists in repository %s." .formatted (component .getPurl (), this .repositoryId ));
311314 return meta ;
312315 }
313316
@@ -353,38 +356,40 @@ private JSONObject expandPackages(JSONObject packages) {
353356 }
354357
355358 private MetaModel analyzePackageVersions (final MetaModel meta , Component component , JSONObject packageVersions ) {
356- final ComparableVersion latestVersion = new ComparableVersion ( stripLeadingV ( component . getPurl (). getVersion ())) ;
359+ Version latestVersion = null ;
357360 final DateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd'T'HH:mm:ssXXX" );
358361
359362 LOGGER .debug ("%s: analyzing package versions in %s: " .formatted (component .getPurl (), this .repositoryId ));
360- packageVersions .keySet (). forEach ( item -> {
361- JSONObject packageVersion = packageVersions .getJSONObject (( String ) item );
363+ for ( final String item : packageVersions .keySet ()) {
364+ final JSONObject packageVersion = packageVersions .getJSONObject (item );
362365 // Sometimes the JSON key differs from the the version inside the JSON value. The latter is leading.
363- String version = packageVersion .getString ("version" );
364- if (version .startsWith ("dev-" ) || version .endsWith ("-dev" )) {
365- // dev versions are excluded, since they are not pinned but a VCS-branch.
366- // this case doesn't seem to happen anymore with V2, as dev (untagged) releases
367- // are not part of the response anymore
368- return ;
369- }
366+ final String version = packageVersion .getString ("version" );
370367
371368 // Some (old?) repositories like composer.amasty.com/enterprise do not include a
372- // 'version_normalized' field
373- // TODO Should we attempt to normalize ourselves? The PHP code uses something
374- // that results in 4 parts instead of 3, i.e. 2.3.8.0 instead of 2.3.8. Not sure
375- // if that works with Semver4j
376- String version_normalized = packageVersion .getString ("version" );
369+ // 'version_normalized' field. versatile handles both 3- and 4-part version
370+ // strings (e.g. 2.3.8 and 2.3.8.0) natively, so falling back to the raw version
371+ // is safe.
372+ String version_normalized = version ;
377373 if (packageVersion .has ("version_normalized" )) {
378374 version_normalized = packageVersion .getString ("version_normalized" );
379375 }
380376
381- ComparableVersion currentComparableVersion = new ComparableVersion (version_normalized );
382- if (currentComparableVersion .compareTo (latestVersion ) < 0 ) {
383- // smaller version can be skipped
384- return ;
377+ final Version currentVersion ;
378+ try {
379+ currentVersion = VersionFactory .forScheme (KnownVersioningSchemes .SCHEME_COMPOSER , version_normalized );
380+ } catch (InvalidVersionException e ) {
381+ LOGGER .debug ("%s: Skipping unparseable Composer version %s in repository %s" .formatted (component .getPurl (), version_normalized , this .repositoryId ), e );
382+ continue ;
383+ }
384+ if (!currentVersion .isStable ()) {
385+ continue ;
386+ }
387+
388+ if (latestVersion != null && currentVersion .compareTo (latestVersion ) < 0 ) {
389+ continue ;
385390 }
386391
387- latestVersion . parseVersion ( stripLeadingV ( version_normalized )) ;
392+ latestVersion = currentVersion ;
388393 meta .setLatestVersion (version );
389394
390395 if (packageVersion .has ("time" )) {
@@ -401,16 +406,10 @@ private MetaModel analyzePackageVersions(final MetaModel meta, Component compone
401406 // do not include the name field for a version, so print purl
402407 LOGGER .warn ("%s: Field 'time' not present in metadata in repository %s" .formatted (component .getPurl (), this .repositoryId ));
403408 }
404- });
409+ }
405410 return meta ;
406411 }
407412
408- private static String stripLeadingV (String s ) {
409- return s .startsWith ("v" ) || s .startsWith ("V" )
410- ? s .substring (1 )
411- : s ;
412- }
413-
414413 private static boolean isMinified (JSONObject data ) {
415414 if (data .has ("minified" ) && "composer/2.0" .equals (data .getString ("minified" ))) {
416415 return true ;
0 commit comments