github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/manifest/maven/pomxml.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package maven provides the manifest parsing and writing for the Maven pom.xml format.
    16  package maven
    17  
    18  import (
    19  	"bytes"
    20  	"cmp"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"iter"
    26  	"maps"
    27  	"os"
    28  	"path/filepath"
    29  	"slices"
    30  	"strings"
    31  
    32  	"deps.dev/util/maven"
    33  	"deps.dev/util/resolve"
    34  	"deps.dev/util/resolve/dep"
    35  	"github.com/google/osv-scalibr/clients/datasource"
    36  	"github.com/google/osv-scalibr/extractor/filesystem"
    37  	scalibrfs "github.com/google/osv-scalibr/fs"
    38  	"github.com/google/osv-scalibr/guidedremediation/internal/manifest"
    39  	"github.com/google/osv-scalibr/guidedremediation/result"
    40  	"github.com/google/osv-scalibr/guidedremediation/strategy"
    41  	"github.com/google/osv-scalibr/internal/mavenutil"
    42  	forkedxml "github.com/michaelkedar/xml"
    43  )
    44  
    45  // RequirementKey is a comparable type that uniquely identifies a package dependency in a manifest.
    46  type RequirementKey struct {
    47  	resolve.PackageKey
    48  
    49  	ArtifactType string
    50  	Classifier   string
    51  }
    52  
    53  var _ map[RequirementKey]any
    54  
    55  // MakeRequirementKey constructs a maven RequirementKey from the given RequirementVersion.
    56  func MakeRequirementKey(requirement resolve.RequirementVersion) RequirementKey {
    57  	// Maven dependencies must have unique groupId:artifactId:type:classifier.
    58  	artifactType, _ := requirement.Type.GetAttr(dep.MavenArtifactType)
    59  	classifier, _ := requirement.Type.GetAttr(dep.MavenClassifier)
    60  
    61  	return RequirementKey{
    62  		PackageKey:   requirement.PackageKey,
    63  		ArtifactType: artifactType,
    64  		Classifier:   classifier,
    65  	}
    66  }
    67  
    68  // ManifestSpecific is ecosystem-specific information needed for the pom.xml manifest.
    69  type ManifestSpecific struct {
    70  	Parent                 maven.Parent
    71  	ParentPaths            []string                     // Paths to the parent pom.xml files
    72  	Properties             []PropertyWithOrigin         // Properties from the base project and any local parent projects
    73  	OriginalRequirements   []DependencyWithOrigin       // Dependencies from the base project
    74  	LocalRequirements      []DependencyWithOrigin       // Dependencies from the base project and any local parent projects
    75  	RequirementsForUpdates []resolve.RequirementVersion // Requirements that we only need for updates
    76  	Repositories           []maven.Repository
    77  }
    78  
    79  // PropertyWithOrigin is a maven property with the origin where it comes from.
    80  type PropertyWithOrigin struct {
    81  	maven.Property
    82  
    83  	Origin string // Origin indicates where the property comes from
    84  }
    85  
    86  // DependencyWithOrigin is a maven dependency with the origin where it comes from.
    87  type DependencyWithOrigin struct {
    88  	maven.Dependency
    89  
    90  	Origin string // Origin indicates where the dependency comes from
    91  }
    92  
    93  type mavenManifest struct {
    94  	filePath     string
    95  	root         resolve.Version
    96  	requirements []resolve.RequirementVersion
    97  	groups       map[manifest.RequirementKey][]string
    98  	specific     ManifestSpecific
    99  }
   100  
   101  // FilePath returns the path to the manifest file.
   102  func (m *mavenManifest) FilePath() string {
   103  	return m.filePath
   104  }
   105  
   106  // Root returns the Version representing this package.
   107  func (m *mavenManifest) Root() resolve.Version {
   108  	return m.root
   109  }
   110  
   111  // System returns the ecosystem of this manifest.
   112  func (m *mavenManifest) System() resolve.System {
   113  	return resolve.Maven
   114  }
   115  
   116  // Requirements returns all direct requirements (including dev).
   117  func (m *mavenManifest) Requirements() []resolve.RequirementVersion {
   118  	return m.requirements
   119  }
   120  
   121  // Groups returns the dependency groups that the direct requirements belong to.
   122  func (m *mavenManifest) Groups() map[manifest.RequirementKey][]string {
   123  	return m.groups
   124  }
   125  
   126  // LocalManifests returns Manifests of any local packages.
   127  func (m *mavenManifest) LocalManifests() []manifest.Manifest {
   128  	return nil
   129  }
   130  
   131  // EcosystemSpecific returns any ecosystem-specific information for this manifest.
   132  func (m *mavenManifest) EcosystemSpecific() any {
   133  	return m.specific
   134  }
   135  
   136  // Clone returns a copy of this manifest that is safe to modify.
   137  func (m *mavenManifest) Clone() manifest.Manifest {
   138  	clone := &mavenManifest{
   139  		filePath:     m.filePath,
   140  		root:         m.root,
   141  		requirements: slices.Clone(m.requirements),
   142  		groups:       maps.Clone(m.groups),
   143  		specific: ManifestSpecific{
   144  			Parent:                 m.specific.Parent,
   145  			ParentPaths:            slices.Clone(m.specific.ParentPaths),
   146  			Properties:             slices.Clone(m.specific.Properties),
   147  			OriginalRequirements:   slices.Clone(m.specific.OriginalRequirements),
   148  			LocalRequirements:      slices.Clone(m.specific.LocalRequirements),
   149  			RequirementsForUpdates: slices.Clone(m.specific.RequirementsForUpdates),
   150  			Repositories:           slices.Clone(m.specific.Repositories),
   151  		},
   152  	}
   153  	clone.root.AttrSet = m.root.Clone()
   154  
   155  	return clone
   156  }
   157  
   158  // PatchRequirement modifies the manifest's requirements to include the new requirement version.
   159  // If the package already is in the requirements, updates the version.
   160  // Otherwise, adds req to the dependencyManagement of the root pom.xml.
   161  func (m *mavenManifest) PatchRequirement(req resolve.RequirementVersion) error {
   162  	found := false
   163  	i := 0
   164  	for _, r := range m.requirements {
   165  		if r.PackageKey != req.PackageKey {
   166  			m.requirements[i] = r
   167  			i++
   168  
   169  			continue
   170  		}
   171  		origin, hasOrigin := r.Type.GetAttr(dep.MavenDependencyOrigin)
   172  		if !hasOrigin || origin == mavenutil.OriginManagement {
   173  			found = true
   174  			r.Version = req.Version
   175  			m.requirements[i] = r
   176  			i++
   177  		}
   178  	}
   179  	m.requirements = m.requirements[:i]
   180  	if !found {
   181  		req.Type.AddAttr(dep.MavenDependencyOrigin, mavenutil.OriginManagement)
   182  		m.requirements = append(m.requirements, req)
   183  	}
   184  
   185  	return nil
   186  }
   187  
   188  type readWriter struct {
   189  	*datasource.MavenRegistryAPIClient
   190  }
   191  
   192  // GetReadWriter returns a ReadWriter for pom.xml manifest files.
   193  func GetReadWriter(client *datasource.MavenRegistryAPIClient) (manifest.ReadWriter, error) {
   194  	return readWriter{MavenRegistryAPIClient: client}, nil
   195  }
   196  
   197  // System returns the ecosystem of this ReadWriter.
   198  func (r readWriter) System() resolve.System {
   199  	return resolve.Maven
   200  }
   201  
   202  // SupportedStrategies returns the remediation strategies supported for this manifest.
   203  func (r readWriter) SupportedStrategies() []strategy.Strategy {
   204  	return []strategy.Strategy{strategy.StrategyOverride}
   205  }
   206  
   207  // Read parses the manifest from the given file.
   208  func (r readWriter) Read(path string, fsys scalibrfs.FS) (manifest.Manifest, error) {
   209  	// TODO(#472): much of this logic is duplicated with the pomxmlnet extractor.
   210  	ctx := context.Background()
   211  	path = filepath.ToSlash(path)
   212  	f, err := fsys.Open(path)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  	defer f.Close()
   217  
   218  	var project maven.Project
   219  	if err := datasource.NewMavenDecoder(f).Decode(&project); err != nil {
   220  		return nil, fmt.Errorf("failed to unmarshal project: %w", err)
   221  	}
   222  	properties := buildPropertiesWithOrigins(project, "")
   223  	origRequirements := buildOriginalRequirements(project, "")
   224  
   225  	var reqsForUpdates []resolve.RequirementVersion
   226  	if project.Parent.GroupID != "" && project.Parent.ArtifactID != "" {
   227  		reqsForUpdates = append(reqsForUpdates, resolve.RequirementVersion{
   228  			VersionKey: resolve.VersionKey{
   229  				PackageKey: resolve.PackageKey{
   230  					System: resolve.Maven,
   231  					Name:   project.Parent.Name(),
   232  				},
   233  				// Parent version is a concrete version, but we model parent as dependency here.
   234  				VersionType: resolve.Requirement,
   235  				Version:     string(project.Parent.Version),
   236  			},
   237  			Type: resolve.MavenDepType(maven.Dependency{Type: "pom"}, mavenutil.OriginParent),
   238  		})
   239  	}
   240  
   241  	// Empty JDK and ActivationOS indicates merging the default profiles.
   242  	if err := project.MergeProfiles("", maven.ActivationOS{}); err != nil {
   243  		return nil, fmt.Errorf("failed to merge profiles: %w", err)
   244  	}
   245  
   246  	// Interpolate the project in case there are properties in any repository.
   247  	if err := project.InterpolateRepositories(); err != nil {
   248  		return nil, fmt.Errorf("failed to interpolate project: %w", err)
   249  	}
   250  	for _, repo := range project.Repositories {
   251  		if repo.URL.ContainsProperty() {
   252  			continue
   253  		}
   254  		if err := r.AddRegistry(ctx, datasource.MavenRegistry{
   255  			URL:              string(repo.URL),
   256  			ID:               string(repo.ID),
   257  			ReleasesEnabled:  repo.Releases.Enabled.Boolean(),
   258  			SnapshotsEnabled: repo.Snapshots.Enabled.Boolean(),
   259  		}); err != nil {
   260  			return nil, fmt.Errorf("failed to add registry %s: %w", repo.URL, err)
   261  		}
   262  	}
   263  
   264  	// Merging parents data by parsing local parent pom.xml or fetching from upstream.
   265  	if err := mavenutil.MergeParents(ctx, project.Parent, &project, mavenutil.Options{
   266  		Input:              &filesystem.ScanInput{FS: fsys, Path: path},
   267  		Client:             r.MavenRegistryAPIClient,
   268  		AddRegistry:        true,
   269  		AllowLocal:         true,
   270  		InitialParentIndex: 1,
   271  	}); err != nil {
   272  		return nil, fmt.Errorf("failed to merge parents: %w", err)
   273  	}
   274  
   275  	// For dependency management imports, the dependencies that imports
   276  	// dependencies from other projects will be replaced by the imported
   277  	// dependencies, so add them to requirements first.
   278  	for _, dep := range project.DependencyManagement.Dependencies {
   279  		if dep.Scope == "import" && dep.Type == "pom" {
   280  			reqsForUpdates = append(reqsForUpdates, makeRequirementVersion(dep, mavenutil.OriginManagement))
   281  		}
   282  	}
   283  
   284  	// Process the dependencies:
   285  	//  - dedupe dependencies and dependency management
   286  	//  - import dependency management
   287  	//  - fill in missing dependency version requirement
   288  	project.ProcessDependencies(func(groupID, artifactID, version maven.String) (maven.DependencyManagement, error) {
   289  		return mavenutil.GetDependencyManagement(ctx, r.MavenRegistryAPIClient, groupID, artifactID, version)
   290  	})
   291  
   292  	groups := make(map[manifest.RequirementKey][]string)
   293  	requirements := addRequirements([]resolve.RequirementVersion{}, groups, project.Dependencies, "")
   294  	requirements = addRequirements(requirements, groups, project.DependencyManagement.Dependencies, mavenutil.OriginManagement)
   295  
   296  	// Requirements may not appear in the dependency graph but needs to be updated.
   297  	for _, profile := range project.Profiles {
   298  		reqsForUpdates = addRequirements(reqsForUpdates, groups, profile.Dependencies, "")
   299  		reqsForUpdates = addRequirements(reqsForUpdates, groups, profile.DependencyManagement.Dependencies, mavenutil.OriginManagement)
   300  	}
   301  	for _, plugin := range project.Build.PluginManagement.Plugins {
   302  		reqsForUpdates = addRequirements(reqsForUpdates, groups, plugin.Dependencies, "")
   303  	}
   304  
   305  	// Get the local dependencies and properties from all parent projects.
   306  	localDeps, localProps, paths, err := getLocalDepsAndProps(fsys, path, project.Parent)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	return &mavenManifest{
   312  		filePath: path,
   313  		root: resolve.Version{
   314  			VersionKey: resolve.VersionKey{
   315  				PackageKey: resolve.PackageKey{
   316  					System: resolve.Maven,
   317  					Name:   project.ProjectKey.Name(),
   318  				},
   319  				VersionType: resolve.Concrete,
   320  				Version:     string(project.Version),
   321  			},
   322  		},
   323  		requirements: requirements,
   324  		groups:       groups,
   325  		specific: ManifestSpecific{
   326  			Parent:                 project.Parent,
   327  			ParentPaths:            paths,
   328  			Properties:             append(properties, localProps...),
   329  			OriginalRequirements:   origRequirements,
   330  			LocalRequirements:      append(origRequirements, localDeps...),
   331  			RequirementsForUpdates: reqsForUpdates,
   332  			Repositories:           project.Repositories,
   333  		},
   334  	}, nil
   335  }
   336  
   337  func addRequirements(reqs []resolve.RequirementVersion, groups map[manifest.RequirementKey][]string, deps []maven.Dependency, origin string) []resolve.RequirementVersion {
   338  	for _, d := range deps {
   339  		reqVer := makeRequirementVersion(d, origin)
   340  		reqs = append(reqs, reqVer)
   341  		if d.Scope != "" {
   342  			reqKey := MakeRequirementKey(reqVer)
   343  			groups[reqKey] = append(groups[reqKey], string(d.Scope))
   344  		}
   345  	}
   346  
   347  	return reqs
   348  }
   349  
   350  func buildPropertiesWithOrigins(project maven.Project, originPrefix string) []PropertyWithOrigin {
   351  	count := len(project.Properties.Properties)
   352  	for _, prof := range project.Profiles {
   353  		count += len(prof.Properties.Properties)
   354  	}
   355  	properties := make([]PropertyWithOrigin, 0, count)
   356  	for _, prop := range project.Properties.Properties {
   357  		properties = append(properties, PropertyWithOrigin{Property: prop})
   358  	}
   359  	for _, profile := range project.Profiles {
   360  		for _, prop := range profile.Properties.Properties {
   361  			properties = append(properties, PropertyWithOrigin{
   362  				Property: prop,
   363  				Origin:   mavenOrigin(originPrefix, mavenutil.OriginProfile, string(profile.ID)),
   364  			})
   365  		}
   366  	}
   367  
   368  	return properties
   369  }
   370  
   371  func buildOriginalRequirements(project maven.Project, originPrefix string) []DependencyWithOrigin {
   372  	var dependencies []DependencyWithOrigin //nolint:prealloc
   373  	if project.Parent.GroupID != "" && project.Parent.ArtifactID != "" {
   374  		dependencies = append(dependencies, DependencyWithOrigin{
   375  			Dependency: maven.Dependency{
   376  				GroupID:    project.Parent.GroupID,
   377  				ArtifactID: project.Parent.ArtifactID,
   378  				Version:    project.Parent.Version,
   379  				Type:       "pom",
   380  			},
   381  			Origin: mavenOrigin(originPrefix, mavenutil.OriginParent),
   382  		})
   383  	}
   384  	for _, d := range project.Dependencies {
   385  		dependencies = append(dependencies, DependencyWithOrigin{Dependency: d, Origin: originPrefix})
   386  	}
   387  	for _, d := range project.DependencyManagement.Dependencies {
   388  		dependencies = append(dependencies, DependencyWithOrigin{
   389  			Dependency: d,
   390  			Origin:     mavenOrigin(originPrefix, mavenutil.OriginManagement),
   391  		})
   392  	}
   393  	for _, prof := range project.Profiles {
   394  		for _, d := range prof.Dependencies {
   395  			dependencies = append(dependencies, DependencyWithOrigin{
   396  				Dependency: d,
   397  				Origin:     mavenOrigin(originPrefix, mavenutil.OriginProfile, string(prof.ID)),
   398  			})
   399  		}
   400  		for _, d := range prof.DependencyManagement.Dependencies {
   401  			dependencies = append(dependencies, DependencyWithOrigin{
   402  				Dependency: d,
   403  				Origin:     mavenOrigin(originPrefix, mavenutil.OriginProfile, string(prof.ID), mavenutil.OriginManagement),
   404  			})
   405  		}
   406  	}
   407  	for _, plugin := range project.Build.PluginManagement.Plugins {
   408  		for _, d := range plugin.Dependencies {
   409  			dependencies = append(dependencies, DependencyWithOrigin{
   410  				Dependency: d,
   411  				Origin:     mavenOrigin(originPrefix, mavenutil.OriginPlugin, plugin.Name()),
   412  			})
   413  		}
   414  	}
   415  
   416  	return dependencies
   417  }
   418  
   419  // For dependencies in profiles and plugins, we use origin to indicate where they are from.
   420  // The origin is in the format prefix@identifier[@postfix] (where @ is the separator):
   421  //   - prefix indicates it is from profile or plugin
   422  //   - identifier to locate the profile/plugin which is profile ID or plugin name
   423  //   - (optional) suffix indicates if this is a dependency management
   424  func makeRequirementVersion(dep maven.Dependency, origin string) resolve.RequirementVersion {
   425  	// Treat test & optional dependencies as regular dependencies to force the resolver to resolve them.
   426  	if dep.Scope == "test" {
   427  		dep.Scope = ""
   428  	}
   429  	dep.Optional = ""
   430  
   431  	return resolve.RequirementVersion{
   432  		VersionKey: resolve.VersionKey{
   433  			PackageKey: resolve.PackageKey{
   434  				System: resolve.Maven,
   435  				Name:   dep.Name(),
   436  			},
   437  			VersionType: resolve.Requirement,
   438  			Version:     string(dep.Version),
   439  		},
   440  		Type: resolve.MavenDepType(dep, origin),
   441  	}
   442  }
   443  
   444  func mavenOrigin(list ...string) string {
   445  	result := ""
   446  	for _, str := range list {
   447  		if result != "" && str != "" {
   448  			result += "@"
   449  		}
   450  		if str != "" {
   451  			result += str
   452  		}
   453  	}
   454  
   455  	return result
   456  }
   457  
   458  // TODO: refactor MergeParents to return local requirements and properties
   459  func getLocalDepsAndProps(fsys scalibrfs.FS, path string, parent maven.Parent) ([]DependencyWithOrigin, []PropertyWithOrigin, []string, error) {
   460  	var localDeps []DependencyWithOrigin
   461  	var localProps []PropertyWithOrigin
   462  
   463  	// Walk through local parent pom.xml for original dependencies and properties.
   464  	currentPath := path
   465  	visited := make(map[maven.ProjectKey]bool, mavenutil.MaxParent)
   466  	paths := []string{currentPath}
   467  	for range mavenutil.MaxParent {
   468  		if parent.GroupID == "" || parent.ArtifactID == "" || parent.Version == "" {
   469  			break
   470  		}
   471  		if visited[parent.ProjectKey] {
   472  			// A cycle of parents is detected
   473  			return nil, nil, nil, errors.New("a cycle of parents is detected")
   474  		}
   475  		visited[parent.ProjectKey] = true
   476  
   477  		currentPath = mavenutil.ParentPOMPath(&filesystem.ScanInput{FS: fsys}, currentPath, string(parent.RelativePath))
   478  		if currentPath == "" {
   479  			// No more local parent pom.xml exists.
   480  			break
   481  		}
   482  
   483  		f, err := fsys.Open(currentPath)
   484  		if err != nil {
   485  			return nil, nil, nil, fmt.Errorf("failed to open parent file %s: %w", currentPath, err)
   486  		}
   487  
   488  		var proj maven.Project
   489  		err = datasource.NewMavenDecoder(f).Decode(&proj)
   490  		f.Close()
   491  		if err != nil {
   492  			return nil, nil, nil, fmt.Errorf("failed to unmarshal project: %w", err)
   493  		}
   494  		if mavenutil.ProjectKey(proj) != parent.ProjectKey || proj.Packaging != "pom" {
   495  			// This is not the project that we are looking for, we should fetch from upstream
   496  			// that we don't have write access so we give up here.
   497  			break
   498  		}
   499  
   500  		origin := mavenOrigin(mavenutil.OriginParent, currentPath)
   501  		localDeps = append(localDeps, buildOriginalRequirements(proj, origin)...)
   502  		localProps = append(localProps, buildPropertiesWithOrigins(proj, origin)...)
   503  		paths = append(paths, currentPath)
   504  		parent = proj.Parent
   505  	}
   506  
   507  	return localDeps, localProps, paths, nil
   508  }
   509  
   510  // Write writes the manifest after applying the patches to outputPath.
   511  //
   512  // original is the manifest without patches. fsys is the FS that the manifest was read from.
   513  // outputPath is the path on disk (*not* in fsys) to write the entire patched manifest to (this can overwrite the original manifest).
   514  //
   515  // If the original manifest referenced local parent POMs, they will be written alongside the patched manifest, maintaining the relative path structure as it existed in the original location.
   516  func (r readWriter) Write(original manifest.Manifest, fsys scalibrfs.FS, patches []result.Patch, outputPath string) error {
   517  	specific, ok := original.EcosystemSpecific().(ManifestSpecific)
   518  	if !ok {
   519  		return errors.New("invalid maven ManifestSpecific data")
   520  	}
   521  
   522  	allPatches, err := buildPatches(patches, specific)
   523  	if err != nil {
   524  		return err
   525  	}
   526  
   527  	for _, patchPath := range specific.ParentPaths {
   528  		patches := allPatches[patchPath]
   529  		if patchPath == original.FilePath() {
   530  			patches = allPatches[""]
   531  		}
   532  		depFile, err := fsys.Open(patchPath)
   533  		if err != nil {
   534  			return err
   535  		}
   536  		in := new(bytes.Buffer)
   537  		if _, err := in.ReadFrom(depFile); err != nil {
   538  			return fmt.Errorf("failed to read from filesystem: %w", err)
   539  		}
   540  		depFile.Close() // Make sure the file is closed before we start writing to it.
   541  
   542  		out := new(bytes.Buffer)
   543  		if err := write(in.String(), out, patches); err != nil {
   544  			return err
   545  		}
   546  		// Write the patched parent relative to the new outputPath
   547  		relativePatch, err := filepath.Rel(original.FilePath(), patchPath)
   548  		if err != nil {
   549  			return err
   550  		}
   551  		patchPath = filepath.Join(outputPath, relativePatch)
   552  		if err := os.MkdirAll(filepath.Dir(patchPath), 0755); err != nil {
   553  			return err
   554  		}
   555  		if err := os.WriteFile(patchPath, out.Bytes(), 0644); err != nil {
   556  			return err
   557  		}
   558  	}
   559  
   560  	return nil
   561  }
   562  
   563  // Patches represents all the dependencies and properties to be updated
   564  type Patches struct {
   565  	DependencyPatches DependencyPatches
   566  	PropertyPatches   PropertyPatches
   567  }
   568  
   569  // Patch represents an individual dependency to be upgraded, and the version to upgrade to
   570  type Patch struct {
   571  	maven.DependencyKey
   572  
   573  	NewRequire string
   574  }
   575  
   576  // DependencyPatches represent the dependencies to be updated, which
   577  // is a map of dependency patches of each origin.
   578  type DependencyPatches map[string]map[Patch]bool //  origin -> patch -> whether from this project
   579  
   580  // addPatch adds a patch to the patches map indexed by origin.
   581  // exist indicates whether this patch comes from the project.
   582  func (m DependencyPatches) addPatch(changedDep result.PackageUpdate, exist bool) error {
   583  	d, o, err := resolve.MavenDepTypeToDependency(changedDep.Type)
   584  	if err != nil {
   585  		return fmt.Errorf("MavenDepTypeToDependency: %w", err)
   586  	}
   587  
   588  	// If this dependency did not already exist in the project, we want to add it to the dependencyManagement section
   589  	if !exist {
   590  		o = mavenutil.OriginManagement
   591  	}
   592  
   593  	substrings := strings.Split(changedDep.Name, ":")
   594  	if len(substrings) != 2 {
   595  		return fmt.Errorf("invalid Maven name: %s", changedDep.Name)
   596  	}
   597  	d.GroupID = maven.String(substrings[0])
   598  	d.ArtifactID = maven.String(substrings[1])
   599  
   600  	if _, ok := m[o]; !ok {
   601  		m[o] = make(map[Patch]bool)
   602  	}
   603  	m[o][Patch{
   604  		DependencyKey: d.Key(),
   605  		NewRequire:    changedDep.VersionTo,
   606  	}] = exist
   607  
   608  	return nil
   609  }
   610  
   611  // PropertyPatches represent the properties to be updated, which
   612  // is a map of properties of each origin.
   613  type PropertyPatches map[string]map[string]string // origin -> tag -> value
   614  
   615  // parentPathFromOrigin returns the parent path embedded in origin,
   616  // as well as the remaining origin string.
   617  func parentPathFromOrigin(origin string) (string, string) {
   618  	tokens := strings.Split(origin, "@")
   619  	if len(tokens) <= 1 {
   620  		return "", origin
   621  	}
   622  	if tokens[0] != mavenutil.OriginParent {
   623  		return "", origin
   624  	}
   625  
   626  	return tokens[1], strings.Join(tokens[2:], "")
   627  }
   628  
   629  func iterUpgrades(patches []result.Patch) iter.Seq[result.PackageUpdate] {
   630  	return func(yield func(result.PackageUpdate) bool) {
   631  		for _, patch := range patches {
   632  			for _, update := range patch.PackageUpdates {
   633  				if !yield(update) {
   634  					return
   635  				}
   636  			}
   637  		}
   638  	}
   639  }
   640  
   641  // buildPatches returns dependency patches ready for updates.
   642  func buildPatches(patches []result.Patch, specific ManifestSpecific) (map[string]Patches, error) {
   643  	result := make(map[string]Patches)
   644  	for patch := range iterUpgrades(patches) {
   645  		var path string
   646  		origDep := OriginalDependency(patch, specific.LocalRequirements)
   647  		path, origDep.Origin = parentPathFromOrigin(origDep.Origin)
   648  		if _, ok := result[path]; !ok {
   649  			result[path] = Patches{
   650  				DependencyPatches: DependencyPatches{},
   651  				PropertyPatches:   PropertyPatches{},
   652  			}
   653  		}
   654  		if origDep.Name() == ":" {
   655  			// An empty name indicates the dependency is not found, so the original dependency is not in the base project.
   656  			// Add it so that it will be written into the dependencyManagement section.
   657  			if err := result[path].DependencyPatches.addPatch(patch, false); err != nil {
   658  				return nil, err
   659  			}
   660  
   661  			continue
   662  		}
   663  
   664  		patch.Type = resolve.MavenDepType(origDep.Dependency, origDep.Origin)
   665  		if !origDep.Version.ContainsProperty() {
   666  			// The original requirement does not contain a property placeholder.
   667  			if err := result[path].DependencyPatches.addPatch(patch, true); err != nil {
   668  				return nil, err
   669  			}
   670  
   671  			continue
   672  		}
   673  
   674  		properties, ok := generatePropertyPatches(string(origDep.Version), patch.VersionTo)
   675  		if !ok {
   676  			// Not able to update properties to update the requirement.
   677  			// Update the dependency directly instead.
   678  			if err := result[path].DependencyPatches.addPatch(patch, true); err != nil {
   679  				return nil, err
   680  			}
   681  
   682  			continue
   683  		}
   684  
   685  		depOrigin := origDep.Origin
   686  		if strings.HasPrefix(depOrigin, mavenutil.OriginProfile) {
   687  			// Dependency management is not indicated in property origin.
   688  			depOrigin, _ = strings.CutSuffix(depOrigin, "@"+mavenutil.OriginManagement)
   689  		} else {
   690  			// Properties are defined either universally or in a profile. For property
   691  			// origin not starting with 'profile', this is an universal property.
   692  			depOrigin = ""
   693  		}
   694  
   695  		for name, value := range properties {
   696  			// A dependency in a profile may contain properties from this profile or
   697  			// properties universally defined. We need to figure out the origin of these
   698  			// properties. If a property is defined both universally and in the profile,
   699  			// we use the profile's origin.
   700  			propertyOrigin := ""
   701  			for _, p := range specific.Properties {
   702  				if p.Name == name && p.Origin != "" && p.Origin == depOrigin {
   703  					propertyOrigin = depOrigin
   704  				}
   705  			}
   706  			if _, ok := result[path].PropertyPatches[propertyOrigin]; !ok {
   707  				result[path].PropertyPatches[propertyOrigin] = make(map[string]string)
   708  			}
   709  			// This property has been set to update to a value. If both values are the
   710  			// same, we do nothing; otherwise, instead of updating the property, we
   711  			// should update the dependency directly.
   712  			if preset, ok := result[path].PropertyPatches[propertyOrigin][name]; !ok {
   713  				result[path].PropertyPatches[propertyOrigin][name] = value
   714  			} else if preset != value {
   715  				if err := result[path].DependencyPatches.addPatch(patch, true); err != nil {
   716  					return nil, err
   717  				}
   718  			}
   719  		}
   720  	}
   721  
   722  	return result, nil
   723  }
   724  
   725  // OriginalDependency returns the original dependency of a dependency patch.
   726  // If the dependency is not found in any local pom.xml, an empty dependency is returned.
   727  func OriginalDependency(patch result.PackageUpdate, origDeps []DependencyWithOrigin) DependencyWithOrigin {
   728  	IDs := strings.Split(patch.Name, ":")
   729  	if len(IDs) != 2 {
   730  		return DependencyWithOrigin{}
   731  	}
   732  
   733  	dependency, _, _ := resolve.MavenDepTypeToDependency(patch.Type)
   734  	dependency.GroupID = maven.String(IDs[0])
   735  	dependency.ArtifactID = maven.String(IDs[1])
   736  
   737  	for _, d := range origDeps {
   738  		if d.Key() == dependency.Key() && d.Version != "" {
   739  			// If the version is empty, keep looking until we find some non-empty requirement.
   740  			return d
   741  		}
   742  	}
   743  
   744  	return DependencyWithOrigin{}
   745  }
   746  
   747  // generatePropertyPatches returns whether we are able to assign values to
   748  // placeholder keys to convert s1 to s2, as well as the generated patches.
   749  // s1 contains property placeholders like '${name}' and s2 is the target string.
   750  func generatePropertyPatches(s1, s2 string) (map[string]string, bool) {
   751  	patches := make(map[string]string)
   752  	ok := generatePropertyPatchesAux(s1, s2, patches)
   753  
   754  	return patches, ok
   755  }
   756  
   757  // generatePropertyPatchesAux generates property patches and store them in patches.
   758  // TODO: property may refer to another property ${${name}.version}
   759  func generatePropertyPatchesAux(s1, s2 string, patches map[string]string) bool {
   760  	start := strings.Index(s1, "${")
   761  	if s1[:start] != s2[:start] {
   762  		// Cannot update property to match the prefix
   763  		return false
   764  	}
   765  	end := strings.Index(s1, "}")
   766  	next := strings.Index(s1[end+1:], "${")
   767  	if next < 0 {
   768  		// There are no more placeholders.
   769  		remainder := s1[end+1:]
   770  		if remainder == s2[len(s2)-len(remainder):] {
   771  			patches[s1[start+2:end]] = s2[start : len(s2)-len(remainder)]
   772  			return true
   773  		}
   774  	} else if match := strings.Index(s2[start:], s1[end+1:end+1+next]); match > 0 {
   775  		// Try to match the substring between two property placeholders.
   776  		patches[s1[start+2:end]] = s2[start : start+match]
   777  		return generatePropertyPatchesAux(s1[end+1:], s2[start+match:], patches)
   778  	}
   779  
   780  	return false
   781  }
   782  
   783  func projectStartElement(raw string) string {
   784  	start := strings.Index(raw, "<project")
   785  	if start < 0 {
   786  		return ""
   787  	}
   788  	end := strings.Index(raw[start:], ">")
   789  	if end < 0 {
   790  		return ""
   791  	}
   792  
   793  	return raw[start : start+end+1]
   794  }
   795  
   796  // Only for writing dependencies that are not from the base project.
   797  type dependencyManagement struct {
   798  	Dependencies []dependency `xml:"dependencies>dependency,omitempty"`
   799  }
   800  
   801  type dependency struct {
   802  	GroupID    string `xml:"groupId,omitempty"`
   803  	ArtifactID string `xml:"artifactId,omitempty"`
   804  	Version    string `xml:"version,omitempty"`
   805  	Type       string `xml:"type,omitempty"`
   806  	Classifier string `xml:"classifier,omitempty"`
   807  }
   808  
   809  func makeDependency(patch Patch) dependency {
   810  	d := dependency{
   811  		GroupID:    string(patch.GroupID),
   812  		ArtifactID: string(patch.ArtifactID),
   813  		Version:    patch.NewRequire,
   814  		Classifier: string(patch.Classifier),
   815  	}
   816  	if patch.Type != "" && patch.Type != "jar" {
   817  		d.Type = string(patch.Type)
   818  	}
   819  
   820  	return d
   821  }
   822  
   823  func compareDependency(d1, d2 dependency) int {
   824  	if i := cmp.Compare(d1.GroupID, d2.GroupID); i != 0 {
   825  		return i
   826  	}
   827  	if i := cmp.Compare(d1.ArtifactID, d2.ArtifactID); i != 0 {
   828  		return i
   829  	}
   830  	if i := cmp.Compare(d1.Type, d2.Type); i != 0 {
   831  		return i
   832  	}
   833  	if i := cmp.Compare(d1.Classifier, d2.Classifier); i != 0 {
   834  		return i
   835  	}
   836  
   837  	return cmp.Compare(d1.Version, d2.Version)
   838  }
   839  
   840  func write(raw string, w io.Writer, patches Patches) error {
   841  	dec := forkedxml.NewDecoder(bytes.NewReader([]byte(raw)))
   842  	enc := forkedxml.NewEncoder(w)
   843  
   844  	for {
   845  		token, err := dec.Token()
   846  		if errors.Is(err, io.EOF) {
   847  			break
   848  		}
   849  		if err != nil {
   850  			return fmt.Errorf("getting token: %w", err)
   851  		}
   852  
   853  		if tt, ok := token.(forkedxml.StartElement); ok {
   854  			if tt.Name.Local == "project" {
   855  				type RawProject struct {
   856  					InnerXML string `xml:",innerxml"`
   857  				}
   858  				var rawProj RawProject
   859  				if err := dec.DecodeElement(&rawProj, &tt); err != nil {
   860  					return err
   861  				}
   862  
   863  				// xml.EncodeToken writes a start element with its all name spaces.
   864  				// It's very common to have a start project element with a few name spaces in Maven.
   865  				// Thus this would cause a big diff when we try to encode the start element of project.
   866  
   867  				// We first capture the raw start element string and write it.
   868  				projectStart := projectStartElement(raw)
   869  				if projectStart == "" {
   870  					return errors.New("unable to get start element of project")
   871  				}
   872  				if _, err := w.Write([]byte(projectStart)); err != nil {
   873  					return fmt.Errorf("writing start element of project: %w", err)
   874  				}
   875  
   876  				// Then we update the project by passing the innerXML and name spaces are not passed.
   877  				updated := make(map[string]bool) // origin -> updated
   878  				if err := writeProject(w, enc, rawProj.InnerXML, "", "", patches.DependencyPatches, patches.PropertyPatches, updated); err != nil {
   879  					return fmt.Errorf("updating project: %w", err)
   880  				}
   881  
   882  				// Check whether dependency management is updated, if not, add a new section of dependency management.
   883  				if dmPatches := patches.DependencyPatches[mavenutil.OriginManagement]; len(dmPatches) > 0 && !updated[mavenutil.OriginManagement] {
   884  					enc.Indent("  ", "  ")
   885  					var dm dependencyManagement
   886  					for p := range dmPatches {
   887  						dm.Dependencies = append(dm.Dependencies, makeDependency(p))
   888  					}
   889  					// Sort dependency management for consistency in testing.
   890  					slices.SortFunc(dm.Dependencies, compareDependency)
   891  					if err := enc.Encode(dm); err != nil {
   892  						return err
   893  					}
   894  					if _, err := w.Write([]byte("\n\n")); err != nil {
   895  						return err
   896  					}
   897  					enc.Indent("", "")
   898  				}
   899  
   900  				// Finally we write the end element of project.
   901  				if _, err := w.Write([]byte("</project>")); err != nil {
   902  					return fmt.Errorf("writing start element of project: %w", err)
   903  				}
   904  
   905  				continue
   906  			}
   907  		}
   908  		if err := enc.EncodeToken(token); err != nil {
   909  			return err
   910  		}
   911  		if err := enc.Flush(); err != nil {
   912  			return err
   913  		}
   914  	}
   915  
   916  	return nil
   917  }
   918  
   919  func writeProject(w io.Writer, enc *forkedxml.Encoder, raw, prefix, id string, patches DependencyPatches, properties PropertyPatches, updated map[string]bool) error {
   920  	dec := forkedxml.NewDecoder(bytes.NewReader([]byte(raw)))
   921  	for {
   922  		token, err := dec.Token()
   923  		if errors.Is(err, io.EOF) {
   924  			break
   925  		}
   926  		if err != nil {
   927  			return err
   928  		}
   929  
   930  		if tt, ok := token.(forkedxml.StartElement); ok {
   931  			switch tt.Name.Local {
   932  			case "parent":
   933  				updated["parent"] = true
   934  				type RawParent struct {
   935  					maven.ProjectKey
   936  
   937  					InnerXML string `xml:",innerxml"`
   938  				}
   939  				var rawParent RawParent
   940  				if err := dec.DecodeElement(&rawParent, &tt); err != nil {
   941  					return err
   942  				}
   943  				req := string(rawParent.Version)
   944  				if parentPatches, ok := patches["parent"]; ok {
   945  					// There should only be one parent patch
   946  					if len(parentPatches) > 1 {
   947  						return fmt.Errorf("multiple parent patches: %v", parentPatches)
   948  					}
   949  					for k := range parentPatches {
   950  						req = k.NewRequire
   951  					}
   952  				}
   953  				if err := writeString(enc, "<parent>"+rawParent.InnerXML+"</parent>", map[string]string{"version": req}); err != nil {
   954  					return fmt.Errorf("updating parent: %w", err)
   955  				}
   956  
   957  				continue
   958  			case "properties":
   959  				type RawProperties struct {
   960  					InnerXML string `xml:",innerxml"`
   961  				}
   962  				var rawProperties RawProperties
   963  				if err := dec.DecodeElement(&rawProperties, &tt); err != nil {
   964  					return err
   965  				}
   966  				if err := writeString(enc, "<properties>"+rawProperties.InnerXML+"</properties>", properties[mavenOrigin(prefix, id)]); err != nil {
   967  					return fmt.Errorf("updating properties: %w", err)
   968  				}
   969  
   970  				continue
   971  			case "profile":
   972  				if prefix != "" || id != "" {
   973  					// Skip updating if prefix or id is set to avoid infinite recursion
   974  					break
   975  				}
   976  				type RawProfile struct {
   977  					maven.Profile
   978  
   979  					InnerXML string `xml:",innerxml"`
   980  				}
   981  				var rawProfile RawProfile
   982  				if err := dec.DecodeElement(&rawProfile, &tt); err != nil {
   983  					return err
   984  				}
   985  				if err := writeProject(w, enc, "<profile>"+rawProfile.InnerXML+"</profile>", mavenutil.OriginProfile, string(rawProfile.ID), patches, properties, updated); err != nil {
   986  					return fmt.Errorf("updating profile: %w", err)
   987  				}
   988  
   989  				continue
   990  			case "plugin":
   991  				if prefix != "" || id != "" {
   992  					// Skip updating if prefix or id is set to avoid infinite recursion
   993  					break
   994  				}
   995  				type RawPlugin struct {
   996  					maven.Plugin
   997  
   998  					InnerXML string `xml:",innerxml"`
   999  				}
  1000  				var rawPlugin RawPlugin
  1001  				if err := dec.DecodeElement(&rawPlugin, &tt); err != nil {
  1002  					return err
  1003  				}
  1004  				if err := writeProject(w, enc, "<plugin>"+rawPlugin.InnerXML+"</plugin>", mavenutil.OriginPlugin, rawPlugin.Name(), patches, properties, updated); err != nil {
  1005  					return fmt.Errorf("updating profile: %w", err)
  1006  				}
  1007  
  1008  				continue
  1009  			case "dependencyManagement":
  1010  				type RawDependencyManagement struct {
  1011  					maven.DependencyManagement
  1012  
  1013  					InnerXML string `xml:",innerxml"`
  1014  				}
  1015  				var rawDepMgmt RawDependencyManagement
  1016  				if err := dec.DecodeElement(&rawDepMgmt, &tt); err != nil {
  1017  					return err
  1018  				}
  1019  				o := mavenOrigin(prefix, id, mavenutil.OriginManagement)
  1020  				updated[o] = true
  1021  				dmPatches := patches[o]
  1022  				if err := writeDependency(w, enc, "<dependencyManagement>"+rawDepMgmt.InnerXML+"</dependencyManagement>", dmPatches); err != nil {
  1023  					return fmt.Errorf("updating dependency management: %w", err)
  1024  				}
  1025  
  1026  				continue
  1027  			case "dependencies":
  1028  				type RawDependencies struct {
  1029  					Dependencies []maven.Dependency `xml:"dependencies"`
  1030  					InnerXML     string             `xml:",innerxml"`
  1031  				}
  1032  				var rawDeps RawDependencies
  1033  				if err := dec.DecodeElement(&rawDeps, &tt); err != nil {
  1034  					return err
  1035  				}
  1036  				o := mavenOrigin(prefix, id)
  1037  				updated[o] = true
  1038  				depPatches := patches[o]
  1039  				if err := writeDependency(w, enc, "<dependencies>"+rawDeps.InnerXML+"</dependencies>", depPatches); err != nil {
  1040  					return fmt.Errorf("updating dependencies: %w", err)
  1041  				}
  1042  
  1043  				continue
  1044  			}
  1045  		}
  1046  		if err := enc.EncodeToken(token); err != nil {
  1047  			return err
  1048  		}
  1049  	}
  1050  
  1051  	return enc.Flush()
  1052  }
  1053  
  1054  // indentation returns the indentation of the dependency element.
  1055  // If dependencies or dependency elements are not found, the default
  1056  // indentation (four space) is returned.
  1057  func indentation(raw string) string {
  1058  	i := strings.Index(raw, "<dependencies>")
  1059  	if i < 0 {
  1060  		return "    "
  1061  	}
  1062  
  1063  	raw = raw[i+len("<dependencies>"):]
  1064  	// Find the first dependency element.
  1065  	j := strings.Index(raw, "<dependency>")
  1066  	if j < 0 {
  1067  		return "    "
  1068  	}
  1069  
  1070  	raw = raw[:j]
  1071  	// Find the last new line and get the space between.
  1072  	k := strings.LastIndex(raw, "\n")
  1073  	if k < 0 {
  1074  		return "    "
  1075  	}
  1076  
  1077  	return raw[k+1:]
  1078  }
  1079  
  1080  func writeDependency(w io.Writer, enc *forkedxml.Encoder, raw string, patches map[Patch]bool) error {
  1081  	dec := forkedxml.NewDecoder(bytes.NewReader([]byte(raw)))
  1082  	for {
  1083  		token, err := dec.Token()
  1084  		if errors.Is(err, io.EOF) {
  1085  			break
  1086  		}
  1087  		if err != nil {
  1088  			return err
  1089  		}
  1090  
  1091  		if tt, ok := token.(forkedxml.StartElement); ok {
  1092  			if tt.Name.Local == "dependencies" {
  1093  				// We still need to write the start element <dependencies>
  1094  				if err := enc.EncodeToken(token); err != nil {
  1095  					return err
  1096  				}
  1097  				if err := enc.Flush(); err != nil {
  1098  					return err
  1099  				}
  1100  
  1101  				// Write patches that are not in the base project.
  1102  				var deps []dependency
  1103  				for p, ok := range patches {
  1104  					if !ok {
  1105  						deps = append(deps, makeDependency(p))
  1106  					}
  1107  				}
  1108  				if len(deps) == 0 {
  1109  					// No dependencies to add
  1110  					continue
  1111  				}
  1112  				// Sort dependencies for consistency in testing.
  1113  				slices.SortFunc(deps, compareDependency)
  1114  
  1115  				enc.Indent(indentation(raw), "  ")
  1116  				// Write a new line to keep the format.
  1117  				if _, err := w.Write([]byte("\n")); err != nil {
  1118  					return err
  1119  				}
  1120  				for _, d := range deps {
  1121  					if err := enc.Encode(d); err != nil {
  1122  						return err
  1123  					}
  1124  				}
  1125  				enc.Indent("", "")
  1126  
  1127  				continue
  1128  			}
  1129  			if tt.Name.Local == "dependency" {
  1130  				type RawDependency struct {
  1131  					maven.Dependency
  1132  
  1133  					InnerXML string `xml:",innerxml"`
  1134  				}
  1135  				var rawDep RawDependency
  1136  				if err := dec.DecodeElement(&rawDep, &tt); err != nil {
  1137  					return err
  1138  				}
  1139  				req := string(rawDep.Version)
  1140  				for patch := range patches {
  1141  					// A Maven dependency key consists of Type and Classifier together with GroupID and ArtifactID.
  1142  					if patch.DependencyKey == rawDep.Key() {
  1143  						req = patch.NewRequire
  1144  					}
  1145  				}
  1146  				// xml.EncodeElement writes all empty elements and may not follow the existing format.
  1147  				// Passing the innerXML can help to keep the original format.
  1148  				if err := writeString(enc, "<dependency>"+rawDep.InnerXML+"</dependency>", map[string]string{"version": req}); err != nil {
  1149  					return fmt.Errorf("updating dependency: %w", err)
  1150  				}
  1151  
  1152  				continue
  1153  			}
  1154  		}
  1155  
  1156  		if err := enc.EncodeToken(token); err != nil {
  1157  			return err
  1158  		}
  1159  	}
  1160  
  1161  	return enc.Flush()
  1162  }
  1163  
  1164  // writeString writes XML string specified by raw with replacements specified in values.
  1165  func writeString(enc *forkedxml.Encoder, raw string, values map[string]string) error {
  1166  	dec := forkedxml.NewDecoder(bytes.NewReader([]byte(raw)))
  1167  	for {
  1168  		token, err := dec.Token()
  1169  		if errors.Is(err, io.EOF) {
  1170  			break
  1171  		}
  1172  		if err != nil {
  1173  			return err
  1174  		}
  1175  		if tt, ok := token.(forkedxml.StartElement); ok {
  1176  			if value, ok2 := values[tt.Name.Local]; ok2 {
  1177  				var str string
  1178  				if err := dec.DecodeElement(&str, &tt); err != nil {
  1179  					return err
  1180  				}
  1181  				if err := enc.EncodeElement(value, tt); err != nil {
  1182  					return err
  1183  				}
  1184  
  1185  				continue
  1186  			}
  1187  		}
  1188  		if err := enc.EncodeToken(token); err != nil {
  1189  			return err
  1190  		}
  1191  	}
  1192  
  1193  	return enc.Flush()
  1194  }