@@ -31,6 +31,7 @@ public ManifestParseResult Parse(string registry, string content, string? fileNa
3131 RegistryNames . NuGet => ParseNuGet ( content ) ,
3232 RegistryNames . PyPI => ParsePyPI ( content , fileName ) ,
3333 RegistryNames . PubDev => ParsePub ( content ) ,
34+ RegistryNames . Packagist => ParseComposer ( content ) ,
3435 _ => throw new ArgumentException ( "Unknown registry." )
3536 } ;
3637 result . Registry = canonical ;
@@ -587,6 +588,86 @@ private static AuditDependency FromPubConstraint(string name, string? requiremen
587588 } ;
588589 }
589590
591+ private static ManifestParseResult ParseComposer ( string content )
592+ {
593+ var root = ParseJson ( content , "a composer.json or composer.lock" ) ;
594+
595+ if ( root . TryGetProperty ( "packages" , out var packages ) && packages . ValueKind == JsonValueKind . Array )
596+ {
597+ var lockResult = new ManifestParseResult { Format = "composer.lock" } ;
598+ ReadComposerLock ( packages , null , lockResult ) ;
599+ if ( root . TryGetProperty ( "packages-dev" , out var devPackages ) && devPackages . ValueKind == JsonValueKind . Array )
600+ ReadComposerLock ( devPackages , Dev , lockResult ) ;
601+ return lockResult ;
602+ }
603+
604+ if ( ! root . TryGetProperty ( "require" , out _ ) && ! root . TryGetProperty ( "require-dev" , out _ ) )
605+ throw new ArgumentException ( "This JSON has neither require nor packages: paste a composer.json or a composer.lock." ) ;
606+
607+ var result = new ManifestParseResult { Format = "composer.json" } ;
608+ foreach ( var ( section , scope ) in new [ ] { ( "require" , ( string ? ) null ) , ( "require-dev" , Dev ) } )
609+ {
610+ if ( ! root . TryGetProperty ( section , out var dependencies ) || dependencies . ValueKind != JsonValueKind . Object )
611+ continue ;
612+ foreach ( var dependency in dependencies . EnumerateObject ( ) )
613+ {
614+ // php, ext-*, lib-* and composer-plugin-api name the platform, not a package
615+ if ( ! dependency . Name . Contains ( '/' ) )
616+ continue ;
617+ var requirement = dependency . Value . ValueKind == JsonValueKind . String ? dependency . Value . GetString ( ) : null ;
618+ result . Dependencies . Add ( FromComposerConstraint ( dependency . Name . ToLowerInvariant ( ) , requirement , scope , result . Warnings ) ) ;
619+ }
620+ }
621+ return result ;
622+ }
623+
624+ private static void ReadComposerLock ( JsonElement packages , string ? scope , ManifestParseResult result )
625+ {
626+ foreach ( var package in packages . EnumerateArray ( ) )
627+ {
628+ var name = Text ( package , "name" ) ;
629+ var version = Text ( package , "version" ) ;
630+ if ( name is null || ! name . Contains ( '/' ) )
631+ continue ;
632+ if ( version is null || version . StartsWith ( "dev-" , StringComparison . OrdinalIgnoreCase ) )
633+ {
634+ result . Warnings . Add ( $ "{ name } : \" { version } \" is a branch, not a release; its latest version is checked.") ;
635+ result . Dependencies . Add ( new AuditDependency { Name = name . ToLowerInvariant ( ) , Requirement = version , Scope = scope } ) ;
636+ continue ;
637+ }
638+ result . Dependencies . Add ( Exact ( name . ToLowerInvariant ( ) , StripComposerV ( version ) , scope ) ) ;
639+ }
640+ }
641+
642+ private static AuditDependency FromComposerConstraint ( string name , string ? requirement , string ? scope , List < string > warnings )
643+ {
644+ var value = requirement ? . Trim ( ) ?? string . Empty ;
645+ if ( value . Length == 0 || value == "*" )
646+ return new AuditDependency { Name = name , Requirement = requirement , Scope = scope } ;
647+
648+ if ( value . Contains ( "dev-" , StringComparison . OrdinalIgnoreCase ) )
649+ {
650+ warnings . Add ( $ "{ name } : \" { value } \" is a branch, not a release; its latest version is checked.") ;
651+ return new AuditDependency { Name = name , Requirement = requirement , Scope = scope } ;
652+ }
653+
654+ // "^1.0 || ^2.0" allows several ranges; the first one is checked, without its @stability flag
655+ var first = ( value . Split ( '|' , StringSplitOptions . RemoveEmptyEntries ) . FirstOrDefault ( ) ?? value ) . Trim ( ) . Split ( '@' ) [ 0 ] ;
656+ var exact = SemVerExact ( ) . IsMatch ( first ) ;
657+ var match = SemVerStart ( ) . Match ( first ) ;
658+ return new AuditDependency
659+ {
660+ Name = name ,
661+ Requirement = requirement ,
662+ Scope = scope ,
663+ IsExact = exact ,
664+ Version = match . Success ? Complete ( match . Value ) : null
665+ } ;
666+ }
667+
668+ private static string StripComposerV ( string version ) =>
669+ version . Length > 1 && ( version [ 0 ] == 'v' || version [ 0 ] == 'V' ) && char . IsDigit ( version [ 1 ] ) ? version [ 1 .. ] : version ;
670+
590671 private static JsonElement ParseJson ( string content , string expected )
591672 {
592673 try
0 commit comments