Skip to content

Commit 5d08d3b

Browse files
authored
Merge pull request #958 from pjbgf/workval
Align worktree validation with upstream and remove build warnings
2 parents cec7da6 + 5bd1d8f commit 5d08d3b

File tree

4 files changed

+188
-6
lines changed

4 files changed

+188
-6
lines changed

.github/workflows/git.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ jobs:
1616
GIT_DIST_PATH: .git-dist/${{ matrix.git[0] }}
1717

1818
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
1922
- name: Install Go
2023
uses: actions/setup-go@v4
2124
with:
2225
go-version: 1.21.x
2326

24-
- name: Checkout code
25-
uses: actions/checkout@v4
26-
2727
- name: Install build dependencies
2828
run: sudo apt-get update && sudo apt-get install gettext libcurl4-openssl-dev
2929

.github/workflows/test.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ jobs:
1313

1414
runs-on: ${{ matrix.platform }}
1515
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
1619
- name: Install Go
1720
uses: actions/setup-go@v4
1821
with:
1922
go-version: ${{ matrix.go-version }}
20-
21-
- name: Checkout code
22-
uses: actions/checkout@v4
2323

2424
- name: Configure known hosts
2525
if: matrix.platform != 'ubuntu-latest'

worktree.go

+107
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
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+
406508
func (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

577679
func (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

worktree_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"errors"
7+
"fmt"
78
"io"
89
"os"
910
"path/filepath"
@@ -2747,3 +2748,77 @@ func (s *WorktreeSuite) TestLinkedWorktree(c *C) {
27472748
c.Assert(err, Equals, ErrRepositoryIncomplete)
27482749
}
27492750
}
2751+
2752+
func TestValidPath(t *testing.T) {
2753+
type testcase struct {
2754+
path string
2755+
wantErr bool
2756+
}
2757+
2758+
tests := []testcase{
2759+
{".git", true},
2760+
{".git/b", true},
2761+
{".git\\b", true},
2762+
{"git~1", true},
2763+
{"a/../b", true},
2764+
{"a\\..\\b", true},
2765+
{".gitmodules", false},
2766+
{".gitignore", false},
2767+
{"a..b", false},
2768+
{".", false},
2769+
{"a/.git", false},
2770+
{"a\\.git", false},
2771+
{"a/.git/b", false},
2772+
{"a\\.git\\b", false},
2773+
}
2774+
2775+
if runtime.GOOS == "windows" {
2776+
tests = append(tests, []testcase{
2777+
{"\\\\a\\b", true},
2778+
{"C:\\a\\b", true},
2779+
{".git . . .", true},
2780+
{".git . . ", true},
2781+
{".git ", true},
2782+
{".git.", true},
2783+
{".git::$INDEX_ALLOCATION", true},
2784+
}...)
2785+
}
2786+
2787+
for _, tc := range tests {
2788+
t.Run(fmt.Sprintf("%s", tc.path), func(t *testing.T) {
2789+
err := validPath(tc.path)
2790+
if tc.wantErr {
2791+
assert.Error(t, err)
2792+
} else {
2793+
assert.NoError(t, err)
2794+
}
2795+
})
2796+
}
2797+
}
2798+
2799+
func TestWindowsValidPath(t *testing.T) {
2800+
tests := []struct {
2801+
path string
2802+
want bool
2803+
}{
2804+
{".git", false},
2805+
{".git . . .", false},
2806+
{".git ", false},
2807+
{".git ", false},
2808+
{".git . .", false},
2809+
{".git . .", false},
2810+
{".git::$INDEX_ALLOCATION", false},
2811+
{".git:", false},
2812+
{"a", true},
2813+
{"a\\b", true},
2814+
{"a/b", true},
2815+
{".gitm", true},
2816+
}
2817+
2818+
for _, tc := range tests {
2819+
t.Run(fmt.Sprintf("%s", tc.path), func(t *testing.T) {
2820+
got := windowsValidPath(tc.path)
2821+
assert.Equal(t, tc.want, got)
2822+
})
2823+
}
2824+
}

0 commit comments

Comments
 (0)