@@ -2,54 +2,164 @@ package dotnet
22
33import (
44 "encoding/xml"
5+ "fmt"
56 "io"
67 "os"
8+ "regexp"
79 "strings"
810
911 "github.com/Checkmarx/manifest-parser/pkg/parser/models"
1012)
1113
14+ // DotnetCsprojParser implements parsing of .NET project files (.csproj)
1215type DotnetCsprojParser struct {}
1316
17+ // PackageReference represents a package reference in the .csproj file
1418type PackageReference struct {
15- Include string `xml:"Include,attr"`
16- Version string `xml:"Version,attr"`
19+ Include string `xml:"Include,attr"`
20+ VersionAttr string `xml:"Version,attr"`
21+ VersionNested string `xml:"Version"`
1722}
1823
24+ // PackageReferenceTag is the XML tag for package references in .csproj files
25+ const PackageReferenceTag = "PackageReference"
26+
27+ // parseVersion handles version resolution
28+ // - Returns exact version if specified
29+ // - Returns "latest" for version ranges or special version specifiers
30+ func parseVersion (version string ) string {
31+ // Handle empty version
32+ if version == "" {
33+ return "latest"
34+ }
35+
36+ // If the version contains any kind of brackets, return "latest"
37+ if strings .ContainsAny (version , "[]()" ) {
38+ return "latest"
39+ }
40+
41+ // Handle special version specifiers
42+ if strings .ContainsAny (version , "*^~><" ) {
43+ return "latest"
44+ }
45+
46+ // Return exact version
47+ return version
48+ }
49+
50+ // computeIndices calculates start and end indices for PackageReference elements
51+ // Returns startIndex and endIndex for the element in the line
52+ func computeIndices (lines []string , lineNum int ) (startIndex , endIndex int , lineStart , lineEnd int ) {
53+ currentLine := lines [lineNum - 1 ] // lineNum is 1-based so we subtract 1
54+
55+ // Find the position of the PackageReference tag start in the line
56+ startIdx := strings .Index (currentLine , "<PackageReference" )
57+ if startIdx < 0 {
58+ return 1 , len (currentLine ), lineNum , lineNum
59+ }
60+
61+ // Check if it's a single-line format
62+ if strings .Contains (currentLine , "/>" ) {
63+ // Single-line format
64+ endIdx := strings .LastIndex (currentLine , "/>" ) + 2 // Include the "/>" itself
65+ return startIdx + 1 , endIdx + 1 , lineNum , lineNum
66+ }
67+
68+ // Multi-line format
69+ // TODO: Multi-line PackageReference support will be handled in the future.
70+ // Currently, if the tag spans multiple lines, we only return the first line.
71+ // The following code is commented out for now:
72+ /*
73+ lineEnd = lineNum
74+ for i := lineNum; i < len(lines) && i < lineNum+10; i++ { // Limit search to 10 lines
75+ if strings.Contains(lines[i-1], "</PackageReference>") {
76+ lineEnd = i
77+ endLine := lines[i-1]
78+ endIdx := strings.Index(endLine, "</PackageReference>") + len("</PackageReference>")
79+ return startIdx + 1, endIdx + 1, lineNum, lineEnd
80+ }
81+ }
82+ */
83+ // No closing tag found, return the end of the current line
84+ return startIdx + 1 , len (currentLine ) + 1 , lineNum , lineNum
85+ }
86+
87+ // Parse implements the Parser interface for .csproj files
1988func (p * DotnetCsprojParser ) Parse (manifestFile string ) ([]models.Package , error ) {
89+ // Read the file content
2090 content , err := os .ReadFile (manifestFile )
2191 if err != nil {
22- return nil , err
92+ return nil , fmt . Errorf ( "failed to read manifest file: %w" , err )
2393 }
2494
25- decoder := xml .NewDecoder (strings .NewReader (string (content )))
95+ // Split content into lines for index computation
96+ strContent := string (content )
97+ lines := strings .Split (strContent , "\n " )
98+
99+ // Create XML decoder
100+ decoder := xml .NewDecoder (strings .NewReader (strContent ))
26101 var packages []models.Package
27- var currentElement * PackageReference
28102
103+ // Parse XML content
29104 for {
30- tok , err := decoder .Token ()
105+ token , err := decoder .Token ()
31106 if err != nil {
32107 if err == io .EOF {
33108 break
34109 }
35- return nil , err
110+ return nil , fmt . Errorf ( "failed to parse XML: %w" , err )
36111 }
37112
38- switch elem := tok .(type ) {
113+ // Process each element
114+ switch elem := token .(type ) {
39115 case xml.StartElement :
40- if elem .Name .Local == "PackageReference" {
41- currentElement = & PackageReference {}
42- err := decoder .DecodeElement (currentElement , & elem )
43- if err != nil {
44- return nil , err
116+ if elem .Name .Local == PackageReferenceTag {
117+ var pkgRef PackageReference
118+ if err := decoder .DecodeElement (& pkgRef , & elem ); err != nil {
119+ return nil , fmt .Errorf ("failed to decode PackageReference: %w" , err )
45120 }
46- line , _ := decoder .InputPos ()
121+
122+ // Skip empty package names
123+ if pkgRef .Include == "" {
124+ continue
125+ }
126+
127+ // Find line number
128+ lineNum := 0
129+ packagePattern := fmt .Sprintf (`PackageReference.*Include="%s"` , pkgRef .Include )
130+ re := regexp .MustCompile (packagePattern )
131+
132+ for i , line := range lines {
133+ if re .MatchString (line ) {
134+ lineNum = i + 1 // 1-indexed line numbers
135+ break
136+ }
137+ }
138+
139+ // Skip if line not found
140+ if lineNum == 0 {
141+ continue
142+ }
143+
144+ // Compute indices for both single-line and multi-line formats
145+ startCol , endCol , lineStart , lineEnd := computeIndices (lines , lineNum )
146+
147+ // Determine the version
148+ version := pkgRef .VersionAttr
149+ if version == "" {
150+ version = pkgRef .VersionNested
151+ }
152+
153+ // Create package entry
47154 packages = append (packages , models.Package {
48- PackageName : currentElement .Include ,
49- Version : currentElement .Version ,
50- LineStart : line ,
51- LineEnd : line ,
52- Filepath : manifestFile ,
155+ PackageManager : "dotnet" ,
156+ PackageName : pkgRef .Include ,
157+ Version : parseVersion (version ),
158+ Filepath : manifestFile ,
159+ LineStart : lineStart ,
160+ LineEnd : lineEnd ,
161+ StartIndex : startCol ,
162+ EndIndex : endCol ,
53163 })
54164 }
55165 }
0 commit comments