chromium/third_party/jni_zero/common.py

# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Common logic needed by other modules."""

import contextlib
import filecmp
import os
import shutil
import tempfile
import pathlib
import zipfile


# Only some methods respect line length, so this is more of a best-effort
# limit.
_TARGET_LINE_LENGTH = 80


class StringBuilder:

  def __init__(self):
    self._sb = []
    self._indent = 0
    self._start_of_line = True

  def __call__(self, value):
    lines = value.splitlines(keepends=True)
    for line in lines:
      if self._start_of_line and line != '\n':
        self._sb.append(' ' * self._indent)
      self._sb.append(line)
      self._start_of_line = line[-1] == '\n'

  def _cur_line_length(self):
    ret = 0
    for l in reversed(self._sb):
      if l.endswith('\n'):
        break
      ret += len(l)
    return ret

  @contextlib.contextmanager
  def _param_list_generator(self):
    values = []
    yield values
    self.param_list(values)

  def param_list(self, values=None):
    if values is None:
      return self._param_list_generator()

    self('(')
    if values:
      punctuation_size = 2 * len(values) # punctuation: ", ()"
      single_line_size = sum(len(v) for v in values) + punctuation_size
      if self._cur_line_length() + single_line_size < _TARGET_LINE_LENGTH:
        self(', '.join(values))
      else:
        self('\n')
        with self.indent(4):
          self(',\n'.join(values))
    self(')')

  @contextlib.contextmanager
  def statement(self):
    yield
    self(';\n')

  @contextlib.contextmanager
  def block(self, start=' {\n', end='}\n'):
    self(start)
    with self.indent(2):
      yield
    self(end)

  @contextlib.contextmanager
  def indent(self, amount):
    self._indent += amount
    yield
    self._indent -= amount

  def to_string(self):
    return ''.join(self._sb)


def capitalize(value):
  return value[0].upper() + value[1:]


def escape_class_name(fully_qualified_class):
  """Returns an escaped string concatenating the Java package and class."""
  escaped = fully_qualified_class.replace('_', '_1')
  return escaped.replace('/', '_').replace('$', '_00024')


@contextlib.contextmanager
def atomic_output(path, mode='w+b'):
  with tempfile.NamedTemporaryFile(mode, delete=False) as f:
    try:
      yield f
    finally:
      f.close()

    if not (os.path.exists(path) and filecmp.cmp(f.name, path)):
      pathlib.Path(path).parents[0].mkdir(parents=True, exist_ok=True)
      shutil.move(f.name, path)
    if os.path.exists(f.name):
      os.unlink(f.name)


def add_to_zip_hermetic(zip_file, zip_path, data=None):
  zipinfo = zipfile.ZipInfo(filename=zip_path)
  zipinfo.external_attr = 0o644 << 16
  zipinfo.date_time = (2001, 1, 1, 0, 0, 0)
  zip_file.writestr(zipinfo, data, zipfile.ZIP_STORED)


def should_rename_package(package_name, filter_list_string):
  # If the filter list is empty, all packages should be renamed.
  if not filter_list_string:
    return True

  return any(
      package_name.startswith(pkg_prefix)
      for pkg_prefix in filter_list_string.split(':'))