1use std::cmp::Ordering;
2use std::collections::HashMap;
3use std::ffi::OsStr;
4use std::fmt::{self, Display};
5use std::fs;
6use std::io::{self, Read, Write};
7use std::path::{Path, PathBuf};
8use std::str::FromStr;
9
10use log::{debug, info};
11use rmp_serde::Serializer;
12use serde::{Deserialize, Serialize};
13
14use crate::data::{LicenseType, MatchData};
15use crate::error::Error;
16
17pub(crate) const CACHE_VERSION: &[u8] = b"scallion-00";
18const HEADER_LENGTH: usize = CACHE_VERSION.len() + 5;
19
20#[derive(Debug, Deserialize, Serialize)]
22pub struct LicenseEntry {
23 pub(crate) original: MatchData,
24 pub(crate) aliases: Vec<String>,
25 pub(crate) headers: Vec<MatchData>,
26 pub(crate) alternates: Vec<MatchData>,
27}
28
29impl LicenseEntry {
30 #[must_use]
31 pub(crate) const fn new(original: MatchData) -> LicenseEntry {
32 LicenseEntry {
33 original,
34 aliases: Vec::new(),
35 alternates: Vec::new(),
36 headers: Vec::new(),
37 }
38 }
39
40 #[must_use]
42 pub const fn original(&self) -> &MatchData {
43 &self.original
44 }
45
46 #[must_use]
48 pub const fn aliases(&self) -> &[String] {
49 self.aliases.as_slice()
50 }
51
52 #[must_use]
54 pub const fn variants(&self) -> &[MatchData] {
55 self.alternates.as_slice()
56 }
57
58 #[must_use]
60 pub const fn headers(&self) -> &[MatchData] {
61 self.headers.as_slice()
62 }
63
64 pub fn add_alias(&mut self, name: String) {
66 self.aliases.push(name);
67 }
68
69 pub fn add_variant(&mut self, data: MatchData) {
71 self.alternates.push(data);
72 }
73
74 pub fn add_header(&mut self, data: MatchData) {
76 self.headers.push(data);
77 }
78}
79
80#[derive(Debug, Default, Deserialize, Serialize)]
101pub struct Store {
102 licenses: HashMap<String, LicenseEntry>,
103}
104
105impl Store {
106 #[must_use]
111 pub fn new() -> Self {
112 Store {
113 licenses: HashMap::new(),
114 }
115 }
116
117 #[must_use]
122 pub fn len(&self) -> usize {
123 self.licenses.len()
124 }
125
126 #[must_use]
128 pub fn is_empty(&self) -> bool {
129 self.licenses.is_empty()
130 }
131
132 pub fn add_license(&mut self, name: String, data: MatchData) -> Option<LicenseEntry> {
137 let entry = LicenseEntry::new(data);
138 self.licenses.insert(name, entry)
139 }
140
141 #[must_use]
145 pub fn get_license(&self, name: &str) -> Option<&LicenseEntry> {
146 self.licenses.get(name)
147 }
148
149 #[must_use]
153 pub fn get_license_mut(&mut self, name: &str) -> Option<&mut LicenseEntry> {
154 self.licenses.get_mut(name)
155 }
156
157 fn insert_or_add_alias(&mut self, name: &str, data: MatchData, header: Option<MatchData>) {
162 let mut already_existed = None;
164 self.licenses.iter_mut().for_each(|(key, ref mut value)| {
165 if value.original.eq_data(&data) {
166 value.aliases.push(name.to_string());
167 already_existed = Some(key.as_str());
168 }
169 });
170 if let Some(prev) = already_existed {
171 info!("{name} already stored; added as an alias for {prev}");
172 return;
173 }
174
175 let license = self
176 .licenses
177 .entry(name.to_string())
178 .or_insert_with(|| LicenseEntry::new(data));
179
180 if let Some(header_text) = header {
181 license.headers = vec![header_text];
182 }
183 }
184
185 pub fn analyze<'a>(&'a self, text: &MatchData) -> Match<'a> {
191 let mut res: Vec<PartialMatch<'a>>;
192
193 let analyze_fold = |mut acc: Vec<PartialMatch<'a>>, (name, data): (&'a String, &'a LicenseEntry)| {
194 acc.push(PartialMatch {
195 score: data.original.match_score(text),
196 name,
197 license_type: LicenseType::Original,
198 data: &data.original,
199 });
200 data.alternates.iter().for_each(|alt| {
201 acc.push(PartialMatch {
202 score: alt.match_score(text),
203 name,
204 license_type: LicenseType::Alternate,
205 data: alt,
206 });
207 });
208 data.headers.iter().for_each(|head| {
209 acc.push(PartialMatch {
210 score: head.match_score(text),
211 name,
212 license_type: LicenseType::Header,
213 data: head,
214 });
215 });
216 acc
217 };
218
219 #[cfg(not(target_arch = "wasm32"))]
221 {
222 use rayon::prelude::*;
223 res = self.licenses.par_iter().fold(Vec::new, analyze_fold).reduce(
224 Vec::new,
225 |mut a: Vec<PartialMatch<'a>>, b: Vec<PartialMatch<'a>>| {
226 a.extend(b);
227 a
228 },
229 );
230 res.par_sort_unstable_by(|a, b| b.partial_cmp(a).unwrap());
231 }
232
233 #[cfg(target_arch = "wasm32")]
235 {
236 res = self
237 .licenses
238 .iter()
239 .fold(Vec::with_capacity(self.licenses.len()), analyze_fold);
241 res.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap());
242 }
243
244 let m = &res[0];
245
246 Match {
247 score: m.score,
248 name: m.name,
249 license_type: m.license_type,
250 data: m.data,
251 }
252 }
253
254 pub fn from_cache<R>(mut readable: R) -> Result<Store, Error>
262 where
263 R: Read + Sized,
264 {
265 let mut header = [0u8; HEADER_LENGTH];
266 readable.read_exact(&mut header).map_err(|e| Error::io(e, None))?;
267
268 let cf = match &header {
269 b"scallion-00-zstd" => CF::Zstd,
270 b"scallion-00-gzip" => CF::Gzip,
271 b"scallion-00-none" => CF::None,
272 _ => return Err(Error::cache_version(header.to_vec())),
273 };
274
275 match cf {
276 CF::Zstd => {
277 #[cfg(feature = "zstd")]
278 {
279 let dec = zstd::Decoder::new(readable).map_err(|e| Error::io(e, None))?;
280 let store = rmp_serde::decode::from_read(dec)?;
281 Ok(store)
282 }
283 #[cfg(not(feature = "zstd"))]
284 {
285 Err(Error::cache_format(cf))
286 }
287 },
288 CF::Gzip => {
289 #[cfg(feature = "gzip")]
290 {
291 let dec = flate2::read::GzDecoder::new(readable);
292 let store = rmp_serde::decode::from_read(dec)?;
293 Ok(store)
294 }
295 #[cfg(not(feature = "gzip"))]
296 {
297 Err(Error::cache_format(cf))
298 }
299 },
300 CF::None => {
301 let store = rmp_serde::decode::from_read(readable)?;
302 Ok(store)
303 },
304 }
305 }
306
307 pub fn to_cache<W>(&self, mut writable: W, format: CF) -> Result<(), Error>
312 where
313 W: Write + Sized,
314 {
315 let serialize = || -> Result<Vec<u8>, Error> {
316 let mut buf = Vec::with_capacity(4 * 1024 * 1024);
318 let mut serializer = Serializer::new(&mut buf);
319 self.serialize(&mut serializer)?;
320 Ok(buf)
321 };
322
323 match format {
324 CF::Zstd => {
325 #[cfg(feature = "zstd")]
326 {
327 writable.write_all(CACHE_VERSION).map_err(|e| Error::io(e, None))?;
328 writable.write_all(b"-zstd").map_err(|e| Error::io(e, None))?;
329
330 let serialized = serialize()?;
331 let mut enc = zstd::Encoder::new(writable, 21).map_err(|e| Error::io(e, None))?;
332
333 io::copy(&mut serialized.as_slice(), &mut enc).map_err(|e| Error::io(e, None))?;
334 enc.finish().map_err(|e| Error::io(e, None))?;
335 Ok(())
336 }
337 #[cfg(not(feature = "zstd"))]
338 {
339 Err(Error::cache_format(format))
340 }
341 },
342 CF::Gzip => {
343 #[cfg(feature = "gzip")]
344 {
345 writable.write_all(CACHE_VERSION).map_err(|e| Error::io(e, None))?;
346 writable.write_all(b"-gzip").map_err(|e| Error::io(e, None))?;
347
348 let serialized = serialize()?;
349 let mut enc = flate2::write::GzEncoder::new(writable, flate2::Compression::default());
350
351 io::copy(&mut serialized.as_slice(), &mut enc).map_err(|e| Error::io(e, None))?;
352 enc.finish().map_err(|e| Error::io(e, None))?;
353 Ok(())
354 }
355 #[cfg(not(feature = "gzip"))]
356 {
357 Err(Error::cache_format(format))
358 }
359 },
360 CF::None => {
361 writable.write_all(CACHE_VERSION).map_err(|e| Error::io(e, None))?;
362 writable.write_all(b"-none").map_err(|e| Error::io(e, None))?;
363
364 let serialized = serialize()?;
365
366 io::copy(&mut serialized.as_slice(), &mut writable).map_err(|e| Error::io(e, None))?;
367 Ok(())
368 },
369 }
370 }
371
372 pub fn load_spdx<P: AsRef<Path>>(&mut self, dir: P) -> Result<(), Error> {
379 let paths = locate_json_files(dir)?;
380
381 for path in paths {
382 let parsed = parse_json_file(&path)?;
383
384 if parsed.deprecated {
385 debug!("Skipping {} (deprecated)", parsed.name);
386 continue;
387 }
388 info!("Processing {}", parsed.name);
389
390 let data = MatchData::new(&parsed.text);
391 let header = parsed.header.as_deref().map(MatchData::new);
392
393 self.insert_or_add_alias(&parsed.name, data, header);
394 }
395
396 Ok(())
397 }
398}
399
400#[derive(Clone, Copy, Debug, PartialEq)]
402pub enum CompressionFormat {
403 Zstd,
405 Gzip,
407 None,
409}
410
411use CompressionFormat as CF;
412
413impl Display for CompressionFormat {
414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415 match self {
416 CF::Zstd => write!(f, "zstd"),
417 CF::Gzip => write!(f, "gzip"),
418 CF::None => write!(f, "none"),
419 }
420 }
421}
422
423impl FromStr for CompressionFormat {
424 type Err = String;
425
426 fn from_str(s: &str) -> Result<Self, Self::Err> {
427 match s {
428 "zstd" => Ok(CF::Zstd),
429 "gzip" => Ok(CF::Gzip),
430 "none" => Ok(CF::None),
431 _ => Err(format!("Invalid compression format: '{s}'")),
432 }
433 }
434}
435
436#[derive(Clone)]
443pub struct Match<'a> {
444 pub score: f32,
446 pub name: &'a str,
449 pub license_type: LicenseType,
452 pub data: &'a MatchData,
455}
456
457struct PartialMatch<'a> {
460 pub name: &'a str,
461 pub score: f32,
462 pub license_type: LicenseType,
463 pub data: &'a MatchData,
464}
465
466impl PartialOrd for PartialMatch<'_> {
467 fn partial_cmp(&self, other: &PartialMatch<'_>) -> Option<Ordering> {
468 self.score.partial_cmp(&other.score)
469 }
470}
471
472impl PartialEq for PartialMatch<'_> {
473 fn eq(&self, other: &PartialMatch<'_>) -> bool {
474 self.score.eq(&other.score) && self.name == other.name && self.license_type == other.license_type
475 }
476}
477
478impl fmt::Debug for Match<'_> {
479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480 write!(
481 f,
482 "Match {{ score: {}, name: {}, license_type: {:?} }}",
483 self.score, self.name, self.license_type
484 )
485 }
486}
487
488fn locate_json_files<P: AsRef<Path>>(dir: P) -> Result<Vec<PathBuf>, Error> {
489 let mut paths: Vec<_> = fs::read_dir(&dir)
491 .map_err(|e| Error::io(e, Some(dir.as_ref().to_path_buf())))?
492 .filter_map(Result::ok)
493 .map(|e| e.path())
494 .filter(|p| p.is_file() && p.extension().unwrap_or_else(|| OsStr::new("")) == "json")
495 .collect();
496
497 paths.sort_by(|a, b| a.file_stem().unwrap().cmp(b.file_stem().unwrap()));
499
500 Ok(paths)
501}
502
503#[derive(Deserialize)]
504struct LicenseListData {
505 #[serde(rename = "licenseId")]
506 name: String,
507 #[serde(rename = "isDeprecatedLicenseId")]
508 deprecated: bool,
509 #[serde(rename = "licenseText")]
510 text: String,
511 #[serde(rename = "standardLicenseHeader")]
512 header: Option<String>,
513 }
515
516fn parse_json_file<P: AsRef<Path>>(path: P) -> Result<LicenseListData, Error> {
517 let path = path.as_ref().to_path_buf();
518 let data = fs::read_to_string(&path).map_err(|e| Error::io(e, Some(path.clone())))?;
519 serde_json::from_str(&data).map_err(|e| Error::spdx(e, path))
520}