Skip to main content

package_parser/pkgs/
composer.rs

1use packageurl::PackageUrl;
2use crate::types::Relation;
3use serde::Deserialize;
4
5use crate::error::SourcePkgError;
6use crate::pkgs::common::model::{DependentPackage, Package, PackageManifest};
7
8use std::collections::HashMap;
9use std::fs::File;
10use std::path::Path;
11
12#[derive(Debug, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14struct ComposerJson {
15    #[serde(default)]
16    require: HashMap<String, String>,
17    #[serde(default)]
18    require_dev: HashMap<String, String>,
19}
20
21#[derive(Debug, Deserialize)]
22struct ComposerLockJson {
23    #[serde(default)]
24    packages: Vec<ComposerLockPackage>,
25}
26
27#[derive(Debug, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29struct ComposerLockPackage {
30    name: String,
31    version: String,
32    #[serde(default)]
33    require: HashMap<String, String>,
34}
35
36fn make_dep_unresolved(name: &str, constraint: &str, is_runtime: bool) -> DependentPackage {
37    let (namespace, name) = if let Some((n, ns)) = name.split_once('/') {
38        (Some(ns), n)
39    } else {
40        (None, name)
41    };
42
43    let mut purl = PackageUrl::new("composer", name).unwrap();
44    if let Some(namespace) = namespace {
45        purl.with_namespace(namespace);
46    }
47
48    DependentPackage {
49        purl: purl.to_string(),
50        requirement: constraint.to_string(),
51        scope: (if is_runtime { "runtime" } else { "dev" }).into(),
52        is_runtime,
53        is_resolved: false,
54        relation: maplit::hashset! {Relation::Direct},
55        ..Default::default()
56    }
57}
58
59fn make_purl_versioned(name: &str, version: &str) -> String {
60    let (namespace, name) = if let Some((ns, n)) = name.split_once('/') {
61        (Some(ns), n)
62    } else {
63        (None, name)
64    };
65
66    let mut purl = PackageUrl::new("composer", name).unwrap();
67    purl.with_version(version);
68    if let Some(namespace) = namespace {
69        purl.with_namespace(namespace);
70    }
71
72    purl.to_string()
73}
74
75pub struct PhpComposer {}
76
77impl PhpComposer {
78    pub fn new() -> Self {
79        Self {}
80    }
81
82    fn parse(path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
83        let path = path.as_ref();
84
85        let manifest: ComposerJson = {
86            let mut file = File::open(path)?;
87            serde_json::from_reader(&mut file)?
88        };
89
90        let lock_path = path.with_file_name("composer.lock");
91        if lock_path.exists() {
92            match process_composer_lock(&manifest, lock_path) {
93                Ok(package) => return Ok(package),
94                Err(err) => {
95                    log::error!("Failed to process composer.lock: {}", err);
96                }
97            }
98        }
99
100        let mut dependencies = vec![];
101
102        for dep in manifest.require.into_iter() {
103            dependencies.push(make_dep_unresolved(&dep.0, &dep.1, true));
104        }
105
106        for dep in manifest.require_dev.into_iter() {
107            dependencies.push(make_dep_unresolved(&dep.0, &dep.1, false));
108        }
109
110        let package = Package {
111            dependencies,
112            ..Default::default()
113        };
114
115        Ok(package)
116    }
117}
118
119#[async_trait::async_trait]
120impl PackageManifest for PhpComposer {
121    fn get_name(&self) -> String {
122        "composer".into()
123    }
124
125    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
126        Self::parse(path)
127    }
128
129    fn file_name_patterns(&self) -> &'static [&'static str] {
130        &["composer.json"]
131    }
132}
133
134fn process_composer_lock(
135    manifest: &ComposerJson,
136    path: impl AsRef<Path>,
137) -> Result<Package, SourcePkgError> {
138    let lock: ComposerLockJson = {
139        let mut file = File::open(path)?;
140        serde_json::from_reader(&mut file)?
141    };
142
143    let mut dependencies = vec![];
144
145    for pkg in &lock.packages {
146        let mut dep = DependentPackage {
147            purl: make_purl_versioned(&pkg.name, &pkg.version),
148            requirement: pkg.version.clone(),
149            is_optional: false,
150            is_runtime: true,
151            is_resolved: true,
152            ..Default::default()
153        };
154
155        for parent_pkg in &lock.packages {
156            if parent_pkg.require.contains_key(pkg.name.as_str()) {
157                // `require-dev` is root-only, development deps for other deps are
158                // not installed
159                dep.parents
160                    .insert(make_purl_versioned(&parent_pkg.name, &parent_pkg.version));
161            }
162        }
163        if !dep.parents.is_empty() {
164            dep.relation.insert(Relation::Indirect);
165        }
166
167        if manifest.require.contains_key(&pkg.name) {
168            dep.scope = "runtime".into();
169            dep.is_runtime = true;
170            dep.relation.insert(Relation::Direct);
171        }
172
173        if manifest.require_dev.contains_key(&pkg.name) && dep.parents.is_empty() {
174            // This package is not a dependency of any other package, but it is
175            // a dev dependency of the root package
176            dep.scope = "dev".into();
177            dep.is_runtime = false;
178            dep.relation.insert(Relation::Direct);
179        }
180
181        dependencies.push(dep);
182    }
183
184    let package = Package {
185        dependencies,
186        ..Default::default()
187    };
188
189    Ok(package)
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use std::path::Path;
196
197    #[tokio::test]
198    async fn test_parse_composer() {
199        let filepath = Path::new(concat!(
200            env!("CARGO_MANIFEST_DIR"),
201            "/testdata/phpcomposer/AdminPanel/composer.json"
202        ));
203
204        let parser = PhpComposer::new();
205        let p = parser.recognize(filepath).await.unwrap();
206        println!("{:#?}", p);
207    }
208}