Skip to main content

package_parser/pkgs/javascript/
manifest.rs

1use 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    /// The package name.
80    pub name: Option<String>,
81    /// The package version.
82    pub version: Option<String>,
83    /// The optional list of dependencies.
84    #[serde(default)]
85    pub dependencies: DepsSet,
86    /// The optional list of development dependencies.
87    #[serde(default)]
88    pub dev_dependencies: DepsSet,
89    /// The optional list of optional dependencies.
90    #[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            // FIXME: having no name may not be a problem See #1514
146            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}