package_parser/pkgs/javascript/
manifest.rs1use maplit::hashset;
2use packageurl::PackageUrl;
3use crate::types::{Reachability, Relation};
4use serde::Deserialize;
5use std::collections::{HashMap, HashSet};
6use std::path::Path;
7
8use crate::error::SourcePkgError;
9use crate::helper::{build_walker, match_ftyp};
10use crate::pkgs::common::model::{Package, PackageManifest, RecognizeContext};
11use crate::DependentPackage;
12
13use super::npm_lock::NpmLock;
14use super::yarn_lock::YarnLock;
15use super::{pnpm, reachability};
16
17pub type DepsSet = HashMap<String, String>;
18
19fn walk_reachability(path: &Path) -> HashSet<String> {
20 let (walk, matcher) = build_walker(path, &["js", "ts"]);
21
22 let mut ret = HashSet::new();
23
24 for entry in walk {
25 let entry = if let Ok(entry) = entry {
26 entry
27 } else {
28 log::error!("Failed to walk entry: {:?}", entry);
29 continue;
30 };
31
32 let ftyp = if let Some(t) = match_ftyp(&entry, &matcher) {
33 t
34 } else {
35 continue;
36 };
37
38 let content = match std::fs::read_to_string(entry.path()) {
39 Ok(c) => c,
40 Err(e) => {
41 log::error!("Failed to read {}: {}", entry.path().display(), e);
42 continue;
43 }
44 };
45
46 let res = match ftyp.as_str() {
47 "js" => reachability::process_js(&content),
48 "ts" => {
49 if let Some(ext) = entry.path().extension() {
50 if ext.eq_ignore_ascii_case("tsx") {
51 reachability::process_tsx(&content)
52 } else {
53 reachability::process_ts(&content)
54 }
55 } else {
56 continue;
57 }
58 }
59 _ => continue,
60 };
61
62 let purls = match res {
63 Ok(purls) => purls,
64 Err(e) => {
65 log::error!("Failed to parse {}: {}", entry.path().display(), e);
66 continue;
67 }
68 };
69
70 ret.extend(purls);
71 }
72
73 ret
74}
75
76#[derive(Debug, Clone, Deserialize)]
77#[serde(rename_all = "camelCase")]
78struct NpmManifest {
79 pub name: Option<String>,
81 pub version: Option<String>,
83 #[serde(default)]
85 pub dependencies: DepsSet,
86 #[serde(default)]
88 pub dev_dependencies: DepsSet,
89 #[serde(default)]
91 pub optional_dependencies: DepsSet,
92}
93
94pub struct PackageJson {
95 lock_scanner: NpmLock,
96 yarn_scanner: YarnLock,
97}
98
99impl PackageJson {
100 pub fn new() -> Self {
101 Self {
102 lock_scanner: NpmLock::new(),
103 yarn_scanner: YarnLock::new(),
104 }
105 }
106
107 fn recognize_fallback(&self, path: &Path) -> Result<Package, SourcePkgError> {
108 let fs = std::fs::File::open(path).map_err(SourcePkgError::Io)?;
109
110 let package_data: NpmManifest =
111 serde_json::from_reader(fs).map_err(SourcePkgError::JsonParse)?;
112
113 let convert = |scope: &str, is_dev: bool, is_optional: bool| {
114 let scope = scope.to_string();
115
116 move |(name, version): (String, String)| DependentPackage {
117 purl: PackageUrl::new("npm", name)
118 .expect("purl arguments are invalid")
119 .to_string(),
120 scope: scope.clone(),
121 requirement: version,
122 is_runtime: !is_dev,
123 is_optional,
124 is_resolved: false,
125 parents: Default::default(),
126 relation: hashset! {Relation::Direct},
127 reachable: Default::default(),
128 }
129 };
130
131 let dependencies = package_data
132 .dependencies
133 .into_iter()
134 .map(convert("prod", false, false));
135 let dev_dependencies = package_data
136 .dev_dependencies
137 .into_iter()
138 .map(convert("dev", true, true));
139 let optional_dependencies = package_data
140 .optional_dependencies
141 .into_iter()
142 .map(convert("prod", false, true));
143
144 let pkg = Package {
145 name: package_data.name.unwrap_or_default(),
147 version: package_data.version.unwrap_or_default(),
148 dependencies: dependencies
149 .chain(dev_dependencies)
150 .chain(optional_dependencies)
151 .collect(),
152 ..Default::default()
153 };
154
155 Ok(pkg)
156 }
157
158 async fn recognize_internal(&self, path: &Path) -> Result<Package, SourcePkgError> {
159 let path_dir = path.parent().unwrap();
160
161 {
162 let lock_path = path_dir.join("yarn.lock");
163 if tokio::fs::metadata(&lock_path).await.is_ok() {
164 log::info!("Found yarn.lock file, using yarn scanner");
165 match self.yarn_scanner.recognize(&lock_path).await {
166 Ok(x) => return Ok(x),
167 Err(e) => {
168 log::warn!("Failed to recognize yarn.lock: {}", e);
169 }
170 }
171 }
172 }
173
174 {
175 let lock_path = path_dir.join("package-lock.json");
176 if tokio::fs::metadata(&lock_path).await.is_ok() {
177 log::info!("Found package-lock.json file, using npm scanner");
178 match self.lock_scanner.recognize(&lock_path).await {
179 Ok(x) => return Ok(x),
180 Err(e) => {
181 log::warn!("Failed to recognize package-lock.json: {}", e);
182 }
183 }
184 }
185 }
186
187 {
188 let lock_path = path_dir.join("pnpm-lock.yaml");
189 if tokio::fs::metadata(&lock_path).await.is_ok() {
190 log::info!("Found pnpm-lock.yaml file, using it to provide resolved versions");
191 match pnpm::parse(&lock_path) {
192 Ok(x) => return Ok(x),
193 Err(e) => {
194 log::warn!("Failed to recognize pnpm-lock.yaml: {}", e);
195 }
196 }
197 }
198 }
199
200 self.recognize_fallback(path)
201 }
202
203 async fn recognize_with_reachability(&self, path: &Path) -> Result<Package, SourcePkgError> {
204 let mut res = self.recognize_internal(path).await?;
205
206 if res.dependencies.is_empty() {
207 return Ok(res);
208 }
209
210 let parent = if let Some(parent) = path.parent() {
211 parent.to_path_buf()
212 } else {
213 return Ok(res);
214 };
215
216 let purls = tokio::task::spawn_blocking(move || walk_reachability(&parent))
217 .await
218 .unwrap();
219
220 log::debug!("Reachable purls: {:?}", purls);
221
222 for dep in res.dependencies.iter_mut() {
223 if dep.is_runtime && dep.relation.contains(&Relation::Direct) {
224 let purl_without_version = dep.purl.split('@').next().unwrap();
225
226 if !purls.contains(purl_without_version) {
227 dep.reachable = Reachability::No;
228 }
229 }
230 }
231
232 Ok(res)
233 }
234}
235
236#[async_trait::async_trait]
237impl PackageManifest for PackageJson {
238 fn get_name(&self) -> String {
239 "npm".into()
240 }
241
242 fn get_identifier(&self) -> String {
243 "npm-manifest".into()
244 }
245
246 async fn recognize_with_config(
247 &self,
248 path: &Path,
249 _context: &RecognizeContext,
250 ) -> Result<Package, SourcePkgError> {
251 self.recognize_with_reachability(path).await
252 }
253
254 async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
255 self.recognize_with_config(path, &RecognizeContext::default())
256 .await
257 }
258
259 fn file_name_patterns(&self) -> &'static [&'static str] {
260 &["package.json", "bower.json"]
261 }
262}