| #!/usr/bin/env python3 |
| # Copyright 2023 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Rewrite protobuf files to have monotonically increasing field numbers. |
| |
| This is safe to do for recipe property protobuf files because they are only ever |
| used to de/serialize JSON-encoding protos, which are only sensitive to field |
| names, not field numbers. |
| """ |
| |
| import argparse |
| import re |
| |
| |
| def renumber_fields(lines): |
| # Copy the lines for modification. |
| newlines = list(lines) |
| # levels is a stack that tracks the next field number to use within a given |
| # scope. |
| levels = [] |
| for i, line in enumerate(lines): |
| stripped = line.split("//")[0].strip() |
| if not stripped: |
| continue |
| if stripped.endswith("{"): |
| if stripped.startswith("enum"): |
| # First field number for enums is 0. |
| levels.append(0) |
| elif stripped.startswith(("message", "oneof")): |
| # First field number for messages and oneofs is 1. |
| levels.append(1) |
| elif stripped.startswith("option"): |
| # option clauses shouldn't contain any numbered fields. |
| levels.append(None) |
| else: |
| raise ValueError("invalid line: %r" % line) |
| continue |
| if stripped.startswith("}"): |
| levels.pop() |
| continue |
| |
| # A single line may contain multiple fields. |
| for match in re.finditer( |
| r"((?P<reserved>reserved\s+\w+(\s*(,|to\s)\s*\w+)*)|(?P<field>\w+ =) \d+(?P<option>\s*(\[.*\])?));", |
| stripped, |
| ): |
| if match.group("reserved"): |
| # Reserved fields are not necessary in recipe property protos. |
| newlines[i] = line.replace(match.group(0), "") |
| continue |
| new = "%s %d%s;" % ( |
| match.group("field"), |
| levels[-1], |
| match.group("option") or "", |
| ) |
| newlines[i] = line.replace(match.group(0), new) |
| levels[-1] += 1 |
| |
| return newlines |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description="Renumber .proto file fields.") |
| parser.add_argument("filename", metavar="FILE", type=str, help="The file to modify") |
| parser.add_argument( |
| "--dry-run", |
| action="store_true", |
| help="Print result to stdout instead of writing to the file", |
| ) |
| args = parser.parse_args() |
| |
| with open(args.filename, encoding="utf-8") as f: |
| lines = f.readlines() |
| |
| new_lines = renumber_fields(lines) |
| |
| if args.dry_run: |
| for l in new_lines: |
| print(l, end="") |
| else: |
| with open(args.filename, "w", encoding="utf-8") as f: |
| f.writelines(new_lines) |
| |
| |
| if __name__ == "__main__": |
| main() |