use std::cmp::Ordering;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt::{self, Display};
use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use log::{debug, info};
use rmp_serde::Serializer;
use serde::{Deserialize, Serialize};
use crate::data::{LicenseType, MatchData, NoData, TextData};
use crate::error::Error;
pub(crate) const CACHE_VERSION: &[u8] = b"scallion-00";
const HEADER_LENGTH: usize = CACHE_VERSION.len() + 5;
#[derive(Debug, Serialize, Deserialize)]
pub struct LicenseEntry<D> {
pub(crate) original: MatchData<D>,
pub(crate) aliases: Vec<String>,
pub(crate) headers: Vec<MatchData<D>>,
pub(crate) alternates: Vec<MatchData<D>>,
}
impl<D> LicenseEntry<D> {
#[must_use]
pub(crate) const fn new(original: MatchData<D>) -> LicenseEntry<D> {
LicenseEntry {
original,
aliases: Vec::new(),
alternates: Vec::new(),
headers: Vec::new(),
}
}
#[must_use]
pub const fn original(&self) -> &MatchData<D> {
&self.original
}
#[must_use]
pub const fn aliases(&self) -> &[String] {
self.aliases.as_slice()
}
#[must_use]
pub const fn variants(&self) -> &[MatchData<D>] {
self.alternates.as_slice()
}
#[must_use]
pub const fn headers(&self) -> &[MatchData<D>] {
self.headers.as_slice()
}
pub fn add_alias(&mut self, name: String) {
self.aliases.push(name);
}
pub fn add_variant(&mut self, data: MatchData<D>) {
self.alternates.push(data);
}
pub fn add_header(&mut self, data: MatchData<D>) {
self.headers.push(data);
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Store<D> {
licenses: HashMap<String, LicenseEntry<D>>,
}
impl<D> Store<D> {
#[must_use]
pub fn new() -> Self {
Store {
licenses: HashMap::new(),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.licenses.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.licenses.is_empty()
}
pub fn add_license(&mut self, name: String, data: MatchData<D>) -> Option<LicenseEntry<D>> {
let entry = LicenseEntry::new(data);
self.licenses.insert(name, entry)
}
#[must_use]
pub fn get_license(&self, name: &str) -> Option<&LicenseEntry<D>> {
self.licenses.get(name)
}
#[must_use]
pub fn get_license_mut(&mut self, name: &str) -> Option<&mut LicenseEntry<D>> {
self.licenses.get_mut(name)
}
fn insert_or_add_alias(&mut self, name: &str, data: MatchData<D>, header: Option<MatchData<D>>) {
let mut already_existed = None;
self.licenses.iter_mut().for_each(|(key, ref mut value)| {
if value.original.eq_data(&data) {
value.aliases.push(name.to_string());
already_existed = Some(key.as_str());
}
});
if let Some(prev) = already_existed {
info!("{name} already stored; added as an alias for {prev}");
return;
}
let license = self
.licenses
.entry(name.to_string())
.or_insert_with(|| LicenseEntry::new(data));
if let Some(header_text) = header {
license.headers = vec![header_text];
}
}
pub fn analyze<'a>(&'a self, text: &MatchData<D>) -> Match<'a, D>
where
D: Sync,
{
let mut res: Vec<PartialMatch<'a, D>>;
let analyze_fold = |mut acc: Vec<PartialMatch<'a, D>>, (name, data): (&'a String, &'a LicenseEntry<D>)| {
acc.push(PartialMatch {
score: data.original.match_score(text),
name,
license_type: LicenseType::Original,
data: &data.original,
});
data.alternates.iter().for_each(|alt| {
acc.push(PartialMatch {
score: alt.match_score(text),
name,
license_type: LicenseType::Alternate,
data: alt,
});
});
data.headers.iter().for_each(|head| {
acc.push(PartialMatch {
score: head.match_score(text),
name,
license_type: LicenseType::Header,
data: head,
});
});
acc
};
#[cfg(not(target_arch = "wasm32"))]
{
use rayon::prelude::*;
res = self.licenses.par_iter().fold(Vec::new, analyze_fold).reduce(
Vec::new,
|mut a: Vec<PartialMatch<'a, D>>, b: Vec<PartialMatch<'a, D>>| {
a.extend(b);
a
},
);
res.par_sort_unstable_by(|a, b| b.partial_cmp(a).unwrap());
}
#[cfg(target_arch = "wasm32")]
{
res = self
.licenses
.iter()
.fold(Vec::with_capacity(self.licenses.len()), analyze_fold);
res.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap());
}
let m = &res[0];
Match {
score: m.score,
name: m.name,
license_type: m.license_type,
data: m.data,
}
}
pub fn from_cache<R>(mut readable: R) -> Result<Store<D>, Error>
where
R: Read + Sized,
D: for<'a> Deserialize<'a>,
{
let mut header = [0u8; HEADER_LENGTH];
readable.read_exact(&mut header).map_err(|e| Error::io(e, None))?;
let cf = match &header {
b"scallion-00-zstd" => CF::Zstd,
b"scallion-00-gzip" => CF::Gzip,
b"scallion-00-none" => CF::None,
_ => return Err(Error::cache_version(header.to_vec())),
};
match cf {
CF::Zstd => {
#[cfg(feature = "zstd")]
{
let dec = zstd::Decoder::new(readable).map_err(|e| Error::io(e, None))?;
let store = rmp_serde::decode::from_read(dec)?;
Ok(store)
}
#[cfg(not(feature = "zstd"))]
{
Err(Error::cache_format(cf))
}
},
CF::Gzip => {
#[cfg(feature = "gzip")]
{
let dec = flate2::read::GzDecoder::new(readable);
let store = rmp_serde::decode::from_read(dec)?;
Ok(store)
}
#[cfg(not(feature = "gzip"))]
{
Err(Error::cache_format(cf))
}
},
CF::None => {
let store = rmp_serde::decode::from_read(readable)?;
Ok(store)
},
}
}
pub fn to_cache<W>(&self, mut writable: W, format: CF) -> Result<(), Error>
where
W: Write + Sized,
D: Serialize,
{
let serialize = || -> Result<Vec<u8>, Error> {
let mut buf = Vec::with_capacity(4 * 1024 * 1024);
let mut serializer = Serializer::new(&mut buf);
self.serialize(&mut serializer)?;
Ok(buf)
};
match format {
CF::Zstd => {
#[cfg(feature = "zstd")]
{
writable.write_all(CACHE_VERSION).map_err(|e| Error::io(e, None))?;
writable.write_all(b"-zstd").map_err(|e| Error::io(e, None))?;
let serialized = serialize()?;
let mut enc = zstd::Encoder::new(writable, 21).map_err(|e| Error::io(e, None))?;
io::copy(&mut serialized.as_slice(), &mut enc).map_err(|e| Error::io(e, None))?;
enc.finish().map_err(|e| Error::io(e, None))?;
Ok(())
}
#[cfg(not(feature = "zstd"))]
{
Err(Error::cache_format(format))
}
},
CF::Gzip => {
#[cfg(feature = "gzip")]
{
writable.write_all(CACHE_VERSION).map_err(|e| Error::io(e, None))?;
writable.write_all(b"-gzip").map_err(|e| Error::io(e, None))?;
let serialized = serialize()?;
let mut enc = flate2::write::GzEncoder::new(writable, flate2::Compression::default());
io::copy(&mut serialized.as_slice(), &mut enc).map_err(|e| Error::io(e, None))?;
enc.finish().map_err(|e| Error::io(e, None))?;
Ok(())
}
#[cfg(not(feature = "gzip"))]
{
Err(Error::cache_format(format))
}
},
CF::None => {
writable.write_all(CACHE_VERSION).map_err(|e| Error::io(e, None))?;
writable.write_all(b"-none").map_err(|e| Error::io(e, None))?;
let serialized = serialize()?;
io::copy(&mut serialized.as_slice(), &mut writable).map_err(|e| Error::io(e, None))?;
Ok(())
},
}
}
}
impl Store<TextData> {
pub fn load_spdx<P: AsRef<Path>>(&mut self, dir: P) -> Result<(), Error> {
let paths = locate_json_files(dir)?;
for path in paths {
let parsed = parse_json_file(&path)?;
if parsed.deprecated {
debug!("Skipping {} (deprecated)", parsed.name);
continue;
}
info!("Processing {}", parsed.name);
let data = MatchData::new(&parsed.text);
let header = parsed.header.as_deref().map(MatchData::new);
self.insert_or_add_alias(&parsed.name, data, header);
}
Ok(())
}
}
impl Store<NoData> {
pub fn load_spdx<P: AsRef<Path>>(&mut self, dir: P) -> Result<(), Error> {
let paths = locate_json_files(dir)?;
for path in paths {
let parsed = parse_json_file(&path)?;
if parsed.deprecated {
debug!("Skipping {} (deprecated)", parsed.name);
continue;
}
info!("Processing {}", parsed.name);
let data = MatchData::new(&parsed.text).without_text();
let header = parsed
.header
.map(|header_text| MatchData::new(&header_text).without_text());
self.insert_or_add_alias(&parsed.name, data, header);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CompressionFormat {
Zstd,
Gzip,
None,
}
use CompressionFormat as CF;
impl Display for CompressionFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CF::Zstd => write!(f, "zstd"),
CF::Gzip => write!(f, "gzip"),
CF::None => write!(f, "none"),
}
}
}
impl FromStr for CompressionFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"zstd" => Ok(CF::Zstd),
"gzip" => Ok(CF::Gzip),
"none" => Ok(CF::None),
_ => Err(format!("Invalid compression format: '{s}'")),
}
}
}
#[derive(Clone)]
pub struct Match<'a, D> {
pub score: f32,
pub name: &'a str,
pub license_type: LicenseType,
pub data: &'a MatchData<D>,
}
struct PartialMatch<'a, D> {
pub name: &'a str,
pub score: f32,
pub license_type: LicenseType,
pub data: &'a MatchData<D>,
}
impl<D> PartialOrd for PartialMatch<'_, D> {
fn partial_cmp(&self, other: &PartialMatch<'_, D>) -> Option<Ordering> {
self.score.partial_cmp(&other.score)
}
}
impl<D> PartialEq for PartialMatch<'_, D> {
fn eq(&self, other: &PartialMatch<'_, D>) -> bool {
self.score.eq(&other.score) && self.name == other.name && self.license_type == other.license_type
}
}
impl<D> fmt::Debug for Match<'_, D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Match {{ score: {}, name: {}, license_type: {:?} }}",
self.score, self.name, self.license_type
)
}
}
fn locate_json_files<P: AsRef<Path>>(dir: P) -> Result<Vec<PathBuf>, Error> {
let mut paths: Vec<_> = fs::read_dir(&dir)
.map_err(|e| Error::io(e, Some(dir.as_ref().to_path_buf())))?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_file() && p.extension().unwrap_or_else(|| OsStr::new("")) == "json")
.collect();
paths.sort_by(|a, b| a.file_stem().unwrap().cmp(b.file_stem().unwrap()));
Ok(paths)
}
#[derive(Deserialize)]
struct LicenseListData {
#[serde(rename = "licenseId")]
name: String,
#[serde(rename = "isDeprecatedLicenseId")]
deprecated: bool,
#[serde(rename = "licenseText")]
text: String,
#[serde(rename = "standardLicenseHeader")]
header: Option<String>,
}
fn parse_json_file<P: AsRef<Path>>(path: P) -> Result<LicenseListData, Error> {
let path = path.as_ref().to_path_buf();
let data = fs::read_to_string(&path).map_err(|e| Error::io(e, Some(path.clone())))?;
serde_json::from_str(&data).map_err(|e| Error::spdx(e, path))
}