Skip to main content

package_parser/pkgs/
opam.rs

1use opam_file_rs::parse as opam_file_parse;
2use opam_file_rs::value::{OpamFileItem, OpamFileSection, RelOp, RelOpKind, Value, ValueKind};
3use packageurl::PackageUrl;
4
5use crate::error::SourcePkgError;
6use crate::pkgs::common::model::{Package, PackageManifest, Party};
7
8use std::fs::File;
9use std::io::Read;
10use std::path::Path;
11
12use super::common::model::DependentPackage;
13
14pub struct OcamlOpam {}
15
16#[derive(Debug)]
17struct OcamlOpamFile {
18    pub homepage: Option<String>,
19    pub description: Option<String>,
20    pub maintainers: Vec<Party>,
21    pub dev_repo: Option<String>,
22    pub license: Option<String>,
23    pub dependencies: Vec<DependentPackage>,
24}
25
26impl OcamlOpamFile {
27    fn new() -> Self {
28        Self {
29            homepage: None,
30            description: None,
31            maintainers: vec![],
32            dev_repo: None,
33            license: None,
34            dependencies: vec![],
35        }
36    }
37}
38
39impl OcamlOpam {
40    pub fn new() -> Self {
41        Self {}
42    }
43
44    fn parse_opam_description(value: Value) -> Option<String> {
45        match value.kind {
46            ValueKind::String(value) => Some(value),
47            _ => {
48                log::error!("unexpected value kind : {:?}", value.kind);
49                None
50            }
51        }
52    }
53
54    fn parse_opam_maintainer(value: Value) -> Vec<Party> {
55        match value.kind {
56            ValueKind::List(values) => {
57                let mut maintainers = Vec::new();
58                for maintainer in values {
59                    match maintainer.kind {
60                        ValueKind::String(maintainer) => {
61                            maintainers.push(Party {
62                                typ: "".into(),
63                                name: "".into(),
64                                email: maintainer.to_string(),
65                                url: "".into(),
66                                ..Default::default()
67                            });
68                        }
69                        _ => {
70                            log::error!("unexpected value kind : {:?}", maintainer.kind);
71                        }
72                    }
73                }
74
75                maintainers
76            }
77            _ => {
78                log::error!("unexpected value kind : {:?}", value.kind);
79                vec![]
80            }
81        }
82    }
83
84    fn parse_opam_homepage(value: Value) -> Option<String> {
85        match value.kind {
86            ValueKind::String(value) => Some(value),
87            _ => {
88                log::error!("unexpected value kind : {:?}", value.kind);
89                None
90            }
91        }
92    }
93
94    fn parse_opam_devrepo(value: Value) -> Option<String> {
95        match value.kind {
96            ValueKind::String(value) => Some(value),
97            _ => {
98                log::error!("unexpected value kind : {:?}", value.kind);
99                None
100            }
101        }
102    }
103
104    fn parse_opam_license(value: Value) -> Option<String> {
105        match value.kind {
106            ValueKind::String(value) => Some(value),
107            _ => {
108                log::error!("unexpected value kind : {:?}", value.kind);
109                None
110            }
111        }
112    }
113
114    fn opam_relop_to_string(op: RelOp) -> String {
115        match op.kind {
116            RelOpKind::Eq => "=".into(),
117            RelOpKind::Neq => "!=".into(),
118            RelOpKind::Lt => "<".into(),
119            RelOpKind::Leq => "<=".into(),
120            RelOpKind::Gt => ">".into(),
121            RelOpKind::Geq => ">=".into(),
122            RelOpKind::Sem => "~".into(),
123        }
124    }
125
126    fn parse_opam_depend_option_item(name: Value, props: Vec<Value>) -> Option<DependentPackage> {
127        match name.kind {
128            ValueKind::String(name) => {
129                let mut version = None;
130                let mut relop_string = None;
131                for prop in props {
132                    match prop.kind {
133                        ValueKind::PrefixRelOp(rel_op, version_value) => match version_value.kind {
134                            ValueKind::String(version_value) => {
135                                version = Some(version_value);
136                                relop_string = Some(Self::opam_relop_to_string(rel_op));
137                            }
138                            _ => {
139                                log::error!("unexpected value kind : {:?}", version_value.kind);
140                            }
141                        },
142                        _ => {
143                            log::error!("unexpected value kind : {:?}", prop.kind);
144                        }
145                    }
146                }
147                return Some(DependentPackage {
148                    purl: PackageUrl::new("opam", name.as_str())
149                        .expect("purl arguments are invalid")
150                        .to_string(),
151                    requirement: format!(
152                        "{}{}",
153                        relop_string.unwrap_or_default(),
154                        version.unwrap_or_default()
155                    )
156                    .trim()
157                    .to_string(),
158                    ..Default::default()
159                });
160            }
161            _ => {
162                log::error!("unexpected value kind : {:?}", name.kind);
163            }
164        }
165
166        None
167    }
168
169    fn parse_opam_depends(value: Value) -> Vec<DependentPackage> {
170        // depends : Value {
171        //     kind: List(
172        //         [
173        //             Value {
174        //                 kind: Option(
175        //                     Value {
176        //                         kind: String(
177        //                             "ocaml",
178        //                         ),
179        //                     },
180        //                     [
181        //                         Value {
182        //                             kind: PrefixRelOp(
183        //                                 RelOp {
184        //                                     kind: Geq,
185        //                                 },
186        //                                 Value {
187        //                                     kind: String(
188        //                                         "4.06.0",
189        //                                     ),
190        //                                 },
191        //                             ),
192        //                         },
193        //                     ],
194        //                 ),
195        //             },
196
197        match value.kind {
198            ValueKind::List(values) => {
199                let mut dependencies = Vec::new();
200                for dependency in values {
201                    match dependency.kind {
202                        ValueKind::Option(name, props) => {
203                            let dep = Self::parse_opam_depend_option_item(*name, props);
204                            if let Some(dep) = dep {
205                                dependencies.push(dep);
206                            }
207                        }
208                        _ => {
209                            log::error!("unexpected value kind : {:?}", dependency.kind);
210                        }
211                    }
212                }
213
214                dependencies
215            }
216            _ => {
217                log::error!("unexpected value kind : {:?}", value.kind);
218                vec![]
219            }
220        }
221    }
222
223    fn parse_opam_section(_section: OpamFileSection) {}
224
225    fn parse_opam_variable(opam_file: &mut OcamlOpamFile, section_name: String, value: Value) {
226        if section_name == "description" {
227            opam_file.description = Self::parse_opam_description(value);
228        } else if section_name == "maintainer" {
229            opam_file.maintainers = Self::parse_opam_maintainer(value);
230        } else if section_name == "homepage" {
231            opam_file.homepage = Self::parse_opam_homepage(value);
232        } else if section_name == "dev-repo" {
233            opam_file.dev_repo = Self::parse_opam_devrepo(value);
234        } else if section_name == "license" {
235            opam_file.license = Self::parse_opam_license(value);
236        } else if section_name == "depends" {
237            opam_file.dependencies = Self::parse_opam_depends(value);
238        } else if section_name == "authors" {
239            opam_file.maintainers = Self::parse_opam_maintainer(value);
240        }
241    }
242
243    fn parse_ocaml_opam(path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
244        let mut fs = File::open(path)?;
245        let mut content = String::new();
246        fs.read_to_string(&mut content)?;
247        let mut opam_file = OcamlOpamFile::new();
248        let opam = opam_file_parse(&content)?;
249        for opam_item in opam.file_contents {
250            match opam_item {
251                OpamFileItem::Section(_, section) => {
252                    Self::parse_opam_section(section);
253                }
254                OpamFileItem::Variable(_, string_value, value) => {
255                    Self::parse_opam_variable(&mut opam_file, string_value, value);
256                }
257            }
258        }
259
260        let package = Package {
261            declared_license: opam_file.license.unwrap_or_default(),
262            dependencies: opam_file.dependencies,
263            ..Default::default()
264        };
265
266        Ok(package)
267    }
268}
269
270#[async_trait::async_trait]
271impl PackageManifest for OcamlOpam {
272    fn get_name(&self) -> String {
273        "opam".into()
274    }
275
276    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
277        Self::parse_ocaml_opam(path)
278    }
279
280    fn file_name_patterns(&self) -> &'static [&'static str] {
281        &["*.opam"]
282    }
283}
284
285#[cfg(test)]
286mod test {
287    use super::*;
288    use std::path::Path;
289
290    #[test]
291    fn test_opam() {
292        let filepath = Path::new(concat!(
293            env!("CARGO_MANIFEST_DIR"),
294            "/testdata/opam/sample1/sample1.opam"
295        ));
296
297        OcamlOpam::parse_ocaml_opam(filepath).unwrap();
298    }
299}