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