From b666522d3021641c062f6f4ae2b5016b0ef0f5e3 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 3 Jun 2026 04:56:21 +0000 Subject: [PATCH 1/2] Patch for CVE-2026-45571 --- SPECS/packer/CVE-2026-45571.patch | 7295 +++++++++++++++++++++++++++++ SPECS/packer/packer.spec | 8 +- 2 files changed, 7301 insertions(+), 2 deletions(-) create mode 100644 SPECS/packer/CVE-2026-45571.patch diff --git a/SPECS/packer/CVE-2026-45571.patch b/SPECS/packer/CVE-2026-45571.patch new file mode 100644 index 00000000000..60bf85a5942 --- /dev/null +++ b/SPECS/packer/CVE-2026-45571.patch @@ -0,0 +1,7295 @@ +From ac438f3ab5fc4039a97243fa78fe67115189853d Mon Sep 17 00:00:00 2001 +From: Aditya Singh +Date: Tue, 2 Jun 2026 14:07:34 +0000 +Subject: [PATCH] Patch for CVE-2026-45571 + +Below changes has been made with this patch - + +1. The "github.com/go-git/go-git/v5" vendor package has been upgraded from v5.13.0 => v5.19.1 + +2. The "github.com/go-git/go-billy/v5" vendor package has been upgraded from v5.6.0 => v5.9.0 + which was needed for compilation of upgraded "github.com/go-git/go-git/v5". + +3. go.mod and vendor/module.txt files have modified to change the version of "Go" module to 1.22, + which was required for compilation of upgraded "github.com/go-git/go-git/v5". + +Upstream Patch reference: https://github.com/go-git/go-git/releases/tag/v5.19.1 +--- + go.mod | 2 +- + .../github.com/go-git/go-billy/v5/README.md | 4 + + vendor/github.com/go-git/go-billy/v5/fs.go | 12 +- + .../go-billy/v5/helper/chroot/chroot.go | 222 ++++++++++- + .../go-billy/v5/helper/polyfill/polyfill.go | 11 +- + .../go-git/go-billy/v5/memfs/memory.go | 4 + + .../go-git/go-billy/v5/memfs/storage.go | 12 + + .../github.com/go-git/go-billy/v5/osfs/os.go | 5 + + .../go-git/go-billy/v5/osfs/os_bound.go | 116 +++++- + .../go-git/go-billy/v5/osfs/os_chroot.go | 14 + + .../go-git/go-billy/v5/util/util.go | 43 +- + .../go-git/go-git/v5/config/config.go | 31 +- + .../go-git/go-git/v5/config/modules.go | 52 +++ + .../go-git/go-git/v5/config/optbool.go | 82 ++++ + .../go-git/v5/internal/pathutil/dotgit.go | 21 + + .../go-git/go-git/v5/internal/pathutil/hfs.go | 99 +++++ + .../go-git/v5/internal/pathutil/ntfs.go | 187 +++++++++ + .../go-git/v5/internal/pathutil/tree.go | 66 +++ + .../go-git/go-git/v5/internal/url/url.go | 37 +- + vendor/github.com/go-git/go-git/v5/options.go | 56 ++- + .../v5/plumbing/format/idxfile/decoder.go | 201 +++++++++- + .../v5/plumbing/format/idxfile/idxfile.go | 75 ++-- + .../v5/plumbing/format/index/decoder.go | 106 +++-- + .../v5/plumbing/format/index/encoder.go | 39 +- + .../go-git/v5/plumbing/format/index/index.go | 2 + + .../v5/plumbing/format/objfile/reader.go | 20 +- + .../v5/plumbing/format/packfile/diff_delta.go | 3 - + .../v5/plumbing/format/packfile/fsobject.go | 8 +- + .../v5/plumbing/format/packfile/packfile.go | 21 +- + .../v5/plumbing/format/packfile/parser.go | 108 +++-- + .../plumbing/format/packfile/patch_delta.go | 111 ++++-- + .../v5/plumbing/format/packfile/scanner.go | 199 ++++++++- + .../go-git/v5/plumbing/object/commit.go | 233 +++++++---- + .../v5/plumbing/object/commit_scanner.go | 377 ++++++++++++++++++ + .../go-git/v5/plumbing/object/signature.go | 122 +++++- + .../go-git/go-git/v5/plumbing/object/tag.go | 135 ++++--- + .../go-git/v5/plumbing/object/tag_scanner.go | 237 +++++++++++ + .../go-git/go-git/v5/plumbing/object/tree.go | 149 +++++-- + .../plumbing/protocol/packp/advrefs_decode.go | 5 +- + .../plumbing/protocol/packp/updreq_encode.go | 2 +- + .../go-git/v5/plumbing/transport/common.go | 12 +- + .../v5/plumbing/transport/http/common.go | 203 ++++++++-- + .../v5/plumbing/transport/http/transport.go | 6 +- + .../v5/plumbing/transport/ssh/auth_method.go | 73 +++- + .../v5/plumbing/transport/ssh/common.go | 54 ++- + vendor/github.com/go-git/go-git/v5/remote.go | 20 +- + .../github.com/go-git/go-git/v5/repository.go | 22 +- + .../go-git/go-git/v5/repository_extensions.go | 121 ++++++ + .../v5/storage/filesystem/dotgit/dotgit.go | 19 +- + .../v5/storage/filesystem/dotgit/writers.go | 68 +++- + .../storage/filesystem/dotgit/writers_unix.go | 29 ++ + .../filesystem/dotgit/writers_windows.go | 58 +++ + .../go-git/v5/storage/filesystem/index.go | 5 + + .../go-git/v5/storage/filesystem/object.go | 27 +- + .../go-git/v5/storage/memory/storage.go | 4 + + .../github.com/go-git/go-git/v5/submodule.go | 76 +++- + .../go-git/go-git/v5/utils/binary/read.go | 15 + + .../go-git/v5/utils/merkletrie/change.go | 6 +- + .../go-git/v5/utils/merkletrie/difftree.go | 43 +- + .../v5/utils/merkletrie/filesystem/node.go | 110 ++++- + .../go-git/v5/utils/merkletrie/index/node.go | 10 +- + .../github.com/go-git/go-git/v5/worktree.go | 175 +++----- + .../go-git/go-git/v5/worktree_fs.go | 264 ++++++++++++ + .../go-git/go-git/v5/worktree_status.go | 13 +- + vendor/modules.txt | 2 +- + 65 files changed, 3990 insertions(+), 674 deletions(-) + create mode 100644 vendor/github.com/go-git/go-git/v5/config/optbool.go + create mode 100644 vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go + create mode 100644 vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go + create mode 100644 vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go + create mode 100644 vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go + create mode 100644 vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go + create mode 100644 vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go + create mode 100644 vendor/github.com/go-git/go-git/v5/repository_extensions.go + create mode 100644 vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go + create mode 100644 vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go + create mode 100644 vendor/github.com/go-git/go-git/v5/worktree_fs.go + +diff --git a/go.mod b/go.mod +index b03fd08..3e4ac77 100644 +--- a/go.mod ++++ b/go.mod +@@ -219,6 +219,6 @@ require ( + gopkg.in/yaml.v3 v3.0.1 // indirect + ) + +-go 1.21 ++go 1.22 + + toolchain go1.23.4 +diff --git a/vendor/github.com/go-git/go-billy/v5/README.md b/vendor/github.com/go-git/go-billy/v5/README.md +index da5c074..f260f79 100644 +--- a/vendor/github.com/go-git/go-billy/v5/README.md ++++ b/vendor/github.com/go-git/go-billy/v5/README.md +@@ -5,6 +5,10 @@ Billy implements an interface based on the `os` standard library, allowing to de + + Billy was born as part of [go-git/go-git](https://github.com/go-git/go-git) project. + ++## Version support ++ ++go-billy v5 is in maintenance mode. Users should upgrade to [go-billy v6](https://pkg.go.dev/github.com/go-git/go-billy/v6) where possible. ++ + ## Installation + + ```go +diff --git a/vendor/github.com/go-git/go-billy/v5/fs.go b/vendor/github.com/go-git/go-billy/v5/fs.go +index d86f9d8..14a020a 100644 +--- a/vendor/github.com/go-git/go-billy/v5/fs.go ++++ b/vendor/github.com/go-git/go-billy/v5/fs.go +@@ -128,12 +128,18 @@ type Symlink interface { + Readlink(link string) (string, error) + } + +-// Change abstract the FileInfo change related operations in a storage-agnostic +-// interface as an extension to the Basic interface +-type Change interface { ++// Chmod abstracts the logic around changing file modes. ++type Chmod interface { + // Chmod changes the mode of the named file to mode. If the file is a + // symbolic link, it changes the mode of the link's target. + Chmod(name string, mode os.FileMode) error ++} ++ ++// Change abstract the FileInfo change related operations in a storage-agnostic ++// interface as an extension to the Basic interface ++type Change interface { ++ Chmod ++ + // Lchown changes the numeric uid and gid of the named file. If the file is + // a symbolic link, it changes the uid and gid of the link itself. + Lchown(name string, uid, gid int) error +diff --git a/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go b/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go +index 8b44e78..299d165 100644 +--- a/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go ++++ b/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go +@@ -1,20 +1,27 @@ + package chroot + + import ( ++ "errors" + "os" ++ "path" + "path/filepath" + "strings" ++ "syscall" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/helper/polyfill" + ) + + // ChrootHelper is a helper to implement billy.Chroot. ++// It is not a security boundary, callers that need containment should use a ++// filesystem implementation that enforces paths at the OS boundary instead. + type ChrootHelper struct { + underlying billy.Filesystem + base string + } + ++const maxFollowedSymlinks = 8 // Aligns with POSIX_SYMLOOP_MAX ++ + // New creates a new filesystem wrapping up the given 'fs'. + // The created filesystem has its base in the given ChrootHelperectory of the + // underlying filesystem. +@@ -33,15 +40,184 @@ func (fs *ChrootHelper) underlyingPath(filename string) (string, error) { + return fs.Join(fs.Root(), filename), nil + } + +-func isCrossBoundaries(path string) bool { +- path = filepath.ToSlash(path) +- path = filepath.Clean(path) ++func (fs *ChrootHelper) followedPath(filename string, followFinal bool, op string) (string, error) { ++ fullpath, err := fs.underlyingPath(filename) ++ if err != nil { ++ return "", err ++ } ++ ++ sl, ok := fs.underlying.(billy.Symlink) ++ if !ok { ++ return fullpath, nil ++ } ++ ++ rel, err := fs.relativeToRoot(fullpath) ++ if err != nil { ++ return "", err ++ } ++ ++ fullpath, err = fs.resolveFollowedPath(rel, followFinal, op, sl) ++ if errors.Is(err, billy.ErrNotSupported) { ++ return fs.underlyingPath(filename) ++ } ++ ++ return fullpath, err ++} ++ ++func (fs *ChrootHelper) resolveFollowedPath(rel string, followFinal bool, op string, sl billy.Symlink) (string, error) { ++ if rel == "" { ++ return fs.resolveFollowedRoot(followFinal, op, sl) ++ } ++ ++ parts := splitRelativePath(rel) ++ resolved := "" ++ followed := 0 ++ ++ for len(parts) > 0 { ++ part := parts[0] ++ parts = parts[1:] ++ ++ currentRel := joinRelativePath(resolved, part) ++ currentPath := fs.Join(fs.Root(), currentRel) ++ if len(parts) == 0 && !followFinal { ++ return currentPath, nil ++ } ++ ++ fi, err := sl.Lstat(currentPath) ++ if err != nil { ++ if os.IsNotExist(err) { ++ return fs.Join(fs.Root(), joinRelativePath(append([]string{currentRel}, parts...)...)), nil ++ } ++ return "", err ++ } ++ ++ if fi.Mode()&os.ModeSymlink == 0 { ++ resolved = currentRel ++ continue ++ } ++ ++ followed++ ++ if followed > maxFollowedSymlinks { ++ return "", symlinkLoopError(op, currentPath) ++ } ++ ++ target, err := sl.Readlink(currentPath) ++ if err != nil { ++ return "", err ++ } ++ ++ targetRel, err := fs.linkTargetRel(currentPath, target) ++ if err != nil { ++ return "", err ++ } ++ if targetRel == currentRel { ++ return "", symlinkLoopError(op, currentPath) ++ } ++ ++ parts = append(splitRelativePath(targetRel), parts...) ++ resolved = "" ++ } ++ ++ return fs.Join(fs.Root(), resolved), nil ++} ++ ++func symlinkLoopError(op, path string) error { ++ return &os.PathError{Op: op, Path: path, Err: syscall.ELOOP} ++} ++ ++func (fs *ChrootHelper) resolveFollowedRoot(followFinal bool, op string, sl billy.Symlink) (string, error) { ++ root := fs.Join(fs.Root(), "") ++ if !followFinal { ++ return root, nil ++ } ++ ++ fi, err := sl.Lstat(root) ++ if err != nil { ++ if os.IsNotExist(err) { ++ return root, nil ++ } ++ return "", err ++ } ++ ++ if fi.Mode()&os.ModeSymlink == 0 { ++ return root, nil ++ } ++ ++ target, err := sl.Readlink(root) ++ if err != nil { ++ return "", err ++ } ++ ++ targetRel, err := fs.linkTargetRel(root, target) ++ if err != nil { ++ return root, err ++ } ++ if targetRel == "" { ++ return "", symlinkLoopError(op, root) ++ } ++ ++ return fs.resolveFollowedPath(targetRel, followFinal, op, sl) ++} ++ ++func (fs *ChrootHelper) relativeToRoot(filename string) (string, error) { ++ rel, err := filepath.Rel(filepath.Clean(fs.Root()), filepath.Clean(filename)) ++ if err != nil || isCrossBoundaries(rel) { ++ return "", billy.ErrCrossedBoundary ++ } ++ ++ if rel == "." { ++ return "", nil ++ } ++ return rel, nil ++} ++ ++func (fs *ChrootHelper) linkTargetRel(linkPath, target string) (string, error) { ++ target = filepath.FromSlash(target) ++ if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) { ++ return fs.relativeToRoot(target) ++ } ++ ++ return fs.relativeToRoot(fs.Join(filepath.Dir(linkPath), target)) ++} ++ ++func splitRelativePath(filename string) []string { ++ filename = filepath.Clean(filename) ++ if filename == "" || filename == "." { ++ return nil ++ } ++ ++ return strings.Split(filepath.ToSlash(filename), "/") ++} ++ ++func joinRelativePath(elem ...string) string { ++ parts := make([]string, 0, len(elem)) ++ for _, part := range elem { ++ if part == "" || part == "." { ++ continue ++ } ++ parts = append(parts, part) ++ } ++ ++ if len(parts) == 0 { ++ return "" ++ } ++ return filepath.Join(parts...) ++} ++ ++func isCreateExclusive(flag int) bool { ++ return flag&os.O_CREATE != 0 && flag&os.O_EXCL != 0 ++} ++ ++func isCrossBoundaries(name string) bool { ++ name = filepath.ToSlash(name) ++ name = strings.TrimLeft(name, "/") ++ name = path.Clean(name) + +- return strings.HasPrefix(path, ".."+string(filepath.Separator)) ++ return name == ".." || strings.HasPrefix(name, "../") + } + + func (fs *ChrootHelper) Create(filename string) (billy.File, error) { +- fullpath, err := fs.underlyingPath(filename) ++ fullpath, err := fs.followedPath(filename, true, "create") + if err != nil { + return nil, err + } +@@ -55,7 +231,7 @@ func (fs *ChrootHelper) Create(filename string) (billy.File, error) { + } + + func (fs *ChrootHelper) Open(filename string) (billy.File, error) { +- fullpath, err := fs.underlyingPath(filename) ++ fullpath, err := fs.followedPath(filename, true, "open") + if err != nil { + return nil, err + } +@@ -69,7 +245,7 @@ func (fs *ChrootHelper) Open(filename string) (billy.File, error) { + } + + func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) { +- fullpath, err := fs.underlyingPath(filename) ++ fullpath, err := fs.followedPath(filename, !isCreateExclusive(flag), "open") + if err != nil { + return nil, err + } +@@ -83,12 +259,16 @@ func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (b + } + + func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) { +- fullpath, err := fs.underlyingPath(filename) ++ fullpath, err := fs.followedPath(filename, true, "stat") + if err != nil { + return nil, err + } + +- return fs.underlying.Stat(fullpath) ++ fi, err := fs.underlying.Stat(fullpath) ++ if err != nil { ++ return nil, err ++ } ++ return fileInfo{FileInfo: fi, name: filepath.Base(filename)}, nil + } + + func (fs *ChrootHelper) Rename(from, to string) error { +@@ -134,7 +314,7 @@ func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) { + } + + func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) { +- fullpath, err := fs.underlyingPath(path) ++ fullpath, err := fs.followedPath(path, true, "readdir") + if err != nil { + return nil, err + } +@@ -200,6 +380,19 @@ func (fs *ChrootHelper) Readlink(link string) (string, error) { + return string(os.PathSeparator) + target, nil + } + ++func (fs *ChrootHelper) Chmod(path string, mode os.FileMode) error { ++ fullpath, err := fs.underlyingPath(path) ++ if err != nil { ++ return err ++ } ++ ++ c, ok := fs.underlying.(billy.Chmod) ++ if !ok { ++ return errors.New("underlying fs does not implement billy.Chmod") ++ } ++ return c.Chmod(fullpath, mode) ++} ++ + func (fs *ChrootHelper) Chroot(path string) (billy.Filesystem, error) { + fullpath, err := fs.underlyingPath(path) + if err != nil { +@@ -227,6 +420,11 @@ type file struct { + name string + } + ++type fileInfo struct { ++ os.FileInfo ++ name string ++} ++ + func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File { + filename = fs.Join(fs.Root(), filename) + filename, _ = filepath.Rel(fs.Root(), filename) +@@ -240,3 +438,7 @@ func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File { + func (f *file) Name() string { + return f.name + } ++ ++func (fi fileInfo) Name() string { ++ return fi.name ++} +diff --git a/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go b/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go +index 1efce0e..9fe131b 100644 +--- a/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go ++++ b/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go +@@ -13,7 +13,7 @@ type Polyfill struct { + c capabilities + } + +-type capabilities struct{ tempfile, dir, symlink, chroot bool } ++type capabilities struct{ tempfile, dir, symlink, chroot, chmod bool } + + // New creates a new filesystem wrapping up 'fs' the intercepts all the calls + // made and errors if fs doesn't implement any of the billy interfaces. +@@ -28,6 +28,7 @@ func New(fs billy.Basic) billy.Filesystem { + _, h.c.dir = h.Basic.(billy.Dir) + _, h.c.symlink = h.Basic.(billy.Symlink) + _, h.c.chroot = h.Basic.(billy.Chroot) ++ _, h.c.chmod = h.Basic.(billy.Chmod) + return h + } + +@@ -87,6 +88,14 @@ func (h *Polyfill) Chroot(path string) (billy.Filesystem, error) { + return h.Basic.(billy.Chroot).Chroot(path) + } + ++func (h *Polyfill) Chmod(path string, mode os.FileMode) error { ++ if !h.c.chmod { ++ return billy.ErrNotSupported ++ } ++ ++ return h.Basic.(billy.Chmod).Chmod(path, mode) ++} ++ + func (h *Polyfill) Root() string { + if !h.c.chroot { + return string(filepath.Separator) +diff --git a/vendor/github.com/go-git/go-billy/v5/memfs/memory.go b/vendor/github.com/go-git/go-billy/v5/memfs/memory.go +index 6cbd7d0..152cb9e 100644 +--- a/vendor/github.com/go-git/go-billy/v5/memfs/memory.go ++++ b/vendor/github.com/go-git/go-billy/v5/memfs/memory.go +@@ -177,6 +177,10 @@ func (fs *Memory) Remove(filename string) error { + return fs.s.Remove(filename) + } + ++func (fs *Memory) Chmod(path string, mode os.FileMode) error { ++ return fs.s.Chmod(path, mode) ++} ++ + // Falls back to Go's filepath.Join, which works differently depending on the + // OS where the code is being executed. + func (fs *Memory) Join(elem ...string) string { +diff --git a/vendor/github.com/go-git/go-billy/v5/memfs/storage.go b/vendor/github.com/go-git/go-billy/v5/memfs/storage.go +index 16b48ce..9960996 100644 +--- a/vendor/github.com/go-git/go-billy/v5/memfs/storage.go ++++ b/vendor/github.com/go-git/go-billy/v5/memfs/storage.go +@@ -169,6 +169,18 @@ func (s *storage) Remove(path string) error { + return nil + } + ++func (s *storage) Chmod(path string, mode os.FileMode) error { ++ path = clean(path) ++ ++ f, has := s.Get(path) ++ if !has { ++ return os.ErrNotExist ++ } ++ ++ f.mode = mode ++ return nil ++} ++ + func clean(path string) string { + return filepath.Clean(filepath.FromSlash(path)) + } +diff --git a/vendor/github.com/go-git/go-billy/v5/osfs/os.go b/vendor/github.com/go-git/go-billy/v5/osfs/os.go +index a7fe79f..0c240ef 100644 +--- a/vendor/github.com/go-git/go-billy/v5/osfs/os.go ++++ b/vendor/github.com/go-git/go-billy/v5/osfs/os.go +@@ -24,6 +24,9 @@ var Default = &ChrootOS{} + // New returns a new OS filesystem. + // By default paths are deduplicated, but still enforced + // under baseDir. For more info refer to WithDeduplicatePath. ++// ++// New returns ChrootOS by default for v5 compatibility. Users should prefer ++// New with WithBoundOS. + func New(baseDir string, opts ...Option) billy.Filesystem { + o := &options{ + deduplicatePath: true, +@@ -47,6 +50,8 @@ func WithBoundOS() Option { + } + + // WithChrootOS returns the option of using a Chroot filesystem OS. ++// ++// Deprecated: use WithBoundOS instead. + func WithChrootOS() Option { + return func(o *options) { + o.Type = ChrootOSFS +diff --git a/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go b/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go +index c0a6109..70e6a72 100644 +--- a/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go ++++ b/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go +@@ -20,6 +20,7 @@ + package osfs + + import ( ++ "errors" + "fmt" + "os" + "path/filepath" +@@ -29,6 +30,31 @@ import ( + "github.com/go-git/go-billy/v5" + ) + ++var ( ++ // ErrBaseDirCannotBeRemoved is returned when removing the BoundOS base dir. ++ ErrBaseDirCannotBeRemoved = errors.New("base dir cannot be removed") ++ ++ // ErrBaseDirCannotBeRenamed is returned when renaming the BoundOS base dir. ++ ErrBaseDirCannotBeRenamed = errors.New("base dir cannot be renamed") ++ ++ dotPrefixes = dotPathPrefixes() ++ dotSeparators = dotPathSeparators() ++) ++ ++func dotPathPrefixes() []string { ++ if filepath.Separator == '\\' { ++ return []string{"./", ".\\"} ++ } ++ return []string{"./"} ++} ++ ++func dotPathSeparators() string { ++ if filepath.Separator == '\\' { ++ return `/\` ++ } ++ return `/` ++} ++ + // BoundOS is a fs implementation based on the OS filesystem which is bound to + // a base dir. + // Prefer this fs implementation over ChrootOS. +@@ -54,6 +80,7 @@ func (fs *BoundOS) Create(filename string) (billy.File, error) { + } + + func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { ++ filename = fs.expandDot(filename) + fn, err := fs.abs(filename) + if err != nil { + return nil, err +@@ -62,6 +89,7 @@ func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy. + } + + func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) { ++ path = fs.expandDot(path) + dir, err := fs.abs(path) + if err != nil { + return nil, err +@@ -71,6 +99,12 @@ func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) { + } + + func (fs *BoundOS) Rename(from, to string) error { ++ if fs.isBaseDir(from) { ++ return ErrBaseDirCannotBeRenamed ++ } ++ from = fs.expandDot(from) ++ to = fs.expandDot(to) ++ + f, err := fs.abs(from) + if err != nil { + return err +@@ -89,6 +123,7 @@ func (fs *BoundOS) Rename(from, to string) error { + } + + func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error { ++ path = fs.expandDot(path) + dir, err := fs.abs(path) + if err != nil { + return err +@@ -101,6 +136,7 @@ func (fs *BoundOS) Open(filename string) (billy.File, error) { + } + + func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) { ++ filename = fs.expandDot(filename) + filename, err := fs.abs(filename) + if err != nil { + return nil, err +@@ -109,6 +145,11 @@ func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) { + } + + func (fs *BoundOS) Remove(filename string) error { ++ if fs.isBaseDir(filename) { ++ return ErrBaseDirCannotBeRemoved ++ } ++ filename = fs.expandDot(filename) ++ + fn, err := fs.abs(filename) + if err != nil { + return err +@@ -122,10 +163,19 @@ func (fs *BoundOS) Remove(filename string) error { + func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) { + if dir != "" { + var err error ++ dir = fs.expandDot(dir) + dir, err = fs.abs(dir) + if err != nil { + return nil, err + } ++ ++ _, err = os.Stat(dir) ++ if err != nil && os.IsNotExist(err) { ++ err = os.MkdirAll(dir, defaultDirectoryMode) ++ if err != nil { ++ return nil, err ++ } ++ } + } + + return tempFile(dir, prefix) +@@ -136,6 +186,11 @@ func (fs *BoundOS) Join(elem ...string) string { + } + + func (fs *BoundOS) RemoveAll(path string) error { ++ if fs.isBaseDir(path) { ++ return ErrBaseDirCannotBeRemoved ++ } ++ path = fs.expandDot(path) ++ + dir, err := fs.abs(path) + if err != nil { + return err +@@ -144,6 +199,7 @@ func (fs *BoundOS) RemoveAll(path string) error { + } + + func (fs *BoundOS) Symlink(target, link string) error { ++ link = fs.expandDot(link) + ln, err := fs.abs(link) + if err != nil { + return err +@@ -156,6 +212,7 @@ func (fs *BoundOS) Symlink(target, link string) error { + } + + func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) { ++ filename = fs.expandDot(filename) + filename = filepath.Clean(filename) + if !filepath.IsAbs(filename) { + filename = filepath.Join(fs.baseDir, filename) +@@ -167,6 +224,7 @@ func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) { + } + + func (fs *BoundOS) Readlink(link string) (string, error) { ++ link = fs.expandDot(link) + if !filepath.IsAbs(link) { + link = filepath.Clean(filepath.Join(fs.baseDir, link)) + } +@@ -176,6 +234,15 @@ func (fs *BoundOS) Readlink(link string) (string, error) { + return os.Readlink(link) + } + ++func (fs *BoundOS) Chmod(path string, mode os.FileMode) error { ++ path = fs.expandDot(path) ++ abspath, err := fs.abs(path) ++ if err != nil { ++ return err ++ } ++ return os.Chmod(abspath, mode) ++} ++ + // Chroot returns a new OS filesystem, with the base dir set to the + // result of joining the provided path with the underlying base dir. + func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) { +@@ -183,7 +250,7 @@ func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) { + if err != nil { + return nil, err + } +- return New(joined), nil ++ return New(joined, WithBoundOS()), nil + } + + // Root returns the current base dir of the billy.Filesystem. +@@ -204,6 +271,37 @@ func (fs *BoundOS) createDir(fullpath string) error { + return nil + } + ++func (fs *BoundOS) expandDot(path string) string { ++ if path == "." { ++ return fs.baseDir ++ } ++ for _, prefix := range dotPrefixes { ++ if strings.HasPrefix(path, prefix) { ++ path = strings.TrimLeft(strings.TrimPrefix(path, prefix), dotSeparators) ++ if path == "" { ++ return fs.baseDir ++ } ++ return path ++ } ++ } ++ return path ++} ++ ++func (fs *BoundOS) isBaseDir(path string) bool { ++ if path == "" || filepath.Clean(path) == "." { ++ return true ++ } ++ path = fs.expandDot(path) ++ if filepath.Clean(path) == filepath.Clean(fs.baseDir) { ++ return true ++ } ++ abspath, err := fs.abs(path) ++ if err != nil { ++ return false ++ } ++ return filepath.Clean(abspath) == filepath.Clean(fs.baseDir) ++} ++ + // abs transforms filename to an absolute path, taking into account the base dir. + // Relative paths won't be allowed to ascend the base dir, so `../file` will become + // `/working-dir/file`. +@@ -217,7 +315,7 @@ func (fs *BoundOS) abs(filename string) (string, error) { + + path, err := securejoin.SecureJoin(fs.baseDir, filename) + if err != nil { +- return "", nil ++ return "", err + } + + if fs.deduplicatePath { +@@ -230,24 +328,12 @@ func (fs *BoundOS) abs(filename string) (string, error) { + return path, nil + } + +-// insideBaseDir checks whether filename is located within +-// the fs.baseDir. +-func (fs *BoundOS) insideBaseDir(filename string) (bool, error) { +- if filename == fs.baseDir { +- return true, nil +- } +- if !strings.HasPrefix(filename, fs.baseDir+string(filepath.Separator)) { +- return false, fmt.Errorf("path outside base dir") +- } +- return true, nil +-} +- + // insideBaseDirEval checks whether filename is contained within + // a dir that is within the fs.baseDir, by first evaluating any symlinks + // that either filename or fs.baseDir may contain. + func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) { + // "/" contains all others. +- if fs.baseDir == "/" { ++ if fs.baseDir == "/" || fs.baseDir == filename { + return true, nil + } + dir, err := filepath.EvalSymlinks(filepath.Dir(filename)) +diff --git a/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go b/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go +index fd65e77..2fa9d8b 100644 +--- a/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go ++++ b/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go +@@ -14,6 +14,8 @@ import ( + // ChrootOS is a legacy filesystem based on a "soft chroot" of the os filesystem. + // Although this is still the default os filesystem, consider using BoundOS instead. + // ++// Deprecated: use New with WithBoundOS instead. ++// + // Behaviours of note: + // 1. A "soft chroot" translates the base dir to "/" for the purposes of the + // fs abstraction. +@@ -24,6 +26,14 @@ import ( + type ChrootOS struct{} + + func newChrootOS(baseDir string) billy.Filesystem { ++ if baseDir != "" { ++ resolved, err := filepath.EvalSymlinks(baseDir) ++ if err != nil { ++ return chroot.New(&ChrootOS{}, baseDir) ++ } ++ baseDir = resolved ++ } ++ + return chroot.New(&ChrootOS{}, baseDir) + } + +@@ -74,6 +84,10 @@ func (fs *ChrootOS) Remove(filename string) error { + return os.Remove(filename) + } + ++func (fs *ChrootOS) Chmod(path string, mode os.FileMode) error { ++ return os.Chmod(path, mode) ++} ++ + func (fs *ChrootOS) TempFile(dir, prefix string) (billy.File, error) { + if err := fs.createDir(dir + string(os.PathSeparator)); err != nil { + return nil, err +diff --git a/vendor/github.com/go-git/go-billy/v5/util/util.go b/vendor/github.com/go-git/go-billy/v5/util/util.go +index 2cdd832..cd869d6 100644 +--- a/vendor/github.com/go-git/go-billy/v5/util/util.go ++++ b/vendor/github.com/go-git/go-billy/v5/util/util.go +@@ -16,8 +16,6 @@ import ( + // can but returns the first error it encounters. If the path does not exist, + // RemoveAll returns nil (no error). + func RemoveAll(fs billy.Basic, path string) error { +- fs, path = getUnderlyingAndPath(fs, path) +- + if r, ok := fs.(removerAll); ok { + return r.RemoveAll(path) + } +@@ -39,7 +37,7 @@ func removeAll(fs billy.Basic, path string) error { + } + + // Otherwise, is this a directory we need to recurse into? +- dir, serr := fs.Stat(path) ++ dir, serr := lstat(fs, path) + if serr != nil { + if errors.Is(serr, os.ErrNotExist) { + return nil +@@ -48,8 +46,8 @@ func removeAll(fs billy.Basic, path string) error { + return serr + } + +- if !dir.IsDir() { +- // Not a directory; return the error from Remove. ++ if dir.Mode()&os.ModeSymlink != 0 || !dir.IsDir() { ++ // Not a directory we should recurse into; return the error from Remove. + return err + } + +@@ -62,7 +60,7 @@ func removeAll(fs billy.Basic, path string) error { + fis, err := dirfs.ReadDir(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { +- // Race. It was deleted between the Lstat and Open. ++ // Race. It was deleted between the Lstat and ReadDir. + // Return nil per RemoveAll's docs. + return nil + } +@@ -91,7 +89,18 @@ func removeAll(fs billy.Basic, path string) error { + } + + return err ++} + ++func lstat(filesystem billy.Basic, path string) (os.FileInfo, error) { ++ if sl, ok := filesystem.(billy.Symlink); ok { ++ // Avoid following a symlink substituted after the initial Remove fails. ++ fi, err := sl.Lstat(path) ++ if err == nil || !errors.Is(err, billy.ErrNotSupported) { ++ return fi, err ++ } ++ } ++ ++ return filesystem.Stat(path) + } + + // WriteFile writes data to a file named by filename in the given filesystem. +@@ -123,8 +132,10 @@ func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) ( + // We generate random temporary file names so that there's a good + // chance the file doesn't exist yet - keeps the number of tries in + // TempFile to a minimum. +-var rand uint32 +-var randmu sync.Mutex ++var ( ++ rand uint32 ++ randmu sync.Mutex ++) + + func reseed() uint32 { + return uint32(time.Now().UnixNano() + int64(os.Getpid())) +@@ -220,22 +231,6 @@ func getTempDir(fs billy.Basic) string { + return ".tmp" + } + +-type underlying interface { +- Underlying() billy.Basic +-} +- +-func getUnderlyingAndPath(fs billy.Basic, path string) (billy.Basic, string) { +- u, ok := fs.(underlying) +- if !ok { +- return fs, path +- } +- if ch, ok := fs.(billy.Chroot); ok { +- path = fs.Join(ch.Root(), path) +- } +- +- return u.Underlying(), path +-} +- + // ReadFile reads the named file and returns the contents from the given filesystem. + // A successful call returns err == nil, not err == EOF. + // Because ReadFile reads the whole file, it does not treat an EOF from Read +diff --git a/vendor/github.com/go-git/go-git/v5/config/config.go b/vendor/github.com/go-git/go-git/v5/config/config.go +index 33f6e37..3ae6a57 100644 +--- a/vendor/github.com/go-git/go-git/v5/config/config.go ++++ b/vendor/github.com/go-git/go-git/v5/config/config.go +@@ -61,6 +61,16 @@ type Config struct { + CommentChar string + // RepositoryFormatVersion identifies the repository format and layout version. + RepositoryFormatVersion format.RepositoryFormatVersion ++ // ProtectNTFS controls whether NTFS-specific path protections are ++ // applied (e.g. rejecting .git trailing spaces/periods, alternate ++ // data streams, 8.3 short names). When unset, defaults to true on ++ // Windows. ++ ProtectNTFS OptBool ++ // ProtectHFS controls whether HFS+-specific path protections are ++ // applied (e.g. rejecting .git with Unicode zero-width or ++ // directional characters that HFS+ would normalize away). ++ // When unset, defaults to true on macOS. ++ ProtectHFS OptBool + } + + User struct { +@@ -266,6 +276,8 @@ const ( + repositoryFormatVersionKey = "repositoryformatversion" + objectFormat = "objectformat" + mirrorKey = "mirror" ++ protectNTFSKey = "protectNTFS" ++ protectHFSKey = "protectHFS" + + // DefaultPackWindow holds the number of previous objects used to + // generate deltas. The value 10 is the same used by git command. +@@ -309,6 +321,14 @@ func (c *Config) unmarshalCore() { + + c.Core.Worktree = s.Options.Get(worktreeKey) + c.Core.CommentChar = s.Options.Get(commentCharKey) ++ ++ if parsed := parseConfigBool(s.Options.Get(protectNTFSKey)); parsed.IsSet() { ++ c.Core.ProtectNTFS = parsed ++ } ++ ++ if parsed := parseConfigBool(s.Options.Get(protectHFSKey)); parsed.IsSet() { ++ c.Core.ProtectHFS = parsed ++ } + } + + func (c *Config) unmarshalUser() { +@@ -379,7 +399,8 @@ func unmarshalSubmodules(fc *format.Config, submodules map[string]*Submodule) { + m := &Submodule{} + m.unmarshal(sub) + +- if m.Validate() == ErrModuleBadPath { ++ if err := m.Validate(); errors.Is(err, ErrModuleBadPath) || ++ errors.Is(err, ErrModuleBadName) { + continue + } + +@@ -436,6 +457,14 @@ func (c *Config) marshalCore() { + if c.Core.Worktree != "" { + s.SetOption(worktreeKey, c.Core.Worktree) + } ++ ++ if c.Core.ProtectNTFS.IsSet() { ++ s.SetOption(protectNTFSKey, c.Core.ProtectNTFS.FormatBool()) ++ } ++ ++ if c.Core.ProtectHFS.IsSet() { ++ s.SetOption(protectHFSKey, c.Core.ProtectHFS.FormatBool()) ++ } + } + + func (c *Config) marshalExtensions() { +diff --git a/vendor/github.com/go-git/go-git/v5/config/modules.go b/vendor/github.com/go-git/go-git/v5/config/modules.go +index 1c10aa3..5fdd838 100644 +--- a/vendor/github.com/go-git/go-git/v5/config/modules.go ++++ b/vendor/github.com/go-git/go-git/v5/config/modules.go +@@ -3,8 +3,11 @@ package config + import ( + "bytes" + "errors" ++ "fmt" + "regexp" ++ "strings" + ++ "github.com/go-git/go-git/v5/internal/pathutil" + format "github.com/go-git/go-git/v5/plumbing/format/config" + ) + +@@ -12,6 +15,7 @@ var ( + ErrModuleEmptyURL = errors.New("module config: empty URL") + ErrModuleEmptyPath = errors.New("module config: empty path") + ErrModuleBadPath = errors.New("submodule has an invalid path") ++ ErrModuleBadName = errors.New("ignoring suspicious submodule name") + ) + + var ( +@@ -94,6 +98,10 @@ type Submodule struct { + + // Validate validates the fields and sets the default values. + func (m *Submodule) Validate() error { ++ if err := validSubmoduleName(m.Name); err != nil { ++ return fmt.Errorf("%w: %q", ErrModuleBadName, m.Name) ++ } ++ + if m.Path == "" { + return ErrModuleEmptyPath + } +@@ -109,6 +117,50 @@ func (m *Submodule) Validate() error { + return nil + } + ++// validSubmoduleName mirrors canonical Git's check_submodule_name in ++// submodule-config.c [1]: reject empty names and any name with a ".." ++// path component, using both '/' and '\\' as separators so the rule ++// is consistent across platforms. The component check is delegated to ++// `pathutil.IsHFSDot` and `pathutil.IsNTFSDot` with `.` as the needle, ++// which both cover the bare ".." case and reject components that ++// resolve to ".." after HFS+ Unicode normalisation (ignored code ++// points, e.g. `..`) or NTFS trailing-space/dot/ADS ++// canonicalisation (e.g. `.. `, `..::$INDEX_ALLOCATION`). ++// `.gitmodules` is attacker-controlled by definition, so both checks ++// run unconditionally regardless of host OS. ++// ++// The additional checks (bare ".", NUL byte, leading or trailing ++// separator, drive-letter prefix) close go-git-specific edge cases ++// the canonical loop does not exercise: canonical Git treats names ++// as opaque C strings, while Go strings carry NULs through and the ++// billy filesystem layer is path-aware in ways Git's working storage ++// is not. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/submodule-config.c#L214-L237 ++func validSubmoduleName(name string) error { ++ if name == "" || name == "." { ++ return ErrModuleBadName ++ } ++ for _, seg := range strings.FieldsFunc(name, isPathSep) { ++ if pathutil.IsHFSDot(seg, ".") || pathutil.IsNTFSDot(seg, ".", "") { ++ return ErrModuleBadName ++ } ++ } ++ // go-git-specific defensive checks beyond canonical Git. ++ if strings.ContainsRune(name, 0) { ++ return ErrModuleBadName ++ } ++ if isPathSep(rune(name[0])) || isPathSep(rune(name[len(name)-1])) { ++ return ErrModuleBadName ++ } ++ if len(name) >= 2 && name[1] == ':' { ++ return ErrModuleBadName ++ } ++ return nil ++} ++ ++func isPathSep(r rune) bool { return r == '/' || r == '\\' } ++ + func (m *Submodule) unmarshal(s *format.Subsection) { + m.raw = s + +diff --git a/vendor/github.com/go-git/go-git/v5/config/optbool.go b/vendor/github.com/go-git/go-git/v5/config/optbool.go +new file mode 100644 +index 0000000..cb89fbf +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/config/optbool.go +@@ -0,0 +1,82 @@ ++package config ++ ++import ( ++ "strconv" ++ "strings" ++) ++ ++// OptBool is a tri-state boolean: unset, explicitly false, or explicitly true. ++// Its zero value (OptBoolUnset) means the setting was not specified, which ++// allows merge logic based on reflect.Value.IsZero to skip unset fields while ++// still letting an explicit "false" override a previously set "true". ++type OptBool byte ++ ++const ( ++ // OptBoolUnset indicates the setting was not specified. ++ OptBoolUnset OptBool = iota ++ // OptBoolFalse indicates the setting was explicitly set to false. ++ OptBoolFalse ++ // OptBoolTrue indicates the setting was explicitly set to true. ++ OptBoolTrue ++) ++ ++// NewOptBool converts a plain bool into an OptBool. ++func NewOptBool(v bool) OptBool { ++ if v { ++ return OptBoolTrue ++ } ++ return OptBoolFalse ++} ++ ++// IsTrue returns whether the value is explicitly true. ++func (o OptBool) IsTrue() bool { return o == OptBoolTrue } ++ ++// IsSet returns whether the value was explicitly specified (true or false). ++func (o OptBool) IsSet() bool { return o != OptBoolUnset } ++ ++func (o OptBool) String() string { ++ switch o { ++ case OptBoolTrue: ++ return "true" ++ case OptBoolFalse: ++ return "false" ++ default: ++ return "unset" ++ } ++} ++ ++// FormatBool returns the strconv-formatted value. Only meaningful when IsSet. ++func (o OptBool) FormatBool() string { ++ return strconv.FormatBool(o.IsTrue()) ++} ++ ++// parseConfigBool mirrors upstream Git's git_parse_maybe_bool: it ++// accepts true/yes/on (→ OptBoolTrue) and false/no/off (→ ++// OptBoolFalse) case-insensitively, plus any decimal integer (zero ++// → OptBoolFalse, non-zero → OptBoolTrue). Empty or otherwise ++// unrecognised values return OptBoolUnset, leaving the caller's ++// platform default in place. The empty-string handling is the only ++// intentional divergence from upstream, which returns false for ++// empty: in our unmarshalCore caller, an empty value means the key ++// is unset and the platform default should apply. ++// ++// Reference: upstream Git git_parse_maybe_bool_text at parse.c ++// L157-L173 and git_parse_maybe_bool at parse.c L174-L182 in tag ++// v2.54.0[1]. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/parse.c#L157-L182 ++func parseConfigBool(v string) OptBool { ++ switch strings.ToLower(v) { ++ case "true", "yes", "on": ++ return OptBoolTrue ++ case "false", "no", "off": ++ return OptBoolFalse ++ } ++ if i, err := strconv.Atoi(v); err == nil { ++ if i != 0 { ++ return OptBoolTrue ++ } ++ return OptBoolFalse ++ } ++ return OptBoolUnset ++} +diff --git a/vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go b/vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go +new file mode 100644 +index 0000000..e50ee9c +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go +@@ -0,0 +1,21 @@ ++package pathutil ++ ++import "strings" ++ ++// IsDotGitName reports whether name is `.git` or its 8.3 NTFS short ++// alias `git~1`, case-insensitively. Both are forbidden as path ++// components (and as submodule names) because they refer to the ++// repository's own metadata directory. ++// ++// File names that do not conform to the 8.3 format (up to eight ++// characters for the basename, three for the file extension) are ++// associated with a so-called "short name" on NTFS — at least on ++// the `C:` drive by default — which means that `git~1/` is a valid ++// way to refer to `.git/`. ++func IsDotGitName(name string) bool { ++ switch strings.ToLower(name) { ++ case ".git", "git~1": ++ return true ++ } ++ return false ++} +diff --git a/vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go b/vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go +new file mode 100644 +index 0000000..66fc12f +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go +@@ -0,0 +1,99 @@ ++package pathutil ++ ++import "unicode" ++ ++// hfsIgnoredCodepoints contains Unicode code points that HFS+ ignores ++// during path normalization. A path component containing these ++// characters between the bytes of ".git" (or ".gitmodules", etc.) ++// will be treated as that name by HFS+, so they have to be filtered ++// out before comparison. ++// ++// See upstream Git utf8.c next_hfs_char in tag v2.54.0[1]. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/utf8.c#L703-L740 ++var hfsIgnoredCodepoints = map[rune]struct{}{ ++ 0x200c: {}, // ZERO WIDTH NON-JOINER ++ 0x200d: {}, // ZERO WIDTH JOINER ++ 0x200e: {}, // LEFT-TO-RIGHT MARK ++ 0x200f: {}, // RIGHT-TO-LEFT MARK ++ 0x202a: {}, // LEFT-TO-RIGHT EMBEDDING ++ 0x202b: {}, // RIGHT-TO-LEFT EMBEDDING ++ 0x202c: {}, // POP DIRECTIONAL FORMATTING ++ 0x202d: {}, // LEFT-TO-RIGHT OVERRIDE ++ 0x202e: {}, // RIGHT-TO-LEFT OVERRIDE ++ 0x206a: {}, // INHIBIT SYMMETRIC SWAPPING ++ 0x206b: {}, // ACTIVATE SYMMETRIC SWAPPING ++ 0x206c: {}, // INHIBIT ARABIC FORM SHAPING ++ 0x206d: {}, // ACTIVATE ARABIC FORM SHAPING ++ 0x206e: {}, // NATIONAL DIGIT SHAPES ++ 0x206f: {}, // NOMINAL DIGIT SHAPES ++ 0xfeff: {}, // ZERO WIDTH NO-BREAK SPACE ++} ++ ++// IsHFSDot reports whether part would be treated as "." on an ++// HFS+ filesystem after stripping ignored Unicode code points and ++// folding ASCII to lower case. The needle is the lowercase ASCII ++// suffix without the leading dot (e.g. "git", "gitmodules"). It ++// mirrors upstream Git's is_hfs_dot_generic and is the building ++// block of IsHFSDotGit / IsHFSDotGitmodules. ++// ++// Reference: upstream Git utf8.c is_hfs_dot_generic at L741-L774 and ++// the dotgit family at L784-L809 in tag v2.54.0[1]. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/utf8.c#L741-L809 ++func IsHFSDot(part, needle string) bool { ++ runes := []rune(part) ++ i := 0 ++ ++ // skip ignored code points, then expect '.' ++ for i < len(runes) { ++ if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok { ++ break ++ } ++ i++ ++ } ++ if i >= len(runes) || runes[i] != '.' { ++ return false ++ } ++ i++ ++ ++ // match needle case-insensitively, skipping ignored code points ++ for _, expected := range needle { ++ for i < len(runes) { ++ if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok { ++ break ++ } ++ i++ ++ } ++ if i >= len(runes) { ++ return false ++ } ++ r := runes[i] ++ if r > 127 { ++ return false ++ } ++ if unicode.ToLower(r) != expected { ++ return false ++ } ++ i++ ++ } ++ ++ // skip trailing ignored code points ++ for i < len(runes) { ++ if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok { ++ break ++ } ++ i++ ++ } ++ ++ // must be at end of component ++ return i == len(runes) ++} ++ ++// IsHFSDotGit reports whether part is an HFS+ equivalent of ".git". ++func IsHFSDotGit(part string) bool { return IsHFSDot(part, "git") } ++ ++// IsHFSDotGitmodules reports whether part is an HFS+ equivalent of ++// ".gitmodules", catching attempts to plant the file via Unicode ++// code points that HFS+ would strip during normalisation. ++func IsHFSDotGitmodules(part string) bool { return IsHFSDot(part, "gitmodules") } +diff --git a/vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go b/vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go +new file mode 100644 +index 0000000..2ca6c28 +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go +@@ -0,0 +1,187 @@ ++package pathutil ++ ++import "strings" ++ ++// IsNTFSDotGit ports upstream Git's is_ntfs_dotgit. It detects path ++// components that NTFS would resolve to ".git": the canonical name ++// itself and its 8.3 short-name alias "git~1", each followed by any ++// number of trailing spaces or periods (which NTFS silently trims) ++// and an optional Alternate Data Stream suffix (":"). The ++// bare strings ".git" and "git~1" also match, mirroring upstream. ++// ++// Reference: upstream Git path.c is_ntfs_dotgit at L1415-L1449 ++// in tag v2.54.0[1]. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/path.c#L1415-L1449 ++func IsNTFSDotGit(part string) bool { ++ var i int ++ switch { ++ case len(part) >= 4 && part[0] == '.' && ++ asciiToLower(part[1]) == 'g' && ++ asciiToLower(part[2]) == 'i' && ++ asciiToLower(part[3]) == 't': ++ i = 4 ++ case len(part) >= 5 && ++ asciiToLower(part[0]) == 'g' && ++ asciiToLower(part[1]) == 'i' && ++ asciiToLower(part[2]) == 't' && ++ part[3] == '~' && part[4] == '1': ++ i = 5 ++ default: ++ return false ++ } ++ ++ for ; i < len(part); i++ { ++ c := part[i] ++ if c == ':' { ++ return true ++ } ++ if c != '.' && c != ' ' { ++ return false ++ } ++ } ++ return true ++} ++ ++// WindowsValidPath reports whether part is a valid Windows / NTFS ++// path component for the worktree filesystem abstraction. It rejects ++// NTFS-disguised variants of `.git` and `git~1` (trailing spaces, ++// periods, Alternate Data Streams) and Windows reserved device ++// names. Bare `.git` and `git~1` are allowed at this layer; the ++// caller decides whether they are permissible at the current path ++// position. ++func WindowsValidPath(part string) bool { ++ if IsNTFSDotGit(part) && !IsDotGitName(part) { ++ return false ++ } ++ return !isWindowsReservedName(part) ++} ++ ++// windowsReservedNames lists the Windows reserved device names. ++// A path component is reserved if its base name (ignoring trailing ++// spaces, extensions, and NTFS Alternate Data Streams) matches one of ++// these case-insensitively. ++// ++// See upstream Git compat/mingw.c is_valid_win32_path(). ++var windowsReservedNames = []string{ ++ "CON", "PRN", "AUX", "NUL", ++ "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", ++ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", ++ "CONIN$", "CONOUT$", ++} ++ ++func isWindowsReservedName(part string) bool { ++ for _, name := range windowsReservedNames { ++ if len(part) < len(name) { ++ continue ++ } ++ if !strings.EqualFold(part[:len(name)], name) { ++ continue ++ } ++ // Exact match or followed by space, dot, colon (ADS), or separator. ++ if len(part) == len(name) { ++ return true ++ } ++ switch part[len(name)] { ++ case ' ', '.', ':': ++ return true ++ } ++ } ++ return false ++} ++ ++// IsNTFSDot ports upstream Git's is_ntfs_dot_generic. It detects NTFS ++// path-component variants of a dotfile name that attackers can use to ++// bypass case-insensitive comparisons against the canonical name on ++// Windows. The dotgit parameter is the lowercase name without the ++// leading dot (e.g. "gitmodules"); shortnamePrefix is the canonical ++// 6-character NTFS short-name prefix used as a fall-back match ++// (e.g. "gi7eba" for ".gitmodules"). ++// ++// Reference: upstream Git path.c is_ntfs_dot_generic at L1451-L1507 ++// in tag v2.54.0[1]. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/path.c#L1451-L1507 ++func IsNTFSDot(name, dotgit, shortnamePrefix string) bool { ++ // onlySpacesAndPeriods returns true when the suffix from start ++ // onwards consists only of trailing spaces and periods, possibly ++ // terminated by a NTFS Alternate Data Stream colon. Mirrors the ++ // only_spaces_and_periods label in upstream's is_ntfs_dot_generic. ++ onlySpacesAndPeriods := func(start int) bool { ++ for i := start; i < len(name); i++ { ++ c := name[i] ++ if c == ':' { ++ return true ++ } ++ if c != ' ' && c != '.' { ++ return false ++ } ++ } ++ return true ++ } ++ ++ // Pattern 1: "." prefix + trailing spaces / periods / ADS. ++ if len(name) >= len(dotgit)+1 && name[0] == '.' && ++ strings.EqualFold(name[1:1+len(dotgit)], dotgit) { ++ if onlySpacesAndPeriods(len(dotgit) + 1) { ++ return true ++ } ++ } ++ ++ // Pattern 2: standard NTFS short name ~[1-4]. ++ if len(dotgit) >= 6 && len(name) >= 8 && ++ strings.EqualFold(name[:6], dotgit[:6]) && ++ name[6] == '~' && name[7] >= '1' && name[7] <= '4' { ++ if onlySpacesAndPeriods(8) { ++ return true ++ } ++ } ++ ++ // Pattern 3: fall-back NTFS short name keyed by shortnamePrefix. ++ if len(shortnamePrefix) < 6 || len(name) < 8 { ++ return false ++ } ++ sawTilde := false ++ i := 0 ++ for i < 8 { ++ c := name[i] ++ switch { ++ case sawTilde: ++ if c < '0' || c > '9' { ++ return false ++ } ++ case c == '~': ++ i++ ++ if i >= len(name) || name[i] < '1' || name[i] > '9' { ++ return false ++ } ++ sawTilde = true ++ case i >= 6: ++ return false ++ case c&0x80 != 0: ++ return false ++ default: ++ if asciiToLower(c) != shortnamePrefix[i] { ++ return false ++ } ++ } ++ i++ ++ } ++ return onlySpacesAndPeriods(8) ++} ++ ++// IsNTFSDotGitmodules reports whether part is an NTFS-equivalent of ++// ".gitmodules" — the file name (or any of its variants that NTFS ++// would resolve to it) that attackers can use to plant submodule ++// configuration disguised as a symlink. The 6-character canonical ++// short-name prefix "gi7eba" mirrors upstream Git's is_ntfs_dotgitmodules. ++func IsNTFSDotGitmodules(part string) bool { ++ return IsNTFSDot(part, "gitmodules", "gi7eba") ++} ++ ++func asciiToLower(c byte) byte { ++ if c >= 'A' && c <= 'Z' { ++ return c + ('a' - 'A') ++ } ++ return c ++} +diff --git a/vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go b/vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go +new file mode 100644 +index 0000000..e610cd4 +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go +@@ -0,0 +1,66 @@ ++package pathutil ++ ++import ( ++ "fmt" ++ "path/filepath" ++ "strings" ++) ++ ++// ErrInvalidPath is returned by ValidTreePath when its argument is ++// not a safe path to materialise into the worktree. ++var ErrInvalidPath = fmt.Errorf("invalid path") ++ ++// ValidTreePath rejects path strings that, if materialised into a ++// worktree, would let an attacker-controlled tree entry escape the ++// worktree or rewrite repository metadata. It rejects: ++// ++// - control characters (< 0x20, 0x7f); ++// - empty paths and "." / ".." components; ++// - Windows volume name prefixes (e.g. C:); ++// - .git, its 8.3 NTFS short-name git~1, plus their HFS+ and NTFS ++// variants — at every position, not just the root. ++// ++// HFS+/NTFS variants of `.git` are always rejected at this layer ++// regardless of runtime config: tree paths are canonical UTF-8 with ++// no zero-width characters or NTFS short-name forms, so an entry ++// that looks like a disguised `.git` is suspicious anywhere. Windows ++// reserved device names (CON, NUL, etc.) are not policed here — they ++// are legitimate filenames on non-Windows filesystems and upstream ++// Git accepts them. The wrapper layer (validPath in package git) ++// rejects them at materialisation time when core.protectNTFS is on. ++// ++// Mirrors upstream Git's verify_path_internal at read-cache.c#L987 ++// in tag v2.54.0[1] with protect_hfs / protect_ntfs treated as ++// always-on for `.git`-disguise detection (tree paths are not ++// application-supplied) and is_valid_win32_path left to the wrapper. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/read-cache.c#L987 ++func ValidTreePath(p string) error { ++ for i := 0; i < len(p); i++ { ++ if p[i] < 0x20 || p[i] == 0x7f { ++ return fmt.Errorf("%w %q: contains control character", ErrInvalidPath, p) ++ } ++ } ++ ++ parts := strings.FieldsFunc(p, func(r rune) bool { return r == '\\' || r == '/' }) ++ if len(parts) == 0 { ++ return fmt.Errorf("%w: %q", ErrInvalidPath, p) ++ } ++ ++ // Volume names are not supported, in both formats: \\ and :. ++ if vol := filepath.VolumeName(p); vol != "" { ++ return fmt.Errorf("%w: %q", ErrInvalidPath, p) ++ } ++ ++ for _, part := range parts { ++ if part == "." || part == ".." { ++ return fmt.Errorf("%w %q: cannot use %q", ErrInvalidPath, p, part) ++ } ++ ++ if IsDotGitName(part) || IsHFSDotGit(part) || IsNTFSDotGit(part) { ++ return fmt.Errorf("%w component: %q", ErrInvalidPath, p) ++ } ++ } ++ ++ return nil ++} +diff --git a/vendor/github.com/go-git/go-git/v5/internal/url/url.go b/vendor/github.com/go-git/go-git/v5/internal/url/url.go +index 2662448..e40947c 100644 +--- a/vendor/github.com/go-git/go-git/v5/internal/url/url.go ++++ b/vendor/github.com/go-git/go-git/v5/internal/url/url.go +@@ -2,12 +2,14 @@ package url + + import ( + "regexp" ++ "runtime" ++ "strings" + ) + + var ( + isSchemeRegExp = regexp.MustCompile(`^[^:]+://`) + +- // Ref: https://github.com/git/git/blob/master/Documentation/urls.txt#L37 ++ // Ref: https://github.com/git/git/blob/v2.54.0/Documentation/urls.adoc#L41-L48 + scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P[^@]+)@)?(?P[^:\s]+):(?:(?P[0-9]{1,5}):)?(?P[^\\].*)$`) + ) + +@@ -20,7 +22,38 @@ func MatchesScheme(url string) bool { + // MatchesScpLike returns true if the given string matches an SCP-like + // format scheme. + func MatchesScpLike(url string) bool { +- return scpLikeUrlRegExp.MatchString(url) ++ if !scpLikeUrlRegExp.MatchString(url) { ++ return false ++ } ++ // Mirror canonical Git's url_is_local_not_ssh in connect.c[1] for ++ // the cases the regex above cannot disambiguate by itself: a URL ++ // is treated as a local path (not SCP-style SSH) when a `/` ++ // precedes the first `:` (e.g. `./relative:path`, ++ // `/abs/with:colon/file`), or — on Windows only — when it has a ++ // DOS drive prefix like `C:foo` where the host is a single ++ // ASCII letter. ++ // ++ // [1]: https://github.com/git/git/blob/v2.54.0/connect.c#L710-L716 ++ if before, _, _ := strings.Cut(url, ":"); strings.Contains(before, "/") { ++ return false ++ } ++ if runtime.GOOS == "windows" && hasDosDrivePrefix(url) { ++ return false ++ } ++ return true ++} ++ ++// hasDosDrivePrefix reports whether s begins with `:` (a ++// Windows drive prefix such as `C:` or `c:`). Mirrors canonical Git's ++// win32_has_dos_drive_prefix[1]. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/compat/win32/path-utils.c#L20-L29 ++func hasDosDrivePrefix(s string) bool { ++ if len(s) < 2 || s[1] != ':' { ++ return false ++ } ++ c := s[0] ++ return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') + } + + // FindScpLikeComponents returns the user, host, port and path of the +diff --git a/vendor/github.com/go-git/go-git/v5/options.go b/vendor/github.com/go-git/go-git/v5/options.go +index 3cd0f95..e2c77ed 100644 +--- a/vendor/github.com/go-git/go-git/v5/options.go ++++ b/vendor/github.com/go-git/go-git/v5/options.go +@@ -8,6 +8,7 @@ import ( + "time" + + "github.com/ProtonMail/go-crypto/openpgp" ++ + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + formatcfg "github.com/go-git/go-git/v5/plumbing/format/config" +@@ -72,9 +73,16 @@ type CloneOptions struct { + // Tags describe how the tags will be fetched from the remote repository, + // by default is AllTags. + Tags TagMode +- // InsecureSkipTLS skips ssl verify if protocol is https ++ // InsecureSkipTLS skips SSL verification if protocol is HTTPS. + InsecureSkipTLS bool +- // CABundle specify additional ca bundle with system cert pool ++ // ClientCert is the client certificate to use for mutual TLS authentication ++ // over the HTTPS protocol. ++ ClientCert []byte ++ // ClientKey is the client key to use for mutual TLS authentication over ++ // the HTTPS protocol. ++ ClientKey []byte ++ // CABundle specifies an additional CA bundle to use together with the ++ // system cert pool. + CABundle []byte + // ProxyOptions provides info required for connecting to a proxy. + ProxyOptions transport.ProxyOptions +@@ -153,9 +161,16 @@ type PullOptions struct { + // Force allows the pull to update a local branch even when the remote + // branch does not descend from it. + Force bool +- // InsecureSkipTLS skips ssl verify if protocol is https ++ // InsecureSkipTLS skips SSL verification if protocol is HTTPS. + InsecureSkipTLS bool +- // CABundle specify additional ca bundle with system cert pool ++ // ClientCert is the client certificate to use for mutual TLS authentication ++ // over the HTTPS protocol. ++ ClientCert []byte ++ // ClientKey is the client key to use for mutual TLS authentication over ++ // the HTTPS protocol. ++ ClientKey []byte ++ // CABundle specifies an additional CA bundle to use together with the ++ // system cert pool. + CABundle []byte + // ProxyOptions provides info required for connecting to a proxy. + ProxyOptions transport.ProxyOptions +@@ -211,9 +226,16 @@ type FetchOptions struct { + // Force allows the fetch to update a local branch even when the remote + // branch does not descend from it. + Force bool +- // InsecureSkipTLS skips ssl verify if protocol is https ++ // InsecureSkipTLS skips SSL verification if protocol is HTTPS. + InsecureSkipTLS bool +- // CABundle specify additional ca bundle with system cert pool ++ // ClientCert is the client certificate to use for mutual TLS authentication ++ // over the HTTPS protocol. ++ ClientCert []byte ++ // ClientKey is the client key to use for mutual TLS authentication over ++ // the HTTPS protocol. ++ ClientKey []byte ++ // CABundle specifies an additional CA bundle to use together with the ++ // system cert pool. + CABundle []byte + // ProxyOptions provides info required for connecting to a proxy. + ProxyOptions transport.ProxyOptions +@@ -267,9 +289,16 @@ type PushOptions struct { + // Force allows the push to update a remote branch even when the local + // branch does not descend from it. + Force bool +- // InsecureSkipTLS skips ssl verify if protocol is https ++ // InsecureSkipTLS skips SSL verification if protocol is HTTPS. + InsecureSkipTLS bool +- // CABundle specify additional ca bundle with system cert pool ++ // ClientCert is the client certificate to use for mutual TLS authentication ++ // over the HTTPS protocol. ++ ClientCert []byte ++ // ClientKey is the client key to use for mutual TLS authentication over ++ // the HTTPS protocol. ++ ClientKey []byte ++ // CABundle specifies an additional CA bundle to use together with the ++ // system cert pool. + CABundle []byte + // RequireRemoteRefs only allows a remote ref to be updated if its current + // value is the one specified here. +@@ -693,9 +722,16 @@ func (o *CreateTagOptions) loadConfigTagger(r *Repository) error { + type ListOptions struct { + // Auth credentials, if required, to use with the remote repository. + Auth transport.AuthMethod +- // InsecureSkipTLS skips ssl verify if protocol is https ++ // InsecureSkipTLS skips SSL verification if protocol is HTTPS. + InsecureSkipTLS bool +- // CABundle specify additional ca bundle with system cert pool ++ // ClientCert is the client certificate to use for mutual TLS authentication ++ // over the HTTPS protocol. ++ ClientCert []byte ++ // ClientKey is the client key to use for mutual TLS authentication over ++ // the HTTPS protocol. ++ ClientKey []byte ++ // CABundle specifies an additional CA bundle to use together with the ++ // system cert pool. + CABundle []byte + // PeelingOption defines how peeled objects are handled during a + // remote list. +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go +index 9afdce3..825fad9 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go +@@ -1,10 +1,13 @@ + package idxfile + + import ( +- "bufio" + "bytes" ++ "crypto" ++ "encoding/hex" + "errors" ++ "fmt" + "io" ++ "io/fs" + + "github.com/go-git/go-git/v5/plumbing/hash" + "github.com/go-git/go-git/v5/utils/binary" +@@ -23,42 +26,108 @@ const ( + objectIDLength = hash.Size + ) + ++// Byte sizes of the idx v2 layout elements, used by the size formula ++// in [validateIdxV2Size]. See [gitformat-pack] for the canonical ++// layout. ++// ++// [gitformat-pack]: https://git-scm.com/docs/gitformat-pack ++const ( ++ headerLen = 8 // magic + version ++ fanoutLen = fanout * 4 // uint32 per bucket ++ crc32Len = 4 // CRC32 per object ++ offset32Len = 4 // 32-bit offset per object ++ offset64Len = 8 // 64-bit overflow offset ++ trailerHashes = 2 // pack checksum + idx checksum, each hashsz ++) ++ ++// statInput is the optional shape the [Decoder] probes for at the ++// start of [Decoder.Decode] to learn the on-disk length of the idx ++// blob, which it uses to validate the canonical-Git size formula ++// before any allocations driven by the fanout table. Callers that ++// pass an [*os.File] or a `billy.File` backed by an `*os.File` ++// (the production call sites in `storage/filesystem`) satisfy it ++// directly; arbitrary [io.Reader]s do not, and decode for them ++// retains the pre-existing behaviour of erroring out at the ++// truncated-payload boundary instead. ++// ++// The interface is intentionally unexported so the public ++// [NewDecoder] signature stays compatible with v5. ++type statInput interface { ++ Stat() (fs.FileInfo, error) ++} ++ + // Decoder reads and decodes idx files from an input stream. + type Decoder struct { +- *bufio.Reader ++ io.Reader ++ src io.Reader ++ h hash.Hash + } + + // NewDecoder builds a new idx stream decoder, that reads from r. + func NewDecoder(r io.Reader) *Decoder { +- return &Decoder{bufio.NewReader(r)} ++ h := hash.New(crypto.SHA1) ++ tr := io.TeeReader(r, h) ++ return &Decoder{tr, r, h} + } + + // Decode reads from the stream and decode the content into the MemoryIndex struct. + func (d *Decoder) Decode(idx *MemoryIndex) error { ++ idxSize := int64(-1) ++ if in, ok := d.src.(statInput); ok { ++ fi, err := in.Stat() ++ if err != nil { ++ return fmt.Errorf("%w: stat input: %w", ErrMalformedIdxFile, err) ++ } ++ idxSize = fi.Size() ++ } ++ + if err := validateHeader(d); err != nil { + return err + } + +- flow := []func(*MemoryIndex, io.Reader) error{ ++ headerFlow := []func(*MemoryIndex, io.Reader) error{ + readVersion, + readFanout, ++ } ++ for _, f := range headerFlow { ++ if err := f(idx, d); err != nil { ++ return err ++ } ++ } ++ ++ if idxSize >= 0 { ++ if err := validateIdxV2Size(idx, idxSize); err != nil { ++ return err ++ } ++ } ++ ++ bodyFlow := []func(*MemoryIndex, io.Reader) error{ + readObjectNames, + readCRC32, + readOffsets, +- readChecksums, ++ readPackChecksum, + } +- +- for _, f := range flow { ++ for _, f := range bodyFlow { + if err := f(idx, d); err != nil { + return err + } + } + ++ actual := d.h.Sum(nil) ++ if err := readIdxChecksum(idx, d); err != nil { ++ return err ++ } ++ ++ if !bytes.Equal(actual, idx.IdxChecksum[:]) { ++ return fmt.Errorf("%w: checksum mismatch: %q instead of %q", ++ ErrMalformedIdxFile, hex.EncodeToString(idx.IdxChecksum[:]), hex.EncodeToString(actual)) ++ } ++ + return nil + } + + func validateHeader(r io.Reader) error { +- var h = make([]byte, 4) ++ h := make([]byte, 4) + if _, err := io.ReadFull(r, h); err != nil { + return err + } +@@ -76,8 +145,8 @@ func readVersion(idx *MemoryIndex, r io.Reader) error { + return err + } + +- if v > VersionSupported { +- return ErrUnsupportedVersion ++ if v != VersionSupported { ++ return fmt.Errorf("%w: v%d", ErrUnsupportedVersion, v) + } + + idx.Version = v +@@ -91,6 +160,10 @@ func readFanout(idx *MemoryIndex, r io.Reader) error { + return err + } + ++ if k > 0 && n < idx.Fanout[k-1] { ++ return fmt.Errorf("%w: fanout table is not monotonically non-decreasing at entry %d", ErrMalformedIdxFile, k) ++ } ++ + idx.Fanout[k] = n + idx.FanoutMapping[k] = noMapping + } +@@ -140,7 +213,7 @@ func readCRC32(idx *MemoryIndex, r io.Reader) error { + } + + func readOffsets(idx *MemoryIndex, r io.Reader) error { +- var o64cnt int ++ var o64cnt int64 + for k := 0; k < fanout; k++ { + if pos := idx.FanoutMapping[k]; pos != noMapping { + if _, err := io.ReadFull(r, idx.Offset32[pos]); err != nil { +@@ -165,14 +238,118 @@ func readOffsets(idx *MemoryIndex, r io.Reader) error { + return nil + } + +-func readChecksums(idx *MemoryIndex, r io.Reader) error { ++func readPackChecksum(idx *MemoryIndex, r io.Reader) error { + if _, err := io.ReadFull(r, idx.PackfileChecksum[:]); err != nil { + return err + } + ++ return nil ++} ++ ++func readIdxChecksum(idx *MemoryIndex, r io.Reader) error { + if _, err := io.ReadFull(r, idx.IdxChecksum[:]); err != nil { + return err + } + + return nil + } ++ ++// validateIdxV2Size enforces the size formula used by canonical Git ++// load_idx for idx v2 files: the on-disk length must lie within ++// [minSize, maxSize] where ++// ++// perObject = hashsz + crc32Len + offset32Len ++// minSize = headerLen + fanoutLen + trailerHashes*hashsz + nr*perObject ++// maxSize = minSize + (nr-1)*offset64Len when nr > 0 ++// ++// with nr taken from the last fanout entry and hashsz fixed at ++// [objectIDLength] (SHA-1 in v5). Multiplications use a self-checking ++// overflow guard so inputs whose claimed object count overflows the ++// formula are rejected rather than wrapping into a smaller value. ++func validateIdxV2Size(idx *MemoryIndex, idxSize int64) error { ++ nr := int64(idx.Fanout[fanout-1]) ++ hashsz := int64(objectIDLength) ++ ++ minSize := minIdxV2Size(nr, hashsz) ++ maxSize := maxIdxV2Size(nr, hashsz) ++ if minSize < 0 || maxSize < 0 { ++ return fmt.Errorf("%w: object count %d is inconsistent with file size", ErrMalformedIdxFile, nr) ++ } ++ ++ if idxSize < minSize || idxSize > maxSize { ++ return fmt.Errorf("%w: file size %d is inconsistent with object count %d", ErrMalformedIdxFile, idxSize, nr) ++ } ++ return nil ++} ++ ++// minIdxV2Size returns the minimum on-disk size of an idx v2 file ++// holding nr objects with the given hash size, mirroring the ++// computation in canonical Git load_idx. Returns -1 when any ++// intermediate multiplication or addition would overflow int64. ++func minIdxV2Size(nr, hashsz int64) int64 { ++ perObject := hashsz + crc32Len + offset32Len ++ fixed := int64(headerLen+fanoutLen) + trailerHashes*hashsz ++ ++ objects, ok := mulInt64(nr, perObject) ++ if !ok { ++ return -1 ++ } ++ sum, ok := addInt64(fixed, objects) ++ if !ok { ++ return -1 ++ } ++ return sum ++} ++ ++// maxIdxV2Size returns the maximum on-disk size of an idx v2 file ++// holding nr objects with the given hash size, mirroring the ++// computation in canonical Git load_idx. Returns -1 on overflow. ++func maxIdxV2Size(nr, hashsz int64) int64 { ++ minSize := minIdxV2Size(nr, hashsz) ++ if minSize < 0 { ++ return -1 ++ } ++ if nr == 0 { ++ return minSize ++ } ++ overflow, ok := mulInt64(nr-1, offset64Len) ++ if !ok { ++ return -1 ++ } ++ sum, ok := addInt64(minSize, overflow) ++ if !ok { ++ return -1 ++ } ++ return sum ++} ++ ++// mulInt64 returns a*b and whether the result fits in an int64 without ++// overflow. Negative operands or overflow yield ok=false. The overflow ++// check uses the standard self-inverse identity: a*b/b == a only when ++// the multiplication did not wrap. ++func mulInt64(a, b int64) (int64, bool) { ++ if a < 0 || b < 0 { ++ return 0, false ++ } ++ if a == 0 || b == 0 { ++ return 0, true ++ } ++ c := a * b ++ if c/b != a { ++ return 0, false ++ } ++ return c, true ++} ++ ++// addInt64 returns a+b and whether the result fits in an int64 without ++// overflow. Negative operands or overflow yield ok=false. ++func addInt64(a, b int64) (int64, bool) { ++ if a < 0 || b < 0 { ++ return 0, false ++ } ++ c := a + b ++ if c < a { ++ return 0, false ++ } ++ return c, true ++} +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go +index 9237a74..f068c25 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go +@@ -2,8 +2,10 @@ package idxfile + + import ( + "bytes" ++ "fmt" + "io" + "sort" ++ "sync" + + encbin "encoding/binary" + +@@ -57,8 +59,9 @@ type MemoryIndex struct { + PackfileChecksum [hash.Size]byte + IdxChecksum [hash.Size]byte + +- offsetHash map[int64]plumbing.Hash +- offsetHashIsFull bool ++ offsetHash map[int64]plumbing.Hash ++ offsetBuildOnce sync.Once ++ mu sync.RWMutex + } + + var _ Index = (*MemoryIndex)(nil) +@@ -124,32 +127,37 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) { + return 0, plumbing.ErrObjectNotFound + } + +- offset := idx.getOffset(k, i) ++ offset, err := idx.getOffset(k, i) ++ if err != nil { ++ return 0, err ++ } + +- if !idx.offsetHashIsFull { +- // Save the offset for reverse lookup +- if idx.offsetHash == nil { +- idx.offsetHash = make(map[int64]plumbing.Hash) +- } +- idx.offsetHash[int64(offset)] = h ++ // Save the offset for reverse lookup ++ idx.mu.Lock() ++ if idx.offsetHash == nil { ++ idx.offsetHash = make(map[int64]plumbing.Hash) + } ++ idx.offsetHash[int64(offset)] = h ++ idx.mu.Unlock() + + return int64(offset), nil + } + + const isO64Mask = uint64(1) << 31 + +-func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) uint64 { ++func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) (uint64, error) { + offset := secondLevel << 2 + ofs := encbin.BigEndian.Uint32(idx.Offset32[firstLevel][offset : offset+4]) + + if (uint64(ofs) & isO64Mask) != 0 { + offset := 8 * (uint64(ofs) & ^isO64Mask) +- n := encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8]) +- return n ++ if l := uint64(len(idx.Offset64)); l < 8 || offset > l-8 { ++ return 0, fmt.Errorf("%w: offset64 index out of range", ErrMalformedIdxFile) ++ } ++ return encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8]), nil + } + +- return uint64(ofs) ++ return uint64(ofs), nil + } + + // FindCRC32 implements the Index interface. +@@ -173,20 +181,17 @@ func (idx *MemoryIndex) FindHash(o int64) (plumbing.Hash, error) { + var hash plumbing.Hash + var ok bool + +- if idx.offsetHash != nil { +- if hash, ok = idx.offsetHash[o]; ok { +- return hash, nil +- } ++ var genErr error ++ idx.offsetBuildOnce.Do(func() { ++ genErr = idx.genOffsetHash() ++ }) ++ if genErr != nil { ++ return plumbing.ZeroHash, genErr + } + +- // Lazily generate the reverse offset/hash map if required. +- if !idx.offsetHashIsFull || idx.offsetHash == nil { +- if err := idx.genOffsetHash(); err != nil { +- return plumbing.ZeroHash, err +- } +- +- hash, ok = idx.offsetHash[o] +- } ++ idx.mu.RLock() ++ hash, ok = idx.offsetHash[o] ++ idx.mu.RUnlock() + + if !ok { + return plumbing.ZeroHash, plumbing.ErrObjectNotFound +@@ -202,8 +207,7 @@ func (idx *MemoryIndex) genOffsetHash() error { + return err + } + +- idx.offsetHash = make(map[int64]plumbing.Hash, count) +- idx.offsetHashIsFull = true ++ offsetHash := make(map[int64]plumbing.Hash, count) + + var hash plumbing.Hash + i := uint32(0) +@@ -211,12 +215,19 @@ func (idx *MemoryIndex) genOffsetHash() error { + mappedFirstLevel := idx.FanoutMapping[firstLevel] + for secondLevel := uint32(0); i < fanoutValue; i++ { + copy(hash[:], idx.Names[mappedFirstLevel][secondLevel*objectIDLength:]) +- offset := int64(idx.getOffset(mappedFirstLevel, int(secondLevel))) +- idx.offsetHash[offset] = hash ++ off, err := idx.getOffset(mappedFirstLevel, int(secondLevel)) ++ if err != nil { ++ return err ++ } ++ offsetHash[int64(off)] = hash + secondLevel++ + } + } + ++ idx.mu.Lock() ++ idx.offsetHash = offsetHash ++ idx.mu.Unlock() ++ + return nil + } + +@@ -289,7 +300,11 @@ func (i *idxfileEntryIter) Next() (*Entry, error) { + mappedFirstLevel := i.idx.FanoutMapping[i.firstLevel] + entry := new(Entry) + copy(entry.Hash[:], i.idx.Names[mappedFirstLevel][i.secondLevel*objectIDLength:]) +- entry.Offset = i.idx.getOffset(mappedFirstLevel, i.secondLevel) ++ var err error ++ entry.Offset, err = i.idx.getOffset(mappedFirstLevel, i.secondLevel) ++ if err != nil { ++ return nil, err ++ } + entry.CRC32 = i.idx.getCRC32(mappedFirstLevel, i.secondLevel) + + i.secondLevel++ +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go +index fc25d37..a1bdf00 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go +@@ -4,8 +4,8 @@ import ( + "bufio" + "bytes" + "errors" ++ "fmt" + "io" +- + "strconv" + "time" + +@@ -26,12 +26,14 @@ var ( + ErrInvalidChecksum = errors.New("invalid checksum") + // ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory + ErrUnknownExtension = errors.New("unknown extension") ++ // ErrMalformedIndexFile is returned when the index file contents are ++ // structurally invalid. ++ ErrMalformedIndexFile = errors.New("index decoder: malformed index file") + ) + + const ( + entryHeaderLength = 62 + entryExtended = 0x4000 +- entryValid = 0x8000 + nameMask = 0xfff + intentToAddMask = 1 << 13 + skipWorkTreeMask = 1 << 14 +@@ -140,33 +142,55 @@ func (d *Decoder) readEntry(idx *Index) (*Entry, error) { + e.SkipWorktree = extended&skipWorkTreeMask != 0 + } + +- if err := d.readEntryName(idx, e, flags); err != nil { ++ nameConsumed, err := d.readEntryName(idx, e, flags) ++ if err != nil { + return nil, err + } + +- return e, d.padEntry(idx, e, read) ++ return e, d.padEntry(idx, e, read, nameConsumed) + } + +-func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) error { +- var name string +- var err error +- ++// readEntryName reads the entry path and sets e.Name. It returns the ++// number of bytes consumed from the stream for the name portion. ++func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) (int, error) { + switch idx.Version { + case 2, 3: +- len := flags & nameMask +- name, err = d.doReadEntryName(len) ++ nameLen := flags & nameMask ++ name, consumed, err := d.doReadEntryName(nameLen) ++ if err != nil { ++ return 0, err ++ } ++ e.Name = name ++ return consumed, nil + case 4: +- name, err = d.doReadEntryNameV4() ++ name, err := d.doReadEntryNameV4() ++ if err != nil { ++ return 0, err ++ } ++ e.Name = name ++ return 0, nil // V4 has no padding; consumed count unused + default: +- return ErrUnsupportedVersion ++ return 0, ErrUnsupportedVersion + } ++} + +- if err != nil { +- return err ++// doReadEntryName reads the entry path for V2/V3 indexes. It returns the ++// name, the number of bytes consumed from the stream, and any error. ++// When nameLen equals nameMask (0xFFF), the name was too long to fit in ++// the 12-bit field and the real length is found by scanning for the NUL ++// terminator — matching C Git's strlen(name) fallback in create_from_disk. ++func (d *Decoder) doReadEntryName(nameLen uint16) (string, int, error) { ++ if nameLen == nameMask { ++ name, err := binary.ReadUntil(d.r, '\x00') ++ if err != nil { ++ return "", 0, err ++ } ++ return string(name), len(name) + 1, nil // +1 for the consumed NUL delimiter + } + +- e.Name = name +- return nil ++ name := make([]byte, nameLen) ++ _, err := io.ReadFull(d.r, name) ++ return string(name), int(nameLen), err + } + + func (d *Decoder) doReadEntryNameV4() (string, error) { +@@ -177,7 +201,14 @@ func (d *Decoder) doReadEntryNameV4() (string, error) { + + var base string + if d.lastEntry != nil { ++ if l < 0 || int(l) > len(d.lastEntry.Name) { ++ return "", fmt.Errorf("%w: invalid V4 entry name strip length %d (previous name length: %d)", ++ ErrMalformedIndexFile, l, len(d.lastEntry.Name)) ++ } + base = d.lastEntry.Name[:len(d.lastEntry.Name)-int(l)] ++ } else if l > 0 { ++ return "", fmt.Errorf("%w: non-zero strip length %d on first V4 entry", ++ ErrMalformedIndexFile, l) + } + + name, err := binary.ReadUntil(d.r, '\x00') +@@ -188,24 +219,23 @@ func (d *Decoder) doReadEntryNameV4() (string, error) { + return base + string(name), nil + } + +-func (d *Decoder) doReadEntryName(len uint16) (string, error) { +- name := make([]byte, len) +- _, err := io.ReadFull(d.r, name) +- +- return string(name), err +-} +- +-// Index entries are padded out to the next 8 byte alignment +-// for historical reasons related to how C Git read the files. +-func (d *Decoder) padEntry(idx *Index, e *Entry, read int) error { ++// padEntry discards NUL padding bytes that follow each V2/V3 entry on ++// disk. nameConsumed is the number of stream bytes consumed while reading ++// the entry name (which may exceed len(e.Name) when a NUL terminator was ++// consumed for long names where the 12-bit length field overflowed). ++func (d *Decoder) padEntry(idx *Index, e *Entry, read, nameConsumed int) error { + if idx.Version == 4 { + return nil + } + + entrySize := read + len(e.Name) + padLen := 8 - entrySize%8 +- _, err := io.CopyN(io.Discard, d.r, int64(padLen)) +- return err ++ padLen -= nameConsumed - len(e.Name) ++ if padLen > 0 { ++ _, err := io.CopyN(io.Discard, d.r, int64(padLen)) ++ return err ++ } ++ return nil + } + + func (d *Decoder) readExtensions(idx *Index) error { +@@ -312,7 +342,7 @@ func (d *Decoder) readChecksum(expected []byte) error { + } + + func validateHeader(r io.Reader) (version uint32, err error) { +- var s = make([]byte, 4) ++ s := make([]byte, 4) + if _, err := io.ReadFull(r, s); err != nil { + return 0, err + } +@@ -376,24 +406,26 @@ func (d *treeExtensionDecoder) readEntry() (*TreeEntry, error) { + return nil, err + } + +- // An entry can be in an invalidated state and is represented by having a +- // negative number in the entry_count field. +- if i == -1 { +- return nil, nil +- } +- + e.Entries = i + trees, err := binary.ReadUntil(d.r, '\n') + if err != nil { + return nil, err + } + +- i, err = strconv.Atoi(string(trees)) ++ subtrees, err := strconv.Atoi(string(trees)) + if err != nil { + return nil, err + } + +- e.Trees = i ++ e.Trees = subtrees ++ ++ // An entry can be in an invalidated state and is represented by having a ++ // negative number in the entry_count field. In this case, there is no ++ // object name and the next entry starts immediately after the newline. ++ if i < 0 { ++ return nil, nil ++ } ++ + _, err = io.ReadFull(d.r, e.Hash[:]) + if err != nil { + return nil, err +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go +index c232e03..161bd97 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go +@@ -5,9 +5,7 @@ import ( + "errors" + "fmt" + "io" +- "path" + "sort" +- "strings" + "time" + + "github.com/go-git/go-git/v5/plumbing/hash" +@@ -160,26 +158,39 @@ func (e *Encoder) encodeEntryName(entry *Entry) error { + } + + func (e *Encoder) encodeEntryNameV4(entry *Entry) error { +- name := entry.Name +- l := 0 ++ // V4 prefix compression: find the longest common prefix between the ++ // previous entry's name and the current one. The strip length tells ++ // the decoder how many bytes to remove from the end of the previous ++ // name, and the suffix is the remainder of the current name. ++ prefix := 0 + if e.lastEntry != nil { +- dir := path.Dir(e.lastEntry.Name) + "/" +- if strings.HasPrefix(entry.Name, dir) { +- l = len(e.lastEntry.Name) - len(dir) +- name = strings.TrimPrefix(entry.Name, dir) +- } else { +- l = len(e.lastEntry.Name) +- } ++ prefix = commonPrefixLen(e.lastEntry.Name, entry.Name) ++ } ++ stripLen := 0 ++ if e.lastEntry != nil { ++ stripLen = len(e.lastEntry.Name) - prefix + } + + e.lastEntry = entry + +- err := binary.WriteVariableWidthInt(e.w, int64(l)) +- if err != nil { ++ if err := binary.WriteVariableWidthInt(e.w, int64(stripLen)); err != nil { + return err + } + +- return binary.Write(e.w, []byte(name+string('\x00'))) ++ suffix := entry.Name[prefix:] ++ return binary.Write(e.w, append([]byte(suffix), '\x00')) ++} ++ ++// commonPrefixLen returns the length of the longest common byte prefix ++// between a and b. ++func commonPrefixLen(a, b string) int { ++ n := min(len(b), len(a)) ++ for i := range n { ++ if a[i] != b[i] { ++ return i ++ } ++ } ++ return n + } + + func (e *Encoder) encodeRawExtension(signature string, data []byte) error { +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go +index f4c7647..30a7e14 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go +@@ -54,6 +54,8 @@ type Index struct { + ResolveUndo *ResolveUndo + // EndOfIndexEntry represents the 'End of Index Entry' extension + EndOfIndexEntry *EndOfIndexEntry ++ // ModTime is the modification time of the index file ++ ModTime time.Time + } + + // Add creates a new Entry and returns it. The caller should first check that +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go +index d7932f4..f9842ed 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go +@@ -11,9 +11,10 @@ import ( + ) + + var ( +- ErrClosed = errors.New("objfile: already closed") +- ErrHeader = errors.New("objfile: invalid header") +- ErrNegativeSize = errors.New("objfile: negative object size") ++ ErrClosed = errors.New("objfile: already closed") ++ ErrHeader = errors.New("objfile: invalid header") ++ ErrHeaderNotRead = errors.New("objfile: Header must be called before Read") ++ ErrNegativeSize = errors.New("objfile: negative object size") + ) + + // Reader reads and decodes compressed objfile data from a provided io.Reader. +@@ -30,7 +31,7 @@ type Reader struct { + func NewReader(r io.Reader) (*Reader, error) { + zlib, err := sync.GetZlibReader(r) + if err != nil { +- return nil, packfile.ErrZLib.AddDetails(err.Error()) ++ return nil, packfile.ErrZLib.AddDetails("%s", err.Error()) + } + + return &Reader{ +@@ -100,12 +101,23 @@ func (r *Reader) prepareForRead(t plumbing.ObjectType, size int64) { + // + // If Read encounters the end of the data stream it will return err == io.EOF, + // either in the current call if n > 0 or in a subsequent call. ++// ++// Read returns ErrHeaderNotRead if Header has not been called successfully. + func (r *Reader) Read(p []byte) (n int, err error) { ++ if r.multi == nil { ++ return 0, ErrHeaderNotRead ++ } + return r.multi.Read(p) + } + + // Hash returns the hash of the object data stream that has been read so far. ++// It returns the zero plumbing.Hash if Header has not been called ++// successfully — guarding against the nil hasher that prepareForRead has ++// not yet allocated. + func (r *Reader) Hash() plumbing.Hash { ++ if r.multi == nil { ++ return plumbing.ZeroHash ++ } + return r.hasher.Sum() + } + +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go +index 8898e58..a24b63b 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go +@@ -19,9 +19,6 @@ const ( + // https://github.com/git/git/blob/f7466e94375b3be27f229c78873f0acf8301c0a5/diff-delta.c#L428 + // Max size of a copy operation (64KB). + maxCopySize = 64 * 1024 +- +- // Min size of a copy operation. +- minCopySize = 4 + ) + + // GetDelta returns an EncodedObject of type OFSDeltaObject. Base and Target object, +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go +index 238339d..93a6faf 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go +@@ -78,7 +78,13 @@ func (o *FSObject) Reader() (io.ReadCloser, error) { + _ = f.Close() + return nil, err + } +- return ioutil.NewReadCloserWithCloser(r, f.Close), nil ++ // Cap the lazy stream at the resolved object size: well-formed ++ // content reaches EOF inside the bound, an inflated stream that ++ // runs past surfaces ErrInflatedSizeMismatch on the byte just ++ // past the limit. For delta-resolved objects o.size is the ++ // expanded size, which is what the caller is reading here. ++ bounded := newBoundedReadCloser(r, o.size) ++ return ioutil.NewReadCloserWithCloser(bounded, f.Close), nil + } + r, err := p.getObjectContent(o.offset) + if err != nil { +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go +index 6852702..f7fb958 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go +@@ -126,11 +126,17 @@ func (p *Packfile) nextObjectHeader() (*ObjectHeader, error) { + return h, err + } + +-func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) int64 { ++func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) (int64, error) { + delta := buf.Bytes() +- _, delta = decodeLEB128(delta) // skip src size +- sz, _ := decodeLEB128(delta) +- return int64(sz) ++ _, delta, err := decodeLEB128(delta) // skip src size ++ if err != nil { ++ return 0, err ++ } ++ sz, _, err := decodeLEB128(delta) ++ if err != nil { ++ return 0, err ++ } ++ return int64(sz), nil + } + + func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) { +@@ -145,7 +151,7 @@ func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) { + return 0, err + } + +- return p.getDeltaObjectSize(buf), nil ++ return p.getDeltaObjectSize(buf) + default: + return 0, ErrInvalidObject.AddDetails("type %q", h.Type) + } +@@ -233,7 +239,10 @@ func (p *Packfile) getNextObject(h *ObjectHeader, hash plumbing.Hash) (plumbing. + return nil, err + } + +- size = p.getDeltaObjectSize(buf) ++ size, err = p.getDeltaObjectSize(buf) ++ if err != nil { ++ return nil, err ++ } + if size <= smallObjectThreshold { + var obj = new(plumbing.MemoryObject) + obj.SetSize(size) +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go +index 62f1d13..7774d2d 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go +@@ -26,6 +26,45 @@ var ( + ErrDeltaNotCached = errors.New("delta could not be found in cache") + ) + ++// maxObjectPreallocBytes caps the up-front size hint passed to ++// bytes.Buffer.Grow when staging an object's contents, so a malformed length ++// cannot trigger a huge or out-of-range allocation. The buffer still grows ++// dynamically as data is written; this is purely a hint cap. ++const maxObjectPreallocBytes = 1 << 30 // 1 GiB ++ ++// maxObjectsPrealloc caps the up-front capacity reserved from the pack's ++// declared object count, so a header advertising an absurd quantity cannot ++// trigger a multi-gigabyte allocation. The slice and maps still grow ++// organically beyond this hint. ++const maxObjectsPrealloc = 1 << 16 // 64 Ki entries ++ ++// Match upstream Git's pack depth ceiling: pack-objects.h OE_DEPTH_BITS, ++// enforced in builtin/pack-objects.c as (1 << OE_DEPTH_BITS) - 1. ++const maxDeltaChainDepth = 4095 ++ ++// growHint returns a non-negative int64 size, clamped to a sane upper bound, ++// suitable for passing to bytes.Buffer.Grow. ++func growHint(n int64) int { ++ switch { ++ case n <= 0: ++ return 0 ++ case n > maxObjectPreallocBytes: ++ return maxObjectPreallocBytes ++ default: ++ return int(n) ++ } ++} ++ ++// objectsHint returns a non-negative count, clamped to maxObjectsPrealloc, ++// suitable for passing to make() as the capacity hint for slices or maps ++// sized from a pack's declared object count. ++func objectsHint(n uint32) int { ++ if n > maxObjectsPrealloc { ++ return maxObjectsPrealloc ++ } ++ return int(n) ++} ++ + // Observer interface is implemented by index encoders. + type Observer interface { + // OnHeader is called when a new packfile is opened. +@@ -47,7 +86,6 @@ type Parser struct { + oi []*objectInfo + oiByHash map[plumbing.Hash]*objectInfo + oiByOffset map[int64]*objectInfo +- checksum plumbing.Hash + + cache *cache.BufferLRU + // delta content by offset, only used if source is not seekable +@@ -133,28 +171,27 @@ func (p *Parser) onFooter(h plumbing.Hash) error { + // Parse start decoding phase of the packfile. + func (p *Parser) Parse() (plumbing.Hash, error) { + if err := p.init(); err != nil { +- return plumbing.ZeroHash, err ++ return plumbing.ZeroHash, wrapEOF(err) + } + + if err := p.indexObjects(); err != nil { +- return plumbing.ZeroHash, err ++ return plumbing.ZeroHash, wrapEOF(err) + } + +- var err error +- p.checksum, err = p.scanner.Checksum() ++ checksum, err := p.scanner.Checksum() + if err != nil && err != io.EOF { +- return plumbing.ZeroHash, err ++ return plumbing.ZeroHash, wrapEOF(err) + } + + if err := p.resolveDeltas(); err != nil { +- return plumbing.ZeroHash, err ++ return plumbing.ZeroHash, wrapEOF(err) + } + +- if err := p.onFooter(p.checksum); err != nil { +- return plumbing.ZeroHash, err ++ if err := p.onFooter(checksum); err != nil { ++ return plumbing.ZeroHash, wrapEOF(err) + } + +- return p.checksum, nil ++ return checksum, nil + } + + func (p *Parser) init() error { +@@ -168,9 +205,10 @@ func (p *Parser) init() error { + } + + p.count = c +- p.oiByHash = make(map[plumbing.Hash]*objectInfo, p.count) +- p.oiByOffset = make(map[int64]*objectInfo, p.count) +- p.oi = make([]*objectInfo, p.count) ++ hint := objectsHint(p.count) ++ p.oiByHash = make(map[plumbing.Hash]*objectInfo, hint) ++ p.oiByOffset = make(map[int64]*objectInfo, hint) ++ p.oi = make([]*objectInfo, 0, hint) + + return nil + } +@@ -218,7 +256,7 @@ func (p *Parser) indexObjects() error { + if !ok { + // can't find referenced object in this pack file + // this must be a "thin" pack. +- parent = &objectInfo{ //Placeholder parent ++ parent = &objectInfo{ // Placeholder parent + SHA1: oh.Reference, + ExternalRef: true, // mark as an external reference that must be resolved + Type: plumbing.AnyObject, +@@ -263,7 +301,7 @@ func (p *Parser) indexObjects() error { + } + if delta && !p.scanner.IsSeekable { + buf.Reset() +- buf.Grow(int(oh.Length)) ++ buf.Grow(growHint(oh.Length)) + writers = append(writers, buf) + } + +@@ -308,7 +346,7 @@ func (p *Parser) indexObjects() error { + } + + p.oiByOffset[oh.Offset] = ota +- p.oi[i] = ota ++ p.oi = append(p.oi, ota) + } + + return nil +@@ -319,8 +357,12 @@ func (p *Parser) resolveDeltas() error { + defer sync.PutBytesBuffer(buf) + + for _, obj := range p.oi { ++ if err := checkDeltaChainDepth(obj); err != nil { ++ return err ++ } ++ + buf.Reset() +- buf.Grow(int(obj.Length)) ++ buf.Grow(growHint(obj.Length)) + err := p.get(obj, buf) + if err != nil { + return err +@@ -339,6 +381,9 @@ func (p *Parser) resolveDeltas() error { + // create it once and reuse across all children. + r := bytes.NewReader(buf.Bytes()) + for _, child := range obj.Children { ++ if err := checkDeltaChainDepth(child); err != nil { ++ return err ++ } + // Even though we are discarding the output, we still need to read it to + // so that the scanner can advance to the next object, and the SHA1 can be + // calculated. +@@ -358,6 +403,17 @@ func (p *Parser) resolveDeltas() error { + return nil + } + ++func checkDeltaChainDepth(o *objectInfo) error { ++ var depth int ++ for current := o; current != nil && current.DiskType.IsDelta(); current = current.Parent { ++ depth++ ++ if depth > maxDeltaChainDepth { ++ return fmt.Errorf("%w: delta chain depth exceeds %d", ErrMalformedPackFile, maxDeltaChainDepth) ++ } ++ } ++ return nil ++} ++ + func (p *Parser) resolveExternalRef(o *objectInfo) { + if ref, ok := p.oiByHash[o.SHA1]; ok && ref.ExternalRef { + p.oiByHash[o.SHA1] = o +@@ -407,7 +463,7 @@ func (p *Parser) get(o *objectInfo, buf *bytes.Buffer) (err error) { + if o.DiskType.IsDelta() { + b := sync.GetBytesBuffer() + defer sync.PutBytesBuffer(b) +- buf.Grow(int(o.Length)) ++ buf.Grow(growHint(o.Length)) + err := p.get(o.Parent, b) + if err != nil { + return err +@@ -531,6 +587,13 @@ func (p *Parser) readData(w io.Writer, o *objectInfo) error { + return nil + } + ++func wrapEOF(err error) error { ++ if err == io.ErrUnexpectedEOF || err == io.EOF { ++ return fmt.Errorf("%w: %w", ErrMalformedPackFile, err) ++ } ++ return err ++} ++ + // applyPatchBase applies the patch to target. + // + // Note that ota will be updated based on the description in resolveObject. +@@ -558,15 +621,6 @@ func applyPatchBase(ota *objectInfo, base io.ReaderAt, delta io.Reader, target i + return nil + } + +-func getSHA1(t plumbing.ObjectType, data []byte) (plumbing.Hash, error) { +- hasher := plumbing.NewHasher(t, int64(len(data))) +- if _, err := hasher.Write(data); err != nil { +- return plumbing.ZeroHash, err +- } +- +- return hasher.Sum(), nil +-} +- + type objectInfo struct { + Offset int64 + Length int64 +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go +index a9c6b9b..4bcb491 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go +@@ -31,10 +31,15 @@ const ( + // premptively made available for a patch operation. + maxPatchPreemptionSize uint = 65536 + +- // minDeltaSize defines the smallest size for a delta. +- minDeltaSize = 4 ++ // minDeltaSize is the smallest valid delta: a 1-byte srcSz LEB128 ++ // header followed by a 1-byte targetSz LEB128 header (the ++ // shortest case being targetSz=0 with no operations). ++ minDeltaSize = 2 + ) + ++// uintBits is the bit width of uint on the current platform (32 or 64). ++const uintBits = 32 << (^uint(0) >> 63) ++ + type offset struct { + mask byte + shift uint +@@ -142,7 +147,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo + baseBuf := bufio.NewReader(baseRd) + basePos := uint(0) + +- for { ++ for remainingTargetSz > 0 { + cmd, err := deltaBuf.ReadByte() + if err == io.EOF { + _ = dstWr.CloseWithError(ErrInvalidDelta) +@@ -166,9 +171,9 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo + return + } + +- if invalidSize(sz, targetSz) || ++ if invalidSize(sz, remainingTargetSz) || + invalidOffsetSize(offset, sz, srcSz) { +- _ = dstWr.Close() ++ _ = dstWr.CloseWithError(ErrInvalidDelta) + return + } + +@@ -210,7 +215,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo + + case isCopyFromDelta(cmd): + sz := uint(cmd) // cmd is the size itself +- if invalidSize(sz, targetSz) { ++ if invalidSize(sz, remainingTargetSz) { + _ = dstWr.CloseWithError(ErrInvalidDelta) + return + } +@@ -225,40 +230,48 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo + _ = dstWr.CloseWithError(ErrDeltaCmd) + return + } ++ } + +- if remainingTargetSz <= 0 { +- _ = dstWr.Close() +- return +- } ++ // Mirror upstream's `data != top` post-loop check: every byte ++ // of the delta payload must be consumed. ++ if _, err := deltaBuf.ReadByte(); err == nil { ++ _ = dstWr.CloseWithError(ErrInvalidDelta) ++ return ++ } else if err != io.EOF { ++ _ = dstWr.CloseWithError(err) ++ return + } ++ ++ _ = dstWr.Close() + }() + + return dstRd, nil + } + + func patchDelta(dst *bytes.Buffer, src, delta []byte) error { +- if len(delta) < minCopySize { +- return ErrInvalidDelta ++ srcSz, delta, err := decodeLEB128(delta) ++ if err != nil { ++ return fmt.Errorf("%w: %w", ErrInvalidDelta, err) + } +- +- srcSz, delta := decodeLEB128(delta) + if srcSz != uint(len(src)) { + return ErrInvalidDelta + } + +- targetSz, delta := decodeLEB128(delta) ++ targetSz, delta, err := decodeLEB128(delta) ++ if err != nil { ++ return fmt.Errorf("%w: %w", ErrInvalidDelta, err) ++ } + remainingTargetSz := targetSz + +- var cmd byte +- + growSz := min(targetSz, maxPatchPreemptionSize) + dst.Grow(int(growSz)) +- for { ++ ++ for remainingTargetSz > 0 { + if len(delta) == 0 { + return ErrInvalidDelta + } + +- cmd = delta[0] ++ cmd := delta[0] + delta = delta[1:] + + switch { +@@ -275,16 +288,16 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error { + return err + } + +- if invalidSize(sz, targetSz) || ++ if invalidSize(sz, remainingTargetSz) || + invalidOffsetSize(offset, sz, srcSz) { +- break ++ return ErrInvalidDelta + } + dst.Write(src[offset : offset+sz]) + remainingTargetSz -= sz + + case isCopyFromDelta(cmd): + sz := uint(cmd) // cmd is the size itself +- if invalidSize(sz, targetSz) { ++ if invalidSize(sz, remainingTargetSz) { + return ErrInvalidDelta + } + +@@ -299,10 +312,12 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error { + default: + return ErrDeltaCmd + } ++ } + +- if remainingTargetSz <= 0 { +- break +- } ++ // Mirror upstream's `data != top` post-loop check: every byte of ++ // the delta payload must be consumed. ++ if len(delta) != 0 { ++ return ErrInvalidDelta + } + + return nil +@@ -354,7 +369,7 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader, + baselr := io.LimitReader(sr, 0).(*io.LimitedReader) + deltalr := io.LimitReader(deltaBuf, 0).(*io.LimitedReader) + +- for { ++ for remainingTargetSz > 0 { + buf := *bufp + cmd, err := deltaBuf.ReadByte() + if err == io.EOF { +@@ -374,9 +389,9 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader, + return 0, plumbing.ZeroHash, err + } + +- if invalidSize(sz, targetSz) || ++ if invalidSize(sz, remainingTargetSz) || + invalidOffsetSize(offset, sz, srcSz) { +- return 0, plumbing.ZeroHash, err ++ return 0, plumbing.ZeroHash, ErrInvalidDelta + } + + if _, err := sr.Seek(int64(offset), io.SeekStart); err != nil { +@@ -389,7 +404,7 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader, + remainingTargetSz -= sz + } else if isCopyFromDelta(cmd) { + sz := uint(cmd) // cmd is the size itself +- if invalidSize(sz, targetSz) { ++ if invalidSize(sz, remainingTargetSz) { + return 0, plumbing.ZeroHash, ErrInvalidDelta + } + deltalr.N = int64(sz) +@@ -399,30 +414,41 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader, + + remainingTargetSz -= sz + } else { +- return 0, plumbing.ZeroHash, err +- } +- if remainingTargetSz <= 0 { +- break ++ return 0, plumbing.ZeroHash, ErrDeltaCmd + } + } + ++ // Mirror upstream's `data != top` post-loop check: every byte of ++ // the delta payload must be consumed. ++ if _, err := deltaBuf.ReadByte(); err == nil { ++ return 0, plumbing.ZeroHash, ErrInvalidDelta ++ } else if err != io.EOF { ++ return 0, plumbing.ZeroHash, err ++ } ++ + return targetSz, hasher.Sum(), nil + } + + // Decodes a number encoded as an unsigned LEB128 at the start of some +-// binary data and returns the decoded number and the rest of the +-// stream. ++// binary data and returns the decoded number, the rest of the stream, ++// and an error if the encoded value does not fit in a uint. + // + // This must be called twice on the delta data buffer, first to get the + // expected source buffer size, and again to get the target buffer size. +-func decodeLEB128(input []byte) (uint, []byte) { ++func decodeLEB128(input []byte) (uint, []byte, error) { + if len(input) == 0 { +- return 0, input ++ return 0, input, nil + } + + var num, sz uint + var b byte + for { ++ // A continuation byte at shift > uintBits-7 cannot contribute ++ // without overflowing the accumulator. ++ if sz*7 > uintBits-7 { ++ return 0, input, ErrLengthOverflow ++ } ++ + b = input[sz] + num |= (uint(b) & payload) << (sz * 7) // concats 7 bits chunks + sz++ +@@ -432,12 +458,16 @@ func decodeLEB128(input []byte) (uint, []byte) { + } + } + +- return num, input[sz:] ++ return num, input[sz:], nil + } + + func decodeLEB128ByteReader(input io.ByteReader) (uint, error) { + var num, sz uint + for { ++ if sz*7 > uintBits-7 { ++ return 0, ErrLengthOverflow ++ } ++ + b, err := input.ReadByte() + if err != nil { + return 0, err +@@ -529,8 +559,9 @@ func decodeSize(cmd byte, delta []byte) (uint, []byte, error) { + return sz, delta, nil + } + +-func invalidSize(sz, targetSz uint) bool { +- return sz > targetSz ++// invalidSize reports whether sz exceeds the remaining target size. ++func invalidSize(sz, remaining uint) bool { ++ return sz > remaining + } + + func invalidOffsetSize(offset, sz, srcSz uint) bool { +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go +index 730343e..6d2907e 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go +@@ -3,12 +3,15 @@ package packfile + import ( + "bufio" + "bytes" ++ "crypto" ++ "errors" + "fmt" +- "hash" ++ gohash "hash" + "hash/crc32" + "io" + + "github.com/go-git/go-git/v5/plumbing" ++ "github.com/go-git/go-git/v5/plumbing/hash" + "github.com/go-git/go-git/v5/utils/binary" + "github.com/go-git/go-git/v5/utils/ioutil" + "github.com/go-git/go-git/v5/utils/sync" +@@ -24,8 +27,102 @@ var ( + ErrUnsupportedVersion = NewError("unsupported packfile version") + // ErrSeekNotSupported returned if seek is not support + ErrSeekNotSupported = NewError("not seek support") ++ // ErrMalformedPackFile is returned by the parser when the pack file is corrupted. ++ ErrMalformedPackFile = errors.New("malformed PACK file") ++ // ErrLengthOverflow is returned when a variable-length integer would not ++ // fit into its accumulator because the input declares more continuation ++ // bytes than the type can hold. ++ ErrLengthOverflow = errors.New("variable-length integer overflow") ++ // ErrInflatedSizeMismatch is returned when a packfile object inflates to ++ // more bytes than the size declared in its object header. A well-formed ++ // packfile never produces more data than the declared size; exceeding it ++ // indicates a structurally invalid entry. ++ ErrInflatedSizeMismatch = errors.New("packfile: inflated object exceeds declared size") + ) + ++// boundedWriter passes writes through to w up to limit bytes total, then ++// returns ErrInflatedSizeMismatch. It is used to enforce that a packfile ++// object's inflated length does not exceed the size declared in its header. ++type boundedWriter struct { ++ w io.Writer ++ limit int64 ++ n int64 ++} ++ ++// Write forwards p to the underlying writer while keeping the running total ++// at or below limit. On overrun it forwards the legal prefix and reports ++// the number of bytes actually consumed alongside ErrInflatedSizeMismatch, ++// matching the contract in io.Writer. A write error from the underlying ++// writer during overrun-handling is joined with ErrInflatedSizeMismatch so ++// it is not silently dropped. ++func (b *boundedWriter) Write(p []byte) (int, error) { ++ if b.n+int64(len(p)) > b.limit { ++ remain := int(b.limit - b.n) ++ err := error(ErrInflatedSizeMismatch) ++ if remain > 0 { ++ n, werr := b.w.Write(p[:remain]) ++ b.n += int64(n) ++ if werr != nil { ++ err = errors.Join(ErrInflatedSizeMismatch, werr) ++ } ++ return n, err ++ } ++ return 0, err ++ } ++ n, err := b.w.Write(p) ++ b.n += int64(n) ++ return n, err ++} ++ ++// boundedReadCloser wraps a ReadCloser and reports ErrInflatedSizeMismatch ++// once more than limit bytes have been read. It is used by the on-demand ++// object reader returned from FSObject.Reader so that a lazy Read of a ++// packfile object cannot stream past its declared inflated size. ++// ++// The implementation builds on io.LimitedReader with the standard ++// overrun-detection trick: request limit+1 bytes from the underlying so ++// that the moment the sentinel byte materializes (LimitedReader.N drops ++// to zero) we know the source produced more than limit bytes. ++type boundedReadCloser struct { ++ lr io.LimitedReader ++ closer io.Closer ++ overrun bool ++} ++ ++// newBoundedReadCloser wraps rc so that the cumulative bytes returned from ++// Read never exceed limit. The first call that would have returned a byte ++// past limit instead returns ErrInflatedSizeMismatch; subsequent calls ++// keep returning the same error. A negative limit is treated as zero, so ++// the first byte produced by rc surfaces ErrInflatedSizeMismatch. ++func newBoundedReadCloser(rc io.ReadCloser, limit int64) *boundedReadCloser { ++ if limit < 0 { ++ limit = 0 ++ } ++ return &boundedReadCloser{ ++ lr: io.LimitedReader{R: rc, N: limit + 1}, ++ closer: rc, ++ } ++} ++ ++// Read forwards Read up to the configured byte limit. When the underlying ++// stream produces the limit+1 sentinel byte, the legal prefix is returned ++// alongside ErrInflatedSizeMismatch; on subsequent calls only the error ++// is returned. ++func (b *boundedReadCloser) Read(p []byte) (int, error) { ++ if b.overrun { ++ return 0, ErrInflatedSizeMismatch ++ } ++ n, err := b.lr.Read(p) ++ if b.lr.N == 0 { ++ b.overrun = true ++ return n - 1, ErrInflatedSizeMismatch ++ } ++ return n, err ++} ++ ++// Close closes the underlying ReadCloser. ++func (b *boundedReadCloser) Close() error { return b.closer.Close() } ++ + // ObjectHeader contains the information related to the object, this information + // is collected from the previous bytes to the content of the object. + type ObjectHeader struct { +@@ -37,8 +134,9 @@ type ObjectHeader struct { + } + + type Scanner struct { +- r *scannerReader +- crc hash.Hash32 ++ r *scannerReader ++ crc gohash.Hash32 ++ packHasher hash.Hash + + // pendingObject is used to detect if an object has been read, or still + // is waiting to be read +@@ -56,10 +154,12 @@ func NewScanner(r io.Reader) *Scanner { + _, ok := r.(io.ReadSeeker) + + crc := crc32.NewIEEE() ++ hasher := hash.New(crypto.SHA1) + return &Scanner{ +- r: newScannerReader(r, crc), ++ r: newScannerReader(r, io.MultiWriter(crc, hasher)), + crc: crc, + IsSeekable: ok, ++ packHasher: hasher, + } + } + +@@ -68,6 +168,7 @@ func (s *Scanner) Reset(r io.Reader) { + + s.r.Reset(r) + s.crc.Reset() ++ s.packHasher.Reset() + s.IsSeekable = ok + s.pendingObject = nil + s.version = 0 +@@ -114,7 +215,7 @@ func (s *Scanner) Header() (version, objects uint32, err error) { + + // readSignature reads a returns the signature field in the packfile. + func (s *Scanner) readSignature() ([]byte, error) { +- var sig = make([]byte, 4) ++ sig := make([]byte, 4) + if _, err := io.ReadFull(s.r, sig); err != nil { + return []byte{}, err + } +@@ -211,6 +312,13 @@ func (s *Scanner) nextObjectHeader() (*ObjectHeader, error) { + return nil, err + } + ++ // An OFS-delta references a base object that appears earlier ++ // in the pack; the negative offset must be strictly positive ++ // and not larger than the current object's offset. ++ if no <= 0 || no > h.Offset { ++ return nil, fmt.Errorf("%w: invalid OFS delta offset", ErrMalformedPackFile) ++ } ++ + h.OffsetReference = h.Offset - no + case plumbing.REFDeltaObject: + var err error +@@ -294,6 +402,13 @@ func (s *Scanner) readLength(first byte) (int64, error) { + shift := firstLengthBits + var err error + for c&maskContinue > 0 { ++ // Mirrors unpack_object_header_buffer in canonical Git's ++ // packfile.c: a continuation byte at shift > 64-7 cannot ++ // contribute without overflowing an int64. ++ if shift > 64-lengthBits { ++ return 0, fmt.Errorf("%w: %w", ErrMalformedPackFile, ErrLengthOverflow) ++ } ++ + if c, err = s.r.ReadByte(); err != nil { + return 0, err + } +@@ -306,10 +421,18 @@ func (s *Scanner) readLength(first byte) (int64, error) { + } + + // NextObject writes the content of the next object into the reader, returns +-// the number of bytes written, the CRC32 of the content and an error, if any ++// the number of bytes written, the CRC32 of the content and an error, if any. ++// ++// When a prior NextObjectHeader has stashed the object header in ++// pendingObject, the inflated stream is bounded by the header's declared ++// length and surfaces ErrInflatedSizeMismatch on overrun. + func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err error) { ++ declaredSize := int64(-1) ++ if s.pendingObject != nil { ++ declaredSize = s.pendingObject.Length ++ } + s.pendingObject = nil +- written, err = s.copyObject(w) ++ written, err = s.copyObject(w, declaredSize) + + s.r.Flush() + crc32 = s.crc.Sum32() +@@ -318,24 +441,39 @@ func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err erro + return + } + +-// ReadObject returns a reader for the object content and an error ++// ReadObject returns a reader for the object content and an error. ++// ++// When a prior NextObjectHeader has stashed the object header in ++// pendingObject, the returned reader is bounded by the header's declared ++// length so callers cannot stream past the declared inflated size; an ++// overrun surfaces ErrInflatedSizeMismatch on the byte just past the ++// limit. + func (s *Scanner) ReadObject() (io.ReadCloser, error) { ++ declaredSize := int64(-1) ++ if s.pendingObject != nil { ++ declaredSize = s.pendingObject.Length ++ } + s.pendingObject = nil + zr, err := sync.GetZlibReader(s.r) +- + if err != nil { + return nil, fmt.Errorf("zlib reset error: %s", err) + } + +- return ioutil.NewReadCloserWithCloser(zr.Reader, func() error { ++ rc := ioutil.NewReadCloserWithCloser(zr.Reader, func() error { + sync.PutZlibReader(zr) + return nil +- }), nil ++ }) ++ if declaredSize >= 0 { ++ return newBoundedReadCloser(rc, declaredSize), nil ++ } ++ return rc, nil + } + +-// ReadRegularObject reads and write a non-deltified object +-// from it zlib stream in an object entry in the packfile. +-func (s *Scanner) copyObject(w io.Writer) (n int64, err error) { ++// copyObject inflates a non-deltified object's zlib stream into w. When ++// declaredSize is non-negative, the write sink is wrapped in a ++// boundedWriter so an overrun surfaces ErrInflatedSizeMismatch instead ++// of being silently appended. ++func (s *Scanner) copyObject(w io.Writer, declaredSize int64) (n int64, err error) { + zr, err := sync.GetZlibReader(s.r) + defer sync.PutZlibReader(zr) + +@@ -344,8 +482,14 @@ func (s *Scanner) copyObject(w io.Writer) (n int64, err error) { + } + + defer ioutil.CheckClose(zr.Reader, &err) ++ ++ sink := w ++ if declaredSize >= 0 { ++ sink = &boundedWriter{w: w, limit: declaredSize} ++ } ++ + buf := sync.GetByteSlice() +- n, err = io.CopyBuffer(w, zr.Reader, *buf) ++ n, err = io.CopyBuffer(sink, zr.Reader, *buf) + sync.PutByteSlice(buf) + return + } +@@ -374,7 +518,18 @@ func (s *Scanner) Checksum() (plumbing.Hash, error) { + return plumbing.ZeroHash, err + } + +- return binary.ReadHash(s.r) ++ s.r.Flush() ++ actual := plumbing.Hash(s.packHasher.Sum(nil)) ++ packChecksum, err := binary.ReadHash(s.r) ++ if err != nil { ++ return plumbing.ZeroHash, err ++ } ++ ++ if actual != packChecksum { ++ return plumbing.ZeroHash, fmt.Errorf("%w: checksum mismatch: %q instead of %q", ErrMalformedPackFile, packChecksum, actual) ++ } ++ ++ return packChecksum, nil + } + + // Close reads the reader until io.EOF +@@ -401,17 +556,17 @@ func (s *Scanner) Flush() error { + // to the crc32 hash writer. + type scannerReader struct { + reader io.Reader +- crc io.Writer ++ writer io.Writer + rbuf *bufio.Reader + wbuf *bufio.Writer + offset int64 + } + +-func newScannerReader(r io.Reader, h io.Writer) *scannerReader { ++func newScannerReader(r io.Reader, w io.Writer) *scannerReader { + sr := &scannerReader{ +- rbuf: bufio.NewReader(nil), +- wbuf: bufio.NewWriterSize(nil, 64), +- crc: h, ++ rbuf: bufio.NewReader(nil), ++ wbuf: bufio.NewWriterSize(nil, 64), ++ writer: w, + } + sr.Reset(r) + +@@ -421,7 +576,7 @@ func newScannerReader(r io.Reader, h io.Writer) *scannerReader { + func (r *scannerReader) Reset(reader io.Reader) { + r.reader = reader + r.rbuf.Reset(r.reader) +- r.wbuf.Reset(r.crc) ++ r.wbuf.Reset(r.writer) + + r.offset = 0 + if seeker, ok := r.reader.(io.ReadSeeker); ok { +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go +index 3d096e1..07034c1 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go +@@ -5,7 +5,7 @@ import ( + "context" + "errors" + "fmt" +- "io" ++ "slices" + "strings" + + "github.com/ProtonMail/go-crypto/openpgp" +@@ -20,6 +20,7 @@ const ( + beginpgp string = "-----BEGIN PGP SIGNATURE-----" + endpgp string = "-----END PGP SIGNATURE-----" + headerpgp string = "gpgsig" ++ headerpgp256 string = "gpgsig-sha256" + headerencoding string = "encoding" + + // https://github.com/git/git/blob/bcb6cae2966cc407ca1afc77413b3ef11103c175/Documentation/gitformat-signature.txt#L153 +@@ -41,6 +42,11 @@ type MessageEncoding string + // in time, such as a timestamp, the author of the changes since the last + // commit, a pointer to the previous commit(s), etc. + // http://shafiulazam.com/gitbook/1_the_git_object_model.html ++// ++// When a Commit is populated by Decode it retains a reference to the source ++// plumbing.EncodedObject so that EncodeWithoutSignature can reproduce the ++// exact bytes the signature was computed over. Refer to EncodeWithoutSignature ++// for more information. + type Commit struct { + // Hash of the commit object. + Hash plumbing.Hash +@@ -62,8 +68,56 @@ type Commit struct { + ParentHashes []plumbing.Hash + // Encoding is the encoding of the commit. + Encoding MessageEncoding ++ // List of extra headers of the commit ++ ExtraHeaders []ExtraHeader + + s storer.EncodedObjectStorer ++ // src holds the encoded object this Commit was decoded from, used by ++ // EncodeWithoutSignature to recover the canonical signed bytes. ++ src plumbing.EncodedObject ++} ++ ++// ExtraHeader holds any non-standard header ++type ExtraHeader struct { ++ // Header name ++ Key string ++ // Value of the header ++ Value string ++} ++ ++// Implement fmt.Formatter for ExtraHeader ++func (h ExtraHeader) Format(f fmt.State, verb rune) { ++ switch verb { ++ case 'v': ++ fmt.Fprintf(f, "ExtraHeader{Key: %v, Value: %v}", h.Key, h.Value) ++ default: ++ fmt.Fprintf(f, "%s", h.Key) ++ if len(h.Value) > 0 { ++ fmt.Fprint(f, " ") ++ // Content may be spread on multiple lines, if so we need to ++ // prepend each of them with a space for "continuation". ++ value := strings.TrimSuffix(h.Value, "\n") ++ lines := strings.Split(value, "\n") ++ fmt.Fprint(f, strings.Join(lines, "\n ")) ++ } ++ } ++} ++ ++// Parse an extra header and indicate whether it may be continue on the next line ++func parseExtraHeader(line []byte) (ExtraHeader, bool) { ++ split := bytes.SplitN(line, []byte{' '}, 2) ++ ++ out := ExtraHeader{ ++ Key: string(bytes.TrimRight(split[0], "\n")), ++ Value: "", ++ } ++ ++ if len(split) == 2 { ++ out.Value += string(split[1]) ++ return out, true ++ } else { ++ return out, false ++ } + } + + // GetCommit gets a commit from an object storer and decodes it. +@@ -136,6 +190,11 @@ func (c *Commit) NumParents() int { + + var ErrParentNotFound = errors.New("commit parent not found") + ++// ErrMalformedCommit is returned when a commit object cannot be decoded ++// because its standard headers (tree, parent, author, committer) are missing, ++// duplicated, or out of order. ++var ErrMalformedCommit = errors.New("malformed commit") ++ + // Parent returns the ith parent of a commit. + func (c *Commit) Parent(i int) (*Commit, error) { + if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 { +@@ -182,14 +241,23 @@ func (c *Commit) Type() plumbing.ObjectType { + return plumbing.CommitObject + } + ++func (c *Commit) reset() { ++ storer := c.s ++ *c = Commit{ ++ Encoding: defaultUtf8CommitMessageEncoding, ++ s: storer, ++ } ++} ++ + // Decode transforms a plumbing.EncodedObject into a Commit struct. + func (c *Commit) Decode(o plumbing.EncodedObject) (err error) { + if o.Type() != plumbing.CommitObject { + return ErrUnsupportedObject + } + ++ c.reset() + c.Hash = o.Hash() +- c.Encoding = defaultUtf8CommitMessageEncoding ++ c.src = o + + reader, err := o.Reader() + if err != nil { +@@ -200,77 +268,17 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) { + r := sync.GetBufioReader(reader) + defer sync.PutBufioReader(r) + +- var message bool +- var mergetag bool +- var pgpsig bool +- var msgbuf bytes.Buffer +- for { +- line, err := r.ReadBytes('\n') +- if err != nil && err != io.EOF { ++ s := &commitScanner{r: r, c: c} ++ for state := scanTree; state != nil; { ++ state, err = state(s) ++ if err != nil { + return err + } +- +- if mergetag { +- if len(line) > 0 && line[0] == ' ' { +- line = bytes.TrimLeft(line, " ") +- c.MergeTag += string(line) +- continue +- } else { +- mergetag = false +- } +- } +- +- if pgpsig { +- if len(line) > 0 && line[0] == ' ' { +- line = bytes.TrimLeft(line, " ") +- c.PGPSignature += string(line) +- continue +- } else { +- pgpsig = false +- } +- } +- +- if !message { +- line = bytes.TrimSpace(line) +- if len(line) == 0 { +- message = true +- continue +- } +- +- split := bytes.SplitN(line, []byte{' '}, 2) +- +- var data []byte +- if len(split) == 2 { +- data = split[1] +- } +- +- switch string(split[0]) { +- case "tree": +- c.TreeHash = plumbing.NewHash(string(data)) +- case "parent": +- c.ParentHashes = append(c.ParentHashes, plumbing.NewHash(string(data))) +- case "author": +- c.Author.Decode(data) +- case "committer": +- c.Committer.Decode(data) +- case headermergetag: +- c.MergeTag += string(data) + "\n" +- mergetag = true +- case headerencoding: +- c.Encoding = MessageEncoding(data) +- case headerpgp: +- c.PGPSignature += string(data) + "\n" +- pgpsig = true +- } +- } else { +- msgbuf.Write(line) +- } +- +- if err == io.EOF { +- break +- } + } +- c.Message = msgbuf.String() ++ if !s.sawTree { ++ return fmt.Errorf("%w: missing tree header", ErrMalformedCommit) ++ } ++ c.Message = s.msgbuf.String() + return nil + } + +@@ -279,11 +287,73 @@ func (c *Commit) Encode(o plumbing.EncodedObject) error { + return c.encode(o, true) + } + +-// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature). ++// EncodeWithoutSignature exports a Commit into a plumbing.EncodedObject ++// without any signature headers, producing the payload that PGP/GPG ++// signatures are computed over. ++// ++// Behaviour depends on how the Commit was created: ++// ++// - For Commits populated by Decode whose exported fields still match the ++// source object, the payload is streamed from the raw source bytes with ++// gpgsig and gpgsig-sha256 headers (and their continuation lines) ++// stripped verbatim. This preserves the exact bytes the signature was ++// computed over, regardless of any normalization performed by Decode. ++// ++// - For Commits constructed in memory, or for decoded Commits whose ++// exported fields have been mutated, the payload is derived from the ++// current struct fields. Mutation is detected by re-decoding the source ++// object and comparing exported fields; if any differ, the in-memory ++// representation prevails. + func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error { ++ if c.matchesSource() { ++ return stripObjectSignatures(o, c.src, plumbing.CommitObject) ++ } + return c.encode(o, false) + } + ++// matchesSource reports whether c.src is set and re-decoding it produces a ++// Commit whose payload-affecting exported fields are identical to those of ++// c. It is the auto-detection used by EncodeWithoutSignature to decide ++// between the raw bytes and the struct-encoded payload. ++// ++// PGPSignature is intentionally excluded from the comparison: neither path ++// emits it, so mutating it must not trigger a switch to struct-encode (which ++// would change the byte layout the caller is trying to verify against). ++func (c *Commit) matchesSource() bool { ++ if c.src == nil { ++ return false ++ } ++ fresh := &Commit{} ++ if err := fresh.Decode(c.src); err != nil { ++ return false ++ } ++ return c.Hash == fresh.Hash && ++ signatureEqual(c.Author, fresh.Author) && ++ signatureEqual(c.Committer, fresh.Committer) && ++ c.MergeTag == fresh.MergeTag && ++ c.Message == fresh.Message && ++ c.TreeHash == fresh.TreeHash && ++ c.Encoding == fresh.Encoding && ++ slices.Equal(c.ParentHashes, fresh.ParentHashes) && ++ slices.Equal(c.ExtraHeaders, fresh.ExtraHeaders) ++} ++ ++func signatureEqual(a, b Signature) bool { ++ return a.Name == b.Name && ++ a.Email == b.Email && ++ a.When.Unix() == b.When.Unix() && ++ a.When.Format("-0700") == b.When.Format("-0700") ++} ++ ++func isStandardHeader(key string) bool { ++ switch key { ++ case "tree", "parent", "author", "committer", ++ headerencoding, headermergetag, headerpgp, headerpgp256: ++ return true ++ } ++ return false ++} ++ + func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) { + o.SetType(plumbing.CommitObject) + w, err := o.Writer() +@@ -341,6 +411,15 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) { + } + } + ++ for _, header := range c.ExtraHeaders { ++ if isStandardHeader(header.Key) { ++ continue ++ } ++ if _, err = fmt.Fprintf(w, "\n%s", header); err != nil { ++ return err ++ } ++ } ++ + if c.PGPSignature != "" && includeSig { + if _, err = fmt.Fprint(w, "\n"+headerpgp+" "); err != nil { + return err +@@ -406,9 +485,21 @@ func (c *Commit) String() string { + ) + } + ++// ErrMultipleSignatures is returned by Verify when the commit carries more ++// than one armored signature block. Mirrors upstream's parse_gpg_output ++// rejection of GOODSIG/BADSIG status lines after the first ++// (gpg-interface.c:257-269): multi-signature commits are intentionally ++// unsupported because their provenance cannot be reduced to a single ++// authoritative signer. ++var ErrMultipleSignatures = errors.New("commit has multiple signatures") ++ + // Verify performs PGP verification of the commit with a provided armored + // keyring and returns openpgp.Entity associated with verifying key on success. + func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) { ++ if countSignatureBlocks([]byte(c.PGPSignature)) > 1 { ++ return nil, ErrMultipleSignatures ++ } ++ + keyRingReader := strings.NewReader(armoredKeyRing) + keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader) + if err != nil { +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go +new file mode 100644 +index 0000000..7e4cf54 +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go +@@ -0,0 +1,377 @@ ++package object ++ ++import ( ++ "bufio" ++ "bytes" ++ "fmt" ++ "io" ++ "strings" ++ ++ "github.com/go-git/go-git/v5/plumbing" ++) ++ ++// commitScanner holds the working state of the commit decoder driven by the ++// stateFn loop in (*Commit).Decode. Each commitState reads one or more lines ++// from r, updates the in-progress *Commit and the scanner's bookkeeping, and ++// returns the state that should run next (or nil to stop). ++type commitScanner struct { ++ r *bufio.Reader ++ c *Commit ++ msgbuf bytes.Buffer ++ ++ // pending holds a line that was read but the current state decided to ++ // hand back to the next state, paired with the io.EOF flag that was ++ // returned when the line was originally read. ++ pending []byte ++ pendingErr error ++ ++ // First-occurrence tracking: once the corresponding field has been ++ // decoded, subsequent occurrences are silently dropped (matches ++ // upstream's find_commit_header / first-wins semantics). ++ // ++ // gpgsig is not tracked here: upstream's parse_buffer_signed_by_header ++ // (commit.c:1186) accumulates every occurrence into one signature buffer, ++ // so we do the same on the scanner side to keep verification payloads ++ // byte-aligned. gpgsig-sha256 is recognized and skipped without exposing a ++ // new field in v5. ++ sawTree, sawAuthor, sawCommitter bool ++ sawEncoding, sawMergetag bool ++ ++ // extra is the multi-line ExtraHeader currently being assembled. ++ extra *ExtraHeader ++} ++ ++// commitState is one step of the decoder state machine. Each function reads ++// the lines it needs, mutates *Commit via s.c, and returns the next state to ++// run (or nil to terminate the loop). ++type commitState func(*commitScanner) (commitState, error) ++ ++// readLine returns the next line from the buffer, transparently consuming any ++// line that was previously pushed back by a state that decided not to handle ++// it. ++func (s *commitScanner) readLine() ([]byte, error) { ++ if s.pending != nil { ++ line, err := s.pending, s.pendingErr ++ s.pending, s.pendingErr = nil, nil ++ return line, err ++ } ++ line, err := s.r.ReadBytes('\n') ++ if err != nil && err != io.EOF { ++ return line, err ++ } ++ return line, err ++} ++ ++// pushBack stashes an unconsumed line so the next state's readLine call sees ++// it. Only one line can be pushed back at a time. ++func (s *commitScanner) pushBack(line []byte, err error) { ++ s.pending = line ++ s.pendingErr = err ++} ++ ++// scanTree expects the first non-empty header to be `tree HASH`. Anything ++// else (or an empty buffer) is rejected with ErrMalformedCommit, matching ++// upstream's `bogus commit object` check. ++func scanTree(s *commitScanner) (commitState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 || isBlankLine(line) { ++ return nil, fmt.Errorf("%w: missing tree header", ErrMalformedCommit) ++ } ++ ++ key, data := splitHeader(line) ++ if key != "tree" { ++ return nil, fmt.Errorf("%w: tree header must be first", ErrMalformedCommit) ++ } ++ h, herr := parseObjectIDHex(data, ErrMalformedCommit, "tree") ++ if herr != nil { ++ return nil, herr ++ } ++ s.c.TreeHash = h ++ s.sawTree = true ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanParents, nil ++} ++ ++// scanParents consumes contiguous `parent HASH` lines. The first non-parent ++// line ends the parent block and is handed off to scanAuthor; any later ++// `parent` line is silently dropped (matches upstream's parse_commit_buffer ++// exiting its parent loop at the first non-parent line and ++// read_commit_extra_header_lines filtering `parent` out of extras). ++func scanParents(s *commitScanner) (commitState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 { ++ return nil, nil ++ } ++ if isBlankLine(line) { ++ return scanMessage, nil ++ } ++ ++ key, data := splitHeader(line) ++ if key == "parent" { ++ h, herr := parseObjectIDHex(data, ErrMalformedCommit, "parent") ++ if herr != nil { ++ return nil, herr ++ } ++ s.c.ParentHashes = append(s.c.ParentHashes, h) ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanParents, nil ++ } ++ s.pushBack(line, err) ++ return scanAuthor, nil ++} ++ ++// scanAuthor accepts an `author` line at its canonical position immediately ++// after the parent block. Any other header here is pushed back for ++// scanCommitter; an out-of-place author is therefore silently dropped. ++// Mirrors upstream's parse_commit_date func. ++func scanAuthor(s *commitScanner) (commitState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 { ++ return nil, nil ++ } ++ if isBlankLine(line) { ++ return scanMessage, nil ++ } ++ ++ key, data := splitHeader(line) ++ if key == "author" { ++ s.c.Author.Decode(data) ++ s.sawAuthor = true ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanCommitter, nil ++ } ++ s.pushBack(line, err) ++ return scanCommitter, nil ++} ++ ++// scanCommitter accepts a `committer` line at its canonical position ++// immediately after the author. Any other header is pushed back for ++// scanHeaders. Same upstream rationale as scanAuthor. ++func scanCommitter(s *commitScanner) (commitState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 { ++ return nil, nil ++ } ++ if isBlankLine(line) { ++ return scanMessage, nil ++ } ++ ++ key, data := splitHeader(line) ++ if key == "committer" { ++ s.c.Committer.Decode(data) ++ s.sawCommitter = true ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanHeaders, nil ++ } ++ s.pushBack(line, err) ++ return scanHeaders, nil ++} ++ ++// scanHeaders dispatches one header line. Continuation-bearing headers ++// (mergetag, gpgsig, gpgsig-sha256, and unknown extras whose value is ++// continued on subsequent lines) hand off to a dedicated continuation state ++// that handles the `...` lines and then returns here. ++func scanHeaders(s *commitScanner) (commitState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 { ++ return nil, nil ++ } ++ if isBlankLine(line) { ++ return scanMessage, nil ++ } ++ ++ originalLine := line ++ key, data := splitHeader(line) ++ ++ var next commitState = scanHeaders ++ switch key { ++ case "tree", "parent", "author", "committer": ++ // Anything reaching scanHeaders with one of these keys is out of ++ // canonical position: duplicate tree, parent past the contiguous ++ // block, or author/committer not at their expected slot. Drop them ++ // the same way upstream's standard_header_field filter excludes ++ // them from the extras list (read_commit_extra_header_lines, ++ // commit.c:1520-1522). ++ case headerencoding: ++ if !s.sawEncoding { ++ s.c.Encoding = MessageEncoding(data) ++ s.sawEncoding = true ++ } ++ case headermergetag: ++ if s.sawMergetag { ++ next = scanSkipCont ++ } else { ++ s.c.MergeTag += string(data) + "\n" ++ s.sawMergetag = true ++ next = scanMergetagCont ++ } ++ case headerpgp: ++ s.c.PGPSignature += string(data) + "\n" ++ next = scanPgpCont ++ case headerpgp256: ++ next = scanSkipCont ++ default: ++ h, multiline := parseExtraHeader(originalLine) ++ if multiline { ++ s.extra = &h ++ next = scanExtraCont ++ } else { ++ s.c.ExtraHeaders = append(s.c.ExtraHeaders, h) ++ } ++ } ++ ++ if err == io.EOF { ++ return nil, nil ++ } ++ return next, nil ++} ++ ++// scanMergetagCont accumulates continuation lines for the first mergetag ++// header. Continuations strip exactly one leading space, mirroring upstream's ++// `line + 1` (commit.c:1509). The first non-continuation line is pushed back ++// so scanHeaders can dispatch it. ++func scanMergetagCont(s *commitScanner) (commitState, error) { ++ return continuationCont(s, &s.c.MergeTag, scanMergetagCont) ++} ++ ++// scanPgpCont accumulates continuation lines for a signature header. ++// Continuations strip exactly one leading space, mirroring upstream's ++// `line + 1` (commit.c:1509). The first non-continuation line is pushed back ++// so scanHeaders can dispatch it. Repeat occurrences of the same signature ++// header land back here and concatenate, matching upstream's ++// parse_buffer_signed_by_header (commit.c:1186). ++func scanPgpCont(s *commitScanner) (commitState, error) { ++ return continuationCont(s, &s.c.PGPSignature, scanPgpCont) ++} ++ ++func continuationCont(s *commitScanner, dst *string, self commitState) (commitState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) > 0 && line[0] == ' ' { ++ *dst += string(line[1:]) ++ if err == io.EOF { ++ return nil, nil ++ } ++ return self, nil ++ } ++ if len(line) > 0 { ++ s.pushBack(line, err) ++ } ++ return scanHeaders, nil ++} ++ ++// scanSkipCont discards continuation lines that belong to a header scanHeaders ++// chose to drop. ++func scanSkipCont(s *commitScanner) (commitState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) > 0 && line[0] == ' ' { ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanSkipCont, nil ++ } ++ if len(line) > 0 { ++ s.pushBack(line, err) ++ } ++ return scanHeaders, nil ++} ++ ++// scanExtraCont accumulates continuation lines for an unknown ExtraHeader ++// whose value spans multiple lines, then finalises the entry once the ++// continuation block ends. ++func scanExtraCont(s *commitScanner) (commitState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) > 0 && line[0] == ' ' { ++ s.extra.Value += string(line[1:]) ++ if err == io.EOF { ++ s.finaliseExtra() ++ return nil, nil ++ } ++ return scanExtraCont, nil ++ } ++ s.finaliseExtra() ++ if len(line) > 0 { ++ s.pushBack(line, err) ++ } ++ return scanHeaders, nil ++} ++ ++func (s *commitScanner) finaliseExtra() { ++ s.extra.Value = strings.TrimRight(s.extra.Value, "\n") ++ s.c.ExtraHeaders = append(s.c.ExtraHeaders, *s.extra) ++ s.extra = nil ++} ++ ++// scanMessage drains the remaining bytes into the message buffer. ++func scanMessage(s *commitScanner) (commitState, error) { ++ for { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) > 0 { ++ s.msgbuf.Write(line) ++ } ++ if err == io.EOF { ++ return nil, nil ++ } ++ } ++} ++ ++// isBlankLine reports whether line is the canonical header/body separator: ++// a single newline. Mirrors upstream's `*line == '\n'` test in ++// read_commit_extra_header_lines (commit.c:1502). ++func isBlankLine(line []byte) bool { ++ return len(line) == 1 && line[0] == '\n' ++} ++ ++// splitHeader returns the header keyword (everything before the first space) ++// and the value (everything after, with the trailing newline stripped). If ++// the header has no value the returned data is nil. ++func splitHeader(line []byte) (string, []byte) { ++ trimmed := bytes.TrimRight(line, "\n") ++ key, value, ok := bytes.Cut(trimmed, []byte{' '}) ++ if !ok { ++ return string(trimmed), nil ++ } ++ return string(key), value ++} ++ ++func parseObjectIDHex(data []byte, malformedErr error, header string) (plumbing.Hash, error) { ++ id := string(data) ++ if !plumbing.IsHash(id) { ++ return plumbing.ZeroHash, fmt.Errorf("%w: bad %s hash", malformedErr, header) ++ } ++ return plumbing.NewHash(id), nil ++} +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go +index f9c3d30..3346e4f 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go +@@ -1,6 +1,13 @@ + package object + +-import "bytes" ++import ( ++ "bytes" ++ "io" ++ ++ "github.com/go-git/go-git/v5/plumbing" ++ "github.com/go-git/go-git/v5/utils/ioutil" ++ "github.com/go-git/go-git/v5/utils/sync" ++) + + const ( + signatureTypeUnknown signatureType = iota +@@ -100,3 +107,116 @@ func parseSignedBytes(b []byte) (int, signatureType) { + } + return match, t + } ++ ++// countSignatureBlocks reports how many distinct armored signature blocks ++// start at a line boundary in b. Used by verification paths to reject ++// multi-signature payloads, matching upstream's check in gpg-interface.c ++// where parse_gpg_output bails out the first time it sees a second ++// exclusive status line (a second GOODSIG/BADSIG/etc.). ++func countSignatureBlocks(b []byte) int { ++ n, count := 0, 0 ++ for n < len(b) { ++ i := b[n:] ++ if typeForSignature(i) != signatureTypeUnknown { ++ count++ ++ } ++ if eol := bytes.IndexByte(i, '\n'); eol >= 0 { ++ n += eol + 1 ++ continue ++ } ++ break ++ } ++ return count ++} ++ ++// isSignatureHeader reports whether line is a canonical "gpgsig "/ ++// "gpgsig-sha256 " header line. Other "gpgsig"-prefixed extra headers ++// are intentionally not matched. ++func isSignatureHeader(line []byte) bool { ++ return bytes.HasPrefix(line, []byte(headerpgp+" ")) || ++ bytes.HasPrefix(line, []byte(headerpgp256+" ")) ++} ++ ++// stripObjectSignatures streams src into dst, producing the byte sequence ++// over which a PGP/GPG signature is computed: ++// ++// - Canonical "gpgsig" and "gpgsig-sha256" headers (and their ++// continuation lines) are dropped, mirroring upstream's ++// remove_signature in commit.c. ++// - For tag objects, the inline trailing PGP signature is additionally ++// truncated, mirroring upstream's parse_signature in gpg-interface.c ++// used by gpg_verify_tag. ++// ++// The returned object's type is set to objType. Used by both ++// Commit.EncodeWithoutSignature and Tag.EncodeWithoutSignature to ++// reproduce the exact bytes the signature was computed over. ++func stripObjectSignatures(dst, src plumbing.EncodedObject, objType plumbing.ObjectType) (err error) { ++ dst.SetType(objType) ++ ++ r, err := src.Reader() ++ if err != nil { ++ return err ++ } ++ defer ioutil.CheckClose(r, &err) ++ ++ var input io.Reader = r ++ if objType == plumbing.TagObject { ++ raw, err := io.ReadAll(r) ++ if err != nil { ++ return err ++ } ++ if sm, _ := parseSignedBytes(raw); sm >= 0 { ++ raw = raw[:sm] ++ } ++ input = bytes.NewReader(raw) ++ } ++ ++ w, err := dst.Writer() ++ if err != nil { ++ return err ++ } ++ defer ioutil.CheckClose(w, &err) ++ ++ return stripHeaderSignatures(w, input) ++} ++ ++// stripHeaderSignatures copies r to w, dropping canonical signature header ++// lines (gpgsig and gpgsig-sha256) and their continuation lines. Lines ++// past the blank line that closes the header block are copied verbatim. ++func stripHeaderSignatures(w io.Writer, r io.Reader) error { ++ br := sync.GetBufioReader(r) ++ defer sync.PutBufioReader(br) ++ ++ var inBody, skipping bool ++ for { ++ line, rerr := br.ReadBytes('\n') ++ if rerr != nil && rerr != io.EOF { ++ return rerr ++ } ++ ++ write := true ++ if !inBody { ++ switch { ++ case skipping && len(line) > 0 && line[0] == ' ': ++ write = false ++ case isSignatureHeader(line): ++ skipping = true ++ write = false ++ case len(line) == 1 && line[0] == '\n': ++ skipping = false ++ inBody = true ++ default: ++ skipping = false ++ } ++ } ++ ++ if write && len(line) > 0 { ++ if _, werr := w.Write(line); werr != nil { ++ return werr ++ } ++ } ++ if rerr == io.EOF { ++ return nil ++ } ++ } ++} +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go +index cf46c08..93e56a4 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go +@@ -1,9 +1,8 @@ + package object + + import ( +- "bytes" ++ "errors" + "fmt" +- "io" + "strings" + + "github.com/ProtonMail/go-crypto/openpgp" +@@ -13,6 +12,10 @@ import ( + "github.com/go-git/go-git/v5/utils/sync" + ) + ++// ErrMalformedTag is returned when a tag object cannot be decoded because ++// its required headers (object, type, tag) are missing or out of order. ++var ErrMalformedTag = errors.New("malformed tag") ++ + // Tag represents an annotated tag object. It points to a single git object of + // any type, but tags typically are applied to commit or blob objects. It + // provides a reference that associates the target with a tag name. It also +@@ -39,6 +42,9 @@ type Tag struct { + Target plumbing.Hash + + s storer.EncodedObjectStorer ++ // src holds the encoded object this Tag was decoded from, used by ++ // EncodeWithoutSignature to recover the canonical signed bytes. ++ src plumbing.EncodedObject + } + + // GetTag gets a tag from an object storer and decodes it. +@@ -77,13 +83,20 @@ func (t *Tag) Type() plumbing.ObjectType { + return plumbing.TagObject + } + ++func (t *Tag) reset() { ++ storer := t.s ++ *t = Tag{s: storer} ++} ++ + // Decode transforms a plumbing.EncodedObject into a Tag struct. + func (t *Tag) Decode(o plumbing.EncodedObject) (err error) { + if o.Type() != plumbing.TagObject { + return ErrUnsupportedObject + } + ++ t.reset() + t.Hash = o.Hash() ++ t.src = o + + reader, err := o.Reader() + if err != nil { +@@ -94,42 +107,15 @@ func (t *Tag) Decode(o plumbing.EncodedObject) (err error) { + r := sync.GetBufioReader(reader) + defer sync.PutBufioReader(r) + +- for { +- var line []byte +- line, err = r.ReadBytes('\n') +- if err != nil && err != io.EOF { ++ scanner := &tagScanner{r: r, t: t} ++ for state := scanTagObject; state != nil; { ++ state, err = state(scanner) ++ if err != nil { + return err + } +- +- line = bytes.TrimSpace(line) +- if len(line) == 0 { +- break // Start of message +- } +- +- split := bytes.SplitN(line, []byte{' '}, 2) +- switch string(split[0]) { +- case "object": +- t.Target = plumbing.NewHash(string(split[1])) +- case "type": +- t.TargetType, err = plumbing.ParseObjectType(string(split[1])) +- if err != nil { +- return err +- } +- case "tag": +- t.Name = string(split[1]) +- case "tagger": +- t.Tagger.Decode(split[1]) +- } +- +- if err == io.EOF { +- return nil +- } + } + +- data, err := io.ReadAll(r) +- if err != nil { +- return err +- } ++ data := scanner.msgbuf.Bytes() + if sm, _ := parseSignedBytes(data); sm >= 0 { + t.PGPSignature = string(data[sm:]) + data = data[:sm] +@@ -144,11 +130,54 @@ func (t *Tag) Encode(o plumbing.EncodedObject) error { + return t.encode(o, true) + } + +-// EncodeWithoutSignature export a Tag into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature). ++// EncodeWithoutSignature exports a Tag into a plumbing.EncodedObject without ++// any signature data, producing the payload that PGP/GPG signatures are ++// computed over. ++// ++// Behaviour mirrors Commit.EncodeWithoutSignature: ++// ++// - For Tags populated by Decode whose exported fields still match the ++// source object, the payload is streamed from the raw source bytes with ++// the inline trailing signature truncated and gpgsig/gpgsig-sha256 ++// headers (and their continuation lines) stripped verbatim. This ++// preserves the exact bytes the signature was computed over, regardless ++// of any normalization performed by Decode. ++// ++// - For Tags constructed in memory, or for decoded Tags whose exported ++// fields have been mutated, the payload is derived from the current ++// struct fields. Mutation is detected by re-decoding the source object ++// and comparing exported fields; if any differ, the in-memory ++// representation prevails. + func (t *Tag) EncodeWithoutSignature(o plumbing.EncodedObject) error { ++ if t.matchesSource() { ++ return stripObjectSignatures(o, t.src, plumbing.TagObject) ++ } + return t.encode(o, false) + } + ++// matchesSource reports whether t.src is set and re-decoding it produces a ++// Tag whose payload-affecting exported fields are identical to those of t. ++// ++// PGPSignature is intentionally excluded from the comparison: neither path ++// emits it as part of the verification payload, so mutating it must not ++// trigger a switch to struct-encode (which would change the byte layout the ++// caller is trying to verify against). ++func (t *Tag) matchesSource() bool { ++ if t.src == nil { ++ return false ++ } ++ fresh := &Tag{} ++ if err := fresh.Decode(t.src); err != nil { ++ return false ++ } ++ return t.Hash == fresh.Hash && ++ t.Name == fresh.Name && ++ signatureEqual(t.Tagger, fresh.Tagger) && ++ t.Message == fresh.Message && ++ t.TargetType == fresh.TargetType && ++ t.Target == fresh.Target ++} ++ + func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { + o.SetType(plumbing.TagObject) + w, err := o.Writer() +@@ -158,16 +187,26 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { + defer ioutil.CheckClose(w, &err) + + if _, err = fmt.Fprintf(w, +- "object %s\ntype %s\ntag %s\ntagger ", ++ "object %s\ntype %s\ntag %s\n", + t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil { + return err + } + +- if err = t.Tagger.Encode(w); err != nil { +- return err ++ if !isZeroSignature(t.Tagger) { ++ if _, err = fmt.Fprint(w, "tagger "); err != nil { ++ return err ++ } ++ ++ if err = t.Tagger.Encode(w); err != nil { ++ return err ++ } ++ ++ if _, err = fmt.Fprint(w, "\n"); err != nil { ++ return err ++ } + } + +- if _, err = fmt.Fprint(w, "\n\n"); err != nil { ++ if _, err = fmt.Fprint(w, "\n"); err != nil { + return err + } + +@@ -175,11 +214,12 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { + return err + } + +- // Note that this is highly sensitive to what it sent along in the message. +- // Message *always* needs to end with a newline, or else the message and the +- // signature will be concatenated into a corrupt object. Since this is a +- // lower-level method, we assume you know what you are doing and have already +- // done the needful on the message in the caller. ++ // Note that this is highly sensitive to what is sent along in the ++ // message. Message *always* needs to end with a newline, or else the ++ // message and the trailing signature will be concatenated into a ++ // corrupt object. Since this is a lower-level method, we assume you ++ // know what you are doing and have already done the needful on the ++ // message in the caller. + if includeSig { + if _, err = fmt.Fprint(w, t.PGPSignature); err != nil { + return err +@@ -189,6 +229,10 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { + return err + } + ++func isZeroSignature(s Signature) bool { ++ return s.Name == "" && s.Email == "" && s.When.IsZero() ++} ++ + // Commit returns the commit pointed to by the tag. If the tag points to a + // different type of object ErrUnsupportedObject will be returned. + func (t *Tag) Commit() (*Commit, error) { +@@ -256,7 +300,8 @@ func (t *Tag) String() string { + } + + // Verify performs PGP verification of the tag with a provided armored +-// keyring and returns openpgp.Entity associated with verifying key on success. ++// keyring and returns openpgp.Entity associated with verifying key on ++// success. + func (t *Tag) Verify(armoredKeyRing string) (*openpgp.Entity, error) { + keyRingReader := strings.NewReader(armoredKeyRing) + keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader) +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go +new file mode 100644 +index 0000000..2bfb3a1 +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go +@@ -0,0 +1,237 @@ ++package object ++ ++import ( ++ "bufio" ++ "bytes" ++ "fmt" ++ "io" ++ ++ "github.com/go-git/go-git/v5/plumbing" ++) ++ ++// tagScanner holds the working state of the tag decoder driven by the ++// stateFn loop in (*Tag).Decode. Each tagState reads one or more lines ++// from r, updates the in-progress *Tag and the scanner's bookkeeping, ++// and returns the state that should run next (or nil to stop). ++type tagScanner struct { ++ r *bufio.Reader ++ t *Tag ++ msgbuf bytes.Buffer ++ ++ // pending holds a line that was read but the current state decided to ++ // hand back to the next state, paired with the io.EOF flag returned ++ // when the line was originally read. ++ pending []byte ++ pendingErr error ++ ++ // First-occurrence tracking: once the corresponding canonical ++ // header has been decoded at its expected position, subsequent ++ // occurrences (or out-of-position lines) are silently dropped, ++ // matching the strict layout enforced by upstream's ++ // parse_tag_buffer (tag.c:130). ++ // ++ // gpgsig-sha256 is recognized and skipped without exposing a new field ++ // in v5. ++ sawObject, sawType, sawName, sawTagger bool ++} ++ ++// tagState is one step of the decoder state machine. Each function reads ++// the lines it needs, mutates *Tag via s.t, and returns the next state ++// to run (or nil to terminate the loop). ++type tagState func(*tagScanner) (tagState, error) ++ ++// readLine returns the next line from the buffer, transparently ++// consuming any line that was previously pushed back by a state that ++// decided not to handle it. ++func (s *tagScanner) readLine() ([]byte, error) { ++ if s.pending != nil { ++ line, err := s.pending, s.pendingErr ++ s.pending, s.pendingErr = nil, nil ++ return line, err ++ } ++ return s.r.ReadBytes('\n') ++} ++ ++// pushBack stashes an unconsumed line so the next state's readLine call ++// sees it. Only one line can be pushed back at a time. ++func (s *tagScanner) pushBack(line []byte, err error) { ++ s.pending = line ++ s.pendingErr = err ++} ++ ++// scanTagObject requires the first line to be `object HASH`, mirroring ++// upstream's strict parse_tag_buffer (tag.c:151-156). Anything else ++// returns ErrMalformedTag. ++func scanTagObject(s *tagScanner) (tagState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 || isBlankLine(line) { ++ return nil, fmt.Errorf("%w: missing object header", ErrMalformedTag) ++ } ++ ++ key, data := splitHeader(line) ++ if key != "object" { ++ return nil, fmt.Errorf("%w: object header must be first", ErrMalformedTag) ++ } ++ h, herr := parseObjectIDHex(data, ErrMalformedTag, "object") ++ if herr != nil { ++ return nil, herr ++ } ++ s.t.Target = h ++ s.sawObject = true ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanTagType, nil ++} ++ ++// scanTagType requires a `type` line immediately after the object header, ++// mirroring upstream's parse_tag_buffer (tag.c:158-166). ++func scanTagType(s *tagScanner) (tagState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 || isBlankLine(line) { ++ return nil, fmt.Errorf("%w: missing type header", ErrMalformedTag) ++ } ++ ++ key, data := splitHeader(line) ++ if key != "type" { ++ return nil, fmt.Errorf("%w: type header must follow object", ErrMalformedTag) ++ } ++ ot, perr := plumbing.ParseObjectType(string(data)) ++ if perr != nil { ++ return nil, perr ++ } ++ s.t.TargetType = ot ++ s.sawType = true ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanTagName, nil ++} ++ ++// scanTagName requires a `tag` line immediately after the type header, ++// mirroring upstream's parse_tag_buffer (tag.c:186-194). ++func scanTagName(s *tagScanner) (tagState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 || isBlankLine(line) { ++ return nil, fmt.Errorf("%w: missing tag header", ErrMalformedTag) ++ } ++ ++ key, data := splitHeader(line) ++ if key != "tag" { ++ return nil, fmt.Errorf("%w: tag header must follow type", ErrMalformedTag) ++ } ++ s.t.Name = string(data) ++ s.sawName = true ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanTagTagger, nil ++} ++ ++// scanTagTagger accepts a `tagger` line at its canonical position. Any ++// other header is pushed back for scanTagHeaders. ++func scanTagTagger(s *tagScanner) (tagState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 { ++ return nil, nil ++ } ++ if isBlankLine(line) { ++ return scanTagMessage, nil ++ } ++ ++ key, data := splitHeader(line) ++ if key == "tagger" { ++ s.t.Tagger.Decode(data) ++ s.sawTagger = true ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanTagHeaders, nil ++ } ++ s.pushBack(line, err) ++ return scanTagHeaders, nil ++} ++ ++// scanTagHeaders dispatches one header line. gpgsig-sha256 hands off to ++// scanTagSkipCont so the continuation block can be consumed; out-of-position ++// canonical fields and unknown headers are silently dropped. ++func scanTagHeaders(s *tagScanner) (tagState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) == 0 { ++ return nil, nil ++ } ++ if isBlankLine(line) { ++ return scanTagMessage, nil ++ } ++ ++ key, _ := splitHeader(line) ++ next := scanTagHeaders ++ switch key { ++ case "object", "type", "tag", "tagger": ++ // Out-of-canonical-position duplicates are dropped, mirroring the ++ // strict ordering of upstream's parse_tag_buffer. ++ case headerpgp256: ++ next = scanTagSkipCont ++ default: ++ // Unknown header: silently dropped (the Tag struct does not ++ // expose ExtraHeaders). ++ } ++ ++ if err == io.EOF { ++ return nil, nil ++ } ++ return next, nil ++} ++ ++// scanTagSkipCont discards continuation lines for a header scanTagHeaders chose ++// to drop. The first non-continuation line is pushed back so scanTagHeaders can ++// dispatch it. ++func scanTagSkipCont(s *tagScanner) (tagState, error) { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) > 0 && line[0] == ' ' { ++ if err == io.EOF { ++ return nil, nil ++ } ++ return scanTagSkipCont, nil ++ } ++ if len(line) > 0 { ++ s.pushBack(line, err) ++ } ++ return scanTagHeaders, nil ++} ++ ++// scanTagMessage drains the remaining bytes into the message buffer. ++// (*Tag).Decode then runs parseSignedBytes over those bytes to peel off ++// the optional inline trailing PGP signature. ++func scanTagMessage(s *tagScanner) (tagState, error) { ++ for { ++ line, err := s.readLine() ++ if err != nil && err != io.EOF { ++ return nil, err ++ } ++ if len(line) > 0 { ++ s.msgbuf.Write(line) ++ } ++ if err == io.EOF { ++ return nil, nil ++ } ++ } ++} +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go +index 2e1b789..3c004f5 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go +@@ -10,6 +10,7 @@ import ( + "sort" + "strings" + ++ "github.com/go-git/go-git/v5/internal/pathutil" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/storer" +@@ -29,6 +30,7 @@ var ( + ErrDirectoryNotFound = errors.New("directory not found") + ErrEntryNotFound = errors.New("entry not found") + ErrEntriesNotSorted = errors.New("entries in tree are not sorted") ++ ErrMalformedTree = errors.New("malformed tree") + ) + + // Tree is basically like a directory - it references a bunch of other trees +@@ -37,9 +39,9 @@ type Tree struct { + Entries []TreeEntry + Hash plumbing.Hash + +- s storer.EncodedObjectStorer +- m map[string]*TreeEntry +- t map[string]*Tree // tree path cache ++ s storer.EncodedObjectStorer ++ t map[string]*Tree // tree path cache ++ entriesSorted bool + } + + // GetTree gets a tree from an object storer and decodes it. +@@ -117,7 +119,16 @@ func (t *Tree) Tree(path string) (*Tree, error) { + } + + // TreeEntryFile returns the *File for a given *TreeEntry. ++// ++// The entry's name is validated against pathutil.ValidTreePath for ++// the same reason FindEntry validates: TreeEntryFile is a boundary ++// where attacker-controlled tree data leaves the trusted store as a ++// *File whose Name a caller can hand to filesystem ops. + func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) { ++ if err := pathutil.ValidTreePath(e.Name); err != nil { ++ return nil, err ++ } ++ + blob, err := GetBlob(t.s, e.Hash) + if err != nil { + return nil, err +@@ -127,7 +138,16 @@ func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) { + } + + // FindEntry search a TreeEntry in this tree or any subtree. ++// ++// The lookup path is validated against pathutil.ValidTreePath to ++// prevent attacker-controlled tree contents from leaking past this ++// boundary as `.git`-shaped or path-traversal-shaped names. Callers ++// that legitimately need to look up unsafe paths should walk the ++// tree manually. + func (t *Tree) FindEntry(path string) (*TreeEntry, error) { ++ if err := pathutil.ValidTreePath(path); err != nil { ++ return nil, err ++ } + if t.t == nil { + t.t = make(map[string]*Tree) + } +@@ -182,16 +202,43 @@ func (t *Tree) dir(baseName string) (*Tree, error) { + } + + func (t *Tree) entry(baseName string) (*TreeEntry, error) { +- if t.m == nil { +- t.buildMap() ++ if t.entriesSorted { ++ if entry := t.searchEntry(baseName); entry != nil { ++ return entry, nil ++ } ++ return nil, ErrEntryNotFound + } + +- entry, ok := t.m[baseName] +- if !ok { +- return nil, ErrEntryNotFound ++ pastName := baseName + "/" ++ for i := range t.Entries { ++ entry := &t.Entries[i] ++ if entry.Name == baseName { ++ return entry, nil ++ } ++ if treeEntrySortName(entry) > pastName { ++ break ++ } ++ } ++ ++ return nil, ErrEntryNotFound ++} ++ ++func (t *Tree) searchEntry(baseName string) *TreeEntry { ++ if i := t.searchEntryIndex(baseName); i < len(t.Entries) && t.Entries[i].Name == baseName { ++ return &t.Entries[i] ++ } ++ ++ if i := t.searchEntryIndex(baseName + "/"); i < len(t.Entries) && t.Entries[i].Name == baseName { ++ return &t.Entries[i] + } + +- return entry, nil ++ return nil ++} ++ ++func (t *Tree) searchEntryIndex(name string) int { ++ return sort.Search(len(t.Entries), func(i int) bool { ++ return treeEntrySortName(&t.Entries[i]) >= name ++ }) + } + + // Files returns a FileIter allowing to iterate over the Tree +@@ -212,20 +259,25 @@ func (t *Tree) Type() plumbing.ObjectType { + return plumbing.TreeObject + } + ++func (t *Tree) reset() { ++ storer := t.s ++ *t = Tree{s: storer} ++} ++ + // Decode transform an plumbing.EncodedObject into a Tree struct + func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { + if o.Type() != plumbing.TreeObject { + return ErrUnsupportedObject + } + ++ t.reset() + t.Hash = o.Hash() ++ // assume tree is sorted as a valid tree should always be sorted. ++ t.entriesSorted = true + if o.Size() == 0 { + return nil + } + +- t.Entries = nil +- t.m = nil +- + reader, err := o.Reader() + if err != nil { + return err +@@ -235,10 +287,14 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { + r := sync.GetBufioReader(reader) + defer sync.PutBufioReader(r) + ++ var prevSortName string + for { + str, err := r.ReadString(' ') + if err != nil { + if err == io.EOF { ++ if len(str) != 0 { ++ return fmt.Errorf("%w: missing mode terminator", ErrMalformedTree) ++ } + break + } + +@@ -248,25 +304,41 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { + + mode, err := filemode.New(str) + if err != nil { +- return err ++ return fmt.Errorf("%w: malformed mode", ErrMalformedTree) + } ++ mode = canonicalTreeMode(mode) + + name, err := r.ReadString(0) +- if err != nil && err != io.EOF { ++ if err != nil { ++ if err == io.EOF { ++ return fmt.Errorf("%w: missing filename terminator", ErrMalformedTree) ++ } + return err + } ++ if len(name) == 1 { ++ return fmt.Errorf("%w: empty filename", ErrMalformedTree) ++ } + + var hash plumbing.Hash + if _, err = io.ReadFull(r, hash[:]); err != nil { ++ if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { ++ return fmt.Errorf("%w: truncated object id", ErrMalformedTree) ++ } + return err + } + + baseName := name[:len(name)-1] +- t.Entries = append(t.Entries, TreeEntry{ ++ entry := TreeEntry{ + Hash: hash, + Mode: mode, + Name: baseName, +- }) ++ } ++ sortName := treeEntrySortName(&entry) ++ if len(t.Entries) != 0 && prevSortName > sortName { ++ t.entriesSorted = false ++ } ++ prevSortName = sortName ++ t.Entries = append(t.Entries, entry) + } + + return nil +@@ -279,21 +351,37 @@ func (s TreeEntrySorter) Len() int { + } + + func (s TreeEntrySorter) Less(i, j int) bool { +- name1 := s[i].Name +- name2 := s[j].Name +- if s[i].Mode == filemode.Dir { +- name1 += "/" +- } +- if s[j].Mode == filemode.Dir { +- name2 += "/" +- } +- return name1 < name2 ++ return treeEntrySortName(&s[i]) < treeEntrySortName(&s[j]) + } + + func (s TreeEntrySorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] + } + ++// Git compares tree entries as if directory names had a trailing slash. ++func treeEntrySortName(e *TreeEntry) string { ++ if e.Mode == filemode.Dir { ++ return e.Name + "/" ++ } ++ return e.Name ++} ++ ++func canonicalTreeMode(mode filemode.FileMode) filemode.FileMode { ++ switch mode & 0o170000 { ++ case 0o040000: ++ return filemode.Dir ++ case 0o100000: ++ if mode&0o111 != 0 { ++ return filemode.Executable ++ } ++ return filemode.Regular ++ case 0o120000: ++ return filemode.Symlink ++ default: ++ return filemode.Submodule ++ } ++} ++ + // Encode transforms a Tree into a plumbing.EncodedObject. + // The tree entries must be sorted by name. + func (t *Tree) Encode(o plumbing.EncodedObject) (err error) { +@@ -329,13 +417,6 @@ func (t *Tree) Encode(o plumbing.EncodedObject) (err error) { + return err + } + +-func (t *Tree) buildMap() { +- t.m = make(map[string]*TreeEntry) +- for i := 0; i < len(t.Entries); i++ { +- t.m[t.Entries[i].Name] = &t.Entries[i] +- } +-} +- + // Diff returns a list of changes between this tree and the provided one + func (t *Tree) Diff(to *Tree) (Changes, error) { + return t.DiffContext(context.Background(), to) +@@ -455,6 +536,10 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) { + continue + } + ++ if err := pathutil.ValidTreePath(entry.Name); err != nil { ++ return name, entry, err ++ } ++ + if entry.Mode == filemode.Dir { + obj, err = GetTree(w.s, entry.Hash) + } +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/advrefs_decode.go b/vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/advrefs_decode.go +index f8d26a2..2a94083 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/advrefs_decode.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/advrefs_decode.go +@@ -262,9 +262,8 @@ func decodeShallow(p *advRefsDecoder) decoderStateFn { + p.line = bytes.TrimPrefix(p.line, shallow) + + if len(p.line) != hashSize { +- p.error(fmt.Sprintf( +- "malformed shallow hash: wrong length, expected 40 bytes, read %d bytes", +- len(p.line))) ++ p.error("malformed shallow hash: wrong length, expected 40 bytes, read %d bytes", ++ len(p.line)) + return nil + } + +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/updreq_encode.go b/vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/updreq_encode.go +index 1205cfa..157fa56 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/updreq_encode.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/protocol/packp/updreq_encode.go +@@ -62,7 +62,7 @@ func (req *ReferenceUpdateRequest) encodeCommands(e *pktline.Encoder, + } + + for _, cmd := range cmds[1:] { +- if err := e.Encodef(formatCommand(cmd)); err != nil { ++ if err := e.Encodef("%s", formatCommand(cmd)); err != nil { + return err + } + } +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/transport/common.go b/vendor/github.com/go-git/go-git/v5/plumbing/transport/common.go +index fae1aa9..b4c5e98 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/transport/common.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/transport/common.go +@@ -113,9 +113,17 @@ type Endpoint struct { + Port int + // Path is the repository path. + Path string +- // InsecureSkipTLS skips ssl verify if protocol is https ++ // InsecureSkipTLS skips SSL verification if Protocol is HTTPS. + InsecureSkipTLS bool +- // CaBundle specify additional ca bundle with system cert pool ++ // ClientCert specifies an optional client certificate to use for mutual ++ // TLS authentication if Protocol is HTTPS. ++ ClientCert []byte ++ // ClientKey specifies an optional client key to use for mutual TLS ++ // authentication if Protocol is HTTPS. ++ ClientKey []byte ++ // CaBundle specifies an optional CA bundle to use for SSL verification ++ // if Protocol is HTTPS. The bundle is added in addition to the system ++ // CA bundle. + CaBundle []byte + // Proxy provides info required for connecting to a proxy. + Proxy ProxyOptions +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go b/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go +index 120008d..83f93f1 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go +@@ -7,7 +7,6 @@ import ( + "crypto/tls" + "crypto/x509" + "fmt" +- "net" + "net/http" + "net/url" + "reflect" +@@ -15,16 +14,45 @@ import ( + "strings" + "sync" + ++ "github.com/golang/groupcache/lru" ++ + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/protocol/packp" ++ "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/utils/ioutil" +- "github.com/golang/groupcache/lru" + ) + ++type contextKey int ++ ++const initialRequestKey contextKey = iota ++ ++// RedirectPolicy controls how the HTTP transport follows redirects. ++// ++// The values mirror Git's http.followRedirects config: ++// "true" follows redirects for all requests, "false" treats redirects as ++// errors, and "initial" follows redirects only for the initial ++// /info/refs discovery request. The zero value defaults to "initial". ++type RedirectPolicy string ++ ++const ( ++ FollowInitialRedirects RedirectPolicy = "initial" ++ FollowRedirects RedirectPolicy = "true" ++ NoFollowRedirects RedirectPolicy = "false" ++) ++ ++func withInitialRequest(ctx context.Context) context.Context { ++ return context.WithValue(ctx, initialRequestKey, true) ++} ++ ++func isInitialRequest(req *http.Request) bool { ++ v, _ := req.Context().Value(initialRequestKey).(bool) ++ return v ++} ++ + // it requires a bytes.Buffer, because we need to know the length + func applyHeadersToRequest(req *http.Request, content *bytes.Buffer, host string, requestType string) { +- req.Header.Add("User-Agent", "git/1.0") ++ req.Header.Add("User-Agent", capability.DefaultAgent()) + req.Header.Add("Host", host) // host:port + + if content == nil { +@@ -52,12 +80,15 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) ( + + s.ApplyAuthToRequest(req) + applyHeadersToRequest(req, nil, s.endpoint.Host, serviceName) +- res, err := s.client.Do(req.WithContext(ctx)) ++ res, err := s.client.Do(req.WithContext(withInitialRequest(ctx))) + if err != nil { + return nil, err + } + +- s.ModifyEndpointIfRedirect(res) ++ if err := s.ModifyEndpointIfRedirect(res); err != nil { ++ _ = res.Body.Close() ++ return nil, err ++ } + defer ioutil.CheckClose(res.Body, &err) + + if err = NewErr(res); err != nil { +@@ -94,6 +125,7 @@ type client struct { + client *http.Client + transports *lru.Cache + mutex sync.RWMutex ++ follow RedirectPolicy + } + + // ClientOptions holds user configurable options for the client. +@@ -104,6 +136,11 @@ type ClientOptions struct { + // size, will result in the least recently used transport getting deleted + // before the provided transport is added to the cache. + CacheMaxEntries int ++ ++ // RedirectPolicy controls redirect handling. Supported values are ++ // "true", "false", and "initial". The zero value defaults to ++ // "initial", matching Git's http.followRedirects default. ++ RedirectPolicy RedirectPolicy + } + + var ( +@@ -148,12 +185,16 @@ func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transpo + } + cl := &client{ + client: c, ++ follow: FollowInitialRedirects, + } + + if opts != nil { + if opts.CacheMaxEntries > 0 { + cl.transports = lru.New(opts.CacheMaxEntries) + } ++ if opts.RedirectPolicy != "" { ++ cl.follow = opts.RedirectPolicy ++ } + } + return cl + } +@@ -184,6 +225,18 @@ func transportWithInsecureTLS(transport *http.Transport) { + transport.TLSClientConfig.InsecureSkipVerify = true + } + ++func transportWithClientCert(transport *http.Transport, cert, key []byte) error { ++ keyPair, err := tls.X509KeyPair(cert, key) ++ if err != nil { ++ return err ++ } ++ if transport.TLSClientConfig == nil { ++ transport.TLSClientConfig = &tls.Config{} ++ } ++ transport.TLSClientConfig.Certificates = []tls.Certificate{keyPair} ++ return nil ++} ++ + func transportWithCABundle(transport *http.Transport, caBundle []byte) error { + rootCAs, err := x509.SystemCertPool() + if err != nil { +@@ -205,6 +258,11 @@ func transportWithProxy(transport *http.Transport, proxyURL *url.URL) { + } + + func configureTransport(transport *http.Transport, ep *transport.Endpoint) error { ++ if len(ep.ClientCert) > 0 && len(ep.ClientKey) > 0 { ++ if err := transportWithClientCert(transport, ep.ClientCert, ep.ClientKey); err != nil { ++ return err ++ } ++ } + if len(ep.CaBundle) > 0 { + if err := transportWithCABundle(transport, ep.CaBundle); err != nil { + return err +@@ -229,7 +287,7 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* + + // We need to configure the http transport if there are transport specific + // options present in the endpoint. +- if len(ep.CaBundle) > 0 || ep.InsecureSkipTLS || ep.Proxy.URL != "" { ++ if len(ep.ClientKey) > 0 || len(ep.ClientCert) > 0 || len(ep.CaBundle) > 0 || ep.InsecureSkipTLS || ep.Proxy.URL != "" { + var transport *http.Transport + // if the client wasn't configured to have a cache for transports then just configure + // the transport and use it directly, otherwise try to use the cache. +@@ -241,9 +299,13 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* + } + + transport = tr.Clone() +- configureTransport(transport, ep) ++ if err := configureTransport(transport, ep); err != nil { ++ return nil, err ++ } + } else { + transportOpts := transportOptions{ ++ clientCert: string(ep.ClientCert), ++ clientKey: string(ep.ClientKey), + caBundle: string(ep.CaBundle), + insecureSkipTLS: ep.InsecureSkipTLS, + } +@@ -259,19 +321,16 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* + + if !found { + transport = c.client.Transport.(*http.Transport).Clone() +- configureTransport(transport, ep) ++ if err := configureTransport(transport, ep); err != nil { ++ return nil, err ++ } + c.addTransport(transportOpts, transport) + } + } + +- httpClient = &http.Client{ +- Transport: transport, +- CheckRedirect: c.client.CheckRedirect, +- Jar: c.client.Jar, +- Timeout: c.client.Timeout, +- } ++ httpClient = c.cloneHTTPClient(transport) + } else { +- httpClient = c.client ++ httpClient = c.cloneHTTPClient(c.client.Transport) + } + + s := &session{ +@@ -299,30 +358,122 @@ func (s *session) ApplyAuthToRequest(req *http.Request) { + s.auth.SetAuth(req) + } + +-func (s *session) ModifyEndpointIfRedirect(res *http.Response) { ++func (s *session) ModifyEndpointIfRedirect(res *http.Response) error { + if res.Request == nil { +- return ++ return nil ++ } ++ if s.endpoint == nil { ++ return fmt.Errorf("http redirect: nil endpoint") + } + + r := res.Request + if !strings.HasSuffix(r.URL.Path, infoRefsPath) { +- return ++ return fmt.Errorf("http redirect: target %q does not end with %s", r.URL.Path, infoRefsPath) ++ } ++ if r.URL.Scheme != "http" && r.URL.Scheme != "https" { ++ return fmt.Errorf("http redirect: unsupported scheme %q", r.URL.Scheme) ++ } ++ if r.URL.Scheme != s.endpoint.Protocol && ++ !(s.endpoint.Protocol == "http" && r.URL.Scheme == "https") { ++ return fmt.Errorf("http redirect: changes scheme from %q to %q", s.endpoint.Protocol, r.URL.Scheme) + } + +- h, p, err := net.SplitHostPort(r.URL.Host) ++ host := endpointHost(r.URL.Hostname()) ++ port, err := endpointPort(r.URL.Port()) + if err != nil { +- h = r.URL.Host ++ return err + } +- if p != "" { +- port, err := strconv.Atoi(p) +- if err == nil { +- s.endpoint.Port = port +- } ++ ++ if host != s.endpoint.Host || effectivePort(r.URL.Scheme, port) != effectivePort(s.endpoint.Protocol, s.endpoint.Port) { ++ s.endpoint.User = "" ++ s.endpoint.Password = "" ++ s.auth = nil + } +- s.endpoint.Host = h ++ ++ s.endpoint.Host = host ++ s.endpoint.Port = port + + s.endpoint.Protocol = r.URL.Scheme + s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)] ++ return nil ++} ++ ++func endpointHost(host string) string { ++ if strings.Contains(host, ":") { ++ return "[" + host + "]" ++ } ++ ++ return host ++} ++ ++func endpointPort(port string) (int, error) { ++ if port == "" { ++ return 0, nil ++ } ++ ++ parsed, err := strconv.Atoi(port) ++ if err != nil { ++ return 0, fmt.Errorf("http redirect: invalid port %q", port) ++ } ++ ++ return parsed, nil ++} ++ ++func effectivePort(scheme string, port int) int { ++ if port != 0 { ++ return port ++ } ++ ++ switch strings.ToLower(scheme) { ++ case "http": ++ return 80 ++ case "https": ++ return 443 ++ default: ++ return 0 ++ } ++} ++ ++func (c *client) cloneHTTPClient(transport http.RoundTripper) *http.Client { ++ return &http.Client{ ++ Transport: transport, ++ CheckRedirect: wrapCheckRedirect(c.follow, c.client.CheckRedirect), ++ Jar: c.client.Jar, ++ Timeout: c.client.Timeout, ++ } ++} ++ ++func wrapCheckRedirect(policy RedirectPolicy, next func(*http.Request, []*http.Request) error) func(*http.Request, []*http.Request) error { ++ return func(req *http.Request, via []*http.Request) error { ++ if err := checkRedirect(req, via, policy); err != nil { ++ return err ++ } ++ if next != nil { ++ return next(req, via) ++ } ++ return nil ++ } ++} ++ ++func checkRedirect(req *http.Request, via []*http.Request, policy RedirectPolicy) error { ++ switch policy { ++ case FollowRedirects: ++ case NoFollowRedirects: ++ return fmt.Errorf("http redirect: redirects disabled to %s", req.URL) ++ case "", FollowInitialRedirects: ++ if !isInitialRequest(req) { ++ return fmt.Errorf("http redirect: redirect on non-initial request to %s", req.URL) ++ } ++ default: ++ return fmt.Errorf("http redirect: invalid redirect policy %q", policy) ++ } ++ if req.URL.Scheme != "http" && req.URL.Scheme != "https" { ++ return fmt.Errorf("http redirect: unsupported scheme %q", req.URL.Scheme) ++ } ++ if len(via) >= 10 { ++ return fmt.Errorf("http redirect: too many redirects") ++ } ++ return nil + } + + func (*session) Close() error { +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/transport.go b/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/transport.go +index c8db389..1991d05 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/transport.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/transport.go +@@ -9,8 +9,10 @@ import ( + type transportOptions struct { + insecureSkipTLS bool + // []byte is not comparable. +- caBundle string +- proxyURL url.URL ++ clientCert string ++ clientKey string ++ caBundle string ++ proxyURL url.URL + } + + func (c *client) addTransport(opts transportOptions, transport *http.Transport) { +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/auth_method.go b/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/auth_method.go +index f9c598e..a616106 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/auth_method.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/auth_method.go +@@ -54,7 +54,7 @@ func (a *KeyboardInteractive) String() string { + } + + func (a *KeyboardInteractive) ClientConfig() (*ssh.ClientConfig, error) { +- return a.SetHostKeyCallback(&ssh.ClientConfig{ ++ return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ + User: a.User, + Auth: []ssh.AuthMethod{ + a.Challenge, +@@ -78,7 +78,7 @@ func (a *Password) String() string { + } + + func (a *Password) ClientConfig() (*ssh.ClientConfig, error) { +- return a.SetHostKeyCallback(&ssh.ClientConfig{ ++ return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ + User: a.User, + Auth: []ssh.AuthMethod{ssh.Password(a.Password)}, + }) +@@ -101,7 +101,7 @@ func (a *PasswordCallback) String() string { + } + + func (a *PasswordCallback) ClientConfig() (*ssh.ClientConfig, error) { +- return a.SetHostKeyCallback(&ssh.ClientConfig{ ++ return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ + User: a.User, + Auth: []ssh.AuthMethod{ssh.PasswordCallback(a.Callback)}, + }) +@@ -150,7 +150,7 @@ func (a *PublicKeys) String() string { + } + + func (a *PublicKeys) ClientConfig() (*ssh.ClientConfig, error) { +- return a.SetHostKeyCallback(&ssh.ClientConfig{ ++ return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ + User: a.User, + Auth: []ssh.AuthMethod{ssh.PublicKeys(a.Signer)}, + }) +@@ -211,7 +211,7 @@ func (a *PublicKeysCallback) String() string { + } + + func (a *PublicKeysCallback) ClientConfig() (*ssh.ClientConfig, error) { +- return a.SetHostKeyCallback(&ssh.ClientConfig{ ++ return a.SetHostKeyCallbackAndAlgorithms(&ssh.ClientConfig{ + User: a.User, + Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(a.Callback)}, + }) +@@ -230,11 +230,26 @@ func (a *PublicKeysCallback) ClientConfig() (*ssh.ClientConfig, error) { + // ~/.ssh/known_hosts + // /etc/ssh/ssh_known_hosts + func NewKnownHostsCallback(files ...string) (ssh.HostKeyCallback, error) { +- db, err := newKnownHostsDb(files...) +- return db.HostKeyCallback(), err ++ kh, err := NewKnownHostsDb(files...) ++ if err != nil { ++ return nil, err ++ } ++ return kh.HostKeyCallback(), nil + } + +-func newKnownHostsDb(files ...string) (*knownhosts.HostKeyDB, error) { ++// NewKnownHostsDb returns knownhosts.HostKeyDB based on a file based on a ++// known_hosts file. http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT ++// ++// If list of files is empty, then it will be read from the SSH_KNOWN_HOSTS ++// environment variable, example: ++// ++// /home/foo/custom_known_hosts_file:/etc/custom_known/hosts_file ++// ++// If SSH_KNOWN_HOSTS is not set the following file locations will be used: ++// ++// ~/.ssh/known_hosts ++// /etc/ssh/ssh_known_hosts ++func NewKnownHostsDb(files ...string) (*knownhosts.HostKeyDB, error) { + var err error + + if len(files) == 0 { +@@ -289,26 +304,50 @@ func filterKnownHostsFiles(files ...string) ([]string, error) { + } + + // HostKeyCallbackHelper is a helper that provides common functionality to +-// configure HostKeyCallback into a ssh.ClientConfig. ++// configure HostKeyCallback and HostKeyAlgorithms into a ssh.ClientConfig. + type HostKeyCallbackHelper struct { + // HostKeyCallback is the function type used for verifying server keys. +- // If nil default callback will be create using NewKnownHostsCallback ++ // If nil, a default callback will be created using NewKnownHostsDb + // without argument. + HostKeyCallback ssh.HostKeyCallback ++ ++ // HostKeyAlgorithms is a list of supported host key algorithms that will ++ // be used for host key verification. ++ HostKeyAlgorithms []string ++ ++ // fallback allows for injecting the fallback call, which is called ++ // when a HostKeyCallback is not set. ++ fallback func(files ...string) (ssh.HostKeyCallback, error) + } + +-// SetHostKeyCallback sets the field HostKeyCallback in the given cfg. If +-// HostKeyCallback is empty a default callback is created using +-// NewKnownHostsCallback. +-func (m *HostKeyCallbackHelper) SetHostKeyCallback(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { ++// SetHostKeyCallbackAndAlgorithms sets the field HostKeyCallback and HostKeyAlgorithms in the given cfg. ++// If the host key callback or algorithms is empty it is left empty. It will be handled by the dial method, ++// falling back to knownhosts. ++func (m *HostKeyCallbackHelper) SetHostKeyCallbackAndAlgorithms(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { ++ if cfg == nil { ++ cfg = &ssh.ClientConfig{} ++ } ++ + if m.HostKeyCallback == nil { +- db, err := newKnownHostsDb() ++ if m.fallback == nil { ++ m.fallback = NewKnownHostsCallback ++ } ++ ++ hkcb, err := m.fallback() + if err != nil { +- return cfg, err ++ return nil, fmt.Errorf("cannot create known hosts callback: %w", err) + } +- m.HostKeyCallback = db.HostKeyCallback() ++ ++ cfg.HostKeyCallback = hkcb ++ cfg.HostKeyAlgorithms = m.HostKeyAlgorithms ++ return cfg, err + } + + cfg.HostKeyCallback = m.HostKeyCallback ++ cfg.HostKeyAlgorithms = m.HostKeyAlgorithms + return cfg, nil + } ++ ++func (m *HostKeyCallbackHelper) SetHostKeyCallback(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) { ++ return m.SetHostKeyCallbackAndAlgorithms(cfg) ++} +diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go b/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go +index a37024f..647955b 100644 +--- a/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go ++++ b/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go +@@ -126,25 +126,17 @@ func (c *command) connect() error { + } + hostWithPort := c.getHostWithPort() + if config.HostKeyCallback == nil { +- db, err := newKnownHostsDb() ++ db, err := NewKnownHostsDb() + if err != nil { + return err + } +- + config.HostKeyCallback = db.HostKeyCallback() + config.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort) +- } else if len(config.HostKeyAlgorithms) == 0 { +- // Set the HostKeyAlgorithms based on HostKeyCallback. +- // For background see https://github.com/go-git/go-git/issues/411 as well as +- // https://github.com/golang/go/issues/29286 for root cause. +- db, err := newKnownHostsDb() +- if err != nil { +- return err +- } +- +- // Note that the knownhost database is used, as it provides additional functionality +- // to handle ssh cert-authorities. +- config.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort) ++ } else { ++ // If the user gave a custom HostKeyCallback, we do not try to detect host key algorithms ++ // based on knownhosts functionality, as the user may be requesting a FixedKey or using a ++ // different key approval strategy. In that case, the user is responsible for populating ++ // HostKeyAlgorithms appropriately + } + + overrideConfig(c.config, config) +@@ -260,7 +252,39 @@ func (c *command) setAuthFromEndpoint() error { + } + + func endpointToCommand(cmd string, ep *transport.Endpoint) string { +- return fmt.Sprintf("%s '%s'", cmd, ep.Path) ++ var b strings.Builder ++ b.WriteString(cmd) ++ b.WriteByte(' ') ++ writeShellQuote(&b, ep.Path) ++ return b.String() ++} ++ ++// writeShellQuote writes s to b, wrapped in single quotes with ++// embedded single quotes and exclamation marks escaped using the ++// POSIX close-escape-reopen idiom: ++// ++// ' becomes '\'' ++// ! becomes '\!' ++// ++// It is a direct port of canonical Git's sq_quote_buf (quote.c). ++// The bang escape keeps the result safe when re-evaluated under ++// csh-derived shells that perform history expansion. The output is ++// safe to pass as a single argument through any POSIX shell and ++// round-trips through git-shell's sq_dequote_to_argv. ++func writeShellQuote(b *strings.Builder, s string) { ++ b.Grow(len(s) + 2) ++ b.WriteByte('\'') ++ for i := 0; i < len(s); i++ { ++ c := s[i] ++ if c == '\'' || c == '!' { ++ b.WriteString(`'\`) ++ b.WriteByte(c) ++ b.WriteByte('\'') ++ continue ++ } ++ b.WriteByte(c) ++ } ++ b.WriteByte('\'') + } + + func overrideConfig(overrides *ssh.ClientConfig, c *ssh.ClientConfig) { +diff --git a/vendor/github.com/go-git/go-git/v5/remote.go b/vendor/github.com/go-git/go-git/v5/remote.go +index e2c734e..7308127 100644 +--- a/vendor/github.com/go-git/go-git/v5/remote.go ++++ b/vendor/github.com/go-git/go-git/v5/remote.go +@@ -114,7 +114,7 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) { + o.RemoteURL = r.c.URLs[len(r.c.URLs)-1] + } + +- s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions) ++ s, err := newSendPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.ClientCert, o.ClientKey, o.CABundle, o.ProxyOptions) + if err != nil { + return err + } +@@ -416,7 +416,7 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen + o.RemoteURL = r.c.URLs[0] + } + +- s, err := newUploadPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions) ++ s, err := newUploadPackSession(o.RemoteURL, o.Auth, o.InsecureSkipTLS, o.ClientCert, o.ClientKey, o.CABundle, o.ProxyOptions) + if err != nil { + return nil, err + } +@@ -532,8 +532,8 @@ func depthChanged(before []plumbing.Hash, s storage.Storer) (bool, error) { + return false, nil + } + +-func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.UploadPackSession, error) { +- c, ep, err := newClient(url, insecure, cabundle, proxyOpts) ++func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool, clientCert, clientKey, caBundle []byte, proxyOpts transport.ProxyOptions) (transport.UploadPackSession, error) { ++ c, ep, err := newClient(url, insecure, clientCert, clientKey, caBundle, proxyOpts) + if err != nil { + return nil, err + } +@@ -541,8 +541,8 @@ func newUploadPackSession(url string, auth transport.AuthMethod, insecure bool, + return c.NewUploadPackSession(ep, auth) + } + +-func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.ReceivePackSession, error) { +- c, ep, err := newClient(url, insecure, cabundle, proxyOpts) ++func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, clientCert, clientKey, caBundle []byte, proxyOpts transport.ProxyOptions) (transport.ReceivePackSession, error) { ++ c, ep, err := newClient(url, insecure, clientCert, clientKey, caBundle, proxyOpts) + if err != nil { + return nil, err + } +@@ -550,13 +550,15 @@ func newSendPackSession(url string, auth transport.AuthMethod, insecure bool, ca + return c.NewReceivePackSession(ep, auth) + } + +-func newClient(url string, insecure bool, cabundle []byte, proxyOpts transport.ProxyOptions) (transport.Transport, *transport.Endpoint, error) { ++func newClient(url string, insecure bool, clientCert, clientKey, caBundle []byte, proxyOpts transport.ProxyOptions) (transport.Transport, *transport.Endpoint, error) { + ep, err := transport.NewEndpoint(url) + if err != nil { + return nil, nil, err + } + ep.InsecureSkipTLS = insecure +- ep.CaBundle = cabundle ++ ep.ClientCert = clientCert ++ ep.ClientKey = clientKey ++ ep.CaBundle = caBundle + ep.Proxy = proxyOpts + + c, err := client.NewClient(ep) +@@ -1356,7 +1358,7 @@ func (r *Remote) list(ctx context.Context, o *ListOptions) (rfs []*plumbing.Refe + return nil, ErrEmptyUrls + } + +- s, err := newUploadPackSession(r.c.URLs[0], o.Auth, o.InsecureSkipTLS, o.CABundle, o.ProxyOptions) ++ s, err := newUploadPackSession(r.c.URLs[0], o.Auth, o.InsecureSkipTLS, o.ClientCert, o.ClientKey, o.CABundle, o.ProxyOptions) + if err != nil { + return nil, err + } +diff --git a/vendor/github.com/go-git/go-git/v5/repository.go b/vendor/github.com/go-git/go-git/v5/repository.go +index 200098e..12af162 100644 +--- a/vendor/github.com/go-git/go-git/v5/repository.go ++++ b/vendor/github.com/go-git/go-git/v5/repository.go +@@ -19,6 +19,7 @@ import ( + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/osfs" + "github.com/go-git/go-billy/v5/util" ++ + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/internal/path_util" + "github.com/go-git/go-git/v5/internal/revision" +@@ -207,6 +208,12 @@ func Open(s storage.Storer, worktree billy.Filesystem) (*Repository, error) { + return nil, ErrRepositoryNotExists + } + ++ cfg, err := s.Config() ++ if err != nil { ++ return nil, err ++ } ++ ++ err = verifyExtensions(s, cfg) + if err != nil { + return nil, err + } +@@ -930,6 +937,8 @@ func (r *Repository) clone(ctx context.Context, o *CloneOptions) error { + Tags: o.Tags, + RemoteName: o.RemoteName, + InsecureSkipTLS: o.InsecureSkipTLS, ++ ClientCert: o.ClientCert, ++ ClientKey: o.ClientKey, + CABundle: o.CABundle, + ProxyOptions: o.ProxyOptions, + }, o.ReferenceName) +@@ -1521,7 +1530,18 @@ func (r *Repository) Worktree() (*Worktree, error) { + return nil, ErrIsBareRepository + } + +- return &Worktree{r: r, Filesystem: r.wt}, nil ++ protectNTFS := defaultProtectNTFS() ++ protectHFS := defaultProtectHFS() ++ if cfg, err := r.Config(); err == nil { ++ if cfg.Core.ProtectNTFS.IsSet() { ++ protectNTFS = cfg.Core.ProtectNTFS.IsTrue() ++ } ++ if cfg.Core.ProtectHFS.IsSet() { ++ protectHFS = cfg.Core.ProtectHFS.IsTrue() ++ } ++ } ++ ++ return &Worktree{r: r, Filesystem: newWorktreeFilesystem(r.wt, protectNTFS, protectHFS)}, nil + } + + func expand_ref(s storer.ReferenceStorer, ref plumbing.ReferenceName) (*plumbing.Reference, error) { +diff --git a/vendor/github.com/go-git/go-git/v5/repository_extensions.go b/vendor/github.com/go-git/go-git/v5/repository_extensions.go +new file mode 100644 +index 0000000..635d9aa +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/repository_extensions.go +@@ -0,0 +1,121 @@ ++package git ++ ++import ( ++ "errors" ++ "fmt" ++ "strings" ++ ++ "github.com/go-git/go-git/v5/config" ++ cfgformat "github.com/go-git/go-git/v5/plumbing/format/config" ++ "github.com/go-git/go-git/v5/storage" ++) ++ ++var ( ++ // ErrUnsupportedExtensionRepositoryFormatVersion represents when an ++ // extension being used is not compatible with the repository's ++ // core.repositoryFormatVersion. ++ ErrUnsupportedExtensionRepositoryFormatVersion = errors.New("core.repositoryformatversion does not support extension") ++ ++ // ErrUnsupportedRepositoryFormatVersion represents when an repository ++ // is using a format version that is not supported. ++ ErrUnsupportedRepositoryFormatVersion = errors.New("core.repositoryformatversion not supported") ++ ++ // ErrUnknownExtension represents when a repository has an extension ++ // which is unknown or unsupported by go-git. ++ ErrUnknownExtension = errors.New("unknown extension") ++ ++ // builtinExtensions defines the Git extensions that are supported by ++ // the core go-git implementation. ++ // ++ // Some extensions are storage-specific, those are defined by the Storers ++ // themselves by implementing the ExtensionChecker interface. ++ builtinExtensions = map[string]struct{}{ ++ // noop does not change git’s behavior at all. ++ // It is useful only for testing format-1 compatibility. ++ // ++ // This extension is respected regardless of the ++ // core.repositoryFormatVersion setting. ++ "noop": {}, ++ ++ // noop-v1 does not change git’s behavior at all. ++ // It is useful only for testing format-1 compatibility. ++ "noop-v1": {}, ++ } ++ ++ // Some Git extensions were supported upstream before the introduction ++ // of repositoryformatversion. These are the only extensions that can be ++ // enabled while core.repositoryformatversion is unset or set to 0. ++ extensionsValidForV0 = map[string]struct{}{ ++ "noop": {}, ++ "partialClone": {}, ++ "preciousObjects": {}, ++ "worktreeConfig": {}, ++ } ++) ++ ++type extension struct { ++ name string ++ value string ++} ++ ++func extensions(cfg *config.Config) []extension { ++ if cfg == nil || cfg.Raw == nil { ++ return nil ++ } ++ ++ if !cfg.Raw.HasSection("extensions") { ++ return nil ++ } ++ ++ section := cfg.Raw.Section("extensions") ++ out := make([]extension, 0, len(section.Options)) ++ for _, opt := range section.Options { ++ out = append(out, extension{name: strings.ToLower(opt.Key), value: strings.ToLower(opt.Value)}) ++ } ++ ++ return out ++} ++ ++func verifyExtensions(st storage.Storer, cfg *config.Config) error { ++ needed := extensions(cfg) ++ ++ switch cfg.Core.RepositoryFormatVersion { ++ case "", cfgformat.Version_0, cfgformat.Version_1: ++ default: ++ return fmt.Errorf("%w: %q", ++ ErrUnsupportedRepositoryFormatVersion, ++ cfg.Core.RepositoryFormatVersion) ++ } ++ ++ if len(needed) > 0 { ++ if cfg.Core.RepositoryFormatVersion == cfgformat.Version_0 || ++ cfg.Core.RepositoryFormatVersion == "" { ++ var unsupported []string ++ for _, ext := range needed { ++ if _, ok := extensionsValidForV0[ext.name]; !ok { ++ unsupported = append(unsupported, ext.name) ++ } ++ } ++ if len(unsupported) > 0 { ++ return fmt.Errorf("%w: %s", ++ ErrUnsupportedExtensionRepositoryFormatVersion, ++ strings.Join(unsupported, ", ")) ++ } ++ } ++ ++ var missing []string ++ for _, ext := range needed { ++ if _, ok := builtinExtensions[ext.name]; ok { ++ continue ++ } ++ ++ missing = append(missing, ext.name) ++ } ++ ++ if len(missing) > 0 { ++ return fmt.Errorf("%w: %s", ErrUnknownExtension, strings.Join(missing, ", ")) ++ } ++ } ++ ++ return nil ++} +diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go +index 72c9ccf..eb85a11 100644 +--- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go ++++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go +@@ -75,6 +75,10 @@ var ( + // ErrEmptyRefFile is returned when a reference file is attempted to be read, + // but the file is empty + ErrEmptyRefFile = errors.New("ref file is empty") ++ // ErrModuleNameEscape is returned when a submodule name would ++ // resolve outside the modules/ subtree, mirroring canonical Git's ++ // "ignoring suspicious submodule name" defence. ++ ErrModuleNameEscape = errors.New("submodule name escapes modules/ directory") + ) + + // Options holds configuration for the storage. +@@ -1127,9 +1131,20 @@ func (d *DotGit) PackRefs() (err error) { + return nil + } + +-// Module return a billy.Filesystem pointing to the module folder ++// Module returns a billy.Filesystem pointing to the module folder. ++// ++// As a defence in depth against submodule name path traversal, ++// refuse names whose joined path leaves the modules/ subtree once ++// cleaned. The config-layer parser also validates submodule names, ++// but Module may be reached from any caller that constructs a ++// Submodule struct programmatically and so bypasses the parser. + func (d *DotGit) Module(name string) (billy.Filesystem, error) { +- return d.fs.Chroot(d.fs.Join(modulePath, name)) ++ p := d.fs.Join(modulePath, name) ++ cleaned := path.Clean(filepath.ToSlash(p)) ++ if cleaned != modulePath && !strings.HasPrefix(cleaned, modulePath+"/") { ++ return nil, ErrModuleNameEscape ++ } ++ return d.fs.Chroot(p) + } + + func (d *DotGit) AddAlternate(remote string) error { +diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go +index 849b7a1..e9be5bc 100644 +--- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go ++++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go +@@ -3,6 +3,7 @@ package dotgit + import ( + "fmt" + "io" ++ "os" + "sync/atomic" + + "github.com/go-git/go-git/v5/plumbing" +@@ -131,20 +132,62 @@ func (w *PackWriter) clean() error { + + func (w *PackWriter) save() error { + base := w.fs.Join(objectsPath, packPath, fmt.Sprintf("pack-%s", w.checksum)) +- idx, err := w.fs.Create(fmt.Sprintf("%s.idx", base)) ++ ++ // Pack files are content addressable. Each file is checked ++ // individually — if it already exists on disk, skip creating it. ++ idxPath := fmt.Sprintf("%s.idx", base) ++ exists, err := fileExists(w.fs, idxPath) + if err != nil { + return err + } ++ if !exists { ++ idx, err := w.fs.Create(idxPath) ++ if err != nil { ++ return err ++ } + +- if err := w.encodeIdx(idx); err != nil { +- return err ++ if err := w.encodeIdx(idx); err != nil { ++ _ = idx.Close() ++ return err ++ } ++ ++ if err := idx.Close(); err != nil { ++ return err ++ } ++ fixPermissions(w.fs, idxPath) + } + +- if err := idx.Close(); err != nil { ++ packPath := fmt.Sprintf("%s.pack", base) ++ exists, err = fileExists(w.fs, packPath) ++ if err != nil { + return err + } ++ if !exists { ++ if err := w.fs.Rename(w.fw.Name(), packPath); err != nil { ++ return err ++ } ++ fixPermissions(w.fs, packPath) ++ } else { ++ // Pack already exists, clean up the temp file. ++ return w.clean() ++ } + +- return w.fs.Rename(w.fw.Name(), fmt.Sprintf("%s.pack", base)) ++ return nil ++} ++ ++// fileExists checks whether path already exists as a regular file. ++// It returns (true, nil) for an existing regular file, (false, nil) when the ++// path does not exist, and (false, err) if the path exists but is not a ++// regular file (e.g. a directory or symlink). ++func fileExists(fs billy.Filesystem, path string) (bool, error) { ++ fi, err := fs.Lstat(path) ++ if err != nil { ++ return false, nil ++ } ++ if !fi.Mode().IsRegular() { ++ return false, fmt.Errorf("unexpected file type for %q: %s", path, fi.Mode().Type()) ++ } ++ return true, nil + } + + func (w *PackWriter) encodeIdx(writer io.Writer) error { +@@ -226,7 +269,6 @@ func (s *syncedReader) sleep() { + atomic.StoreUint32(&s.blocked, 1) + <-s.news + } +- + } + + func (s *syncedReader) Seek(offset int64, whence int) (int64, error) { +@@ -281,5 +323,17 @@ func (w *ObjectWriter) save() error { + hex := w.Hash().String() + file := w.fs.Join(objectsPath, hex[0:2], hex[2:hash.HexSize]) + +- return w.fs.Rename(w.f.Name(), file) ++ // Loose objects are content addressable, if they already exist ++ // we can safely delete the temporary file and short-circuit the ++ // operation. ++ if _, err := w.fs.Lstat(file); err == nil || os.IsExist(err) { ++ return w.fs.Remove(w.f.Name()) ++ } ++ ++ if err := w.fs.Rename(w.f.Name(), file); err != nil { ++ return err ++ } ++ fixPermissions(w.fs, file) ++ ++ return nil + } +diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go +new file mode 100644 +index 0000000..134a258 +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go +@@ -0,0 +1,29 @@ ++//go:build !windows ++ ++package dotgit ++ ++import ( ++ "github.com/go-git/go-billy/v5" ++ "github.com/go-git/go-git/v5/utils/trace" ++) ++ ++func fixPermissions(fs billy.Filesystem, path string) { ++ if chmodFS, ok := fs.(billy.Chmod); ok { ++ if err := chmodFS.Chmod(path, 0o444); err != nil { ++ trace.General.Printf("failed to chmod %s: %v", path, err) ++ } ++ } ++} ++ ++func isReadOnly(fs billy.Filesystem, path string) (bool, error) { ++ fi, err := fs.Stat(path) ++ if err != nil { ++ return false, err ++ } ++ ++ if fi.Mode().Perm() == 0o444 { ++ return true, nil ++ } ++ ++ return false, nil ++} +diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go +new file mode 100644 +index 0000000..c22abcc +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go +@@ -0,0 +1,58 @@ ++//go:build windows ++ ++package dotgit ++ ++import ( ++ "fmt" ++ "path/filepath" ++ ++ "github.com/go-git/go-billy/v5" ++ "github.com/go-git/go-git/v5/utils/trace" ++ "golang.org/x/sys/windows" ++) ++ ++func fixPermissions(fs billy.Filesystem, path string) { ++ fullpath := filepath.Join(fs.Root(), path) ++ p, err := windows.UTF16PtrFromString(fullpath) ++ if err != nil { ++ trace.General.Printf("failed to chmod %s: %v", fullpath, err) ++ return ++ } ++ ++ attrs, err := windows.GetFileAttributes(p) ++ if err != nil { ++ trace.General.Printf("failed to chmod %s: %v", fullpath, err) ++ return ++ } ++ ++ if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 { ++ return ++ } ++ ++ err = windows.SetFileAttributes(p, ++ attrs|windows.FILE_ATTRIBUTE_READONLY, ++ ) ++ ++ if err != nil { ++ trace.General.Printf("failed to chmod %s: %v", fullpath, err) ++ } ++} ++ ++func isReadOnly(fs billy.Filesystem, path string) (bool, error) { ++ fullpath := filepath.Join(fs.Root(), path) ++ p, err := windows.UTF16PtrFromString(fullpath) ++ if err != nil { ++ return false, fmt.Errorf("%w: %q", err, fullpath) ++ } ++ ++ attrs, err := windows.GetFileAttributes(p) ++ if err != nil { ++ return false, fmt.Errorf("%w: %q", err, fullpath) ++ } ++ ++ if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 { ++ return true, nil ++ } ++ ++ return false, nil ++} +diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go +index a86ef3e..b5b9f95 100644 +--- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go ++++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go +@@ -48,6 +48,11 @@ func (s *IndexStorage) Index() (i *index.Index, err error) { + + defer ioutil.CheckClose(f, &err) + ++ fi, statErr := s.dir.Fs().Stat(f.Name()) ++ if statErr == nil { ++ idx.ModTime = fi.ModTime() ++ } ++ + d := index.NewDecoder(f) + err = d.Decode(idx) + return idx, err +diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/object.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/object.go +index 91b4ace..db82fef 100644 +--- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/object.go ++++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/object.go +@@ -2,6 +2,8 @@ package filesystem + + import ( + "bytes" ++ "encoding/hex" ++ "fmt" + "io" + "os" + "sync" +@@ -87,6 +89,11 @@ func (s *ObjectStorage) loadIdxFile(h plumbing.Hash) (err error) { + return err + } + ++ if !bytes.Equal(idxf.PackfileChecksum[:], h[:]) { ++ return fmt.Errorf("%w: packfile mismatch: target is %q not %q", ++ idxfile.ErrMalformedIdxFile, hex.EncodeToString(idxf.PackfileChecksum[:]), h.String()) ++ } ++ + s.index[h] = idxf + return err + } +@@ -186,7 +193,8 @@ func (s *ObjectStorage) HasEncodedObject(h plumbing.Hash) (err error) { + } + + func (s *ObjectStorage) encodedObjectSizeFromUnpacked(h plumbing.Hash) ( +- size int64, err error) { ++ size int64, err error, ++) { + f, err := s.dir.Object(h) + if err != nil { + if os.IsNotExist(err) { +@@ -274,7 +282,8 @@ func (s *ObjectStorage) storePackfileInCache(hash plumbing.Hash, p *packfile.Pac + } + + func (s *ObjectStorage) encodedObjectSizeFromPackfile(h plumbing.Hash) ( +- size int64, err error) { ++ size int64, err error, ++) { + if err := s.requireIndex(); err != nil { + return 0, err + } +@@ -310,7 +319,8 @@ func (s *ObjectStorage) encodedObjectSizeFromPackfile(h plumbing.Hash) ( + // EncodedObjectSize returns the plaintext size of the given object, + // without actually reading the full object data from storage. + func (s *ObjectStorage) EncodedObjectSize(h plumbing.Hash) ( +- size int64, err error) { ++ size int64, err error, ++) { + size, err = s.encodedObjectSizeFromUnpacked(h) + if err != nil && err != plumbing.ErrObjectNotFound { + return 0, err +@@ -371,7 +381,8 @@ func (s *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (p + // DeltaObject returns the object with the given hash, by searching for + // it in the packfile and the git object directories. + func (s *ObjectStorage) DeltaObject(t plumbing.ObjectType, +- h plumbing.Hash) (plumbing.EncodedObject, error) { ++ h plumbing.Hash, ++) (plumbing.EncodedObject, error) { + obj, err := s.getFromUnpacked(h) + if err == plumbing.ErrObjectNotFound { + obj, err = s.getFromPackfile(h, true) +@@ -451,8 +462,8 @@ var copyBufferPool = sync.Pool{ + // Get returns the object with the given hash, by searching for it in + // the packfile. + func (s *ObjectStorage) getFromPackfile(h plumbing.Hash, canBeDelta bool) ( +- plumbing.EncodedObject, error) { +- ++ plumbing.EncodedObject, error, ++) { + if err := s.requireIndex(); err != nil { + return nil, err + } +@@ -509,9 +520,7 @@ func (s *ObjectStorage) decodeDeltaObjectAt( + return nil, err + } + +- var ( +- base plumbing.Hash +- ) ++ var base plumbing.Hash + + switch header.Type { + case plumbing.REFDeltaObject: +diff --git a/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go b/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go +index 79211c7..b5d0aa7 100644 +--- a/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go ++++ b/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go +@@ -69,7 +69,11 @@ type IndexStorage struct { + index *index.Index + } + ++// SetIndex stores the given index. ++// Note: this method sets idx.ModTime to simulate filesystem storage behavior. + func (c *IndexStorage) SetIndex(idx *index.Index) error { ++ // Set ModTime to enable racy git detection in the metadata optimization. ++ idx.ModTime = time.Now() + c.index = idx + return nil + } +diff --git a/vendor/github.com/go-git/go-git/v5/submodule.go b/vendor/github.com/go-git/go-git/v5/submodule.go +index afabb6a..2fe4ca2 100644 +--- a/vendor/github.com/go-git/go-git/v5/submodule.go ++++ b/vendor/github.com/go-git/go-git/v5/submodule.go +@@ -6,9 +6,12 @@ import ( + "errors" + "fmt" + "path" ++ "path/filepath" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5/config" ++ "github.com/go-git/go-git/v5/internal/pathutil" ++ giturl "github.com/go-git/go-git/v5/internal/url" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/format/index" + "github.com/go-git/go-git/v5/plumbing/transport" +@@ -119,6 +122,16 @@ func (s *Submodule) Repository() (*Repository, error) { + exists = true + } + ++ // s.c.Path is sourced from the worktree's .gitmodules and is ++ // therefore tree-controlled. Apply the strict tree-path validator ++ // before chroot — the wrapper's tolerant validPath would let a ++ // final-position .git component through (e.g. "submodule/.git"), ++ // which a malicious .gitmodules could use to chroot the submodule ++ // worktree into the repository's actual .git directory. ++ if err := pathutil.ValidTreePath(s.c.Path); err != nil { ++ return nil, err ++ } ++ + var worktree billy.Filesystem + if worktree, err = s.w.Filesystem.Chroot(s.c.Path); err != nil { + return nil, err +@@ -138,18 +151,25 @@ func (s *Submodule) Repository() (*Repository, error) { + return nil, err + } + +- if !path.IsAbs(moduleEndpoint.Path) && moduleEndpoint.Protocol == "file" { +- remotes, err := s.w.r.Remotes() ++ // A relative submodule URL such as "../X.git" must resolve against ++ // the parent repository's remote URL, not against the process CWD. ++ // Detect relativity from the raw configured URL because ++ // transport.NewEndpoint normalizes local paths to absolute form via ++ // filepath.Abs, which would otherwise mask the relative form here. ++ if giturl.IsLocalEndpoint(s.c.URL) && ++ !path.IsAbs(s.c.URL) && !filepath.IsAbs(s.c.URL) { ++ ++ base, err := defaultRemote(s.w.r) + if err != nil { +- return nil, err ++ return nil, fmt.Errorf("resolving relative submodule URL: %w", err) + } + +- rootEndpoint, err := transport.NewEndpoint(remotes[0].c.URLs[0]) ++ rootEndpoint, err := transport.NewEndpoint(base.URLs[0]) + if err != nil { + return nil, err + } + +- rootEndpoint.Path = path.Join(rootEndpoint.Path, moduleEndpoint.Path) ++ rootEndpoint.Path = path.Join(rootEndpoint.Path, s.c.URL) + *moduleEndpoint = *rootEndpoint + } + +@@ -161,6 +181,52 @@ func (s *Submodule) Repository() (*Repository, error) { + return r, err + } + ++// defaultRemote returns the remote that relative submodule URLs are ++// resolved against, mirroring canonical Git's repo_default_remote ++// (remote.c) and resolve_relative_url (builtin/submodule--helper.c): ++// ++// 1. if HEAD is on a branch with branch..remote configured, ++// use that remote; ++// 2. else if exactly one remote is configured, use it; ++// 3. otherwise fall back to DefaultRemoteName ("origin"). ++// ++// Each rule falls through unconditionally: a branch lookup that ++// finds the branch but with an empty Remote does not short-circuit ++// rule (2). Returns an error when the chosen remote is not configured. ++func defaultRemote(r *Repository) (*config.RemoteConfig, error) { ++ cfg, err := r.Config() ++ if err != nil { ++ return nil, err ++ } ++ ++ if ref, err := r.Reference(plumbing.HEAD, false); err == nil && ++ ref.Type() == plumbing.SymbolicReference && ++ ref.Target().IsBranch() { ++ if b, ok := cfg.Branches[ref.Target().Short()]; ok && b.Remote != "" { ++ return lookupRemote(cfg, b.Remote) ++ } ++ } ++ ++ if len(cfg.Remotes) == 1 { ++ for name := range cfg.Remotes { ++ return lookupRemote(cfg, name) ++ } ++ } ++ ++ return lookupRemote(cfg, DefaultRemoteName) ++} ++ ++func lookupRemote(cfg *config.Config, name string) (*config.RemoteConfig, error) { ++ rc, ok := cfg.Remotes[name] ++ if !ok { ++ return nil, fmt.Errorf("remote %q not found", name) ++ } ++ if len(rc.URLs) == 0 { ++ return nil, fmt.Errorf("remote %q has no configured URL", name) ++ } ++ return rc, nil ++} ++ + // Update the registered submodule to match what the superproject expects, the + // submodule should be initialized first calling the Init method or setting in + // the options SubmoduleUpdateOptions.Init equals true +diff --git a/vendor/github.com/go-git/go-git/v5/utils/binary/read.go b/vendor/github.com/go-git/go-git/v5/utils/binary/read.go +index b8f9df1..71d9ad6 100644 +--- a/vendor/github.com/go-git/go-git/v5/utils/binary/read.go ++++ b/vendor/github.com/go-git/go-git/v5/utils/binary/read.go +@@ -5,11 +5,18 @@ package binary + import ( + "bufio" + "encoding/binary" ++ "errors" + "io" ++ "math" + + "github.com/go-git/go-git/v5/plumbing" + ) + ++// ErrIntegerOverflow is returned when a Git-format variable-width integer ++// would not fit into an int64 because the input declares more continuation ++// bytes than the type can hold. ++var ErrIntegerOverflow = errors.New("variable-width integer overflow") ++ + // Read reads structured binary data from r into data. Bytes are read and + // decoded in BigEndian order + // https://golang.org/pkg/encoding/binary/#Read +@@ -92,6 +99,14 @@ func ReadVariableWidthInt(r io.Reader) (int64, error) { + + var v = int64(c & maskLength) + for c&maskContinue > 0 { ++ // Reject input that, after the v++ and shift below, would ++ // not fit in an int64. With v < (MaxInt64-127)>>7, the ++ // post-increment v is at most (MaxInt64-127)>>7 and the ++ // final (v << 7) + (c & 0x7F) stays within int64. ++ if v >= (math.MaxInt64-int64(maskLength))>>lengthBits { ++ return 0, ErrIntegerOverflow ++ } ++ + v++ + if err := Read(r, &c); err != nil { + return 0, err +diff --git a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/change.go b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/change.go +index 450feb4..d32b5a7 100644 +--- a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/change.go ++++ b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/change.go +@@ -131,7 +131,9 @@ func (l *Changes) addRecursive(root noder.Path, ctor noderToChangeFn) error { + } + + if !root.IsDir() { +- l.Add(ctor(root)) ++ if !root.Skip() { ++ l.Add(ctor(root)) ++ } + return nil + } + +@@ -148,7 +150,7 @@ func (l *Changes) addRecursive(root noder.Path, ctor noderToChangeFn) error { + } + return err + } +- if current.IsDir() { ++ if current.IsDir() || current.Skip() { + continue + } + l.Add(ctor(current)) +diff --git a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/difftree.go b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/difftree.go +index 4ef2d99..7fc8d02 100644 +--- a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/difftree.go ++++ b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/difftree.go +@@ -297,18 +297,16 @@ func DiffTreeContext(ctx context.Context, fromTree, toTree noder.Noder, + case noMoreNoders: + return ret, nil + case onlyFromRemains: +- if err = ret.AddRecursiveDelete(from); err != nil { +- return nil, err ++ if !from.Skip() { ++ if err = ret.AddRecursiveDelete(from); err != nil { ++ return nil, err ++ } + } + if err = ii.nextFrom(); err != nil { + return nil, err + } + case onlyToRemains: +- if to.Skip() { +- if err = ret.AddRecursiveDelete(to); err != nil { +- return nil, err +- } +- } else { ++ if !to.Skip() { + if err = ret.AddRecursiveInsert(to); err != nil { + return nil, err + } +@@ -317,26 +315,25 @@ func DiffTreeContext(ctx context.Context, fromTree, toTree noder.Noder, + return nil, err + } + case bothHaveNodes: +- if from.Skip() { +- if err = ret.AddRecursiveDelete(from); err != nil { +- return nil, err +- } +- if err := ii.nextBoth(); err != nil { +- return nil, err ++ var err error ++ switch { ++ case from.Skip(): ++ if from.Name() == to.Name() { ++ err = ii.nextBoth() ++ } else { ++ err = ii.nextFrom() + } +- break +- } +- if to.Skip() { +- if err = ret.AddRecursiveDelete(to); err != nil { +- return nil, err +- } +- if err := ii.nextBoth(); err != nil { +- return nil, err ++ case to.Skip(): ++ if from.Name() == to.Name() { ++ err = ii.nextBoth() ++ } else { ++ err = ii.nextTo() + } +- break ++ default: ++ err = diffNodes(&ret, ii) + } + +- if err = diffNodes(&ret, ii); err != nil { ++ if err != nil { + return nil, err + } + default: +diff --git a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go +index 3380062..83df4dd 100644 +--- a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go ++++ b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go +@@ -4,9 +4,11 @@ import ( + "io" + "os" + "path" ++ "time" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" ++ "github.com/go-git/go-git/v5/plumbing/format/index" + "github.com/go-git/go-git/v5/utils/merkletrie/noder" + + "github.com/go-git/go-billy/v5" +@@ -16,6 +18,14 @@ var ignore = map[string]bool{ + ".git": true, + } + ++// Options contains configuration for the filesystem node. ++type Options struct { ++ // Index is used to enable the metadata-first comparison optimization while ++ // correctly handling the "racy git" condition. If no index is provided, ++ // the function works without the optimization. ++ Index *index.Index ++} ++ + // The node represents a file or a directory in a billy.Filesystem. It + // implements the interface noder.Noder of merkletrie package. + // +@@ -24,6 +34,8 @@ var ignore = map[string]bool{ + type node struct { + fs billy.Filesystem + submodules map[string]plumbing.Hash ++ idx *index.Index ++ idxMap map[string]*index.Entry + + path string + hash []byte +@@ -31,6 +43,7 @@ type node struct { + isDir bool + mode os.FileMode + size int64 ++ modTime time.Time + } + + // NewRootNode returns the root node based on a given billy.Filesystem. +@@ -42,7 +55,41 @@ func NewRootNode( + fs billy.Filesystem, + submodules map[string]plumbing.Hash, + ) noder.Noder { +- return &node{fs: fs, submodules: submodules, isDir: true} ++ return NewRootNodeWithOptions(fs, submodules, Options{}) ++} ++ ++// NewRootNodeWithOptions returns the root node based on a given billy.Filesystem ++// with options to set an index. Providing an index enables the metadata-first ++// comparison optimization while correctly handling the "racy git" condition. If ++// no index is provided, the function works without the optimization. ++// ++// The index's ModTime field is used to detect the racy git condition. When a file's ++// mtime equals or is newer than the index ModTime, we must hash the file content ++// even if other metadata matches, because the file may have been modified in the ++// same second that the index was written. ++// ++// Reference: https://git-scm.com/docs/racy-git ++func NewRootNodeWithOptions( ++ fs billy.Filesystem, ++ submodules map[string]plumbing.Hash, ++ options Options, ++) noder.Noder { ++ var idxMap map[string]*index.Entry ++ ++ if options.Index != nil { ++ idxMap = make(map[string]*index.Entry, len(options.Index.Entries)) ++ for _, entry := range options.Index.Entries { ++ idxMap[entry.Name] = entry ++ } ++ } ++ ++ return &node{ ++ fs: fs, ++ submodules: submodules, ++ idx: options.Index, ++ idxMap: idxMap, ++ isDir: true, ++ } + } + + // Hash the hash of a filesystem is the result of concatenating the computed +@@ -133,11 +180,14 @@ func (n *node) newChildNode(file os.FileInfo) (*node, error) { + node := &node{ + fs: n.fs, + submodules: n.submodules, +- +- path: path, +- isDir: file.IsDir(), +- size: file.Size(), +- mode: file.Mode(), ++ idx: n.idx, ++ idxMap: n.idxMap, ++ ++ path: path, ++ isDir: file.IsDir(), ++ size: file.Size(), ++ mode: file.Mode(), ++ modTime: file.ModTime(), + } + + if _, isSubmodule := n.submodules[path]; isSubmodule { +@@ -161,6 +211,16 @@ func (n *node) calculateHash() { + n.hash = append(submoduleHash[:], filemode.Submodule.Bytes()...) + return + } ++ ++ if n.idxMap != nil { ++ if entry, ok := n.idxMap[n.path]; ok { ++ if n.metadataMatches(entry) { ++ n.hash = append(entry.Hash[:], mode.Bytes()...) ++ return ++ } ++ } ++ } ++ + var hash plumbing.Hash + if n.mode&os.ModeSymlink != 0 { + hash = n.doCalculateHashForSymlink() +@@ -170,6 +230,44 @@ func (n *node) calculateHash() { + n.hash = append(hash[:], mode.Bytes()...) + } + ++func (n *node) metadataMatches(entry *index.Entry) bool { ++ if entry == nil { ++ return false ++ } ++ ++ if uint32(n.size) != entry.Size { ++ return false ++ } ++ ++ if !n.modTime.IsZero() && !n.modTime.Equal(entry.ModifiedAt) { ++ return false ++ } ++ ++ mode, err := filemode.NewFromOSFileMode(n.mode) ++ if err != nil { ++ return false ++ } ++ ++ if mode != entry.Mode { ++ return false ++ } ++ ++ if n.idx != nil && !n.idx.ModTime.IsZero() && !n.modTime.IsZero() { ++ if !n.modTime.Before(n.idx.ModTime) { ++ return false ++ } ++ } ++ ++ // If we couldn't perform the racy git check (idx is nil or idx.ModTime is zero), ++ // we cannot safely rely on metadata alone — force content hashing. ++ // This can occur with in-memory storage where the index file timestamp is unavailable. ++ if n.idx == nil || n.idx.ModTime.IsZero() { ++ return false ++ } ++ ++ return true ++} ++ + func (n *node) doCalculateHashForRegular() plumbing.Hash { + f, err := n.fs.Open(n.path) + if err != nil { +diff --git a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/index/node.go b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/index/node.go +index c1809f7..5bc63f8 100644 +--- a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/index/node.go ++++ b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/index/node.go +@@ -36,7 +36,15 @@ func NewRootNode(idx *index.Index) noder.Noder { + parent := fullpath + fullpath = path.Join(fullpath, part) + +- if _, ok := m[fullpath]; ok { ++ // It's possible that the first occurrence of subdirectory is skipped. ++ // The parent node can be created with SkipWorktree set to true, but ++ // if any future children do not skip their subtree, the entire lineage ++ // of the tree needs to have this value set to false so that subdirectories ++ // are not ignored. ++ if parentNode, ok := m[fullpath]; ok { ++ if e.SkipWorktree == false { ++ parentNode.skip = false ++ } + continue + } + +diff --git a/vendor/github.com/go-git/go-git/v5/worktree.go b/vendor/github.com/go-git/go-git/v5/worktree.go +index 8dfa50b..d8ee9fd 100644 +--- a/vendor/github.com/go-git/go-git/v5/worktree.go ++++ b/vendor/github.com/go-git/go-git/v5/worktree.go +@@ -7,11 +7,11 @@ import ( + "io" + "os" + "path/filepath" +- "runtime" + "strings" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/util" ++ + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" +@@ -79,6 +79,8 @@ func (w *Worktree) PullContext(ctx context.Context, o *PullOptions) error { + Progress: o.Progress, + Force: o.Force, + InsecureSkipTLS: o.InsecureSkipTLS, ++ ClientCert: o.ClientCert, ++ ClientKey: o.ClientKey, + CABundle: o.CABundle, + ProxyOptions: o.ProxyOptions, + }) +@@ -307,13 +309,20 @@ func (w *Worktree) ResetSparsely(opts *ResetOptions, dirs []string) error { + return err + } + ++ var removedFiles []string + if opts.Mode == MixedReset || opts.Mode == MergeReset || opts.Mode == HardReset { +- if err := w.resetIndex(t, dirs, opts.Files); err != nil { ++ if removedFiles, err = w.resetIndex(t, dirs, opts.Files); err != nil { ++ return err ++ } ++ } ++ ++ if opts.Mode == MergeReset && len(removedFiles) > 0 { ++ if err := w.resetWorktree(t, removedFiles); err != nil { + return err + } + } + +- if opts.Mode == MergeReset || opts.Mode == HardReset { ++ if opts.Mode == HardReset { + if err := w.resetWorktree(t, opts.Files); err != nil { + return err + } +@@ -362,23 +371,25 @@ func (w *Worktree) Reset(opts *ResetOptions) error { + return w.ResetSparsely(opts, nil) + } + +-func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) error { ++func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([]string, error) { + idx, err := w.r.Storer.Index() + if err != nil { +- return err ++ return nil, err + } + + b := newIndexBuilder(idx) + + changes, err := w.diffTreeWithStaging(t, true) + if err != nil { +- return err ++ return nil, err + } + ++ removedFiles := make([]string, 0, len(changes)) ++ filesMap := buildFilePathMap(files) + for _, ch := range changes { + a, err := ch.Action() + if err != nil { +- return err ++ return nil, err + } + + var name string +@@ -389,20 +400,21 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) err + name = ch.To.String() + e, err = t.FindEntry(name) + if err != nil { +- return err ++ return nil, err + } + case merkletrie.Delete: + name = ch.From.String() + } + + if len(files) > 0 { +- contains := inFiles(files, name) ++ contains := inFiles(filesMap, name) + if !contains { + continue + } + } + + b.Remove(name) ++ removedFiles = append(removedFiles, name) + if e == nil { + continue + } +@@ -421,17 +433,14 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) err + idx.SkipUnless(dirs) + } + +- return w.r.Storer.SetIndex(idx) ++ return removedFiles, w.r.Storer.SetIndex(idx) + } + +-func inFiles(files []string, v string) bool { +- for _, s := range files { +- if s == v { +- return true +- } +- } +- +- return false ++// inFiles checks if the given file is in the list of files. The incoming filepaths in files should be cleaned before calling this function. ++func inFiles(files map[string]struct{}, v string) bool { ++ v = filepath.Clean(v) ++ _, exists := files[v] ++ return exists + } + + func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { +@@ -446,11 +455,8 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { + } + b := newIndexBuilder(idx) + ++ filesMap := buildFilePathMap(files) + for _, ch := range changes { +- if err := w.validChange(ch); err != nil { +- return err +- } +- + if len(files) > 0 { + file := "" + if ch.From != nil { +@@ -463,7 +469,7 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { + continue + } + +- contains := inFiles(files, file) ++ contains := inFiles(filesMap, file) + if !contains { + continue + } +@@ -478,108 +484,6 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { + return w.r.Storer.SetIndex(idx) + } + +-// worktreeDeny is a list of paths that are not allowed +-// to be used when resetting the worktree. +-var worktreeDeny = map[string]struct{}{ +- // .git +- GitDirName: {}, +- +- // For other historical reasons, file names that do not conform to the 8.3 +- // format (up to eight characters for the basename, three for the file +- // extension, certain characters not allowed such as `+`, etc) are associated +- // with a so-called "short name", at least on the `C:` drive by default. +- // Which means that `git~1/` is a valid way to refer to `.git/`. +- "git~1": {}, +-} +- +-// validPath checks whether paths are valid. +-// The rules around invalid paths could differ from upstream based on how +-// filesystems are managed within go-git, but they are largely the same. +-// +-// For upstream rules: +-// https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/read-cache.c#L946 +-// https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/path.c#L1383 +-func validPath(paths ...string) error { +- for _, p := range paths { +- parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') }) +- if len(parts) == 0 { +- return fmt.Errorf("invalid path: %q", p) +- } +- +- if _, denied := worktreeDeny[strings.ToLower(parts[0])]; denied { +- return fmt.Errorf("invalid path prefix: %q", p) +- } +- +- if runtime.GOOS == "windows" { +- // Volume names are not supported, in both formats: \\ and :. +- if vol := filepath.VolumeName(p); vol != "" { +- return fmt.Errorf("invalid path: %q", p) +- } +- +- if !windowsValidPath(parts[0]) { +- return fmt.Errorf("invalid path: %q", p) +- } +- } +- +- for _, part := range parts { +- if part == ".." { +- return fmt.Errorf("invalid path %q: cannot use '..'", p) +- } +- } +- } +- return nil +-} +- +-// windowsPathReplacer defines the chars that need to be replaced +-// as part of windowsValidPath. +-var windowsPathReplacer *strings.Replacer +- +-func init() { +- windowsPathReplacer = strings.NewReplacer(" ", "", ".", "") +-} +- +-func windowsValidPath(part string) bool { +- if len(part) > 3 && strings.EqualFold(part[:4], GitDirName) { +- // For historical reasons, file names that end in spaces or periods are +- // automatically trimmed. Therefore, `.git . . ./` is a valid way to refer +- // to `.git/`. +- if windowsPathReplacer.Replace(part[4:]) == "" { +- return false +- } +- +- // For yet other historical reasons, NTFS supports so-called "Alternate Data +- // Streams", i.e. metadata associated with a given file, referred to via +- // `::`. There exists a default stream +- // type for directories, allowing `.git/` to be accessed via +- // `.git::$INDEX_ALLOCATION/`. +- // +- // For performance reasons, _all_ Alternate Data Streams of `.git/` are +- // forbidden, not just `::$INDEX_ALLOCATION`. +- if len(part) > 4 && part[4:5] == ":" { +- return false +- } +- } +- return true +-} +- +-func (w *Worktree) validChange(ch merkletrie.Change) error { +- action, err := ch.Action() +- if err != nil { +- return nil +- } +- +- switch action { +- case merkletrie.Delete: +- return validPath(ch.From.String()) +- case merkletrie.Insert: +- return validPath(ch.To.String()) +- case merkletrie.Modify: +- return validPath(ch.From.String(), ch.To.String()) +- } +- +- return nil +-} +- + func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *indexBuilder) error { + a, err := ch.Action() + if err != nil { +@@ -752,10 +656,10 @@ func (w *Worktree) checkoutFile(f *object.File) (err error) { + } + + func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) { +- // https://github.com/git/git/commit/10ecfa76491e4923988337b2e2243b05376b40de +- if strings.EqualFold(f.Name, gitmodulesFile) { +- return ErrGitModulesSymlink +- } ++ // .gitmodules symlink rejection (and its NTFS / HFS variants) is ++ // enforced by the worktreeFilesystem wrapper's Symlink method via ++ // validSymlinkName. See https://github.com/git/git/commit/10ecfa7 ++ // for the upstream rationale. + + from, err := f.Reader() + if err != nil { +@@ -1193,3 +1097,16 @@ func (b *indexBuilder) Add(e *index.Entry) { + func (b *indexBuilder) Remove(name string) { + delete(b.entries, filepath.ToSlash(name)) + } ++ ++// buildFilePathMap creates a map of cleaned file paths for efficient lookup. ++// Returns nil if the input slice is empty. ++func buildFilePathMap(files []string) map[string]struct{} { ++ if len(files) == 0 { ++ return nil ++ } ++ filesMap := make(map[string]struct{}, len(files)) ++ for _, f := range files { ++ filesMap[filepath.Clean(f)] = struct{}{} ++ } ++ return filesMap ++} +diff --git a/vendor/github.com/go-git/go-git/v5/worktree_fs.go b/vendor/github.com/go-git/go-git/v5/worktree_fs.go +new file mode 100644 +index 0000000..9bc2fd9 +--- /dev/null ++++ b/vendor/github.com/go-git/go-git/v5/worktree_fs.go +@@ -0,0 +1,264 @@ ++package git ++ ++import ( ++ "errors" ++ "fmt" ++ "os" ++ "path/filepath" ++ "runtime" ++ "strings" ++ ++ "github.com/go-git/go-billy/v5" ++ ++ "github.com/go-git/go-git/v5/internal/pathutil" ++) ++ ++// defaultProtectHFS returns the default value for core.protectHFS ++// when not explicitly configured. Matches upstream Git's ++// PROTECT_HFS_DEFAULT[1], which the Makefile sets to 1 on Darwin ++// and leaves at 0 on every other platform. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/config.mak.uname#L146 ++func defaultProtectHFS() bool { ++ return runtime.GOOS == "darwin" ++} ++ ++// defaultProtectNTFS returns the default value for core.protectNTFS ++// when not explicitly configured. Matches upstream Git's ++// PROTECT_NTFS_DEFAULT, which has been 1 on every platform since ++// 9102f958ee5 (CVE-2019-1353)[1]: WSL allows Linux processes to ++// reach NTFS-mounted worktrees on Windows hosts, so the ++// is_ntfs_dotgit guard cannot safely be gated on the runtime OS. ++// ++// [1]: https://github.com/git/git/commit/9102f958ee5 ++func defaultProtectNTFS() bool { ++ return true ++} ++ ++// worktreeFilesystem wraps a billy.Filesystem and validates every path passed ++// to a mutating operation. This prevents writing to, or deleting from, ++// dangerous locations (e.g. .git/*, ../) regardless of which worktree ++// code path triggers the operation. ++type worktreeFilesystem struct { ++ billy.Filesystem ++ protectNTFS bool ++ protectHFS bool ++} ++ ++func newWorktreeFilesystem(fs billy.Filesystem, protectNTFS, protectHFS bool) *worktreeFilesystem { ++ return &worktreeFilesystem{Filesystem: fs, protectNTFS: protectNTFS, protectHFS: protectHFS} ++} ++ ++func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) { ++ if err := sfs.validPath(filename); err != nil { ++ return nil, fmt.Errorf("create: %w", err) ++ } ++ return sfs.Filesystem.Create(filename) ++} ++ ++func (sfs *worktreeFilesystem) Open(filename string) (billy.File, error) { ++ if err := sfs.validReadPath(filename); err != nil { ++ return nil, fmt.Errorf("open: %w", err) ++ } ++ return sfs.Filesystem.Open(filename) ++} ++ ++func (sfs *worktreeFilesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { ++ if err := sfs.validPath(filename); err != nil { ++ return nil, fmt.Errorf("openfile: %w", err) ++ } ++ return sfs.Filesystem.OpenFile(filename, flag, perm) ++} ++ ++func (sfs *worktreeFilesystem) Stat(filename string) (os.FileInfo, error) { ++ if err := sfs.validReadPath(filename); err != nil { ++ return nil, fmt.Errorf("stat: %w", err) ++ } ++ return sfs.Filesystem.Stat(filename) ++} ++ ++func (sfs *worktreeFilesystem) Remove(filename string) error { ++ if err := sfs.validPath(filename); err != nil { ++ return fmt.Errorf("remove: %w", err) ++ } ++ return sfs.Filesystem.Remove(filename) ++} ++ ++func (sfs *worktreeFilesystem) Rename(from, to string) error { ++ if err := sfs.validPath(from, to); err != nil { ++ return fmt.Errorf("rename: %w", err) ++ } ++ return sfs.Filesystem.Rename(from, to) ++} ++ ++func (sfs *worktreeFilesystem) ReadDir(path string) ([]os.FileInfo, error) { ++ if err := sfs.validReadPath(path); err != nil { ++ return nil, fmt.Errorf("readdir: %w", err) ++ } ++ return sfs.Filesystem.ReadDir(path) ++} ++ ++func (sfs *worktreeFilesystem) Lstat(filename string) (os.FileInfo, error) { ++ if err := sfs.validReadPath(filename); err != nil { ++ return nil, fmt.Errorf("lstat: %w", err) ++ } ++ return sfs.Filesystem.Lstat(filename) ++} ++ ++func (sfs *worktreeFilesystem) Symlink(target, link string) error { ++ if err := sfs.validPath(link); err != nil { ++ return fmt.Errorf("symlink: %w", err) ++ } ++ if err := sfs.validSymlinkName(link); err != nil { ++ return fmt.Errorf("symlink: %w", err) ++ } ++ return sfs.Filesystem.Symlink(target, link) ++} ++ ++func (sfs *worktreeFilesystem) Readlink(link string) (string, error) { ++ if err := sfs.validReadPath(link); err != nil { ++ return "", fmt.Errorf("readlink: %w", err) ++ } ++ return sfs.Filesystem.Readlink(link) ++} ++ ++func (sfs *worktreeFilesystem) MkdirAll(path string, perm os.FileMode) error { ++ // MkdirAll on the worktree root is a no-op: the root always exists, ++ // so there is nothing to materialise. Mirroring the tolerance that ++ // validReadPath gives to read-side operations avoids breaking callers ++ // that walk a directory tree and pass the relative-to-root prefix ++ // ("") through to the worktree FS. ++ if path == "" || path == "." || path == "/" { ++ return nil ++ } ++ if err := sfs.validPath(path); err != nil { ++ return fmt.Errorf("mkdirall: %w", err) ++ } ++ return sfs.Filesystem.MkdirAll(path, perm) ++} ++ ++func (sfs *worktreeFilesystem) TempFile(_, _ string) (billy.File, error) { ++ return nil, fmt.Errorf("tempfile: %w", errUnsupportedOperation) ++} ++ ++func (sfs *worktreeFilesystem) Chroot(path string) (billy.Filesystem, error) { ++ if err := sfs.validReadPath(path); err != nil { ++ return nil, fmt.Errorf("chroot: %w", err) ++ } ++ return sfs.Filesystem.Chroot(path) ++} ++ ++// validReadPath is like validPath but treats the empty string and "." as ++// valid references to the worktree root. Read-side operations on the root ++// (e.g. ReadDir(""), Lstat(".")) are legitimate; mutating the root itself ++// is not, so write-side operations continue to use validPath directly. ++func (sfs *worktreeFilesystem) validReadPath(p string) error { ++ if p == "" || p == "." || p == "/" { ++ return nil ++ } ++ return sfs.validPath(p) ++} ++ ++var errUnsupportedOperation = errors.New("unsupported operation") ++ ++// isDotGitVariant reports whether part is .git, git~1, or an HFS+ ++// equivalent of .git (when protectHFS is true). NTFS variants of .git ++// (e.g. ".git " with trailing space, ".git::$INDEX_ALLOCATION") are ++// detected separately by pathutil.WindowsValidPath, which applies ++// regardless of position in the path. Both validators reuse this ++// helper. ++func isDotGitVariant(part string, protectHFS bool) bool { ++ if pathutil.IsDotGitName(part) { ++ return true ++ } ++ if protectHFS && pathutil.IsHFSDotGit(part) { ++ return true ++ } ++ return false ++} ++ ++// validPath checks whether paths are valid for the worktree ++// filesystem abstraction. It is intentionally tolerant of .git as ++// the final path component of a multi-component path ++// (e.g. "submodule/.git"), so that legitimate gitlink pointer files ++// can still be Stat'd, Read, and Removed via the wrapper during ++// submodule cleanup. Attacker-controlled tree-entry paths are ++// validated separately by pathutil.ValidTreePath at the boundaries ++// where data leaves the trusted store (Tree.FindEntry, the explicit ++// callers in CherryPick and Submodule.Repository). ++// ++// For upstream rules: ++// https://github.com/git/git/blob/v2.54.0/read-cache.c#L987 ++// https://github.com/git/git/blob/v2.54.0/path.c#L1419 ++func (sfs *worktreeFilesystem) validPath(paths ...string) error { ++ for _, p := range paths { ++ for i := 0; i < len(p); i++ { ++ if p[i] < 0x20 || p[i] == 0x7f { ++ return fmt.Errorf("invalid path %q: contains control character", p) ++ } ++ } ++ ++ parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') }) ++ if len(parts) == 0 { ++ return fmt.Errorf("invalid path: %q", p) ++ } ++ ++ if sfs.protectNTFS { ++ // Volume names are not supported, in both formats: \\ and :. ++ if vol := filepath.VolumeName(p); vol != "" { ++ return fmt.Errorf("invalid path: %q", p) ++ } ++ } ++ ++ for i, part := range parts { ++ if part == "." || part == ".." { ++ return fmt.Errorf("invalid path %q: cannot use %q", p, part) ++ } ++ ++ // Reject .git (and equivalents) as a path component when it is ++ // either the first component (root-level .git) or a non-final ++ // component (traversal into a .git directory, e.g. "a/.git/config"). ++ // A final non-first .git component (e.g. "submodule/.git") is ++ // allowed because submodule worktrees contain a .git pointer file. ++ if isDotGitVariant(part, sfs.protectHFS) && (i == 0 || i < len(parts)-1) { ++ return fmt.Errorf("invalid path component: %q", p) ++ } ++ ++ if sfs.protectNTFS && !pathutil.WindowsValidPath(part) { ++ return fmt.Errorf("invalid path: %q", p) ++ } ++ } ++ } ++ return nil ++} ++ ++// validSymlinkName checks the per-component name of a symlink for ++// dotfile names that attackers can use to trick a checkout into ++// writing a dangerous symlink. Each path component is compared ++// against .gitmodules case-insensitively, against its NTFS variants ++// (e.g. ".gitmodules .", ".gitmodules::$INDEX_ALLOCATION", or 8.3 ++// short-name forms) when protectNTFS is on, and against its HFS+ ++// variants (Unicode ignored code points folded into ".gitmodules") ++// when protectHFS is on. ++// ++// Reference: upstream Git verify_path_internal at read-cache.c#L1004-L1024 ++// in tag v2.54.0[1]. ++// ++// [1]: https://github.com/git/git/blob/v2.54.0/read-cache.c#L1004-L1024 ++func (sfs *worktreeFilesystem) validSymlinkName(name string) error { ++ parts := strings.FieldsFunc(name, func(r rune) bool { ++ return r == '/' || r == '\\' ++ }) ++ for _, part := range parts { ++ if strings.EqualFold(part, gitmodulesFile) { ++ return ErrGitModulesSymlink ++ } ++ if sfs.protectNTFS && pathutil.IsNTFSDotGitmodules(part) { ++ return ErrGitModulesSymlink ++ } ++ if sfs.protectHFS && pathutil.IsHFSDotGitmodules(part) { ++ return ErrGitModulesSymlink ++ } ++ } ++ return nil ++} +diff --git a/vendor/github.com/go-git/go-git/v5/worktree_status.go b/vendor/github.com/go-git/go-git/v5/worktree_status.go +index 6e72db9..ecc3d7a 100644 +--- a/vendor/github.com/go-git/go-git/v5/worktree_status.go ++++ b/vendor/github.com/go-git/go-git/v5/worktree_status.go +@@ -10,6 +10,7 @@ import ( + "strings" + + "github.com/go-git/go-billy/v5/util" ++ "github.com/go-git/go-git/v5/internal/pathutil" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/format/gitignore" +@@ -141,7 +142,7 @@ func (w *Worktree) diffStagingWithWorktree(reverse, excludeIgnoredChanges bool) + return nil, err + } + +- to := filesystem.NewRootNode(w.Filesystem, submodules) ++ to := filesystem.NewRootNodeWithOptions(w.Filesystem, submodules, filesystem.Options{Index: idx}) + + var c merkletrie.Changes + if reverse { +@@ -370,6 +371,8 @@ func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipSta + } + } + ++ path = filepath.Clean(path) ++ + if err != nil || !fi.IsDir() { + added, h, err = w.doAddFile(idx, s, path, ignorePattern) + } else { +@@ -543,6 +546,14 @@ func (w *Worktree) addOrUpdateFileToIndex(idx *index.Index, filename string, h p + } + + func (w *Worktree) doAddFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error { ++ // Mirror upstream's Index.Add gate at the v5 caller boundary: the ++ // index feeds future trees, so a name that the tree-side ++ // pathutil.ValidTreePath gate would reject must not enter the ++ // index in the first place. v5 keeps Index.Add's existing signature ++ // for API compatibility, so the validation happens here. ++ if err := pathutil.ValidTreePath(filename); err != nil { ++ return err ++ } + return w.doUpdateFileToIndex(idx.Add(filename), filename, h) + } + +diff --git a/vendor/modules.txt b/vendor/modules.txt +index fd35c87..897966f 100644 +--- a/vendor/modules.txt ++++ b/vendor/modules.txt +@@ -303,7 +303,7 @@ github.com/go-git/go-billy/v5/memfs + github.com/go-git/go-billy/v5/osfs + github.com/go-git/go-billy/v5/util + # github.com/go-git/go-git/v5 v5.13.0 +-## explicit; go 1.21 ++## explicit; go 1.22 + github.com/go-git/go-git/v5 + github.com/go-git/go-git/v5/config + github.com/go-git/go-git/v5/internal/path_util +-- +2.45.4 + diff --git a/SPECS/packer/packer.spec b/SPECS/packer/packer.spec index 33ea69b7806..549de9b18d6 100644 --- a/SPECS/packer/packer.spec +++ b/SPECS/packer/packer.spec @@ -4,7 +4,7 @@ Summary: Tool for creating identical machine images for multiple platforms from a single source configuration. Name: packer Version: 1.9.5 -Release: 15%{?dist} +Release: 16%{?dist} License: MPLv2.0 Vendor: Microsoft Corporation Distribution: Azure Linux @@ -65,8 +65,9 @@ Patch30: CVE-2026-39835.patch Patch31: CVE-2026-42502.patch Patch32: CVE-2026-46598.patch Patch33: CVE-2026-33814.patch +Patch34: CVE-2026-45571.patch -BuildRequires: golang >= 1.21 +BuildRequires: golang >= 1.22 BuildRequires: kernel-headers BuildRequires: glibc-devel @@ -96,6 +97,9 @@ go test -mod=vendor %{_bindir}/packer %changelog +* Wed Jun 03 2026 Aditya Singh - 1.9.5-16 +- Patch for CVE-2026-45571 + * Fri May 29 2026 Azure Linux Security Servicing Account - 1.9.5-15 - Patch for CVE-2026-33814 From 21c6a3e32e6914c4bcb6ac43be393a1a277d6d4d Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 3 Jun 2026 10:23:00 +0000 Subject: [PATCH 2/2] Updated Patch header and name to add info for CVE-2026-45570 --- ...26-45571.patch => CVE-2026-45570-and-CVE-2026-45571.patch} | 2 +- SPECS/packer/packer.spec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename SPECS/packer/{CVE-2026-45571.patch => CVE-2026-45570-and-CVE-2026-45571.patch} (99%) diff --git a/SPECS/packer/CVE-2026-45571.patch b/SPECS/packer/CVE-2026-45570-and-CVE-2026-45571.patch similarity index 99% rename from SPECS/packer/CVE-2026-45571.patch rename to SPECS/packer/CVE-2026-45570-and-CVE-2026-45571.patch index 60bf85a5942..251a921d989 100644 --- a/SPECS/packer/CVE-2026-45571.patch +++ b/SPECS/packer/CVE-2026-45570-and-CVE-2026-45571.patch @@ -1,7 +1,7 @@ From ac438f3ab5fc4039a97243fa78fe67115189853d Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Tue, 2 Jun 2026 14:07:34 +0000 -Subject: [PATCH] Patch for CVE-2026-45571 +Subject: [PATCH] Patch for CVE-2026-45570 and CVE-2026-45571 Below changes has been made with this patch - diff --git a/SPECS/packer/packer.spec b/SPECS/packer/packer.spec index 549de9b18d6..a60c0436c64 100644 --- a/SPECS/packer/packer.spec +++ b/SPECS/packer/packer.spec @@ -65,7 +65,7 @@ Patch30: CVE-2026-39835.patch Patch31: CVE-2026-42502.patch Patch32: CVE-2026-46598.patch Patch33: CVE-2026-33814.patch -Patch34: CVE-2026-45571.patch +Patch34: CVE-2026-45570-and-CVE-2026-45571.patch BuildRequires: golang >= 1.22 BuildRequires: kernel-headers @@ -98,7 +98,7 @@ go test -mod=vendor %changelog * Wed Jun 03 2026 Aditya Singh - 1.9.5-16 -- Patch for CVE-2026-45571 +- Patch for CVE-2026-45570 and CVE-2026-45571 * Fri May 29 2026 Azure Linux Security Servicing Account - 1.9.5-15 - Patch for CVE-2026-33814