1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3 4# 5# Copyright (c) 2025 Northeastern University 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18 19import xml.etree.ElementTree as ET 20from typing import Optional, Union 21 22from ohos.sbom.common.utils import generate_purl, get_purl_type_from_url 23from ohos.sbom.data.target import Target 24 25 26class Remote: 27 def __init__(self, name, fetch, review=""): 28 self._name = name 29 self._fetch = fetch 30 self._review = review 31 32 @property 33 def name(self): 34 return self._name 35 36 @property 37 def fetch(self): 38 return self._fetch 39 40 @property 41 def review(self): 42 return self._review 43 44 @staticmethod 45 def from_element(element): 46 name = element.attrib.get("name", "") 47 fetch = element.attrib.get("fetch", "") 48 review = element.attrib.get("review", "") 49 return Remote(name, fetch, review) 50 51 52class Project: 53 def __init__(self, name, path, revision, upstream, dest_branch, groups, remote): 54 self._name = name 55 self._path = path 56 self._revision = revision 57 self._upstream = upstream 58 self._dest_branch = dest_branch 59 self._groups = groups 60 self._remote = remote 61 self._linkfiles = [] 62 63 @property 64 def name(self): 65 return self._name 66 67 @property 68 def path(self): 69 return self._path 70 71 @property 72 def revision(self): 73 return self._revision 74 75 @property 76 def upstream(self): 77 return self._upstream 78 79 @property 80 def dest_branch(self): 81 return self._dest_branch 82 83 @property 84 def groups(self): 85 return self._groups 86 87 @property 88 def remote(self): 89 return self._remote 90 91 @property 92 def linkfiles(self): 93 return self._linkfiles 94 95 @property 96 def type(self): 97 if "application" in self._path: 98 return "application" 99 elif "framework" in self._path: 100 return "framework" 101 return "library" 102 103 @staticmethod 104 def from_element(element): 105 name = element.attrib.get("name", "") 106 path = element.attrib.get("path", "") 107 revision = element.attrib.get("revision", "") 108 upstream = element.attrib.get("upstream", "") 109 dest_branch = element.attrib.get("dest-branch", "") 110 groups = element.attrib.get("groups", "").split(",") 111 remote = element.attrib.get("remote", "") 112 project = Project(name, path, revision, upstream, dest_branch, groups, remote) 113 114 for linkfile_element in element.findall("linkfile"): 115 src = linkfile_element.attrib.get("src", "") 116 dest = linkfile_element.attrib.get("dest", "") 117 if src and dest: 118 project.add_linkfile(src, dest) 119 120 return project 121 122 def add_linkfile(self, src, dest): 123 self._linkfiles.append({"src": src, "dest": dest}) 124 125 126class Manifest: 127 128 def __init__(self): 129 self._remotes = [] 130 self._default = None 131 self._projects = [] 132 133 @property 134 def remotes(self): 135 return self._remotes 136 137 @property 138 def default(self): 139 return self._default 140 141 @property 142 def projects(self): 143 return self._projects 144 145 @staticmethod 146 def from_file(file_path): 147 tree = ET.parse(file_path) 148 root = tree.getroot() 149 150 manifest = Manifest() 151 152 # Parse remote elements 153 for remote_element in root.findall("remote"): 154 remote = Remote.from_element(remote_element) 155 manifest.add_remote(remote) 156 157 # Parse default element 158 default_element = root.find("default") 159 if default_element is not None: 160 remote = default_element.attrib["remote"] 161 revision = default_element.attrib["revision"] 162 sync_j = default_element.attrib["sync-j"] 163 manifest.set_default(remote, revision, sync_j) 164 165 # Parse project elements 166 for project_element in root.findall("project"): 167 project = Project.from_element(project_element) 168 manifest.add_project(project) 169 170 return manifest 171 172 def find_project(self, src: Optional[Union[str, Target]]) -> Optional[Project]: 173 if isinstance(src, str): 174 target_name = src 175 else: 176 target_name = src.target_name 177 if not target_name.startswith("//"): 178 return None 179 path_part = target_name[2:].split(":")[0] 180 matched_projects = [] 181 for project in self._projects: 182 project_path = project.path 183 if path_part.startswith(project_path): 184 matched_projects.append((project, len(project_path))) 185 186 if matched_projects: 187 matched_projects.sort(key=lambda x: -x[1]) 188 return matched_projects[0][0] 189 else: 190 return None 191 192 def add_remote(self, remote): 193 self._remotes.append(remote) 194 195 def set_default(self, remote, revision, sync_j): 196 self._default = {"remote": remote, "revision": revision, "sync-j": sync_j} 197 198 def add_project(self, project): 199 self._projects.append(project) 200 201 def remote_url_of(self, project, target_remote: Remote = None): 202 if not project or not hasattr(project, 'name'): 203 return "" 204 if target_remote is None: 205 target_remote = self.remote_of(project) 206 if not target_remote: 207 return "" 208 fetch = target_remote.fetch.strip() 209 project_name = project.name.strip() 210 211 if (not fetch or fetch == '.') and hasattr(target_remote, 'review'): 212 fetch = target_remote.review.strip() 213 214 if fetch: 215 fetch = fetch.rstrip('/') + '/' if fetch else '' 216 url = f"{fetch}{project_name}" 217 else: 218 url = project_name 219 220 url = url.replace('//', '/').replace(':/', '://') 221 222 return url 223 224 def remote_of(self, project) -> Optional[Remote]: 225 if project.remote != "": 226 remote_name = project.remote 227 elif self._default is not None: 228 remote_name = self._default["remote"] 229 else: 230 return None 231 target_remote = next( 232 (remote for remote in self._remotes if remote.name == remote_name), 233 None 234 ) 235 return target_remote 236 237 def purl_of(self, project, target_remote: Remote = None) -> Optional[str]: 238 if target_remote is None: 239 target_remote = self.remote_of(project) 240 if not target_remote or not target_remote.fetch: 241 raise ValueError(f"No fetch URL found for project: {project.name}") 242 243 url = self.remote_url_of(project, target_remote) 244 return generate_purl( 245 pkg_type=get_purl_type_from_url(url), 246 namespace="OpenHarmony", 247 name=project.name, 248 version=project.revision, 249 ) 250