xref: /aosp_15_r20/external/cronet/testing/libfuzzer/gen_fuzzer_owners.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1*6777b538SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*6777b538SAndroid Build Coastguard Worker#
3*6777b538SAndroid Build Coastguard Worker# Copyright 2018 The Chromium Authors
4*6777b538SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
5*6777b538SAndroid Build Coastguard Worker# found in the LICENSE file.
6*6777b538SAndroid Build Coastguard Worker"""Generates a `foo.owners` file for a `fuzzer_test("foo", ...)` GN target.
7*6777b538SAndroid Build Coastguard Worker
8*6777b538SAndroid Build Coastguard WorkerBy default, the closest `OWNERS` file is located and copied, except for
9*6777b538SAndroid Build Coastguard Worker`//OWNERS` and `//third_party/OWNERS` for fear of spamming top-level owners with
10*6777b538SAndroid Build Coastguard Workerfuzzer bugs they know nothing about.
11*6777b538SAndroid Build Coastguard Worker
12*6777b538SAndroid Build Coastguard WorkerIf no such file can be located, then we attempt to use `git blame` to identify
13*6777b538SAndroid Build Coastguard Workerthe author of the main fuzzer `.cc` file. Note that this does not work for code
14*6777b538SAndroid Build Coastguard Workerin git submodules (e.g. most code in `third_party/`), in which case we generate
15*6777b538SAndroid Build Coastguard Workeran empty file.
16*6777b538SAndroid Build Coastguard Worker
17*6777b538SAndroid Build Coastguard WorkerInvoked by GN from `fuzzer_test.gni`.
18*6777b538SAndroid Build Coastguard Worker"""
19*6777b538SAndroid Build Coastguard Worker
20*6777b538SAndroid Build Coastguard Workerimport argparse
21*6777b538SAndroid Build Coastguard Workerimport os
22*6777b538SAndroid Build Coastguard Workerimport re
23*6777b538SAndroid Build Coastguard Workerimport subprocess
24*6777b538SAndroid Build Coastguard Workerimport sys
25*6777b538SAndroid Build Coastguard Worker
26*6777b538SAndroid Build Coastguard Workerfrom typing import Optional
27*6777b538SAndroid Build Coastguard Worker
28*6777b538SAndroid Build Coastguard WorkerAUTHOR_REGEX = re.compile('author-mail <(.+)>')
29*6777b538SAndroid Build Coastguard WorkerCHROMIUM_SRC_DIR = os.path.dirname(
30*6777b538SAndroid Build Coastguard Worker    os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31*6777b538SAndroid Build Coastguard WorkerOWNERS_FILENAME = 'OWNERS'
32*6777b538SAndroid Build Coastguard WorkerTHIRD_PARTY = 'third_party'
33*6777b538SAndroid Build Coastguard WorkerTHIRD_PARTY_SEARCH_STRING = THIRD_PARTY + os.path.sep
34*6777b538SAndroid Build Coastguard Worker
35*6777b538SAndroid Build Coastguard Worker
36*6777b538SAndroid Build Coastguard Workerdef GetAuthorFromGitBlame(blame_output):
37*6777b538SAndroid Build Coastguard Worker  """Return author from git blame output."""
38*6777b538SAndroid Build Coastguard Worker  for line in blame_output.decode('utf-8').splitlines():
39*6777b538SAndroid Build Coastguard Worker    m = AUTHOR_REGEX.match(line)
40*6777b538SAndroid Build Coastguard Worker    if m:
41*6777b538SAndroid Build Coastguard Worker      return m.group(1)
42*6777b538SAndroid Build Coastguard Worker
43*6777b538SAndroid Build Coastguard Worker  return None
44*6777b538SAndroid Build Coastguard Worker
45*6777b538SAndroid Build Coastguard Worker
46*6777b538SAndroid Build Coastguard Workerdef GetGitCommand():
47*6777b538SAndroid Build Coastguard Worker  """Returns a git command that does not need to be executed using shell=True.
48*6777b538SAndroid Build Coastguard Worker  On non-Windows platforms: 'git'. On Windows: 'git.bat'.
49*6777b538SAndroid Build Coastguard Worker  """
50*6777b538SAndroid Build Coastguard Worker  return 'git.bat' if sys.platform == 'win32' else 'git'
51*6777b538SAndroid Build Coastguard Worker
52*6777b538SAndroid Build Coastguard Worker
53*6777b538SAndroid Build Coastguard Workerdef GetOwnersFromOwnersFile(source: str) -> Optional[str]:
54*6777b538SAndroid Build Coastguard Worker  """Finds the owners of `source` from the closest OWNERS file.
55*6777b538SAndroid Build Coastguard Worker
56*6777b538SAndroid Build Coastguard Worker  Both //OWNERS or */third_party/OWNERS are ignored so as not to spam top-level
57*6777b538SAndroid Build Coastguard Worker  owners with unowned fuzzer bugs.
58*6777b538SAndroid Build Coastguard Worker
59*6777b538SAndroid Build Coastguard Worker  Args:
60*6777b538SAndroid Build Coastguard Worker    source: Relative path from the chromium src directory to the target source
61*6777b538SAndroid Build Coastguard Worker      file.
62*6777b538SAndroid Build Coastguard Worker
63*6777b538SAndroid Build Coastguard Worker  Returns:
64*6777b538SAndroid Build Coastguard Worker    The entire contents of the closest OWNERS file. That is, the first OWNERS
65*6777b538SAndroid Build Coastguard Worker    file encountered while walking up through the ancestor directories of the
66*6777b538SAndroid Build Coastguard Worker    target source file.
67*6777b538SAndroid Build Coastguard Worker  """
68*6777b538SAndroid Build Coastguard Worker  # TODO(https://crbug.com/1513729): Use `pathlib` instead of `os.path` for
69*6777b538SAndroid Build Coastguard Worker  # better ergonomics and robustness.
70*6777b538SAndroid Build Coastguard Worker  dirs = source.split(os.path.sep)[:-1]
71*6777b538SAndroid Build Coastguard Worker
72*6777b538SAndroid Build Coastguard Worker  # Note: We never test for //OWNERS, i.e. when `dirs` is empty.
73*6777b538SAndroid Build Coastguard Worker  while dirs:
74*6777b538SAndroid Build Coastguard Worker    # Never return the contents of */third_party/OWNERS, and stop searching.
75*6777b538SAndroid Build Coastguard Worker    if dirs[-1] == THIRD_PARTY:
76*6777b538SAndroid Build Coastguard Worker      break
77*6777b538SAndroid Build Coastguard Worker
78*6777b538SAndroid Build Coastguard Worker    owners_file_path = os.path.join(CHROMIUM_SRC_DIR, *dirs, OWNERS_FILENAME)
79*6777b538SAndroid Build Coastguard Worker    if os.path.exists(owners_file_path):
80*6777b538SAndroid Build Coastguard Worker      # TODO(https://crbug.com/1513729): OWNERS files can reference others,
81*6777b538SAndroid Build Coastguard Worker      # have per-file directives, etc. We should be cleverer than this.
82*6777b538SAndroid Build Coastguard Worker      return open(owners_file_path).read()
83*6777b538SAndroid Build Coastguard Worker
84*6777b538SAndroid Build Coastguard Worker    dirs.pop()
85*6777b538SAndroid Build Coastguard Worker
86*6777b538SAndroid Build Coastguard Worker  return None
87*6777b538SAndroid Build Coastguard Worker
88*6777b538SAndroid Build Coastguard Workerdef GetOwnersForFuzzer(sources):
89*6777b538SAndroid Build Coastguard Worker  """Return owners given a list of sources as input."""
90*6777b538SAndroid Build Coastguard Worker  if not sources:
91*6777b538SAndroid Build Coastguard Worker    return None
92*6777b538SAndroid Build Coastguard Worker
93*6777b538SAndroid Build Coastguard Worker  for source in sources:
94*6777b538SAndroid Build Coastguard Worker    full_source_path = os.path.join(CHROMIUM_SRC_DIR, source)
95*6777b538SAndroid Build Coastguard Worker    if not os.path.exists(full_source_path):
96*6777b538SAndroid Build Coastguard Worker      continue
97*6777b538SAndroid Build Coastguard Worker
98*6777b538SAndroid Build Coastguard Worker    with open(full_source_path, 'r') as source_file_handle:
99*6777b538SAndroid Build Coastguard Worker      source_content = source_file_handle.read()
100*6777b538SAndroid Build Coastguard Worker
101*6777b538SAndroid Build Coastguard Worker    if SubStringExistsIn(
102*6777b538SAndroid Build Coastguard Worker        ['FuzzOneInput', 'LLVMFuzzerTestOneInput', 'PROTO_FUZZER'],
103*6777b538SAndroid Build Coastguard Worker        source_content):
104*6777b538SAndroid Build Coastguard Worker      # Found the fuzzer source (and not dependency of fuzzer).
105*6777b538SAndroid Build Coastguard Worker
106*6777b538SAndroid Build Coastguard Worker      # Try finding the closest OWNERS file first.
107*6777b538SAndroid Build Coastguard Worker      owners = GetOwnersFromOwnersFile(source)
108*6777b538SAndroid Build Coastguard Worker      if owners:
109*6777b538SAndroid Build Coastguard Worker        return owners
110*6777b538SAndroid Build Coastguard Worker
111*6777b538SAndroid Build Coastguard Worker      git_dir = os.path.join(CHROMIUM_SRC_DIR, '.git')
112*6777b538SAndroid Build Coastguard Worker      git_command = GetGitCommand()
113*6777b538SAndroid Build Coastguard Worker      is_git_file = bool(subprocess.check_output(
114*6777b538SAndroid Build Coastguard Worker          [git_command, '--git-dir', git_dir, 'ls-files', source],
115*6777b538SAndroid Build Coastguard Worker          cwd=CHROMIUM_SRC_DIR))
116*6777b538SAndroid Build Coastguard Worker      if not is_git_file:
117*6777b538SAndroid Build Coastguard Worker        # File is not in working tree. If no OWNERS file was found, we cannot
118*6777b538SAndroid Build Coastguard Worker        # tell who it belongs to.
119*6777b538SAndroid Build Coastguard Worker        return None
120*6777b538SAndroid Build Coastguard Worker
121*6777b538SAndroid Build Coastguard Worker      # `git log --follow` and `--reverse` don't work together and using just
122*6777b538SAndroid Build Coastguard Worker      # `--follow` is too slow. Make a best estimate with an assumption that the
123*6777b538SAndroid Build Coastguard Worker      # original author has authored the copyright block, which (generally) does
124*6777b538SAndroid Build Coastguard Worker      # not change even with file rename/move. Look at the last line of the
125*6777b538SAndroid Build Coastguard Worker      # block, as a copyright block update sweep in late 2022 made one person
126*6777b538SAndroid Build Coastguard Worker      # responsible for changing the first line of every copyright block in the
127*6777b538SAndroid Build Coastguard Worker      # repo, and it would be best to avoid assigning ownership of every fuzz
128*6777b538SAndroid Build Coastguard Worker      # issue predating that year to that one person.
129*6777b538SAndroid Build Coastguard Worker      blame_output = subprocess.check_output(
130*6777b538SAndroid Build Coastguard Worker          [git_command, '--git-dir', git_dir, 'blame', '--porcelain', '-L3,3',
131*6777b538SAndroid Build Coastguard Worker           source], cwd=CHROMIUM_SRC_DIR)
132*6777b538SAndroid Build Coastguard Worker      return GetAuthorFromGitBlame(blame_output)
133*6777b538SAndroid Build Coastguard Worker
134*6777b538SAndroid Build Coastguard Worker  return None
135*6777b538SAndroid Build Coastguard Worker
136*6777b538SAndroid Build Coastguard Workerdef FindGroupsAndDepsInDeps(deps_list, build_dir):
137*6777b538SAndroid Build Coastguard Worker  """Return list of groups, as well as their deps, from a list of deps."""
138*6777b538SAndroid Build Coastguard Worker  groups = []
139*6777b538SAndroid Build Coastguard Worker  deps_for_groups = {}
140*6777b538SAndroid Build Coastguard Worker  for deps in deps_list:
141*6777b538SAndroid Build Coastguard Worker    output = subprocess.check_output(
142*6777b538SAndroid Build Coastguard Worker        [GNPath(), 'desc', '--fail-on-unused-args', build_dir, deps]).decode(
143*6777b538SAndroid Build Coastguard Worker                'utf8')
144*6777b538SAndroid Build Coastguard Worker    needle = 'Type: '
145*6777b538SAndroid Build Coastguard Worker    for line in output.splitlines():
146*6777b538SAndroid Build Coastguard Worker      if needle and not line.startswith(needle):
147*6777b538SAndroid Build Coastguard Worker        continue
148*6777b538SAndroid Build Coastguard Worker      if needle == 'Type: ':
149*6777b538SAndroid Build Coastguard Worker        if line != 'Type: group':
150*6777b538SAndroid Build Coastguard Worker          break
151*6777b538SAndroid Build Coastguard Worker        groups.append(deps)
152*6777b538SAndroid Build Coastguard Worker        assert deps not in deps_for_groups
153*6777b538SAndroid Build Coastguard Worker        deps_for_groups[deps] = []
154*6777b538SAndroid Build Coastguard Worker        needle = 'Direct dependencies'
155*6777b538SAndroid Build Coastguard Worker      elif needle == 'Direct dependencies':
156*6777b538SAndroid Build Coastguard Worker        needle = ''
157*6777b538SAndroid Build Coastguard Worker      else:
158*6777b538SAndroid Build Coastguard Worker        assert needle == ''
159*6777b538SAndroid Build Coastguard Worker        if needle == line:
160*6777b538SAndroid Build Coastguard Worker          break
161*6777b538SAndroid Build Coastguard Worker        deps_for_groups[deps].append(line.strip())
162*6777b538SAndroid Build Coastguard Worker
163*6777b538SAndroid Build Coastguard Worker  return groups, deps_for_groups
164*6777b538SAndroid Build Coastguard Worker
165*6777b538SAndroid Build Coastguard Worker
166*6777b538SAndroid Build Coastguard Workerdef TraverseGroups(deps_list, build_dir):
167*6777b538SAndroid Build Coastguard Worker  """Filter out groups from a deps list. Add groups' direct dependencies."""
168*6777b538SAndroid Build Coastguard Worker  full_deps_set = set(deps_list)
169*6777b538SAndroid Build Coastguard Worker  deps_to_check = full_deps_set.copy()
170*6777b538SAndroid Build Coastguard Worker
171*6777b538SAndroid Build Coastguard Worker  # Keep track of groups to break circular dependendies, if any.
172*6777b538SAndroid Build Coastguard Worker  seen_groups = set()
173*6777b538SAndroid Build Coastguard Worker
174*6777b538SAndroid Build Coastguard Worker  while deps_to_check:
175*6777b538SAndroid Build Coastguard Worker    # Look for groups from the deps set.
176*6777b538SAndroid Build Coastguard Worker    groups, deps_for_groups = FindGroupsAndDepsInDeps(deps_to_check, build_dir)
177*6777b538SAndroid Build Coastguard Worker    groups = set(groups).difference(seen_groups)
178*6777b538SAndroid Build Coastguard Worker    if not groups:
179*6777b538SAndroid Build Coastguard Worker      break
180*6777b538SAndroid Build Coastguard Worker
181*6777b538SAndroid Build Coastguard Worker    # Update sets. Filter out groups from the full deps set.
182*6777b538SAndroid Build Coastguard Worker    full_deps_set.difference_update(groups)
183*6777b538SAndroid Build Coastguard Worker    deps_to_check.clear()
184*6777b538SAndroid Build Coastguard Worker    seen_groups.update(groups)
185*6777b538SAndroid Build Coastguard Worker
186*6777b538SAndroid Build Coastguard Worker    # Get the direct dependencies, and filter out known groups there too.
187*6777b538SAndroid Build Coastguard Worker    for group in groups:
188*6777b538SAndroid Build Coastguard Worker      deps_to_check.update(deps_for_groups[group])
189*6777b538SAndroid Build Coastguard Worker    deps_to_check.difference_update(seen_groups)
190*6777b538SAndroid Build Coastguard Worker    full_deps_set.update(deps_to_check)
191*6777b538SAndroid Build Coastguard Worker  return list(full_deps_set)
192*6777b538SAndroid Build Coastguard Worker
193*6777b538SAndroid Build Coastguard Worker
194*6777b538SAndroid Build Coastguard Workerdef GetSourcesFromDeps(deps_list, build_dir):
195*6777b538SAndroid Build Coastguard Worker  """Return list of sources from parsing deps."""
196*6777b538SAndroid Build Coastguard Worker  if not deps_list:
197*6777b538SAndroid Build Coastguard Worker    return None
198*6777b538SAndroid Build Coastguard Worker
199*6777b538SAndroid Build Coastguard Worker  full_deps_list = TraverseGroups(deps_list, build_dir)
200*6777b538SAndroid Build Coastguard Worker  all_sources = []
201*6777b538SAndroid Build Coastguard Worker  for deps in full_deps_list:
202*6777b538SAndroid Build Coastguard Worker    output = subprocess.check_output(
203*6777b538SAndroid Build Coastguard Worker        [GNPath(), 'desc', '--fail-on-unused-args', build_dir, deps, 'sources'])
204*6777b538SAndroid Build Coastguard Worker    for source in bytes(output).decode('utf8').splitlines():
205*6777b538SAndroid Build Coastguard Worker      if source.startswith('//'):
206*6777b538SAndroid Build Coastguard Worker        source = source[2:]
207*6777b538SAndroid Build Coastguard Worker      all_sources.append(source)
208*6777b538SAndroid Build Coastguard Worker
209*6777b538SAndroid Build Coastguard Worker  return all_sources
210*6777b538SAndroid Build Coastguard Worker
211*6777b538SAndroid Build Coastguard Worker
212*6777b538SAndroid Build Coastguard Workerdef GNPath():
213*6777b538SAndroid Build Coastguard Worker  if sys.platform.startswith('linux'):
214*6777b538SAndroid Build Coastguard Worker    subdir, exe = 'linux64', 'gn'
215*6777b538SAndroid Build Coastguard Worker  elif sys.platform == 'darwin':
216*6777b538SAndroid Build Coastguard Worker    subdir, exe = 'mac', 'gn'
217*6777b538SAndroid Build Coastguard Worker  else:
218*6777b538SAndroid Build Coastguard Worker    subdir, exe = 'win', 'gn.exe'
219*6777b538SAndroid Build Coastguard Worker
220*6777b538SAndroid Build Coastguard Worker  return os.path.join(CHROMIUM_SRC_DIR, 'buildtools', subdir, exe)
221*6777b538SAndroid Build Coastguard Worker
222*6777b538SAndroid Build Coastguard Worker
223*6777b538SAndroid Build Coastguard Workerdef SubStringExistsIn(substring_list, string):
224*6777b538SAndroid Build Coastguard Worker  """Return true if one of the substring in the list is found in |string|."""
225*6777b538SAndroid Build Coastguard Worker  return any(substring in string for substring in substring_list)
226*6777b538SAndroid Build Coastguard Worker
227*6777b538SAndroid Build Coastguard Worker
228*6777b538SAndroid Build Coastguard Workerdef main():
229*6777b538SAndroid Build Coastguard Worker  parser = argparse.ArgumentParser(description='Generate fuzzer owners file.')
230*6777b538SAndroid Build Coastguard Worker  parser.add_argument('--owners', required=True)
231*6777b538SAndroid Build Coastguard Worker  parser.add_argument('--build-dir')
232*6777b538SAndroid Build Coastguard Worker  parser.add_argument('--deps', nargs='+')
233*6777b538SAndroid Build Coastguard Worker  parser.add_argument('--sources', nargs='+')
234*6777b538SAndroid Build Coastguard Worker  args = parser.parse_args()
235*6777b538SAndroid Build Coastguard Worker
236*6777b538SAndroid Build Coastguard Worker  # Generate owners file.
237*6777b538SAndroid Build Coastguard Worker  with open(args.owners, 'w') as owners_file:
238*6777b538SAndroid Build Coastguard Worker    # If we found an owner, then write it to file.
239*6777b538SAndroid Build Coastguard Worker    # Otherwise, leave empty file to keep ninja happy.
240*6777b538SAndroid Build Coastguard Worker    owners = GetOwnersForFuzzer(args.sources)
241*6777b538SAndroid Build Coastguard Worker    if owners:
242*6777b538SAndroid Build Coastguard Worker      owners_file.write(owners)
243*6777b538SAndroid Build Coastguard Worker      return
244*6777b538SAndroid Build Coastguard Worker
245*6777b538SAndroid Build Coastguard Worker    # Could not determine owners from |args.sources|.
246*6777b538SAndroid Build Coastguard Worker    # So, try parsing sources from |args.deps|.
247*6777b538SAndroid Build Coastguard Worker    deps_sources = GetSourcesFromDeps(args.deps, args.build_dir)
248*6777b538SAndroid Build Coastguard Worker    owners = GetOwnersForFuzzer(deps_sources)
249*6777b538SAndroid Build Coastguard Worker    if owners:
250*6777b538SAndroid Build Coastguard Worker      owners_file.write(owners)
251*6777b538SAndroid Build Coastguard Worker
252*6777b538SAndroid Build Coastguard Worker
253*6777b538SAndroid Build Coastguard Workerif __name__ == '__main__':
254*6777b538SAndroid Build Coastguard Worker  main()
255