1use crate::models::{DatasourceId, Dependency, PackageData, PackageType, Party};
26use crate::parser_warn as warn;
27use crate::parsers::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
28use packageurl::PackageUrl;
29use serde_json::Value;
30use std::path::Path;
31
32use super::PackageParser;
33use super::license_normalization::{
34 DeclaredLicenseMatchMetadata, build_declared_license_data, combine_normalized_licenses,
35 empty_declared_license_data, normalize_declared_license_key, normalize_spdx_declared_license,
36};
37
38const FIELD_NAME: &str = "name";
39const FIELD_VERSION: &str = "version";
40const FIELD_DESCRIPTION: &str = "description";
41const FIELD_LICENSE: &str = "license";
42const FIELD_KEYWORDS: &str = "keywords";
43const FIELD_AUTHORS: &str = "authors";
44const FIELD_HOMEPAGE: &str = "homepage";
45const FIELD_REPOSITORY: &str = "repository";
46const FIELD_DEPENDENCIES: &str = "dependencies";
47const FIELD_DEV_DEPENDENCIES: &str = "devDependencies";
48const FIELD_PRIVATE: &str = "private";
49
50pub struct BowerJsonParser;
55
56impl PackageParser for BowerJsonParser {
57 const PACKAGE_TYPE: PackageType = PackageType::Bower;
58
59 fn extract_packages(path: &Path) -> Vec<PackageData> {
60 let json = match read_and_parse_json(path) {
61 Ok(json) => json,
62 Err(e) => {
63 warn!("Failed to read or parse bower.json at {:?}: {}", path, e);
64 return vec![default_package_data()];
65 }
66 };
67
68 let name = json
69 .get(FIELD_NAME)
70 .and_then(|v| v.as_str())
71 .map(|s| truncate_field(s.to_string()));
72
73 let is_private = if name.is_none() {
75 true
76 } else {
77 json.get(FIELD_PRIVATE)
78 .and_then(|v| v.as_bool())
79 .unwrap_or(false)
80 };
81
82 let version = json
83 .get(FIELD_VERSION)
84 .and_then(|v| v.as_str())
85 .map(|s| truncate_field(s.to_string()));
86
87 let description = json
88 .get(FIELD_DESCRIPTION)
89 .and_then(|v| v.as_str())
90 .map(|s| truncate_field(s.to_string()));
91
92 let extracted_license_statement = extract_license_statement(&json);
93 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
94 normalize_bower_declared_license(&json, extracted_license_statement.as_deref());
95 let declared_license_expression = declared_license_expression.map(truncate_field);
96 let declared_license_expression_spdx = declared_license_expression_spdx.map(truncate_field);
97 let keywords = extract_keywords(&json);
98 let parties = extract_parties(&json);
99 let homepage_url = json
100 .get(FIELD_HOMEPAGE)
101 .and_then(|v| v.as_str())
102 .map(|s| truncate_field(s.to_string()));
103
104 let vcs_url = extract_vcs_url(&json);
105 let dependencies = extract_dependencies(&json, FIELD_DEPENDENCIES, "dependencies", true);
106 let dev_dependencies =
107 extract_dependencies(&json, FIELD_DEV_DEPENDENCIES, "devDependencies", false);
108
109 vec![PackageData {
110 package_type: Some(Self::PACKAGE_TYPE),
111 namespace: None,
112 name,
113 version,
114 qualifiers: None,
115 subpath: None,
116 primary_language: Some("JavaScript".to_string()),
117 description,
118 release_date: None,
119 parties,
120 keywords,
121 homepage_url,
122 download_url: None,
123 size: None,
124 sha1: None,
125 md5: None,
126 sha256: None,
127 sha512: None,
128 bug_tracking_url: None,
129 code_view_url: None,
130 vcs_url,
131 copyright: None,
132 holder: None,
133 declared_license_expression,
134 declared_license_expression_spdx,
135 license_detections,
136 other_license_expression: None,
137 other_license_expression_spdx: None,
138 other_license_detections: Vec::new(),
139 extracted_license_statement,
140 notice_text: None,
141 source_packages: Vec::new(),
142 file_references: Vec::new(),
143 is_private,
144 is_virtual: false,
145 extra_data: None,
146 dependencies: [dependencies, dev_dependencies].concat(),
147 repository_homepage_url: None,
148 repository_download_url: None,
149 api_data_url: None,
150 datasource_id: Some(DatasourceId::BowerJson),
151 purl: None,
152 }]
153 }
154
155 fn is_match(path: &Path) -> bool {
156 path.file_name()
157 .is_some_and(|name| name == "bower.json" || name == ".bower.json")
158 }
159}
160
161fn read_and_parse_json(path: &Path) -> Result<Value, String> {
163 let content =
164 read_file_to_string(path, None).map_err(|e| format!("Failed to read file: {}", e))?;
165 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))
166}
167
168fn extract_license_statement(json: &Value) -> Option<String> {
171 json.get(FIELD_LICENSE)
172 .and_then(|license_value| match license_value {
173 Value::String(s) => {
174 let trimmed = s.trim();
175 if trimmed.is_empty() {
176 None
177 } else {
178 Some(truncate_field(trimmed.to_string()))
179 }
180 }
181 Value::Array(licenses) => {
182 let license_strings: Vec<String> = licenses
183 .iter()
184 .take(MAX_ITERATION_COUNT)
185 .filter_map(|v| v.as_str())
186 .map(|s| s.trim())
187 .filter(|s| !s.is_empty())
188 .map(String::from)
189 .collect();
190
191 if license_strings.is_empty() {
192 None
193 } else {
194 Some(truncate_field(license_strings.join(" AND ")))
195 }
196 }
197 _ => None,
198 })
199}
200
201fn normalize_bower_declared_license(
202 json: &Value,
203 extracted_license_statement: Option<&str>,
204) -> (
205 Option<String>,
206 Option<String>,
207 Vec<crate::models::LicenseDetection>,
208) {
209 match json.get(FIELD_LICENSE) {
210 Some(Value::Array(licenses)) => {
211 let normalized = licenses
212 .iter()
213 .take(MAX_ITERATION_COUNT)
214 .filter_map(|value| value.as_str().map(str::trim))
215 .filter(|value| !value.is_empty())
216 .map(normalize_declared_license_key)
217 .collect::<Option<Vec<_>>>();
218
219 if let Some(normalized) = normalized
220 && let Some(combined) = combine_normalized_licenses(normalized, " AND ")
221 {
222 return build_declared_license_data(
223 combined,
224 DeclaredLicenseMatchMetadata::single_line(
225 extracted_license_statement.unwrap_or_default(),
226 ),
227 );
228 }
229
230 empty_declared_license_data()
231 }
232 _ => normalize_spdx_declared_license(extracted_license_statement),
233 }
234}
235
236fn extract_keywords(json: &Value) -> Vec<String> {
238 json.get(FIELD_KEYWORDS)
239 .and_then(|v| v.as_array())
240 .map(|arr| {
241 arr.iter()
242 .take(MAX_ITERATION_COUNT)
243 .filter_map(|v| v.as_str())
244 .map(|s| truncate_field(s.to_string()))
245 .collect()
246 })
247 .unwrap_or_default()
248}
249
250fn extract_parties(json: &Value) -> Vec<Party> {
253 let mut parties = Vec::new();
254
255 if let Some(authors) = json.get(FIELD_AUTHORS).and_then(|v| v.as_array()) {
256 for author in authors.iter().take(MAX_ITERATION_COUNT) {
257 if let Some(party) = extract_party_from_author(author) {
258 parties.push(party);
259 }
260 }
261 }
262
263 parties
264}
265
266fn extract_party_from_author(author: &Value) -> Option<Party> {
268 match author {
269 Value::String(s) => {
270 let (name, email) = parse_author_string(s);
271 Some(Party {
272 r#type: Some("person".to_string()),
273 role: Some("author".to_string()),
274 name: name.map(truncate_field),
275 email: email.map(truncate_field),
276 url: None,
277 organization: None,
278 organization_url: None,
279 timezone: None,
280 })
281 }
282 Value::Object(obj) => {
283 let name = obj
284 .get("name")
285 .and_then(|v| v.as_str())
286 .map(|s| truncate_field(s.to_string()));
287 let email = obj
288 .get("email")
289 .and_then(|v| v.as_str())
290 .map(|s| truncate_field(s.to_string()));
291 let url = obj
292 .get("homepage")
293 .and_then(|v| v.as_str())
294 .map(|s| truncate_field(s.to_string()));
295
296 Some(Party {
297 r#type: Some("person".to_string()),
298 role: Some("author".to_string()),
299 name,
300 email,
301 url,
302 organization: None,
303 organization_url: None,
304 timezone: None,
305 })
306 }
307 _ => Some(Party {
308 r#type: Some("person".to_string()),
309 role: Some("author".to_string()),
310 name: Some(truncate_field(format!("{:?}", author))),
311 email: None,
312 url: None,
313 organization: None,
314 organization_url: None,
315 timezone: None,
316 }),
317 }
318}
319
320fn parse_author_string(author_str: &str) -> (Option<String>, Option<String>) {
323 if let Some(email_start) = author_str.find('<')
324 && let Some(email_end) = author_str.find('>')
325 && email_start < email_end
326 {
327 let name = author_str[..email_start].trim();
328 let email = author_str[email_start + 1..email_end].trim();
329
330 let name = if name.is_empty() {
331 None
332 } else {
333 Some(truncate_field(name.to_string()))
334 };
335 let email = if email.is_empty() {
336 None
337 } else {
338 Some(truncate_field(email.to_string()))
339 };
340
341 return (name, email);
342 }
343
344 let trimmed = author_str.trim();
345 if trimmed.is_empty() {
346 (None, None)
347 } else {
348 (Some(truncate_field(trimmed.to_string())), None)
349 }
350}
351
352fn extract_vcs_url(json: &Value) -> Option<String> {
355 json.get(FIELD_REPOSITORY).and_then(|repo| {
356 if let Some(repo_obj) = repo.as_object() {
357 let repo_type = repo_obj.get("type").and_then(|v| v.as_str());
358 let repo_url = repo_obj.get("url").and_then(|v| v.as_str());
359
360 match (repo_type, repo_url) {
361 (Some(t), Some(u)) if !t.is_empty() && !u.is_empty() => {
362 Some(truncate_field(format!("{}+{}", t, u)))
363 }
364 _ => None,
365 }
366 } else {
367 None
368 }
369 })
370}
371
372fn extract_dependencies(
374 json: &Value,
375 field: &str,
376 scope: &str,
377 is_runtime: bool,
378) -> Vec<Dependency> {
379 json.get(field)
380 .and_then(|deps| deps.as_object())
381 .map_or_else(Vec::new, |deps| {
382 deps.iter()
383 .take(MAX_ITERATION_COUNT)
384 .filter_map(|(name, requirement)| {
385 let requirement_str = requirement.as_str()?;
386 let package_url =
387 PackageUrl::new(BowerJsonParser::PACKAGE_TYPE.as_str(), name).ok()?;
388
389 Some(Dependency {
390 purl: Some(truncate_field(package_url.to_string())),
391 extracted_requirement: Some(truncate_field(requirement_str.to_string())),
392 scope: Some(scope.to_string()),
393 is_runtime: Some(is_runtime),
394 is_optional: Some(!is_runtime),
395 is_pinned: None,
396 is_direct: Some(true),
397 resolved_package: None,
398 extra_data: None,
399 })
400 })
401 .collect()
402 })
403}
404
405fn default_package_data() -> PackageData {
406 PackageData {
407 package_type: Some(BowerJsonParser::PACKAGE_TYPE),
408 primary_language: Some("JavaScript".to_string()),
409 datasource_id: Some(DatasourceId::BowerJson),
410 ..Default::default()
411 }
412}
413
414crate::register_parser!(
415 "Bower package manifest",
416 &["**/bower.json", "**/.bower.json"],
417 "bower",
418 "JavaScript",
419 Some("https://bower.io"),
420);