77 "io"
88 "os"
99 "path/filepath"
10+ "runtime"
1011 "strings"
1112
1213 "github.com/go-git/go-billy/v5"
@@ -394,6 +395,9 @@ func (w *Worktree) resetWorktree(t *object.Tree) error {
394395 b := newIndexBuilder (idx )
395396
396397 for _ , ch := range changes {
398+ if err := w .validChange (ch ); err != nil {
399+ return err
400+ }
397401 if err := w .checkoutChange (ch , t , b ); err != nil {
398402 return err
399403 }
@@ -403,6 +407,104 @@ func (w *Worktree) resetWorktree(t *object.Tree) error {
403407 return w .r .Storer .SetIndex (idx )
404408}
405409
410+ // worktreeDeny is a list of paths that are not allowed
411+ // to be used when resetting the worktree.
412+ var worktreeDeny = map [string ]struct {}{
413+ // .git
414+ GitDirName : {},
415+
416+ // For other historical reasons, file names that do not conform to the 8.3
417+ // format (up to eight characters for the basename, three for the file
418+ // extension, certain characters not allowed such as `+`, etc) are associated
419+ // with a so-called "short name", at least on the `C:` drive by default.
420+ // Which means that `git~1/` is a valid way to refer to `.git/`.
421+ "git~1" : {},
422+ }
423+
424+ // validPath checks whether paths are valid.
425+ // The rules around invalid paths could differ from upstream based on how
426+ // filesystems are managed within go-git, but they are largely the same.
427+ //
428+ // For upstream rules:
429+ // https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/read-cache.c#L946
430+ // https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/path.c#L1383
431+ func validPath (paths ... string ) error {
432+ for _ , p := range paths {
433+ parts := strings .FieldsFunc (p , func (r rune ) bool { return (r == '\\' || r == '/' ) })
434+ if _ , denied := worktreeDeny [strings .ToLower (parts [0 ])]; denied {
435+ return fmt .Errorf ("invalid path prefix: %q" , p )
436+ }
437+
438+ if runtime .GOOS == "windows" {
439+ // Volume names are not supported, in both formats: \\ and <DRIVE_LETTER>:.
440+ if vol := filepath .VolumeName (p ); vol != "" {
441+ return fmt .Errorf ("invalid path: %q" , p )
442+ }
443+
444+ if ! windowsValidPath (parts [0 ]) {
445+ return fmt .Errorf ("invalid path: %q" , p )
446+ }
447+ }
448+
449+ for _ , part := range parts {
450+ if part == ".." {
451+ return fmt .Errorf ("invalid path %q: cannot use '..'" , p )
452+ }
453+ }
454+ }
455+ return nil
456+ }
457+
458+ // windowsPathReplacer defines the chars that need to be replaced
459+ // as part of windowsValidPath.
460+ var windowsPathReplacer * strings.Replacer
461+
462+ func init () {
463+ windowsPathReplacer = strings .NewReplacer (" " , "" , "." , "" )
464+ }
465+
466+ func windowsValidPath (part string ) bool {
467+ if len (part ) > 3 && strings .EqualFold (part [:4 ], GitDirName ) {
468+ // For historical reasons, file names that end in spaces or periods are
469+ // automatically trimmed. Therefore, `.git . . ./` is a valid way to refer
470+ // to `.git/`.
471+ if windowsPathReplacer .Replace (part [4 :]) == "" {
472+ return false
473+ }
474+
475+ // For yet other historical reasons, NTFS supports so-called "Alternate Data
476+ // Streams", i.e. metadata associated with a given file, referred to via
477+ // `<filename>:<stream-name>:<stream-type>`. There exists a default stream
478+ // type for directories, allowing `.git/` to be accessed via
479+ // `.git::$INDEX_ALLOCATION/`.
480+ //
481+ // For performance reasons, _all_ Alternate Data Streams of `.git/` are
482+ // forbidden, not just `::$INDEX_ALLOCATION`.
483+ if len (part ) > 4 && part [4 :5 ] == ":" {
484+ return false
485+ }
486+ }
487+ return true
488+ }
489+
490+ func (w * Worktree ) validChange (ch merkletrie.Change ) error {
491+ action , err := ch .Action ()
492+ if err != nil {
493+ return nil
494+ }
495+
496+ switch action {
497+ case merkletrie .Delete :
498+ return validPath (ch .From .String ())
499+ case merkletrie .Insert :
500+ return validPath (ch .To .String ())
501+ case merkletrie .Modify :
502+ return validPath (ch .From .String (), ch .To .String ())
503+ }
504+
505+ return nil
506+ }
507+
406508func (w * Worktree ) checkoutChange (ch merkletrie.Change , t * object.Tree , idx * indexBuilder ) error {
407509 a , err := ch .Action ()
408510 if err != nil {
@@ -575,6 +677,11 @@ func (w *Worktree) checkoutFile(f *object.File) (err error) {
575677}
576678
577679func (w * Worktree ) checkoutFileSymlink (f * object.File ) (err error ) {
680+ // https://github.com/git/git/commit/10ecfa76491e4923988337b2e2243b05376b40de
681+ if strings .EqualFold (f .Name , gitmodulesFile ) {
682+ return ErrGitModulesSymlink
683+ }
684+
578685 from , err := f .Reader ()
579686 if err != nil {
580687 return
0 commit comments