diff --git a/internal/resolution/pm/nuget/cmd_factory.go b/internal/resolution/pm/nuget/cmd_factory.go
index cdb3e665..3bb423d7 100644
--- a/internal/resolution/pm/nuget/cmd_factory.go
+++ b/internal/resolution/pm/nuget/cmd_factory.go
@@ -1,8 +1,16 @@
package nuget
import (
+ "bytes"
+ "encoding/xml"
+ "html/template"
+ "io"
+ "os"
"os/exec"
"path/filepath"
+ "regexp"
+ "sort"
+ "strings"
)
type ICmdFactory interface {
@@ -20,13 +28,55 @@ func (ExecPath) LookPath(file string) (string, error) {
return exec.LookPath(file)
}
+var packagesConfigTemplate = `
+
+
+ {{.TargetFrameworks}}
+
+
+ {{- range .Packages}}
+
+ {{- end}}
+
+
+`
+
type CmdFactory struct {
- execPath IExecPath
+ execPath IExecPath
+ packageConfgRegex string
+ packagesConfigTemplate string
+}
+
+func NewCmdFactory(execPath IExecPath) CmdFactory {
+ return CmdFactory{
+ execPath: execPath,
+ packageConfgRegex: PackagesConfigRegex,
+ packagesConfigTemplate: packagesConfigTemplate,
+ }
}
func (cmdf CmdFactory) MakeInstallCmd(command string, file string) (*exec.Cmd, error) {
+
+ // If the file is a packages.config file, convert it to a .csproj file
+ // check regex with PackagesConfigRegex
+ packageConfig, err := regexp.Compile(cmdf.packageConfgRegex)
+ if err != nil {
+ return nil, err
+ }
+
+ if packageConfig.MatchString(file) {
+ file, err = cmdf.convertPackagesConfigToCsproj(file)
+ if err != nil {
+ return nil, err
+ }
+ }
+
path, err := cmdf.execPath.LookPath(command)
+ if err != nil {
+ return nil, err
+ }
+
fileDir := filepath.Dir(file)
return &exec.Cmd{
@@ -37,3 +87,112 @@ func (cmdf CmdFactory) MakeInstallCmd(command string, file string) (*exec.Cmd, e
Dir: fileDir,
}, err
}
+
+type Packages struct {
+ Packages []Package `xml:"package"`
+}
+
+type Package struct {
+ ID string `xml:"id,attr"`
+ Version string `xml:"version,attr"`
+ TargetFramework string `xml:"targetFramework,attr"`
+}
+
+// convertPackagesConfigtoCsproj converts a packages.config file to a .csproj file
+// this is to enable the use of the dotnet restore command
+// that enables debricked to parse out transitive dependencies.
+// This may add some additional framework dependencies that will not show up if
+// we only scan the packages.config file.
+func (cmdf CmdFactory) convertPackagesConfigToCsproj(filePath string) (string, error) {
+ packages, err := parsePackagesConfig(filePath)
+ if err != nil {
+ return "", err
+ }
+
+ targetFrameworksStr := collectUniqueTargetFrameworks(packages.Packages)
+ csprojContent, err := cmdf.createCsprojContentWithTemplate(targetFrameworksStr, packages.Packages)
+ if err != nil {
+ return "", err
+ }
+
+ newFilename := filePath + ".csproj"
+ err = writeContentToCsprojFile(newFilename, csprojContent)
+ if err != nil {
+ return "", err
+ }
+
+ return newFilename, nil
+}
+
+var ioReadAllCsproj = io.ReadAll
+
+func parsePackagesConfig(filePath string) (*Packages, error) {
+ xmlFile, err := os.Open(filePath)
+ if err != nil {
+ return nil, err
+ }
+ defer xmlFile.Close()
+
+ byteValue, err := ioReadAllCsproj(xmlFile)
+ if err != nil {
+ return nil, err
+ }
+
+ var packages Packages
+ err = xml.Unmarshal(byteValue, &packages)
+ if err != nil {
+ return nil, err
+ }
+
+ return &packages, nil
+}
+
+func collectUniqueTargetFrameworks(packages []Package) string {
+ uniqueTargetFrameworks := make(map[string]struct{})
+ for _, pkg := range packages {
+ uniqueTargetFrameworks[pkg.TargetFramework] = struct{}{}
+ }
+
+ var targetFrameworks []string
+ for framework := range uniqueTargetFrameworks {
+ if framework != "" {
+ targetFrameworks = append(targetFrameworks, framework)
+ }
+ }
+
+ sort.Strings(targetFrameworks) // Sort the targetFrameworks slice
+
+ return strings.Join(targetFrameworks, ";")
+}
+func (cmdf CmdFactory) createCsprojContentWithTemplate(targetFrameworksStr string, packages []Package) (string, error) {
+ tmplParsed, err := template.New("csproj").Parse(cmdf.packagesConfigTemplate)
+ if err != nil {
+ return "", err
+ }
+
+ var tpl bytes.Buffer
+ err = tmplParsed.Execute(&tpl, map[string]interface{}{
+ "TargetFrameworks": targetFrameworksStr,
+ "Packages": packages,
+ })
+ if err != nil {
+ return "", err
+ }
+
+ return tpl.String(), nil
+}
+
+var osCreateCsproj = os.Create
+
+func writeContentToCsprojFile(newFilename string, content string) error {
+
+ csprojFile, err := osCreateCsproj(newFilename)
+ if err != nil {
+ return err
+ }
+ defer csprojFile.Close()
+
+ _, err = csprojFile.WriteString(content)
+
+ return err
+}
diff --git a/internal/resolution/pm/nuget/cmd_factory_test.go b/internal/resolution/pm/nuget/cmd_factory_test.go
index 8bb78d4f..5679ddff 100644
--- a/internal/resolution/pm/nuget/cmd_factory_test.go
+++ b/internal/resolution/pm/nuget/cmd_factory_test.go
@@ -1,19 +1,411 @@
package nuget
import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMakeInstallCmd(t *testing.T) {
- nugetCommand := "dotnet"
- cmd, err := CmdFactory{
- execPath: ExecPath{},
- }.MakeInstallCmd(nugetCommand, "file")
+ cmd, err := NewCmdFactory(
+ ExecPath{},
+ ).MakeInstallCmd(nuget, "file")
+ assert.NoError(t, err)
+ assert.NotNil(t, cmd)
+ args := cmd.Args
+ assert.Contains(t, args, "dotnet")
+ assert.Contains(t, args, "restore")
+}
+
+func TestMakeInstallCmdPackagsConfig(t *testing.T) {
+
+ cmd, err := NewCmdFactory(
+ ExecPath{},
+ ).MakeInstallCmd(nuget, "testdata/valid/packages.config")
assert.NoError(t, err)
assert.NotNil(t, cmd)
args := cmd.Args
assert.Contains(t, args, "dotnet")
assert.Contains(t, args, "restore")
+
+ // Cleanup: Remove the created .csproj file
+ if err := os.Remove("testdata/valid/packages.config.csproj"); err != nil {
+ t.Fatalf("Failed to remove test file: %v", err)
+ }
+}
+
+func MockReadAll(r io.Reader) ([]byte, error) {
+ return nil, fmt.Errorf("mock error")
+}
+func TestParsePackagesConfig(t *testing.T) {
+ tests := []struct {
+ name string
+ setup func() string // function to set up the test environment
+ teardown func() // function to clean up after the test
+ shouldFail bool
+ }{
+ {
+ name: "Non-existent file",
+ setup: func() string {
+ return "nonexistent_file.config"
+ },
+ shouldFail: true,
+ },
+ {
+ name: "Unreadable file",
+ setup: func() string {
+ file, err := os.CreateTemp("", "unreadable_*.config")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = file.Chmod(0222) // write-only permissions
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return file.Name()
+ },
+ teardown: func() {
+ os.Remove("unreadable_file.config") // clean up the unreadable file
+ },
+ shouldFail: true,
+ },
+ {
+ name: "Malformed XML",
+ setup: func() string {
+ file, err := os.CreateTemp("", "malformed_*.config")
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = file.WriteString("malformed xml content")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return file.Name()
+ },
+ teardown: func() {
+ os.Remove("malformed_file.config") // clean up the malformed file
+ },
+ shouldFail: true,
+ },
+ {
+ name: "ReadALL error",
+ setup: func() string {
+ ioReadAllCsproj = MockReadAll
+
+ return "testdata/valid/packages.config"
+ },
+ teardown: func() {
+ ioReadAllCsproj = io.ReadAll
+ },
+ shouldFail: true,
+ },
+ {
+ name: "Valid packages.config",
+ setup: func() string {
+
+ return "testdata/valid/packages.config"
+ },
+ teardown: func() {
+ fmt.Println("teardown")
+ },
+ shouldFail: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ filePath := test.setup()
+ if test.teardown != nil {
+ defer test.teardown() // clean up after the test
+ }
+
+ _, err := parsePackagesConfig(filePath)
+ if (err != nil) != test.shouldFail {
+ t.Errorf("parsePackagesConfig() error = %v, shouldFail = %v", err, test.shouldFail)
+ }
+ })
+ }
+}
+
+func TestCollectUniqueTargetFrameworks(t *testing.T) {
+ packages := []Package{
+ {TargetFramework: "net45"},
+ {TargetFramework: "net46"},
+ {TargetFramework: "net45"},
+ }
+ got := collectUniqueTargetFrameworks(packages)
+ want := "net45;net46"
+ if got != want {
+ t.Errorf("collectUniqueTargetFrameworks() = %v, want %v", got, want)
+ }
+}
+
+func TestWriteContentToCsprojFile(t *testing.T) {
+ newFilename := "testdata/test_output.csproj"
+ content := ""
+
+ if err := writeContentToCsprojFile(newFilename, content); err != nil {
+ t.Fatalf("writeContentToCsprojFile() failed: %v", err)
+ }
+
+ if _, err := os.Stat(newFilename); os.IsNotExist(err) {
+ t.Fatalf("writeContentToCsprojFile() did not create file")
+ }
+
+ // Cleanup: Remove the created file
+ if err := os.Remove(newFilename); err != nil {
+ t.Fatalf("Failed to remove test file: %v", err)
+ }
+}
+
+func TestWriteContentToCsprojFileErr(t *testing.T) {
+ tests := []struct {
+ name string
+ filename string
+ content string
+ shouldFail bool
+ setup func() // function to set up the environment for the test
+ teardown func() // function to clean up after the test
+ }{
+ {
+ name: "Valid file name and content",
+ filename: "test.csproj",
+ content: "",
+ shouldFail: false,
+ teardown: func() {
+ os.Remove("test.csproj") // Clean up the created file
+ },
+ },
+ {
+ name: "Invalid file name",
+ filename: "", // Empty filename is invalid
+ content: "",
+ shouldFail: true,
+ },
+ {
+ name: "Write to a read-only file",
+ filename: "readonly.csproj",
+ content: "",
+ shouldFail: true,
+ setup: func() {
+ // Create a read-only file
+ file, err := os.Create("readonly.csproj")
+ if err != nil {
+ panic(err)
+ }
+ file.Close()
+ err = os.Chmod("readonly.csproj", 0444) // Set file permissions to read-only
+ if err != nil {
+ panic(err)
+ }
+
+ },
+ teardown: func() {
+ os.Remove("readonly.csproj") // Clean up the read-only file
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ if test.setup != nil {
+ test.setup() // set up the environment for the test
+ }
+ err := writeContentToCsprojFile(test.filename, test.content)
+ if (err != nil) != test.shouldFail {
+ t.Errorf("writeContentToCsprojFile() error = %v, shouldFail = %v", err, test.shouldFail)
+ }
+ if test.teardown != nil {
+ test.teardown() // clean up after the test
+ }
+ })
+ }
+}
+
+func TestCreateCsprojContent(t *testing.T) {
+ tests := []struct {
+ name string
+ targetFrameworksStr string
+ packages []Package
+ shouldFail bool
+ tmpl string
+ }{
+ {
+ name: "Valid template action",
+ targetFrameworksStr: "netcoreapp3.1",
+ packages: []Package{{ID: "SomePackage", Version: "1.0.0"}},
+ shouldFail: false,
+ tmpl: packagesConfigTemplate,
+ },
+ {
+ name: "Invalid template action",
+ targetFrameworksStr: "netcoreapp3.1",
+ packages: []Package{{ID: "SomePackage", Version: "1.0.0"}},
+ shouldFail: true,
+ tmpl: `
+
+
+ {{.TargetFrameworks}}
+
+
+ {{- range .Packages}}
+
+ {{- end}}
+
+
+ `,
+ },
+ {
+ name: "Non-existent field",
+ targetFrameworksStr: "netcoreapp3.1",
+ packages: []Package{{ID: "SomePackage", Version: "1.0.0"}},
+ shouldFail: true,
+ tmpl: `
+
+
+ {{.NonExistentField}}
+
+
+ {{- range .Packages}}
+
+ {{- end}}
+
+
+ `,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ cmd := CmdFactory{
+ execPath: ExecPath{},
+ packageConfgRegex: PackagesConfigRegex,
+ packagesConfigTemplate: test.tmpl,
+ }
+ _, err := cmd.createCsprojContentWithTemplate(test.targetFrameworksStr, test.packages)
+ if (err != nil) != test.shouldFail {
+ t.Errorf("createCsprojContentWithTemplate() error = %v, shouldFail = %v", err, test.shouldFail)
+ }
+ })
+ }
+}
+
+func TestMakeInstallCmdBadPackagesConfigRegex(t *testing.T) {
+
+ cmd, err := CmdFactory{
+ execPath: ExecPath{},
+ packageConfgRegex: "[",
+ }.MakeInstallCmd(nuget, "file")
+
+ assert.Error(t, err)
+ assert.Nil(t, cmd)
+}
+
+func TestMakeInstallCmdNotAccessToFile(t *testing.T) {
+
+ tempDir, err := os.MkdirTemp("", "TestMakeInstallCmdNotAccessToFile")
+ if err != nil {
+ panic(err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ filePath := filepath.Join(tempDir, "packages.config")
+
+ file, err := os.Create(filePath)
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+
+ err = file.Chmod(0222) // write-only permissions
+
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = NewCmdFactory(
+ ExecPath{},
+ ).MakeInstallCmd(nuget, file.Name())
+
+ assert.Error(t, err)
+}
+
+type ExecPathErr struct {
+}
+
+func (ExecPathErr) LookPath(file string) (string, error) {
+ return "", errors.New("error")
+}
+
+func TestMakeInstallCmdExecPathError(t *testing.T) {
+
+ cmd, err := CmdFactory{
+ execPath: ExecPathErr{},
+ packageConfgRegex: PackagesConfigRegex,
+ }.MakeInstallCmd(nuget, "file")
+
+ assert.Error(t, err)
+ assert.Nil(t, cmd)
+}
+
+// Define a mock function that always returns an error
+func mockCreate(name string) (*os.File, error) {
+ return nil, fmt.Errorf("mock error")
+}
+func TestConvertPackagesConfigToCsproj(t *testing.T) {
+ tests := []struct {
+ name string
+ filePath string
+ wantError bool
+ packagesConfigTemplate string
+ setup func() // function to set up the test environment
+ teardown func() // function to clean up after the test
+ }{
+ {"Valid packages config", "testdata/valid/packages.config", false, packagesConfigTemplate, nil, nil},
+ {"Invalid packages config", "testdata/invalid/packages.config", true, packagesConfigTemplate, nil, nil},
+ {"Bad template", "testdata/valid/packages.config", true, "{{.TargetFramewo", nil, nil},
+ {"File without write premisions", "testdata/valid/packages.config", true, packagesConfigTemplate,
+ func() {
+ osCreateCsproj = mockCreate
+ },
+ func() {
+ osCreateCsproj = os.Create
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.setup != nil {
+ tt.setup() // set up the environment for the test
+ }
+
+ if tt.teardown != nil {
+ defer tt.teardown() // clean up after the test
+ }
+
+ cmd := CmdFactory{
+ execPath: ExecPath{},
+ packageConfgRegex: PackagesConfigRegex,
+ packagesConfigTemplate: tt.packagesConfigTemplate,
+ }
+ _, err := cmd.convertPackagesConfigToCsproj(tt.filePath)
+ if (err != nil) != tt.wantError {
+ t.Errorf("convertPackagesConfigToCsproj(%q) = %v, want error: %v", tt.filePath, err, tt.wantError)
+ }
+
+ })
+ }
+
+ // Cleanup: Remove the created .csproj file
+ if err := os.Remove("testdata/valid/packages.config.csproj"); err != nil {
+ t.Fatalf("Failed to remove test file: %v", err)
+ }
}
diff --git a/internal/resolution/pm/nuget/pm.go b/internal/resolution/pm/nuget/pm.go
index 21d43d60..8012f732 100644
--- a/internal/resolution/pm/nuget/pm.go
+++ b/internal/resolution/pm/nuget/pm.go
@@ -1,6 +1,8 @@
package nuget
const Name = "nuget"
+const CsprojRegex = `\.csproj$`
+const PackagesConfigRegex = `packages\.config$`
type Pm struct {
name string
@@ -18,6 +20,7 @@ func (pm Pm) Name() string {
func (Pm) Manifests() []string {
return []string{
- `\.csproj$`,
+ CsprojRegex,
+ PackagesConfigRegex,
}
}
diff --git a/internal/resolution/pm/nuget/pm_test.go b/internal/resolution/pm/nuget/pm_test.go
index 4e53bc79..ff762f41 100644
--- a/internal/resolution/pm/nuget/pm_test.go
+++ b/internal/resolution/pm/nuget/pm_test.go
@@ -20,10 +20,15 @@ func TestName(t *testing.T) {
func TestManifests(t *testing.T) {
pm := Pm{}
manifests := pm.Manifests()
- assert.Len(t, manifests, 1)
- manifest := manifests[0]
- assert.Equal(t, `\.csproj$`, manifest)
- _, err := regexp.Compile(manifest)
+ assert.Len(t, manifests, 2)
+ manifestCs := manifests[0]
+ assert.Equal(t, `\.csproj$`, manifestCs)
+ _, err := regexp.Compile(manifestCs)
+ assert.NoError(t, err)
+
+ manifestPc := manifests[1]
+ assert.Equal(t, `packages\.config$`, manifestPc)
+ _, err = regexp.Compile(manifestPc)
assert.NoError(t, err)
cases := map[string]bool{
@@ -34,11 +39,16 @@ func TestManifests(t *testing.T) {
"test.csproj.nuget": false,
"test.csproj.nuget.props": false,
"package.json.lock": false,
+ "packages.config": true,
}
for file, isMatch := range cases {
t.Run(file, func(t *testing.T) {
- matched, _ := regexp.MatchString(manifest, file)
- assert.Equal(t, isMatch, matched)
+
+ matchedCs, _ := regexp.MatchString(manifestCs, file)
+ matchedPc, _ := regexp.MatchString(manifestPc, file)
+ matched := matchedCs || matchedPc
+
+ assert.Equal(t, isMatch, matched, "file: %s", file)
})
}
}
diff --git a/internal/resolution/pm/nuget/strategy.go b/internal/resolution/pm/nuget/strategy.go
index 355c1ccd..0d4cd48c 100644
--- a/internal/resolution/pm/nuget/strategy.go
+++ b/internal/resolution/pm/nuget/strategy.go
@@ -14,9 +14,7 @@ func (s Strategy) Invoke() ([]job.IJob, error) {
jobs = append(jobs, NewJob(
file,
true,
- CmdFactory{
- execPath: ExecPath{},
- },
+ NewCmdFactory(ExecPath{}),
),
)
}
diff --git a/internal/resolution/pm/nuget/testdata/invalid/packages.config b/internal/resolution/pm/nuget/testdata/invalid/packages.config
new file mode 100644
index 00000000..e2ed7284
--- /dev/null
+++ b/internal/resolution/pm/nuget/testdata/invalid/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/internal/resolution/pm/nuget/testdata/valid/packages.config b/internal/resolution/pm/nuget/testdata/valid/packages.config
new file mode 100644
index 00000000..69f628d7
--- /dev/null
+++ b/internal/resolution/pm/nuget/testdata/valid/packages.config
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file