1# Copyright 2023 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Common logic needed by other modules.""" 5 6import contextlib 7import filecmp 8import os 9import shutil 10import tempfile 11import pathlib 12import zipfile 13 14 15# Only some methods respect line length, so this is more of a best-effort 16# limit. 17_TARGET_LINE_LENGTH = 80 18 19 20class StringBuilder: 21 22 def __init__(self): 23 self._sb = [] 24 self._indent = 0 25 self._start_of_line = True 26 27 def __call__(self, value): 28 lines = value.splitlines(keepends=True) 29 for line in lines: 30 if self._start_of_line and line != '\n': 31 self._sb.append(' ' * self._indent) 32 self._sb.append(line) 33 self._start_of_line = line[-1] == '\n' 34 35 def _cur_line_length(self): 36 ret = 0 37 for l in reversed(self._sb): 38 if l.endswith('\n'): 39 break 40 ret += len(l) 41 return ret 42 43 @contextlib.contextmanager 44 def _param_list_generator(self): 45 values = [] 46 yield values 47 self.param_list(values) 48 49 def param_list(self, values=None): 50 if values is None: 51 return self._param_list_generator() 52 53 self('(') 54 punctuation_size = 2 * len(values) # punctuation: ", ()" 55 single_line_size = sum(len(v) for v in values) + punctuation_size 56 if self._cur_line_length() + single_line_size < _TARGET_LINE_LENGTH: 57 self(', '.join(values)) 58 else: 59 self('\n') 60 with self.indent(4): 61 self(',\n'.join(values)) 62 self(')') 63 64 @contextlib.contextmanager 65 def statement(self): 66 yield 67 self(';\n') 68 69 @contextlib.contextmanager 70 def block(self): 71 self(' {\n') 72 with self.indent(2): 73 yield 74 self('}\n') 75 76 @contextlib.contextmanager 77 def indent(self, amount): 78 self._indent += amount 79 yield 80 self._indent -= amount 81 82 def to_string(self): 83 return ''.join(self._sb) 84 85 86def capitalize(value): 87 return value[0].upper() + value[1:] 88 89 90def escape_class_name(fully_qualified_class): 91 """Returns an escaped string concatenating the Java package and class.""" 92 escaped = fully_qualified_class.replace('_', '_1') 93 return escaped.replace('/', '_').replace('$', '_00024') 94 95 96@contextlib.contextmanager 97def atomic_output(path, mode='w+b'): 98 with tempfile.NamedTemporaryFile(mode, delete=False) as f: 99 try: 100 yield f 101 finally: 102 f.close() 103 104 if not (os.path.exists(path) and filecmp.cmp(f.name, path)): 105 pathlib.Path(path).parents[0].mkdir(parents=True, exist_ok=True) 106 shutil.move(f.name, path) 107 if os.path.exists(f.name): 108 os.unlink(f.name) 109 110 111def add_to_zip_hermetic(zip_file, zip_path, data=None): 112 zipinfo = zipfile.ZipInfo(filename=zip_path) 113 zipinfo.external_attr = 0o644 << 16 114 zipinfo.date_time = (2001, 1, 1, 0, 0, 0) 115 zip_file.writestr(zipinfo, data, zipfile.ZIP_STORED) 116