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