package_parser/pkgs/
composer.rs1use 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 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 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}