From e2ae17308bddc36a9dbf891517b84756dd2e733e Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Mon, 15 Jun 2026 22:09:31 +0100 Subject: [PATCH] Add Options.FSRoot to bound pom.xml parent resolution --- go.mod | 2 ++ go.sum | 2 -- internal/core/types.go | 8 +++++++ internal/maven/maven.go | 13 +++++++---- internal/maven/maven_test.go | 44 +++++++++++++++++++++++++++++++++++- manifests.go | 26 +++++++++++++++++++-- 6 files changed, 86 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 71232c6..9f72c50 100644 --- a/go.mod +++ b/go.mod @@ -14,3 +14,5 @@ require ( github.com/git-pkgs/vers v0.2.5 // indirect github.com/package-url/packageurl-go v0.1.6 // indirect ) + +replace github.com/git-pkgs/pom => ../pom diff --git a/go.sum b/go.sum index e1f7b58..6eec500 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/bazelbuild/buildtools v0.0.0-20260319080235-05d2ebe49b0f h1:khsR2MoXO+WButt3L5nnkAOGO17BAr0G/pGHZczF5/E= github.com/bazelbuild/buildtools v0.0.0-20260319080235-05d2ebe49b0f/go.mod h1:PLNUetjLa77TCCziPsz0EI8a6CUxgC+1jgmWv0H25tg= -github.com/git-pkgs/pom v0.1.4 h1:C6st+XSbF75eKuwfdkDZZtYHoTcaWRIEQYar5VtszUo= -github.com/git-pkgs/pom v0.1.4/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk= github.com/git-pkgs/purl v0.1.12 h1:qCskrEU1LWQhCkIVZd992W5++Bsxazvx2Cx1/65qCvU= github.com/git-pkgs/purl v0.1.12/go.mod h1:ofp4mHsR0cUeVONQaf33n6Wxg2QTEvtUdRfCedI8ouA= github.com/git-pkgs/vers v0.2.5 h1:tDtUMik9Iw1lyPHdT5V6LXjLo9LsJc0xOawURz7ibQU= diff --git a/internal/core/types.go b/internal/core/types.go index 6506065..1f2105d 100644 --- a/internal/core/types.go +++ b/internal/core/types.go @@ -37,6 +37,14 @@ type Parser interface { Parse(filename string, content []byte) ([]Dependency, error) } +// FSRootParser is optionally implemented by parsers that can consult +// neighbouring files on disk (e.g. pom.xml following to a +// parent). The fsRoot argument bounds that lookup; an empty string means +// no filesystem access. +type FSRootParser interface { + ParseInRoot(filename string, content []byte, fsRoot string) ([]Dependency, error) +} + // ParseError is returned when parsing fails. type ParseError struct { Filename string diff --git a/internal/maven/maven.go b/internal/maven/maven.go index c257b9a..554ad80 100644 --- a/internal/maven/maven.go +++ b/internal/maven/maven.go @@ -27,20 +27,25 @@ func init() { } // pomXMLParser parses pom.xml files. It computes a local-only effective -// POM: parents reachable via on disk are merged so that -// ${project.version} and properties defined in a multi-module root -// resolve, but nothing is fetched over the network. Anything that would +// POM: when given a filesystem root, parents reachable via +// on disk inside that root are merged so that ${project.version} and +// properties defined in a multi-module root resolve. Nothing is fetched +// over the network and nothing outside fsRoot is read. Anything that would // need a remote parent or BOM is left as-is and the dependency keeps its // raw ${...} version. type pomXMLParser struct{} func (p *pomXMLParser) Parse(filename string, content []byte) ([]core.Dependency, error) { + return p.ParseInRoot(filename, content, "") +} + +func (p *pomXMLParser) ParseInRoot(filename string, content []byte, fsRoot string) ([]core.Dependency, error) { root, err := pom.ParsePOM(content) if err != nil { return nil, &core.ParseError{Filename: filename, Err: err} } - fetcher := pom.NewLocalFetcherFrom(root, filepath.Dir(filename)) + fetcher := pom.NewLocalFetcherFrom(root, filepath.Dir(filename), fsRoot) ep, err := pom.NewResolver(fetcher).ResolvePOM(context.Background(), root, pom.Options{}) if err != nil { return nil, &core.ParseError{Filename: filename, Err: err} diff --git a/internal/maven/maven_test.go b/internal/maven/maven_test.go index deb77d1..156f00e 100644 --- a/internal/maven/maven_test.go +++ b/internal/maven/maven_test.go @@ -362,7 +362,7 @@ func TestPomMultiModuleLocalParent(t *testing.T) { t.Fatalf("read fixture: %v", err) } parser := &pomXMLParser{} - deps, err := parser.Parse("../../testdata/maven/multimodule/child/pom.xml", content) + deps, err := parser.ParseInRoot("../../testdata/maven/multimodule/child/pom.xml", content, "../../testdata/maven/multimodule") if err != nil { t.Fatalf("Parse failed: %v", err) } @@ -384,6 +384,48 @@ func TestPomMultiModuleLocalParent(t *testing.T) { } } +func TestPomMultiModuleNoFSRoot(t *testing.T) { + content, err := os.ReadFile("../../testdata/maven/multimodule/child/pom.xml") + if err != nil { + t.Fatalf("read fixture: %v", err) + } + parser := &pomXMLParser{} + deps, err := parser.Parse("../../testdata/maven/multimodule/child/pom.xml", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + got := map[string]core.Dependency{} + for _, d := range deps { + got[d.Name] = d + } + if v := got["org.openjdk.jmh:jmh-core"].Version; v == "1.37" { + t.Errorf("jmh-core resolved to %q without FSRoot; parent should not have been read", v) + } + if v := got["org.lib:lib"].Version; v == "2.5" { + t.Errorf("lib resolved to %q without FSRoot; parent depMgmt should not have been read", v) + } +} + +func TestPomMultiModuleFSRootJail(t *testing.T) { + content, err := os.ReadFile("../../testdata/maven/multimodule/child/pom.xml") + if err != nil { + t.Fatalf("read fixture: %v", err) + } + parser := &pomXMLParser{} + // fsRoot is the child dir itself, so ../pom.xml is outside the jail. + deps, err := parser.ParseInRoot("../../testdata/maven/multimodule/child/pom.xml", content, "../../testdata/maven/multimodule/child") + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + got := map[string]core.Dependency{} + for _, d := range deps { + got[d.Name] = d + } + if v := got["org.lib:lib"].Version; v == "2.5" { + t.Errorf("lib resolved to %q; parent outside fsRoot should have been refused", v) + } +} + func TestPomDependenciesNoRequirement(t *testing.T) { content, err := os.ReadFile("../../testdata/maven/pom_dependencies_no_requirement.xml") if err != nil { diff --git a/manifests.go b/manifests.go index c31aef0..fb5364b 100644 --- a/manifests.go +++ b/manifests.go @@ -47,14 +47,36 @@ type ParseResult struct { Dependencies []Dependency } +// Options configures Parse. +type Options struct { + // FSRoot, when non-empty, allows parsers that consult neighbouring + // files on disk (currently only pom.xml, for parent + // resolution) to do so within this directory. Paths outside it are + // refused. When empty, parsing is a pure function of content and no + // filesystem access occurs; this is the safe choice for untrusted + // input. + FSRoot string +} + // Parse parses a manifest or lockfile and returns its dependencies. -func Parse(filename string, content []byte) (*ParseResult, error) { +func Parse(filename string, content []byte, opts ...Options) (*ParseResult, error) { + var o Options + if len(opts) > 0 { + o = opts[0] + } + parser, eco, kind := core.IdentifyParser(filename) if parser == nil { return nil, &UnknownFileError{Filename: filename} } - deps, err := parser.Parse(filename, content) + var deps []Dependency + var err error + if fp, ok := parser.(core.FSRootParser); ok { + deps, err = fp.ParseInRoot(filename, content, o.FSRoot) + } else { + deps, err = parser.Parse(filename, content) + } if err != nil { return nil, err }