• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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