| use super::Expression; |
| use crate::{LicenseReq, Licensee}; |
| use std::fmt; |
| |
| /// Errors that can occur when trying to minimize the requirements for an [`Expression`] |
| #[derive(Debug, PartialEq, Eq)] |
| pub enum MinimizeError { |
| /// More than `64` unique licensees satisfied a requirement in the [`Expression`] |
| TooManyRequirements(usize), |
| /// The list of licensees did not fully satisfy the requirements in the [`Expression`] |
| RequirementsUnmet, |
| } |
| |
| impl fmt::Display for MinimizeError { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self { |
| Self::TooManyRequirements(n) => write!( |
| f, |
| "the license expression required {} licensees which exceeds the limit of 64", |
| n |
| ), |
| Self::RequirementsUnmet => { |
| f.write_str("the expression was not satisfied by the provided list of licensees") |
| } |
| } |
| } |
| } |
| |
| impl std::error::Error for MinimizeError { |
| fn description(&self) -> &str { |
| match self { |
| Self::TooManyRequirements(_) => "too many requirements in license expression", |
| Self::RequirementsUnmet => { |
| "the expression was not satisfied by the provided list of licensees" |
| } |
| } |
| } |
| } |
| |
| impl Expression { |
| /// Given a set of [`Licensee`]s, attempts to find the minimum number that |
| /// satisfy this [`Expression`]. |
| /// |
| /// The list of licensees should be given in priority order, eg, if you wish |
| /// to accept the `Apache-2.0` license if it is available, and the `MIT` if |
| /// not, putting `Apache-2.0` before `MIT` will cause the ubiquitous |
| /// `Apache-2.0 OR MIT` expression to minimize to just `Apache-2.0` as only |
| /// 1 of the licenses is required, and `Apache-2.0` has priority. |
| /// |
| /// # Errors |
| /// |
| /// This method will fail if more than 64 unique licensees are satisfied by |
| /// this expression, but such a case is unlikely in a real world scenario. |
| /// The list of licensees must also actually satisfy this expression, |
| /// otherwise it can't be minimized. |
| /// |
| /// # Example |
| /// |
| /// ``` |
| /// let expr = spdx::Expression::parse("Apache-2.0 OR MIT").unwrap(); |
| /// |
| /// let apache_licensee = spdx::Licensee::parse("Apache-2.0").unwrap(); |
| /// assert_eq!( |
| /// expr.minimized_requirements([&apache_licensee, &spdx::Licensee::parse("MIT").unwrap()]).unwrap(), |
| /// vec![apache_licensee.into_req()], |
| /// ); |
| /// ``` |
| pub fn minimized_requirements<'lic>( |
| &self, |
| accepted: impl IntoIterator<Item = &'lic Licensee>, |
| ) -> Result<Vec<LicenseReq>, MinimizeError> { |
| let found_set = { |
| let mut found_set = smallvec::SmallVec::<[Licensee; 5]>::new(); |
| |
| for lic in accepted { |
| if !found_set.contains(lic) |
| && self.requirements().any(|ereq| lic.satisfies(&ereq.req)) |
| { |
| found_set.push(lic.clone()); |
| } |
| } |
| |
| if found_set.len() > 64 { |
| return Err(MinimizeError::TooManyRequirements(found_set.len())); |
| } |
| |
| // Ensure that the licensees provided actually _can_ be accepted by |
| // this expression |
| if !self.evaluate(|ereq| found_set.iter().any(|lic| lic.satisfies(ereq))) { |
| return Err(MinimizeError::RequirementsUnmet); |
| } |
| |
| found_set |
| }; |
| |
| let set_size = (1 << found_set.len()) as u64; |
| |
| for mask in 1..=set_size { |
| let eval_res = self.evaluate(|req| { |
| for (ind, lic) in found_set.iter().enumerate() { |
| if mask & (1 << ind) != 0 && lic.satisfies(req) { |
| return true; |
| } |
| } |
| |
| false |
| }); |
| |
| if eval_res { |
| return Ok(found_set |
| .into_iter() |
| .enumerate() |
| .filter_map(|(ind, lic)| { |
| if mask & (1 << ind) == 0 { |
| None |
| } else { |
| Some(lic.into_req()) |
| } |
| }) |
| .collect()); |
| } |
| } |
| |
| // This should be impossible, but would rather not panic |
| Ok(found_set.into_iter().map(Licensee::into_req).collect()) |
| } |
| } |