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