1// Copyright 2018 syzkaller project authors. All rights reserved.
2// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3
4// Package vcs provides helper functions for working with various repositories (e.g. git).
5package vcs
6
7import (
8	"bytes"
9	"fmt"
10	"io"
11	"regexp"
12	"strings"
13	"time"
14
15	"github.com/google/syzkaller/pkg/osutil"
16)
17
18type Repo interface {
19	// Poll checkouts the specified repository/branch.
20	// This involves fetching/resetting/cloning as necessary to recover from all possible problems.
21	// Returns hash of the HEAD commit in the specified branch.
22	Poll(repo, branch string) (*Commit, error)
23
24	// CheckoutBranch checkouts the specified repository/branch.
25	CheckoutBranch(repo, branch string) (*Commit, error)
26
27	// CheckoutCommit checkouts the specified repository on the specified commit.
28	CheckoutCommit(repo, commit string) (*Commit, error)
29
30	// SwitchCommit checkouts the specified commit without fetching.
31	SwitchCommit(commit string) (*Commit, error)
32
33	// HeadCommit returns info about the HEAD commit of the current branch of git repository.
34	HeadCommit() (*Commit, error)
35
36	// ListRecentCommits returns list of recent commit titles starting from baseCommit.
37	ListRecentCommits(baseCommit string) ([]string, error)
38
39	// ExtractFixTagsFromCommits extracts fixing tags for bugs from git log.
40	// Given email = "user@domain.com", it searches for tags of the form "user+tag@domain.com"
41	// and return pairs {tag, commit title}.
42	ExtractFixTagsFromCommits(baseCommit, email string) ([]FixCommit, error)
43
44	// PreviousReleaseTags returns list of preceding release tags that are reachable from the given commit.
45	PreviousReleaseTags(commit string) ([]string, error)
46
47	// Bisect bisects good..bad commit range against the provided predicate (wrapper around git bisect).
48	// The predicate should return an error only if there is no way to proceed
49	// (it will abort the process), if possible it should prefer to return BisectSkip.
50	// Progress of the process is streamed to the provided trace.
51	// Returns the first commit on which the predicate returns BisectBad.
52	Bisect(bad, good string, trace io.Writer, pred func() (BisectResult, error)) (*Commit, error)
53}
54
55type Commit struct {
56	Hash   string
57	Title  string
58	Author string
59	CC     []string
60	Date   time.Time
61}
62
63type FixCommit struct {
64	Tag   string
65	Title string
66}
67
68type BisectResult int
69
70const (
71	BisectBad BisectResult = iota
72	BisectGood
73	BisectSkip
74)
75
76func NewRepo(os, vm, dir string) (Repo, error) {
77	switch os {
78	case "linux":
79		return newGit(os, vm, dir), nil
80	case "akaros":
81		return newAkaros(vm, dir), nil
82	case "fuchsia":
83		return newFuchsia(vm, dir), nil
84	}
85	return nil, fmt.Errorf("vcs is unsupported for %v", os)
86}
87
88func NewSyzkallerRepo(dir string) Repo {
89	return newGit("syzkaller", "", dir)
90}
91
92func Patch(dir string, patch []byte) error {
93	// Do --dry-run first to not mess with partially consistent state.
94	cmd := osutil.Command("patch", "-p1", "--force", "--ignore-whitespace", "--dry-run")
95	if err := osutil.Sandbox(cmd, true, true); err != nil {
96		return err
97	}
98	cmd.Stdin = bytes.NewReader(patch)
99	cmd.Dir = dir
100	if output, err := cmd.CombinedOutput(); err != nil {
101		// If it reverses clean, then it's already applied
102		// (seems to be the easiest way to detect it).
103		cmd = osutil.Command("patch", "-p1", "--force", "--ignore-whitespace", "--reverse", "--dry-run")
104		if err := osutil.Sandbox(cmd, true, true); err != nil {
105			return err
106		}
107		cmd.Stdin = bytes.NewReader(patch)
108		cmd.Dir = dir
109		if _, err := cmd.CombinedOutput(); err == nil {
110			return fmt.Errorf("patch is already applied")
111		}
112		return fmt.Errorf("failed to apply patch:\n%s", output)
113	}
114	// Now apply for real.
115	cmd = osutil.Command("patch", "-p1", "--force", "--ignore-whitespace")
116	if err := osutil.Sandbox(cmd, true, true); err != nil {
117		return err
118	}
119	cmd.Stdin = bytes.NewReader(patch)
120	cmd.Dir = dir
121	if output, err := cmd.CombinedOutput(); err != nil {
122		return fmt.Errorf("failed to apply patch after dry run:\n%s", output)
123	}
124	return nil
125}
126
127// CheckRepoAddress does a best-effort approximate check of a git repo address.
128func CheckRepoAddress(repo string) bool {
129	return gitRepoRe.MatchString(repo)
130}
131
132// CheckBranch does a best-effort approximate check of a git branch name.
133func CheckBranch(branch string) bool {
134	return gitBranchRe.MatchString(branch)
135}
136
137func CheckCommitHash(hash string) bool {
138	if !gitHashRe.MatchString(hash) {
139		return false
140	}
141	ln := len(hash)
142	return ln == 8 || ln == 10 || ln == 12 || ln == 16 || ln == 20 || ln == 40
143}
144
145func runSandboxed(dir, command string, args ...string) ([]byte, error) {
146	cmd := osutil.Command(command, args...)
147	cmd.Dir = dir
148	if err := osutil.Sandbox(cmd, true, false); err != nil {
149		return nil, err
150	}
151	return osutil.Run(time.Hour, cmd)
152}
153
154var (
155	// nolint: lll
156	gitRepoRe    = regexp.MustCompile(`^(git|ssh|http|https|ftp|ftps)://[a-zA-Z0-9-_]+(\.[a-zA-Z0-9-_]+)+(:[0-9]+)?/[a-zA-Z0-9-_./]+\.git(/)?$`)
157	gitBranchRe  = regexp.MustCompile("^[a-zA-Z0-9-_/.]{2,200}$")
158	gitHashRe    = regexp.MustCompile("^[a-f0-9]+$")
159	releaseTagRe = regexp.MustCompile(`^v([0-9]+).([0-9]+)(?:\.([0-9]+))?$`)
160	ccRes        = []*regexp.Regexp{
161		regexp.MustCompile(`^Reviewed\-.*: (.*)$`),
162		regexp.MustCompile(`^[A-Za-z-]+\-and\-[Rr]eviewed\-.*: (.*)$`),
163		regexp.MustCompile(`^Acked\-.*: (.*)$`),
164		regexp.MustCompile(`^[A-Za-z-]+\-and\-[Aa]cked\-.*: (.*)$`),
165		regexp.MustCompile(`^Tested\-.*: (.*)$`),
166		regexp.MustCompile(`^[A-Za-z-]+\-and\-[Tt]ested\-.*: (.*)$`),
167	}
168)
169
170// CanonicalizeCommit returns commit title that can be used when checking
171// if a particular commit is present in a git tree.
172// Some trees add prefixes to commit titles during backporting,
173// so we want e.g. commit "foo bar" match "BACKPORT: foo bar".
174func CanonicalizeCommit(title string) string {
175	for _, prefix := range commitPrefixes {
176		if strings.HasPrefix(title, prefix) {
177			title = title[len(prefix):]
178			break
179		}
180	}
181	return strings.TrimSpace(title)
182}
183
184var commitPrefixes = []string{
185	"UPSTREAM:",
186	"CHROMIUM:",
187	"FROMLIST:",
188	"BACKPORT:",
189	"FROMGIT:",
190	"net-backports:",
191}
192