github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/subsystem/linux/maintainers.go (about)

     1  // Copyright 2023 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 linux
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"net/mail"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"unicode"
    15  
    16  	"github.com/google/syzkaller/pkg/subsystem"
    17  )
    18  
    19  // maintainersRecord represents a single raw record in the MAINTAINERS file.
    20  type maintainersRecord struct {
    21  	name            string
    22  	includePatterns []string
    23  	excludePatterns []string
    24  	regexps         []string
    25  	lists           []string
    26  	maintainers     []string
    27  	trees           []string
    28  }
    29  
    30  func parseLinuxMaintainers(content io.Reader) ([]*maintainersRecord, error) {
    31  	scanner := bufio.NewScanner(content)
    32  	// First skip the headers.
    33  	var skippedLines int
    34  	for scanner.Scan() {
    35  		skippedLines++
    36  		line := scanner.Text()
    37  		if line == "Maintainers List" {
    38  			// Also skip ------.
    39  			scanner.Scan()
    40  			break
    41  		}
    42  	}
    43  	ml := &maintainersLexer{scanner: scanner, currentLine: skippedLines + 1}
    44  	ret := []*maintainersRecord{}
    45  loop:
    46  	for {
    47  		item := ml.next()
    48  		switch v := item.(type) {
    49  		case recordTitle:
    50  			// The new subsystem begins.
    51  			ret = append(ret, &maintainersRecord{name: string(v)})
    52  		case recordProperty:
    53  			if len(ret) == 0 {
    54  				return nil, fmt.Errorf("line %d: property without subsystem", ml.currentLine)
    55  			}
    56  			err := applyProperty(ret[len(ret)-1], &v)
    57  			if err != nil {
    58  				return nil, fmt.Errorf("line %d: failed to apply the property %#v: %w",
    59  					ml.currentLine, v, err)
    60  			}
    61  		case endOfFile:
    62  			break loop
    63  		}
    64  	}
    65  	if err := scanner.Err(); err != nil {
    66  		return nil, err
    67  	}
    68  	return ret, nil
    69  }
    70  
    71  type maintainersLexer struct {
    72  	scanner     *bufio.Scanner
    73  	currentLine int
    74  	inComment   bool
    75  }
    76  
    77  type recordTitle string
    78  type recordProperty struct {
    79  	key   string
    80  	value string
    81  }
    82  type endOfFile struct{}
    83  
    84  var propertyRe = regexp.MustCompile(`^([[:alpha:]]):\s+(.*).*$`)
    85  
    86  func (ml *maintainersLexer) next() interface{} {
    87  	for ml.scanner.Scan() {
    88  		ml.currentLine++
    89  		rawLine := ml.scanner.Text()
    90  		line := strings.TrimSpace(rawLine)
    91  		if strings.HasPrefix(line, ".") {
    92  			ml.inComment = true
    93  			continue
    94  		}
    95  		// A comment continues to the next line(s) if they begin with a space character.
    96  		if ml.inComment && (line == "" || !unicode.IsSpace(rune(rawLine[0]))) {
    97  			ml.inComment = false
    98  		}
    99  		if ml.inComment || line == "" {
   100  			continue
   101  		}
   102  		// Now let's consider the possible line types.
   103  		if matches := propertyRe.FindStringSubmatch(line); matches != nil {
   104  			return recordProperty{key: matches[1], value: matches[2]}
   105  		}
   106  		return recordTitle(line)
   107  	}
   108  	return endOfFile{}
   109  }
   110  
   111  func applyProperty(record *maintainersRecord, property *recordProperty) error {
   112  	switch property.key {
   113  	case "F":
   114  		record.includePatterns = append(record.includePatterns, property.value)
   115  	case "X":
   116  		record.excludePatterns = append(record.excludePatterns, property.value)
   117  	case "N":
   118  		if _, err := regexp.Compile(property.value); err != nil {
   119  			return fmt.Errorf("invalid regexp: %s", property.value)
   120  		}
   121  		record.regexps = append(record.regexps, property.value)
   122  	case "M":
   123  		value, err := parseEmail(property.value)
   124  		if err != nil {
   125  			return err
   126  		}
   127  		record.maintainers = append(record.maintainers, value)
   128  	case "L":
   129  		value, err := parseEmail(property.value)
   130  		if err != nil {
   131  			return err
   132  		}
   133  		record.lists = append(record.lists, value)
   134  	case "T":
   135  		record.trees = append(record.trees, property.value)
   136  	}
   137  	return nil
   138  }
   139  
   140  func parseEmail(value string) (string, error) {
   141  	// Sometimes there happen extra symbols at the end of the line,
   142  	// let's make this parser more error tolerant.
   143  	if pos := strings.LastIndexAny(value, ">)"); pos >= 0 {
   144  		value = value[:pos+1]
   145  	}
   146  	// Let's also make the parser more robust by skipping everything before the first <,
   147  	// if it exists.
   148  	if pos := strings.LastIndexAny(value, "<"); pos >= 0 {
   149  		value = value[pos:]
   150  	}
   151  	addr, err := mail.ParseAddress(value)
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	return addr.Address, nil
   156  }
   157  
   158  func (r maintainersRecord) ToPathRule() subsystem.PathRule {
   159  	inclRe := strings.Builder{}
   160  	for i, wildcard := range r.includePatterns {
   161  		if i > 0 {
   162  			inclRe.WriteByte('|')
   163  		}
   164  		wildcardToRegexp(wildcard, &inclRe)
   165  	}
   166  	for _, rg := range r.regexps {
   167  		if inclRe.Len() > 0 {
   168  			inclRe.WriteByte('|')
   169  		}
   170  		inclRe.WriteString(rg)
   171  	}
   172  	exclRe := strings.Builder{}
   173  	for i, wildcard := range r.excludePatterns {
   174  		if i > 0 {
   175  			exclRe.WriteByte('|')
   176  		}
   177  		wildcardToRegexp(wildcard, &exclRe)
   178  	}
   179  	return subsystem.PathRule{
   180  		IncludeRegexp: inclRe.String(),
   181  		ExcludeRegexp: exclRe.String(),
   182  	}
   183  }
   184  
   185  func removeMatchingPatterns(records []*maintainersRecord, rule *regexp.Regexp) {
   186  	filter := func(list []string) []string {
   187  		ret := []string{}
   188  		for _, item := range list {
   189  			if !rule.MatchString(item) {
   190  				ret = append(ret, item)
   191  			}
   192  		}
   193  		return ret
   194  	}
   195  	for _, record := range records {
   196  		record.includePatterns = filter(record.includePatterns)
   197  		record.excludePatterns = filter(record.excludePatterns)
   198  	}
   199  }
   200  
   201  var (
   202  	escapedSeparator = regexp.QuoteMeta(fmt.Sprintf("%c", filepath.Separator))
   203  	wildcardReplace  = map[byte]string{
   204  		'*': `[^` + escapedSeparator + `]*`,
   205  		'?': `.`,
   206  		'/': escapedSeparator,
   207  	}
   208  )
   209  
   210  func wildcardToRegexp(wildcard string, store *strings.Builder) {
   211  	store.WriteByte('^')
   212  
   213  	// We diverge a bit from the standard MAINTAINERS rule semantics.
   214  	// path/* corresponds to the files belonging to the `path` folder,
   215  	// but, since we also infer the parent-child relationship, it's
   216  	// easier to make it cover the whole subtree.
   217  	if len(wildcard) >= 2 && wildcard[len(wildcard)-2:] == "/*" {
   218  		wildcard = wildcard[:len(wildcard)-1]
   219  	}
   220  
   221  	tokenStart := 0
   222  	for i, c := range wildcard {
   223  		replace, exists := wildcardReplace[byte(c)]
   224  		if !exists {
   225  			continue
   226  		}
   227  		store.WriteString(regexp.QuoteMeta(wildcard[tokenStart:i]))
   228  		store.WriteString(replace)
   229  		tokenStart = i + 1
   230  	}
   231  	if tokenStart < len(wildcard) {
   232  		store.WriteString(regexp.QuoteMeta(wildcard[tokenStart:]))
   233  	}
   234  	if wildcard == "" || wildcard[len(wildcard)-1] != '/' {
   235  		store.WriteByte('$')
   236  	}
   237  }