1use crate::models::{DatasourceId, Dependency, PackageData, PackageType, Party, PartyType};
28use crate::parser_warn as warn;
29use crate::parsers::utils::{MAX_ITERATION_COUNT, 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 license_strings: Vec<String> = licenses
195 .iter()
196 .take(MAX_ITERATION_COUNT)
197 .filter_map(|v| v.as_str())
198 .map(|s| s.trim())
199 .filter(|s| !s.is_empty())
200 .map(String::from)
201 .collect();
202
203 if license_strings.is_empty() {
204 None
205 } else {
206 Some(truncate_field(license_strings.join(" AND ")))
207 }
208 }
209 _ => None,
210 })
211}
212
213fn normalize_bower_declared_license(
214 json: &Value,
215 extracted_license_statement: Option<&str>,
216) -> (
217 Option<String>,
218 Option<String>,
219 Vec<crate::models::LicenseDetection>,
220) {
221 match json.get(FIELD_LICENSE) {
222 Some(Value::Array(licenses)) => {
223 let normalized = licenses
224 .iter()
225 .take(MAX_ITERATION_COUNT)
226 .filter_map(|value| value.as_str().map(str::trim))
227 .filter(|value| !value.is_empty())
228 .map(normalize_declared_license_key)
229 .collect::<Option<Vec<_>>>();
230
231 if let Some(normalized) = normalized
232 && let Some(combined) = combine_normalized_licenses(normalized, " AND ")
233 {
234 return build_declared_license_data(
235 combined,
236 DeclaredLicenseMatchMetadata::single_line(
237 extracted_license_statement.unwrap_or_default(),
238 ),
239 );
240 }
241
242 empty_declared_license_data()
243 }
244 _ => normalize_spdx_declared_license(extracted_license_statement),
245 }
246}
247
248fn extract_keywords(json: &Value) -> Vec<String> {
250 json.get(FIELD_KEYWORDS)
251 .and_then(|v| v.as_array())
252 .map(|arr| {
253 arr.iter()
254 .take(MAX_ITERATION_COUNT)
255 .filter_map(|v| v.as_str())
256 .map(|s| truncate_field(s.to_string()))
257 .collect()
258 })
259 .unwrap_or_default()
260}
261
262fn extract_parties(json: &Value) -> Vec<Party> {
265 let mut parties = Vec::new();
266
267 if let Some(authors) = json.get(FIELD_AUTHORS).and_then(|v| v.as_array()) {
268 for author in authors.iter().take(MAX_ITERATION_COUNT) {
269 if let Some(party) = extract_party_from_author(author) {
270 parties.push(party);
271 }
272 }
273 }
274
275 parties
276}
277
278fn extract_party_from_author(author: &Value) -> Option<Party> {
280 match author {
281 Value::String(s) => {
282 let (name, email) = parse_author_string(s);
283 Some(Party {
284 r#type: Some(PartyType::Person),
285 role: Some("author".to_string()),
286 name: name.map(truncate_field),
287 email: email.map(truncate_field),
288 url: None,
289 organization: None,
290 organization_url: None,
291 timezone: None,
292 })
293 }
294 Value::Object(obj) => {
295 let name = obj
296 .get("name")
297 .and_then(|v| v.as_str())
298 .map(|s| truncate_field(s.to_string()));
299 let email = obj
300 .get("email")
301 .and_then(|v| v.as_str())
302 .map(|s| truncate_field(s.to_string()));
303 let url = obj
304 .get("homepage")
305 .and_then(|v| v.as_str())
306 .map(|s| truncate_field(s.to_string()));
307
308 Some(Party {
309 r#type: Some(PartyType::Person),
310 role: Some("author".to_string()),
311 name,
312 email,
313 url,
314 organization: None,
315 organization_url: None,
316 timezone: None,
317 })
318 }
319 _ => Some(Party {
320 r#type: Some(PartyType::Person),
321 role: Some("author".to_string()),
322 name: Some(truncate_field(format!("{:?}", author))),
323 email: None,
324 url: None,
325 organization: None,
326 organization_url: None,
327 timezone: None,
328 }),
329 }
330}
331
332fn parse_author_string(author_str: &str) -> (Option<String>, Option<String>) {
335 if let Some(email_start) = author_str.find('<')
336 && let Some(email_end) = author_str.find('>')
337 && email_start < email_end
338 {
339 let name = author_str[..email_start].trim();
340 let email = author_str[email_start + 1..email_end].trim();
341
342 let name = if name.is_empty() {
343 None
344 } else {
345 Some(truncate_field(name.to_string()))
346 };
347 let email = if email.is_empty() {
348 None
349 } else {
350 Some(truncate_field(email.to_string()))
351 };
352
353 return (name, email);
354 }
355
356 let trimmed = author_str.trim();
357 if trimmed.is_empty() {
358 (None, None)
359 } else {
360 (Some(truncate_field(trimmed.to_string())), None)
361 }
362}
363
364fn extract_vcs_url(json: &Value) -> Option<String> {
367 json.get(FIELD_REPOSITORY).and_then(|repo| {
368 if let Some(repo_obj) = repo.as_object() {
369 let repo_type = repo_obj.get("type").and_then(|v| v.as_str());
370 let repo_url = repo_obj.get("url").and_then(|v| v.as_str());
371
372 match (repo_type, repo_url) {
373 (Some(t), Some(u)) if !t.is_empty() && !u.is_empty() => {
374 Some(truncate_field(format!("{}+{}", t, u)))
375 }
376 _ => None,
377 }
378 } else {
379 None
380 }
381 })
382}
383
384fn extract_dependencies(
386 json: &Value,
387 field: &str,
388 scope: &str,
389 is_runtime: bool,
390) -> Vec<Dependency> {
391 json.get(field)
392 .and_then(|deps| deps.as_object())
393 .map_or_else(Vec::new, |deps| {
394 deps.iter()
395 .take(MAX_ITERATION_COUNT)
396 .filter_map(|(name, requirement)| {
397 let requirement_str = requirement.as_str()?;
398 let package_url =
399 PackageUrl::new(BowerJsonParser::PACKAGE_TYPE.as_str(), name).ok()?;
400
401 Some(Dependency {
402 purl: Some(truncate_field(package_url.to_string())),
403 extracted_requirement: Some(truncate_field(requirement_str.to_string())),
404 scope: Some(scope.to_string()),
405 is_runtime: Some(is_runtime),
406 is_optional: Some(!is_runtime),
407 is_pinned: None,
408 is_direct: Some(true),
409 resolved_package: None,
410 extra_data: None,
411 })
412 })
413 .collect()
414 })
415}
416
417fn default_package_data() -> PackageData {
418 PackageData {
419 package_type: Some(BowerJsonParser::PACKAGE_TYPE),
420 primary_language: Some("JavaScript".to_string()),
421 datasource_id: Some(DatasourceId::BowerJson),
422 ..Default::default()
423 }
424}