From 52c9456208cf194265132626dd6d50ed5b6c81a1 Mon Sep 17 00:00:00 2001 From: Evelyn Chen Date: Mon, 7 Sep 2026 14:55:37 +0000 Subject: [PATCH] Resolve selected lazy modules by exact native cache identity Authored-By: Codex (GPT-6) --- tools/private-modules/main.go | 26 +++++++++++++++++++------- tools/private-modules/main_test.go | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/tools/private-modules/main.go b/tools/private-modules/main.go index a102437..aba1c57 100644 --- a/tools/private-modules/main.go +++ b/tools/private-modules/main.go @@ -99,13 +99,9 @@ func generate() error { return errors.New("private module must have exact unreplaced version") } mod := module{Path: src.Path, Version: src.Version, Files: []artifact{}} - rel, err := filepath.Rel(cacheRoot, src.GoMod) - if err != nil || !safePath(rel) || !strings.HasSuffix(rel, ".mod") || !strings.HasSuffix(filepath.Dir(rel), "/@v") { - return errors.New("unexpected native module cache layout") - } - stem, err := moduleStem(src.Path, src.Version) - if err != nil || filepath.ToSlash(rel) != stem+".mod" { - return errors.New("native artifact path differs from module identity") + stem, err := cacheStem(cacheRoot, src.Path, src.Version, src.GoMod) + if err != nil { + return err } for _, ext := range []string{".info", ".mod", ".zip"} { name := stem + ext @@ -185,6 +181,22 @@ func moduleStem(path, version string) (string, error) { return escape(path) + "/@v/" + escape(version), nil } +func cacheStem(cacheRoot, path, version, goMod string) (string, error) { + stem, err := moduleStem(path, version) + if err != nil { + return "", err + } + // Lazy module graphs omit GoMod for selected but unexpanded modules. + // Their canonical cache path is still determined by exact identity. + if goMod != "" { + rel, err := filepath.Rel(cacheRoot, goMod) + if err != nil || filepath.ToSlash(rel) != stem+".mod" { + return "", errors.New("native artifact path differs from module identity") + } + } + return stem, nil +} + func safePath(p string) bool { return p != "" && filepath.Clean(p) == p && !filepath.IsAbs(p) && p != ".." && !strings.HasPrefix(p, "../") && !strings.ContainsAny(p, "\\\x00") } diff --git a/tools/private-modules/main_test.go b/tools/private-modules/main_test.go index 35a644e..f16fd52 100644 --- a/tools/private-modules/main_test.go +++ b/tools/private-modules/main_test.go @@ -164,3 +164,17 @@ func TestCanonicalModuleArtifactPathBinding(t *testing.T) { } } } + +func TestLazyGraphModuleCacheIdentity(t *testing.T) { + cache := t.TempDir() + path, version := "code.fritzlab.net/fixture/module", "v0.1.0" + stem, err := cacheStem(cache, path, version, "") + if err != nil || stem != path+"/@v/"+version { + t.Fatalf("selected lazy module rejected: %q %v", stem, err) + } + for _, metadata := range []string{filepath.Join(cache, "foreign/@v/v0.1.0.mod"), filepath.Join(cache, "../escaped.mod"), filepath.Join(cache, path, "@v/v0.2.0.mod")} { + if _, err := cacheStem(cache, path, version, metadata); err == nil { + t.Fatal("contradictory cache identity accepted") + } + } +}