sancus_lib/
rpm_info.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6//
7// SPDX-License-Identifier: MIT OR Apache-2.0
8//
9// SPDX-FileCopyrightText: 2024 X-Software GmbH <opensource@x-software.com>
10
11use anyhow::{Context, Result};
12use log::*;
13use std::{path::Path, process::Command};
14
15use crate::license_info::LicenseInfo;
16use crate::settings;
17use crate::{
18    file_info::FileInfo,
19    license_detector::{LicenseDetector, LicenseFile},
20};
21
22const RPM_EXECUTABLE: &str = "rpm";
23const RPM_QUERY_LICENSE: &str = "LICENSE";
24const RPM_QUERY_VERSION: &str = "VERSION";
25const RPM_QUERY_URL: &str = "URL";
26
27fn package_collect_files(package: &str, patterns: Vec<&str>) -> Result<Vec<String>> {
28    let mut found_files: Vec<_> = vec![];
29
30    let list_packages_args = vec!["-ql", package];
31
32    let output = Command::new(RPM_EXECUTABLE)
33        .args(list_packages_args.clone())
34        .output()
35        .context(format!(
36            "Cannot create command '{} {}'",
37            RPM_EXECUTABLE,
38            list_packages_args.join(" ")
39        ))?;
40    if !output.status.success() {
41        let error = String::from_utf8(output.stderr)?;
42        return Err(anyhow::anyhow!(
43            "Execution of '{} {}' failed",
44            RPM_EXECUTABLE,
45            list_packages_args.join(" ")
46        )
47        .context(error)
48        .context(format!("Cannot get package content of {package}")));
49    }
50    let stdout = String::from_utf8(output.stdout)?;
51    for file in stdout.lines() {
52        if patterns.iter().any(|p| file.contains(p)) {
53            let path = Path::new(file).to_path_buf();
54            if path.is_file() {
55                found_files.push(path.to_string_lossy().into_owned());
56            } else {
57                return Err(anyhow::anyhow!(
58                    "The installed file {} of package {package} is missing in the file system",
59                    path.to_string_lossy().into_owned(),
60                ));
61            }
62        }
63    }
64
65    Ok(found_files)
66}
67
68fn package_contains_file(package: &str, file: &str) -> Result<bool> {
69    let list_packages_args = vec!["-ql", package];
70
71    let output = Command::new(RPM_EXECUTABLE)
72        .args(list_packages_args.clone())
73        .output()
74        .context(format!(
75            "Cannot create command '{} {}'",
76            RPM_EXECUTABLE,
77            list_packages_args.join(" ")
78        ))?;
79    if !output.status.success() {
80        let error = String::from_utf8(output.stderr)?;
81        return Err(anyhow::anyhow!(
82            "Execution of '{} {}' failed",
83            RPM_EXECUTABLE,
84            list_packages_args.join(" ")
85        )
86        .context(error)
87        .context(format!("Cannot get package content of {package}")));
88    }
89    let stdout = String::from_utf8(output.stdout)?;
90    for line in stdout.lines() {
91        if line.contains(file) {
92            return Ok(true);
93        }
94    }
95
96    Ok(false)
97}
98
99pub fn package_name_of_lib(lib: &String) -> Result<String> {
100    let list_packages_args = vec!["--query", "--all", "--queryformat", "%{NAME}\\n"];
101
102    let output = Command::new(RPM_EXECUTABLE)
103        .args(list_packages_args.clone())
104        .output()
105        .context(format!(
106            "Cannot create command '{} {}'",
107            RPM_EXECUTABLE,
108            list_packages_args.join(" ")
109        ))?;
110    if !output.status.success() {
111        let error = String::from_utf8(output.stderr)?;
112        return Err(anyhow::anyhow!(
113            "Execution of '{} {}' failed",
114            RPM_EXECUTABLE,
115            list_packages_args.join(" ")
116        )
117        .context(error)
118        .context(format!("Cannot get package name for library {lib}")));
119    }
120    let mut packages: Vec<_> = vec![];
121    let stdout = String::from_utf8(output.stdout)?;
122    for package in stdout.lines() {
123        if package_contains_file(package, lib)? {
124            packages.push(package.to_string());
125        }
126    }
127    if packages.len() > 1 {
128        return Err(anyhow::anyhow!(
129            "Cannot find unique package containing the library '{lib}': {}",
130            packages.join(", ")
131        ));
132    }
133    if let Some(package) = packages.first() {
134        return Ok(package.clone());
135    }
136    Err(anyhow::anyhow!(
137        "Cannot find any package containing the library '{lib}'"
138    ))
139}
140
141fn query_package_info(package: &str, info: &str) -> Result<Option<String>> {
142    let query = format!("%{{{info}}}");
143    let list_packages_args = vec!["-q", package, "--queryformat", query.as_str()];
144
145    trace!(
146        "Query package info: {} {}",
147        RPM_EXECUTABLE,
148        list_packages_args.join(" ")
149    );
150    let output = Command::new(RPM_EXECUTABLE)
151        .args(list_packages_args.clone())
152        .output()
153        .context(format!(
154            "Cannot create command '{} {}'",
155            RPM_EXECUTABLE,
156            list_packages_args.join(" ")
157        ))?;
158    if !output.status.success() {
159        let error = String::from_utf8(output.stderr)?;
160        return Err(anyhow::anyhow!(
161            "Execution of '{} {}' failed",
162            RPM_EXECUTABLE,
163            list_packages_args.join(" ")
164        )
165        .context(error)
166        .context(format!("Cannot get query {info} for package {package}")));
167    }
168    let stdout = String::from_utf8(output.stdout)?;
169    if stdout.is_empty() {
170        return Ok(None);
171    }
172    Ok(Some(stdout))
173}
174
175pub fn package_info(package: &String, lib_info: &FileInfo, overrides: &[settings::Override]) -> Result<LicenseInfo> {
176    let override_info = settings::Override::find_override(package, overrides);
177
178    let license = if override_info.is_some_and(|x| x.license_id.is_some()) {
179        override_info.unwrap().license_id.clone()
180    } else {
181        query_package_info(package, RPM_QUERY_LICENSE)?
182    };
183    let version = query_package_info(package, RPM_QUERY_VERSION)?;
184    let url = query_package_info(package, RPM_QUERY_URL)?;
185
186    let license = if let Some(license) = license {
187        license
188    } else {
189        return Err(anyhow::anyhow!(
190            "Missing license identifier for RPM package {}",
191            package
192        ));
193    };
194
195    let license_file_patterns = vec!["COPY", "LICENSE", "License"];
196    let license_files: Vec<_> = if override_info.is_some_and(|x| !x.license_files.is_empty()) {
197        override_info
198            .unwrap()
199            .license_files
200            .iter()
201            .map(|license_file| LicenseFile {
202                id: license_file.id.clone(),
203                file: license_file.file.clone(),
204            })
205            .collect()
206    } else {
207        package_collect_files(package, license_file_patterns)?
208            .iter()
209            .map(|file| LicenseFile {
210                id: None,
211                file: file.clone(),
212            })
213            .collect()
214    };
215
216    // Create SPDX license expression from the license IDs:
217    let license_expression = if !license.is_empty() {
218        match spdx::Expression::parse_mode(license.as_str(), spdx::ParseMode::LAX)
219            .context(format!("Cannot parse license expression for package {package}"))
220        {
221            Ok(expr) => Some(expr),
222            Err(error) => {
223                warn!("{error:?}");
224                None
225            }
226        }
227    } else {
228        None
229    };
230
231    let license_ids = if let Some(expr) = &license_expression {
232        let mut collection = vec![];
233        expr.iter()
234            .map(|l| match l {
235                spdx::expression::ExprNode::Req(req) => Some(req),
236                spdx::expression::ExprNode::Op(_op) => None,
237            })
238            .for_each(|expr| {
239                if let Some(expr) = expr {
240                    if let Some(license) = &expr.req.license.id() {
241                        collection.push(*license);
242                    }
243                }
244            });
245        collection
246    } else {
247        vec![]
248    };
249
250    // Detect license ids of license texts:
251    let license_texts =
252        LicenseDetector::instance().detect_licenses(package, license_ids.as_slice(), license_files.as_slice())?;
253
254    Ok(LicenseInfo {
255        lib_info: lib_info.clone(),
256        package_name: package.clone(),
257        license,
258        license_expression,
259        license_texts,
260        version,
261        url,
262    })
263}