Download
161 lines 4.5 KiBGoModified 2026-10-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
package policy

import (
	"path/filepath"
	"reflect"
	"strings"
	"testing"
)

// A policy that parses writes back to one that parses to the same rules,
// and writes back the same again: blinker edits policies by rewriting
// them, and what the user reviewed must be what is saved.
func FuzzParseMarshal(f *testing.F) {
	f.Add([]byte("project /p\n# note\nfile read dir with spaces/x\nhost ask *.GitHub.com\nenv deny GH_*\nenv set GOTOOLCHAIN local\ntool allow Commit\ninclude agent-safe\n"))
	f.Add([]byte("file\thide\thide\t[ ]\n\n# lone comment\n"))
	f.Fuzz(func(t *testing.T, data []byte) {
		a, err := Parse(data)
		if err != nil {
			return
		}
		out := a.Marshal()
		b, err := Parse(out)
		if err != nil {
			t.Fatalf("Marshal wrote what Parse refuses: %v\n%s", err, out)
		}
		strip := func(f *File) File {
			g := *f
			g.Files, g.Network, g.Env, g.Tools, g.Includes = nil, nil, nil, nil, nil
			for _, inc := range f.Includes {
				inc.Line = 0
				g.Includes = append(g.Includes, inc)
			}
			for _, r := range f.Files {
				r.Line = 0
				g.Files = append(g.Files, r)
			}
			for _, r := range f.Network {
				r.Line = 0
				g.Network = append(g.Network, r)
			}
			for _, r := range f.Env {
				r.Line = 0
				g.Env = append(g.Env, r)
			}
			for _, r := range f.Tools {
				r.Line = 0
				g.Tools = append(g.Tools, r)
			}
			g.text = nil
			return g
		}
		if sa, sb := strip(a), strip(b); !reflect.DeepEqual(sa, sb) {
			t.Fatalf("rules changed in a round trip:\n%+v\n%+v\n%s", sa, sb, out)
		}
		if again := b.Marshal(); string(again) != string(out) {
			t.Fatalf("Marshal is not stable:\n%s\n---\n%s", out, again)
		}
	})
}

// fuzzPath turns bytes into a path of components from a small alphabet,
// so patterns and paths meet often.
func fuzzPath(b []byte, glob bool) string {
	names := []string{"a", "b", ".env", "x.go", "ab"}
	if glob {
		names = append(names, "*", "**", "?", "[ab]", "*.go", ".e*", "[!a]")
	}
	var parts []string
	for _, c := range b {
		parts = append(parts, names[int(c)%len(names)])
	}
	return strings.Join(parts, "/")
}

// Match is MatchAnyExact of the path or one of its ancestors, and Covers
// never claims more than Match does.
func FuzzMatcher(f *testing.F) {
	f.Add([]byte{6, 2}, []byte{5, 1}, []byte{0, 1, 2})
	f.Add([]byte{6, 6}, []byte{7, 3}, []byte{0, 4, 3})
	f.Fuzz(func(t *testing.T, gb, mb, pb []byte) {
		if len(gb) > 6 || len(mb) > 6 || len(pb) > 8 {
			return
		}
		home, project := "/h", "/h/p"
		n, err := NewMatcher(fuzzPath(gb, true)+"x", home, project) // never empty, never ".."
		if err != nil {
			return
		}
		m, err := NewMatcher(fuzzPath(mb, true)+"x", home, project)
		if err != nil {
			return
		}
		p := filepath.Join(project, fuzzPath(pb, false))
		anc := false
		for cur := p; ; cur = filepath.Dir(cur) {
			if MatchAnyExact([]Matcher{n}, cur) {
				anc = true
				break
			}
			if cur == "/" {
				break
			}
		}
		if n.Match(p) != anc {
			t.Fatalf("%q: Match(%s) = %v, but an ancestor-or-self matches exactly: %v", n.Path, p, n.Match(p), anc)
		}
		if n.Covers(m) && m.Match(p) && !n.Match(p) {
			t.Fatalf("%q covers %q, but only the latter matches %s", n.Path, m.Path, p)
		}
	})
}

// A host pattern matches its own domain's hosts only, whatever their case
// or final dot.
func FuzzHostMatches(f *testing.F) {
	f.Add("api.github.com", "*.github.com")
	f.Add("github.com", "*.github.com")
	f.Add("evilgithub.com", "*.github.com")
	f.Add("GitHub.COM.", "github.com")
	f.Fuzz(func(t *testing.T, host, pattern string) {
		p, err := ValidateHost(pattern)
		if err != nil {
			return
		}
		got := HostMatches(host, p)
		// Case is ASCII case: host names are ASCII.
		upper := strings.Map(func(r rune) rune {
			if r >= 'a' && r <= 'z' {
				return r - 'a' + 'A'
			}
			return r
		}, host)
		if HostMatches(upper, p) != got || HostMatches(lowerASCII(host), p) != got {
			t.Fatalf("HostMatches(%q, %q) depends on case", host, p)
		}
		// One final dot is the root's, so it changes nothing; "a.." is no
		// host name, and not one of a's.
		norm := lowerASCII(host)
		if !strings.HasSuffix(host, ".") {
			if HostMatches(host+".", p) != got {
				t.Fatalf("HostMatches(%q, %q) depends on a final dot", host, p)
			}
		} else {
			norm = strings.TrimSuffix(norm, ".")
		}
		switch base, sub := strings.CutPrefix(p, "*."); {
		case p == "*":
			if !got {
				t.Fatalf("* does not match %q", host)
			}
		case sub:
			if want := strings.HasSuffix(norm, "."+base) && len(norm) > len(base)+1; got != want {
				t.Fatalf("HostMatches(%q, %q) = %v", host, p, got)
			}
		default:
			if got != (norm == p) {
				t.Fatalf("HostMatches(%q, %q) = %v", host, p, got)
			}
		}
	})
}