import xml.etree.ElementTree as ET
from typing import Optional, Union
from ohos.sbom.common.utils import generate_purl, get_purl_type_from_url
from ohos.sbom.data.target import Target
class Remote:
def __init__(self, name, fetch, review=""):
self._name = name
self._fetch = fetch
self._review = review
@property
def name(self):
return self._name
@property
def fetch(self):
return self._fetch
@property
def review(self):
return self._review
@staticmethod
def from_element(element):
name = element.attrib.get("name", "")
fetch = element.attrib.get("fetch", "")
review = element.attrib.get("review", "")
return Remote(name, fetch, review)
class Project:
def __init__(self, name, path, revision, upstream, dest_branch, groups, remote):
self._name = name
self._path = path
self._revision = revision
self._upstream = upstream
self._dest_branch = dest_branch
self._groups = groups
self._remote = remote
self._linkfiles = []
@property
def name(self):
return self._name
@property
def path(self):
return self._path
@property
def revision(self):
return self._revision
@property
def upstream(self):
return self._upstream
@property
def dest_branch(self):
return self._dest_branch
@property
def groups(self):
return self._groups
@property
def remote(self):
return self._remote
@property
def linkfiles(self):
return self._linkfiles
@property
def type(self):
if "application" in self._path:
return "application"
elif "framework" in self._path:
return "framework"
return "library"
@staticmethod
def from_element(element):
name = element.attrib.get("name", "")
path = element.attrib.get("path", "")
revision = element.attrib.get("revision", "")
upstream = element.attrib.get("upstream", "")
dest_branch = element.attrib.get("dest-branch", "")
groups = element.attrib.get("groups", "").split(",")
remote = element.attrib.get("remote", "")
project = Project(name, path, revision, upstream, dest_branch, groups, remote)
for linkfile_element in element.findall("linkfile"):
src = linkfile_element.attrib.get("src", "")
dest = linkfile_element.attrib.get("dest", "")
if src and dest:
project.add_linkfile(src, dest)
return project
def add_linkfile(self, src, dest):
self._linkfiles.append({"src": src, "dest": dest})
class Manifest:
def __init__(self):
self._remotes = []
self._default = None
self._projects = []
@property
def remotes(self):
return self._remotes
@property
def default(self):
return self._default
@property
def projects(self):
return self._projects
@staticmethod
def from_file(file_path):
tree = ET.parse(file_path)
root = tree.getroot()
manifest = Manifest()
for remote_element in root.findall("remote"):
remote = Remote.from_element(remote_element)
manifest.add_remote(remote)
default_element = root.find("default")
if default_element is not None:
remote = default_element.attrib["remote"]
revision = default_element.attrib["revision"]
sync_j = default_element.attrib["sync-j"]
manifest.set_default(remote, revision, sync_j)
for project_element in root.findall("project"):
project = Project.from_element(project_element)
manifest.add_project(project)
return manifest
def find_project(self, src: Optional[Union[str, Target]]) -> Optional[Project]:
if isinstance(src, str):
target_name = src
else:
target_name = src.target_name
if not target_name.startswith("//"):
return None
path_part = target_name[2:].split(":")[0]
matched_projects = []
for project in self._projects:
project_path = project.path
if path_part.startswith(project_path):
matched_projects.append((project, len(project_path)))
if matched_projects:
matched_projects.sort(key=lambda x: -x[1])
return matched_projects[0][0]
else:
return None
def add_remote(self, remote):
self._remotes.append(remote)
def set_default(self, remote, revision, sync_j):
self._default = {"remote": remote, "revision": revision, "sync-j": sync_j}
def add_project(self, project):
self._projects.append(project)
def remote_url_of(self, project, target_remote: Remote = None):
if not project or not hasattr(project, 'name'):
return ""
if target_remote is None:
target_remote = self.remote_of(project)
if not target_remote:
return ""
fetch = target_remote.fetch.strip()
project_name = project.name.strip()
if (not fetch or fetch == '.') and hasattr(target_remote, 'review'):
fetch = target_remote.review.strip()
if fetch:
fetch = fetch.rstrip('/') + '/' if fetch else ''
url = f"{fetch}{project_name}"
else:
url = project_name
url = url.replace('//', '/').replace(':/', '://')
return url
def remote_of(self, project) -> Optional[Remote]:
if project.remote != "":
remote_name = project.remote
elif self._default is not None:
remote_name = self._default["remote"]
else:
return None
target_remote = next(
(remote for remote in self._remotes if remote.name == remote_name),
None
)
return target_remote
def purl_of(self, project, target_remote: Remote = None) -> Optional[str]:
if target_remote is None:
target_remote = self.remote_of(project)
if not target_remote or not target_remote.fetch:
raise ValueError(f"No fetch URL found for project: {project.name}")
url = self.remote_url_of(project, target_remote)
return generate_purl(
pkg_type=get_purl_type_from_url(url),
namespace="OpenHarmony",
name=project.name,
version=project.revision,
)