github.com/jfrog/jfrog-cli-core@v1.12.1/artifactory/utils/dotnet/dependencies/packagesconfig.go (about)

     1  package dependencies
     2  
     3  import (
     4  	"encoding/xml"
     5  	"fmt"
     6  	gofrogcmd "github.com/jfrog/gofrog/io"
     7  	"github.com/jfrog/jfrog-cli-core/artifactory/utils/dotnet"
     8  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
     9  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    10  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    11  	"github.com/jfrog/jfrog-client-go/utils/log"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  )
    16  
    17  const PackagesFileName = "packages.config"
    18  
    19  // Register packages.config extractor
    20  func init() {
    21  	register(&packagesExtractor{})
    22  }
    23  
    24  // packages.config dependency extractor
    25  type packagesExtractor struct {
    26  	allDependencies  map[string]*buildinfo.Dependency
    27  	childrenMap      map[string][]string
    28  	rootDependencies []string
    29  }
    30  
    31  func (extractor *packagesExtractor) IsCompatible(projectName, dependenciesSource string) bool {
    32  	if strings.HasSuffix(dependenciesSource, PackagesFileName) {
    33  		log.Debug("Found", dependenciesSource, "file for project:", projectName)
    34  		return true
    35  	}
    36  	return false
    37  }
    38  
    39  func (extractor *packagesExtractor) DirectDependencies() ([]string, error) {
    40  	return getDirectDependencies(extractor.allDependencies, extractor.childrenMap), nil
    41  }
    42  
    43  func (extractor *packagesExtractor) AllDependencies() (map[string]*buildinfo.Dependency, error) {
    44  	return extractor.allDependencies, nil
    45  }
    46  
    47  func (extractor *packagesExtractor) ChildrenMap() (map[string][]string, error) {
    48  	return extractor.childrenMap, nil
    49  }
    50  
    51  // Create new packages.config extractor
    52  func (extractor *packagesExtractor) new(dependenciesSource string) (Extractor, error) {
    53  	newExtractor := &packagesExtractor{allDependencies: map[string]*buildinfo.Dependency{}, childrenMap: map[string][]string{}}
    54  	packagesConfig, err := newExtractor.loadPackagesConfig(dependenciesSource)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	globalPackagesCache, err := newExtractor.getGlobalPackagesCache()
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	err = newExtractor.extract(packagesConfig, globalPackagesCache)
    65  	return newExtractor, err
    66  }
    67  
    68  func (extractor *packagesExtractor) extract(packagesConfig *packagesConfig, globalPackagesCache string) error {
    69  	for _, nuget := range packagesConfig.XmlPackages {
    70  		id := strings.ToLower(nuget.Id)
    71  		nPackage := &nugetPackage{id: id, version: nuget.Version, dependencies: map[string]bool{}}
    72  		// First lets check if the original version exists within the file system:
    73  		pack, err := createNugetPackage(globalPackagesCache, nuget, nPackage)
    74  		if err != nil {
    75  			return err
    76  		}
    77  		if pack == nil {
    78  			// If doesn't exists lets build the array of alternative versions.
    79  			alternativeVersions := createAlternativeVersionForms(nuget.Version)
    80  			// Now lets do a loop to run over the alternative possibilities
    81  			for i := 0; i < len(alternativeVersions); i++ {
    82  				nPackage.version = alternativeVersions[i]
    83  				pack, err = createNugetPackage(globalPackagesCache, nuget, nPackage)
    84  				if err != nil {
    85  					return err
    86  				}
    87  				if pack != nil {
    88  					break
    89  				}
    90  			}
    91  		}
    92  		if pack != nil {
    93  			extractor.allDependencies[id] = pack.dependency
    94  			extractor.childrenMap[id] = pack.getDependencies()
    95  		} else {
    96  			log.Warn(fmt.Sprintf("The following NuGet package %s with version %s was not found in the NuGet cache %s."+absentNupkgWarnMsg, nuget.Id, nuget.Version, globalPackagesCache))
    97  		}
    98  	}
    99  	return nil
   100  }
   101  
   102  // NuGet allows the version will be with missing or unnecessary zeros
   103  // This method will return a list of the possible alternative versions
   104  // "1.0" --> []string{"1.0.0.0", "1.0.0", "1"}
   105  // "1" --> []string{"1.0.0.0", "1.0.0", "1.0"}
   106  // "1.2" --> []string{"1.2.0.0", "1.2.0"}
   107  // "1.22.33" --> []string{"1.22.33.0"}
   108  // "1.22.33.44" --> []string{}
   109  // "1.0.2" --> []string{"1.0.2.0"}
   110  func createAlternativeVersionForms(originalVersion string) []string {
   111  	versionSlice := strings.Split(originalVersion, ".")
   112  	versionSliceSize := len(versionSlice)
   113  	for i := 4; i > versionSliceSize; i-- {
   114  		versionSlice = append(versionSlice, "0")
   115  	}
   116  
   117  	var alternativeVersions []string
   118  
   119  	for i := 4; i > 0; i-- {
   120  		version := strings.Join(versionSlice[:i], ".")
   121  		if version != originalVersion {
   122  			alternativeVersions = append(alternativeVersions, version)
   123  		}
   124  		if !strings.HasSuffix(version, ".0") {
   125  			return alternativeVersions
   126  		}
   127  	}
   128  	return alternativeVersions
   129  }
   130  
   131  func (extractor *packagesExtractor) loadPackagesConfig(dependenciesSource string) (*packagesConfig, error) {
   132  	content, err := os.ReadFile(dependenciesSource)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  
   137  	config := &packagesConfig{}
   138  	err = xml.Unmarshal(content, config)
   139  	if err != nil {
   140  		return nil, errorutils.CheckError(err)
   141  	}
   142  	return config, nil
   143  }
   144  
   145  type dfsHelper struct {
   146  	visited  bool
   147  	notRoot  bool
   148  	circular bool
   149  }
   150  
   151  func getDirectDependencies(allDependencies map[string]*buildinfo.Dependency, childrenMap map[string][]string) []string {
   152  	helper := map[string]*dfsHelper{}
   153  	for id := range allDependencies {
   154  		helper[id] = &dfsHelper{}
   155  	}
   156  
   157  	for id := range allDependencies {
   158  		if helper[id].visited {
   159  			continue
   160  		}
   161  		searchRootDependencies(helper, id, allDependencies, childrenMap, map[string]bool{id: true})
   162  	}
   163  	var rootDependencies []string
   164  	for id, nodeData := range helper {
   165  		if !nodeData.notRoot || nodeData.circular {
   166  			rootDependencies = append(rootDependencies, id)
   167  		}
   168  	}
   169  
   170  	return rootDependencies
   171  }
   172  
   173  func searchRootDependencies(dfsHelper map[string]*dfsHelper, currentId string, allDependencies map[string]*buildinfo.Dependency, childrenMap map[string][]string, traversePath map[string]bool) {
   174  	if dfsHelper[currentId].visited {
   175  		return
   176  	}
   177  	for _, next := range childrenMap[currentId] {
   178  		if _, ok := allDependencies[next]; !ok {
   179  			// No such dependency
   180  			continue
   181  		}
   182  		if traversePath[next] {
   183  			for circular := range traversePath {
   184  				dfsHelper[circular].circular = true
   185  			}
   186  			continue
   187  		}
   188  
   189  		// Not root dependency
   190  		dfsHelper[next].notRoot = true
   191  		traversePath[next] = true
   192  		searchRootDependencies(dfsHelper, next, allDependencies, childrenMap, traversePath)
   193  		delete(traversePath, next)
   194  	}
   195  	dfsHelper[currentId].visited = true
   196  }
   197  
   198  func createNugetPackage(packagesPath string, nuget xmlPackage, nPackage *nugetPackage) (*nugetPackage, error) {
   199  	nupkgPath := filepath.Join(packagesPath, nPackage.id, nPackage.version, strings.Join([]string{nPackage.id, nPackage.version, "nupkg"}, "."))
   200  
   201  	exists, err := fileutils.IsFileExists(nupkgPath, false)
   202  
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	if !exists {
   208  		return nil, nil
   209  	}
   210  
   211  	fileDetails, err := fileutils.GetFileDetails(nupkgPath)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	nPackage.dependency = &buildinfo.Dependency{Id: nuget.Id + ":" + nuget.Version, Checksum: buildinfo.Checksum{Sha1: fileDetails.Checksum.Sha1, Md5: fileDetails.Checksum.Md5}}
   216  
   217  	// Nuspec file that holds the metadata for the package.
   218  	nuspecPath := filepath.Join(packagesPath, nPackage.id, nPackage.version, strings.Join([]string{nPackage.id, "nuspec"}, "."))
   219  	nuspecContent, err := os.ReadFile(nuspecPath)
   220  	if err != nil {
   221  		return nil, errorutils.CheckError(err)
   222  	}
   223  
   224  	nuspec := &nuspec{}
   225  	err = xml.Unmarshal(nuspecContent, nuspec)
   226  	if err != nil {
   227  		pack := nPackage.id + ":" + nPackage.version
   228  		log.Warn("Package:", pack, "couldn't be parsed due to:", err.Error(), ". Skipping the package dependency.")
   229  		return nPackage, nil
   230  	}
   231  
   232  	for _, dependency := range nuspec.Metadata.Dependencies.Dependencies {
   233  		nPackage.dependencies[strings.ToLower(dependency.Id)] = true
   234  	}
   235  
   236  	for _, group := range nuspec.Metadata.Dependencies.Groups {
   237  		for _, dependency := range group.Dependencies {
   238  			nPackage.dependencies[strings.ToLower(dependency.Id)] = true
   239  		}
   240  	}
   241  
   242  	return nPackage, nil
   243  }
   244  
   245  type nugetPackage struct {
   246  	id           string
   247  	version      string
   248  	dependency   *buildinfo.Dependency
   249  	dependencies map[string]bool // Set of dependencies
   250  }
   251  
   252  func (nugetPackage *nugetPackage) getDependencies() []string {
   253  	var dependencies []string
   254  	for key := range nugetPackage.dependencies {
   255  		dependencies = append(dependencies, key)
   256  	}
   257  
   258  	return dependencies
   259  }
   260  
   261  func (extractor *packagesExtractor) getGlobalPackagesCache() (string, error) {
   262  	localsCmd, err := dotnet.NewToolchainCmd(dotnet.Nuget)
   263  	if err != nil {
   264  		return "", err
   265  	}
   266  	//nuget locals global-packages -list
   267  	localsCmd.Command = append(localsCmd.Command, []string{"locals", "global-packages"}...)
   268  	localsCmd.CommandFlags = []string{"-list"}
   269  
   270  	output, err := gofrogcmd.RunCmdOutput(localsCmd)
   271  	if err != nil {
   272  		return "", err
   273  	}
   274  
   275  	globalPackagesPath := strings.TrimSpace(strings.TrimPrefix(output, "global-packages:"))
   276  	exists, err := fileutils.IsDirExists(globalPackagesPath, false)
   277  	if err != nil {
   278  		return "", err
   279  	}
   280  	if !exists {
   281  		return "", errorutils.CheckError(fmt.Errorf("Could not find global packages path at: %s", globalPackagesPath))
   282  	}
   283  	return globalPackagesPath, nil
   284  }
   285  
   286  // packages.config xml objects for unmarshalling
   287  type packagesConfig struct {
   288  	XMLName     xml.Name     `xml:"packages"`
   289  	XmlPackages []xmlPackage `xml:"package"`
   290  }
   291  
   292  type xmlPackage struct {
   293  	Id      string `xml:"id,attr"`
   294  	Version string `xml:"version,attr"`
   295  }
   296  
   297  type nuspec struct {
   298  	XMLName  xml.Name `xml:"package"`
   299  	Metadata metadata `xml:"metadata"`
   300  }
   301  
   302  type metadata struct {
   303  	Dependencies xmlDependencies `xml:"dependencies"`
   304  }
   305  
   306  type xmlDependencies struct {
   307  	Groups       []group      `xml:"group"`
   308  	Dependencies []xmlPackage `xml:"dependency"`
   309  }
   310  
   311  type group struct {
   312  	TargetFramework string       `xml:"targetFramework,attr"`
   313  	Dependencies    []xmlPackage `xml:"dependency"`
   314  }