Move bulletin and patches to dedicated classes
- Rename BulletinZipFile to SecurityBulletin and move all high-level
logic for finding and applying patches into it.
- Move SecurityPatch and subclasses, and SecurityBulletin to new
bulletin.py file.
Issue: INFRA-97
Change-Id: I2eb679b3034259ee412a02e0a9458427293a80ee
diff --git a/bullseye/bulletin.py b/bullseye/bulletin.py
new file mode 100644
index 0000000..a9f3d13
--- /dev/null
+++ b/bullseye/bulletin.py
@@ -0,0 +1,326 @@
+import os
+import re
+import zipfile
+
+from manifest import ProjectNotFoundError
+from utility import exec_command as utility_exec_command
+
+
+class SecurityPatch(object):
+ """Abstract representation of a Security Patch from a Security Bulletin."""
+
+ def __init__(self, zip_file, filename, android_ids, manifest, args):
+ self.args = args
+ self.android_ids = android_ids
+ self.aosp_project_path = self._get_aosp_project_path(filename)
+
+ if self.aosp_project_path in manifest.projects:
+ self.project_rel_path = manifest.projects[self.aosp_project_path]
+ self.project_abs_path = os.path.join(
+ self.args.aosp_root, self.project_rel_path
+ )
+ else:
+ raise ProjectNotFoundError(self.aosp_project_path)
+
+ self.filename = filename
+ self.zip_file = zip_file
+ self.change_id = self.find_last_change_id()
+
+ def __repr__(self):
+ return self.filename
+
+ def _get_aosp_project_path(self, filename):
+ """Get project path as expected by the AndroidManifest class.
+
+ The returned path matches or is close to the naming conventions for the
+ `path` attribute in AOSP manifest XMLs. The AndroidManifest class knows
+ how to map this path to actual file system paths.
+
+ """
+ dirs_to_skip = (
+ 2 if self.args.use_old_format else self.args.ignore_n_leading
+ )
+ return "/".join(filename.split("/")[dirs_to_skip:-1])
+
+ def find_last_change_id(self):
+ """Find the Change-Id mentioned last in the patch.
+
+ Returns:
+ The Change-Id identifying the patch, or `None` if none was found.
+
+ """
+ change_id = None
+ with self.zip_file.open(self.filename) as patchfile:
+ for raw_line in patchfile:
+ # TODO: Currently we actually expect a patch file here, but also
+ # get passed in zipped kernel patch files, which of course can't
+ # be parsed directly. This causes the decoding from bytes to
+ # string to fail, which we ignore for now.
+ line = raw_line.decode("utf-8", errors="ignore")
+ if line.startswith("Change-Id"):
+ change_id = line.split()[1]
+ if line.startswith("---"):
+ break
+ return change_id
+
+ def find_issues_for_patch(self):
+ return self.android_ids
+
+ def get_description(self):
+ if self.args.display_all:
+ return "{} {} {}".format(
+ str(self.find_issues_for_patch()),
+ self.aosp_project_path,
+ os.path.basename(self.filename),
+ )
+ elif self.args.use_a_number:
+ return self.find_issues_for_patch()
+ else:
+ return (
+ self.aosp_project_path + " " + os.path.basename(self.filename)
+ )
+
+ def check_and_apply(self):
+ if not self.change_id:
+ print(
+ "[BE]: Unable to verify patch, could not determine its "
+ "Change-Id: {}.".format(self.get_description())
+ )
+ return
+
+ if not self.is_applied():
+ print("[BE]: Not yet applied: {}".format(self.get_description()))
+ if self.args.apply_to_branch is not None:
+ self.apply()
+ else:
+ print("[BE]: Already applied: {}".format(self.get_description()))
+
+ def is_applied(self):
+ assert self.change_id
+ found = (
+ self.exec_command(
+ "( cd {path} && git log --oneline --grep={changeid} | "
+ " grep [a-Z] >/dev/null )".format(
+ path=self.project_abs_path, changeid=self.change_id
+ )
+ )
+ == 0
+ )
+ return found
+
+ def rollback(self):
+ print("[BE]: Rolling back... ".format(self.filename))
+ self.exec_command(
+ "( cd {path} && git am --abort && git reset --hard )".format(
+ path=self.project_abs_path
+ )
+ )
+ self.exec_command(
+ "( cd {path} && git reset --hard )".format(
+ path=self.project_abs_path
+ )
+ )
+ self.exec_command(
+ "( cd {path} && repo sync .)".format(path=self.project_abs_path)
+ )
+
+ def amend(self):
+ print(
+ "[BE]: This patch addresses {} ".format(
+ self.find_issues_for_patch()
+ )
+ )
+ try:
+ input("Press enter to continue and change the commit message.")
+ except (KeyboardInterrupt, EOFError):
+ pass
+
+ os.system(
+ "( cd {path} && git commit --amend )".format(
+ path=self.project_abs_path,
+ tmpbranchname=self.args.apply_to_branch,
+ )
+ )
+
+ def apply(self):
+ self.zip_file.extract(self.filename, "/tmp/")
+ print("[BE]: Applying {}.".format(self.filename.split("/")[-1]))
+ print("[BE]: for Project {}.".format(self.aosp_project_path))
+ applies = (
+ self.exec_command(
+ "( cd {path} && repo start {tmpbranchname} . ;"
+ " git am -3 {filename} )".format(
+ path=self.project_abs_path,
+ tmpbranchname=self.args.apply_to_branch,
+ filename="/tmp/" + self.filename,
+ )
+ )
+ == 0
+ )
+ if not applies:
+ print("[BE]: FAIL: doesn't apply.".format(self.filename))
+ self.rollback()
+ else:
+ print("[BE]: SUCCESS: applied.".format(self.filename))
+ if not self.args.skip_amend and not self.args.dry_run:
+ self.amend()
+
+ def exec_command(self, command):
+ return utility_exec_command(command, self.args.quiet)
+
+
+class PlatformPatch(SecurityPatch):
+ pass
+
+
+class KernelPatch(SecurityPatch):
+ def __init__(
+ self,
+ zip_file,
+ filename,
+ android_id,
+ manifest,
+ args,
+ subpatch,
+ kernel,
+ extra,
+ ):
+ super().__init__(zip_file, filename, [android_id], manifest, args)
+
+ self.subpatch = subpatch
+ self.kernel = kernel
+ self.extra = extra
+
+ def _get_aosp_project_path(self, filename):
+ return "kernel/msm"
+
+ def get_description(self):
+ return "(snippet) " + str(super().get_description())
+
+
+class SecurityBulletin:
+ """A collection of patches for the Android platform and kernel.
+
+ The Android Security Bulletins regroup security patches needed for an
+ Android operating system in order to comply with a so-called *Security Patch
+ Level*.
+
+ """
+
+ # Handle special kernel snippet names before the machine-readable metadata
+ # introduced in August 2017. Before that:
+ # - ASB of 2017-05 introduces e.g.:
+ # - ANDROID-32402303
+ # - ANDROID-36000515_1
+ # - ASB of 2017-06 introduces e.g.:
+ # - ANDROID-35472278_dsx
+ # - ASB of 2017-07 introduces e.g.:
+ # - ANDROID-34620535_kernel3.10
+ KERNEL_SNIPPETS_FILENAME_PATTERN = re.compile(
+ r"code_snippets/ANDROID-(?P<android_id>[0-9]+)"
+ r"(_(?P<subpatch>[0-9]+)|_(kernel)?(?P<kernel>[0-9]\.[0-9]+)|"
+ r"(?P<extra>.*))?$"
+ )
+ #:
+ _ISSUE_MAPPING_FILENAME = re.compile(
+ r"patches/issue-mapping/ANDROID-(?P<android_id>\d+)"
+ )
+
+ def __init__(self, args, manifest):
+ self._args = args
+ #: The archive containing the bulletin patches and metadata.
+ self._archive = zipfile.ZipFile(self._args.zipfile)
+ #: The mapping of patch files to Android bug ids.
+ self._issue_mapping = self._get_issue_mapping()
+ self._manifest = manifest
+
+ def _get_issue_mapping(self):
+ """Get the mapping of patch files to Android bug ids.
+
+ Returns:
+ The mapping of patch files to a list of Android bug id.
+
+ """
+ mapping = {}
+
+ for filename in self._archive.namelist():
+ match = self._ISSUE_MAPPING_FILENAME.match(filename)
+ if not match:
+ continue
+ android_id = "A-" + match.group("android_id")
+
+ with self._archive.open(filename) as issue_file:
+ for raw_line in issue_file:
+ line = raw_line.decode("utf-8").rstrip()
+ if line not in mapping:
+ mapping[line] = []
+ mapping[line].append(android_id)
+
+ return mapping
+
+ def _get_patches(self, android_version, platform, kernel):
+ """Find the patches grouped in this bulletin.
+
+ Arguments:
+ android_version: The version of the Android operating system to
+ find patches for.
+ platform (optional): Whether to find platform security patches.
+ kernel (optional): Whether to find kernel snippets.
+ Returns:
+ The list of patches found.
+
+ """
+ patches = []
+
+ for item in self._archive.namelist():
+ snippet_match = (
+ self.KERNEL_SNIPPETS_FILENAME_PATTERN.search(item)
+ if kernel
+ else False
+ )
+ if snippet_match:
+ patches.append(
+ KernelPatch(
+ self._archive,
+ item,
+ "A-" + snippet_match.group("android_id"),
+ self._manifest,
+ self._args,
+ snippet_match.group("subpatch"),
+ snippet_match.group("kernel"),
+ snippet_match.group("extra"),
+ )
+ )
+ elif (
+ platform
+ and "android-{}".format(android_version) in item
+ and ".patch" in item
+ ):
+ issues = self._issue_mapping.get(item, None)
+ # TODO: `issues` will always be None, also before the latest
+ # refactoring.
+ patches.append(
+ PlatformPatch(
+ self._archive, item, issues, self._manifest, self._args
+ )
+ )
+
+ return patches
+
+ def apply(self):
+ """Apply all not yet applied patches in this bulletin.
+
+ This high-level function groups all functionality of this
+ SecurityBulletin class together: It finds all patches included in the
+ bulletin archive, checks if they are applied and applies them if
+ necessary.
+
+ """
+ patches = self._get_patches(
+ self._args.version,
+ platform=not self._args.skip_patches,
+ kernel=not self._args.skip_snippets,
+ )
+
+ for patch in sorted(patches, key=lambda p: p.filename):
+ patch.check_and_apply()