1pub mod aho_match;
10pub mod automaton;
11pub mod build_policy;
12pub mod dataset;
13pub(crate) mod detection;
14pub mod embedded;
15pub mod license_cache;
16mod position_set;
17mod token_multiset;
18mod token_set;
19
20#[cfg(test)]
21mod embedded_test;
22pub mod expression;
23#[cfg(feature = "golden-tests")]
24pub mod golden_utils;
25pub mod hash_match;
26pub mod index;
27mod match_refine;
28pub mod models;
29pub mod query;
30pub mod rules;
31pub mod seq_match;
32pub mod spdx_key_canonicalization;
33pub mod spdx_lid;
34pub mod spdx_mapping;
35#[cfg(test)]
36pub(crate) mod test_utils;
37pub mod tokenize;
38pub mod unknown_match;
39
40use bit_set::BitSet;
41use std::collections::HashSet;
42use std::fs;
43use std::path::Path;
44use std::sync::Arc;
45use std::time::Instant;
46
47use anyhow::Result;
48
49use crate::license_detection::build_policy::EMBEDDED_LICENSE_INDEX_SOURCE;
50use crate::license_detection::dataset::{
51 CUSTOM_LICENSE_DATASET_SOURCE, LoadedLicenseDataset, compute_dataset_fingerprint_string,
52 load_license_dataset_from_root,
53};
54use crate::license_detection::embedded::index::{
55 load_embedded_artifact_metadata_from_bytes, load_loader_snapshot_from_bytes,
56};
57use crate::license_detection::index::build_index_from_loaded;
58use crate::license_detection::license_cache::{
59 LicenseCacheConfig, LicenseCacheNamespace, cache_file_size, compute_artifact_fingerprint,
60 compute_rules_fingerprint, delete_cache, load_cached_index, save_cached_index,
61};
62use crate::license_detection::query::Query;
63use crate::license_detection::spdx_mapping::{SpdxMapping, build_spdx_mapping};
64use crate::models::LicenseIndexProvenance;
65use crate::utils::text::strip_utf8_bom_str;
66
67use crate::license_detection::detection::{
68 attach_source_path_to_detections, empty_detection, populate_detection_from_group_with_spdx,
69 split_groups_across_frontmatter_boundary,
70};
71
72#[allow(dead_code)]
75pub const SCANCODE_LICENSES_RULES_PATH: &str =
76 "reference/scancode-toolkit/src/licensedcode/data/rules";
77
78#[allow(dead_code)]
81pub const SCANCODE_LICENSES_LICENSES_PATH: &str =
82 "reference/scancode-toolkit/src/licensedcode/data/licenses";
83
84#[allow(dead_code)]
87pub const SCANCODE_LICENSES_DATA_PATH: &str = "reference/scancode-toolkit/src/licensedcode/data";
88
89pub const DEFAULT_LICENSEDB_URL_TEMPLATE: &str = "https://scancode-licensedb.aboutcode.org/{}";
90#[derive(Debug, Clone, thiserror::Error)]
91pub(crate) enum LicenseDetectionError {
92 #[error("license detection timed out")]
93 Timeout,
94}
95
96pub(crate) use detection::{
97 LicenseDetection, group_matches_by_region, post_process_detections, sort_matches_by_line,
98};
99pub use models::LicenseMatch;
100pub use models::MatcherKind;
101
102pub use aho_match::aho_match;
103pub use hash_match::hash_match;
104pub use match_refine::{
105 filter_invalid_contained_unknown_matches, merge_overlapping_matches, refine_matches,
106 refine_matches_without_false_positive_filter, split_weak_matches,
107};
108pub use position_set::PositionSet;
109pub use spdx_lid::spdx_lid_match;
110pub use token_multiset::TokenMultiset;
111pub use token_set::{HighBitset, TokenSet};
112pub use unknown_match::unknown_match;
113
114use self::seq_match::{
115 MAX_NEAR_DUPE_CANDIDATES, select_seq_candidates_with_deadline,
116 seq_match_with_candidates_and_deadline,
117};
118
119#[derive(Debug, Clone)]
125pub struct LicenseDetectionEngine {
126 index: Arc<index::LicenseIndex>,
127 spdx_mapping: SpdxMapping,
128 spdx_license_list_version: Option<String>,
129 license_index_provenance: Option<LicenseIndexProvenance>,
130}
131
132const MAX_DETECTION_SIZE: usize = 10 * 1024 * 1024; const MAX_REGULAR_SEQ_CANDIDATES: usize = 70;
134
135const MAX_SEQ_QUERY_RUN_TOKENS: usize = 50_000;
155const MAX_REDUNDANT_SEQ_CONTAINER_BOUNDARY_GAP: usize = 8;
156const MAX_REDUNDANT_SEQ_CONTAINER_UNMATCHED_GAP: usize = 2;
157
158pub(crate) fn deadline_exceeded(deadline: Option<Instant>) -> bool {
159 deadline.is_some_and(|deadline| Instant::now() >= deadline)
160}
161
162pub(crate) fn ensure_within_deadline(
163 deadline: Option<Instant>,
164) -> Result<(), LicenseDetectionError> {
165 if deadline_exceeded(deadline) {
166 Err(LicenseDetectionError::Timeout)
167 } else {
168 Ok(())
169 }
170}
171
172fn truncate_detection_text(clean_text: &str) -> &str {
173 if clean_text.len() <= MAX_DETECTION_SIZE {
174 return clean_text;
175 }
176
177 log::debug!(
178 "Content size {} exceeds limit {}, truncating for detection",
179 clean_text.len(),
180 MAX_DETECTION_SIZE
181 );
182
183 let boundary = clean_text.floor_char_boundary(MAX_DETECTION_SIZE);
184 &clean_text[..boundary]
185}
186
187fn query_span_for_match(m: &LicenseMatch) -> Option<models::PositionSpan> {
188 (!m.query_span().is_empty()).then(|| m.query_span().clone())
189}
190
191fn has_full_match_coverage(m: &LicenseMatch) -> bool {
192 m.coverage() == 100.0
193}
194
195fn is_redundant_same_expression_seq_container(
196 container: &LicenseMatch,
197 candidate_contained_matches: &[LicenseMatch],
198) -> bool {
199 let container_is_redundant_coverage =
200 has_full_match_coverage(container) || container.coverage() >= 99.0;
201 if container.matcher != MatcherKind::Seq || !container_is_redundant_coverage {
202 return false;
203 }
204
205 let container_qspan_set = container.qspan_set();
206
207 let mut contained: Vec<&LicenseMatch> = candidate_contained_matches
208 .iter()
209 .filter(|m| {
210 m.matcher == MatcherKind::Aho
211 && has_full_match_coverage(m)
212 && m.license_expression == container.license_expression
213 && m.overlaps_with(&container_qspan_set)
214 })
215 .collect();
216
217 if contained.len() < 2 {
218 return false;
219 }
220
221 let material_children = contained.iter().filter(|m| m.matched_length > 1).count();
222 if material_children < 2 {
223 return false;
224 }
225
226 contained.sort_by_key(|m| m.qspan_bounds());
227
228 let mut child_union = PositionSet::new();
229 for m in &contained {
230 child_union.extend_from_span(m.query_span());
231 }
232
233 let container_only_positions = container_qspan_set.difference(&child_union);
234 let child_only_positions = child_union.difference(&container_qspan_set);
235
236 let mut bridge_positions = BitSet::new();
237 for pair in contained.windows(2) {
238 let (_, previous_end) = pair[0].qspan_bounds();
239 let (next_start, _) = pair[1].qspan_bounds();
240
241 if next_start < previous_end {
242 return false;
243 }
244
245 for pos in previous_end..next_start {
246 bridge_positions.insert(pos);
247 }
248 }
249
250 let container_only_boundary_positions = container_only_positions
251 .iter()
252 .filter(|&pos| !bridge_positions.contains(pos))
253 .count();
254
255 if container_only_positions.len() == 1
256 && container_only_boundary_positions == 0
257 && child_only_positions.is_empty()
258 {
259 return false;
260 }
261
262 if child_only_positions.is_empty()
263 && container_only_positions.len() == container_only_boundary_positions
264 && container_only_boundary_positions <= 3
265 {
266 let earliest_child = contained
267 .iter()
268 .map(|m| m.qspan_bounds().0)
269 .min()
270 .unwrap_or(usize::MAX);
271 let latest_child = contained
272 .iter()
273 .map(|m| m.qspan_bounds().1.saturating_sub(1))
274 .max()
275 .unwrap_or(0);
276
277 let is_one_sided_boundary = container_only_positions
278 .iter()
279 .all(|pos| pos < earliest_child)
280 || container_only_positions
281 .iter()
282 .all(|pos| pos > latest_child);
283
284 if is_one_sided_boundary {
285 return false;
286 }
287 }
288
289 let max_container_only_positions =
290 MAX_REDUNDANT_SEQ_CONTAINER_BOUNDARY_GAP * contained.len() + 1;
291 let max_container_boundary_positions =
292 MAX_REDUNDANT_SEQ_CONTAINER_BOUNDARY_GAP * (contained.len() - 1);
293 let max_child_only_positions = MAX_REDUNDANT_SEQ_CONTAINER_UNMATCHED_GAP + 1;
294
295 container_only_positions.len() <= max_container_only_positions
296 && container_only_boundary_positions <= max_container_boundary_positions
297 && child_only_positions.len() <= max_child_only_positions
298}
299
300fn filter_redundant_same_expression_seq_containers(
301 seq_matches: Vec<LicenseMatch>,
302 candidate_contained_matches: &[LicenseMatch],
303) -> Vec<LicenseMatch> {
304 seq_matches
305 .into_iter()
306 .filter(|m| !is_redundant_same_expression_seq_container(m, candidate_contained_matches))
307 .collect()
308}
309
310fn is_redundant_low_coverage_composite_seq_wrapper(
311 container: &LicenseMatch,
312 candidate_contained_matches: &[LicenseMatch],
313) -> bool {
314 if container.matcher != seq_match::MATCH_SEQ || container.coverage() >= 30.0 {
319 return false;
320 }
321
322 let container_qspan_set = container.qspan_set();
323
324 let children: Vec<&LicenseMatch> = candidate_contained_matches
325 .iter()
326 .filter(|m| {
327 m.matcher == aho_match::MATCH_AHO
328 && has_full_match_coverage(m)
329 && m.license_expression != container.license_expression
330 && m.overlaps_with(&container_qspan_set)
331 })
332 .collect();
333
334 if children.len() < 2 {
335 return false;
336 }
337
338 let unique_expressions: HashSet<&str> = children
339 .iter()
340 .map(|m| m.license_expression.as_str())
341 .collect();
342 if unique_expressions.len() < 2 {
343 return false;
344 }
345
346 let mut child_union = PositionSet::new();
347 for m in &children {
348 child_union.extend_from_span(m.query_span());
349 }
350
351 let container_only_positions = container_qspan_set.difference(&child_union);
352 let child_only_positions = child_union.difference(&container_qspan_set);
353
354 let mut sorted_children = children;
355 sorted_children.sort_by_key(|m| m.qspan_bounds());
356
357 let mut bridge_positions = BitSet::new();
358 for pair in sorted_children.windows(2) {
359 let (_, previous_end) = pair[0].qspan_bounds();
360 let (next_start, _) = pair[1].qspan_bounds();
361 for pos in previous_end..next_start {
362 bridge_positions.insert(pos);
363 }
364 }
365
366 let container_only_boundary_positions = container_only_positions
367 .iter()
368 .filter(|&pos| !bridge_positions.contains(pos))
369 .count();
370
371 child_only_positions.is_empty()
372 && container_only_positions.len() <= MAX_REDUNDANT_SEQ_CONTAINER_BOUNDARY_GAP
373 && container_only_boundary_positions <= MAX_REDUNDANT_SEQ_CONTAINER_BOUNDARY_GAP
374}
375
376fn filter_redundant_low_coverage_composite_seq_wrappers(
377 seq_matches: Vec<LicenseMatch>,
378 candidate_contained_matches: &[LicenseMatch],
379) -> Vec<LicenseMatch> {
380 seq_matches
381 .into_iter()
382 .filter(|m| {
383 !is_redundant_low_coverage_composite_seq_wrapper(m, candidate_contained_matches)
384 })
385 .collect()
386}
387
388fn subtract_spdx_match_qspans(
389 query: &mut Query<'_>,
390 matched_qspans: &mut Vec<models::PositionSpan>,
391 aho_extra_matchables: &mut PositionSet,
392 spdx_matches: &[LicenseMatch],
393) {
394 for m in spdx_matches {
395 let Some(span) = query_span_for_match(m) else {
396 continue;
397 };
398
399 aho_extra_matchables.extend_from_span(&span);
400 query.subtract(&span);
401
402 if has_full_match_coverage(m) {
403 matched_qspans.push(span);
404 }
405 }
406}
407
408fn merge_and_prepare_aho_matches(
409 index: &index::LicenseIndex,
410 query: &mut Query<'_>,
411 matched_qspans: &mut Vec<models::PositionSpan>,
412 refined_aho: &[LicenseMatch],
413) -> (Vec<LicenseMatch>, bool) {
414 let merged_aho = merge_overlapping_matches(refined_aho);
415 let mut saw_long_exact_license_text_match = false;
416
417 for m in &merged_aho {
418 let Some(span) = query_span_for_match(m) else {
419 continue;
420 };
421
422 if has_full_match_coverage(m) {
423 matched_qspans.push(span.clone());
424 }
425
426 if index.rule(m.rid).is_some_and(|rule| rule.is_license_text())
427 && m.rule_length > 120
428 && m.coverage() > 98.0
429 {
430 query.subtract(&span);
431 saw_long_exact_license_text_match = true;
432 }
433 }
434
435 (merged_aho, saw_long_exact_license_text_match)
436}
437
438fn collect_whole_query_exact_followup_matches(
439 index: &index::LicenseIndex,
440 query: &mut Query<'_>,
441 matched_qspans: &mut Vec<models::PositionSpan>,
442 whole_run: &query::QueryRun<'_>,
443 enable_sequence_matching: bool,
444 deadline: Option<Instant>,
445) -> Result<Vec<LicenseMatch>, LicenseDetectionError> {
446 if !enable_sequence_matching {
447 return Ok(Vec::new());
448 }
449
450 let mut seq_all_matches = Vec::new();
451
452 if whole_run.tokens().len() > MAX_SEQ_QUERY_RUN_TOKENS {
455 return Ok(seq_all_matches);
456 }
457
458 if whole_run.is_matchable(false, matched_qspans) {
459 let near_dupe_candidates = if deadline.is_some() {
460 select_seq_candidates_with_deadline(
461 index,
462 whole_run,
463 true,
464 MAX_NEAR_DUPE_CANDIDATES,
465 deadline,
466 )?
467 } else {
468 self::seq_match::select_seq_candidates(index, whole_run, true, MAX_NEAR_DUPE_CANDIDATES)
469 };
470
471 if !near_dupe_candidates.is_empty() {
472 let near_dupe_matches = if deadline.is_some() {
473 seq_match_with_candidates_and_deadline(
474 index,
475 whole_run,
476 &near_dupe_candidates,
477 deadline,
478 )?
479 } else {
480 self::seq_match::seq_match_with_candidates(index, whole_run, &near_dupe_candidates)
481 };
482
483 for m in &near_dupe_matches {
484 if !m.query_span().is_empty() {
485 let span = m.query_span().clone();
486 query.subtract(&span);
487 matched_qspans.push(span);
488 }
489 }
490
491 seq_all_matches.extend(near_dupe_matches);
492 }
493 }
494
495 Ok(seq_all_matches)
496}
497
498fn collect_regular_seq_matches(
499 index: &index::LicenseIndex,
500 query: &Query<'_>,
501 matched_qspans: &[models::PositionSpan],
502 candidate_contained_matches: &[LicenseMatch],
503 deadline: Option<Instant>,
504) -> Result<Vec<LicenseMatch>, LicenseDetectionError> {
505 let mut seq_all_matches = Vec::new();
506
507 for (query_run_index, query_run) in query.query_runs().into_iter().enumerate() {
508 if query_run_index % 8 == 0 {
509 ensure_within_deadline(deadline)?;
510 }
511
512 if !query_run.is_matchable(false, matched_qspans) {
513 continue;
514 }
515
516 if query_run.tokens().len() > MAX_SEQ_QUERY_RUN_TOKENS {
519 continue;
520 }
521
522 let candidates = if deadline.is_some() {
523 select_seq_candidates_with_deadline(
524 index,
525 &query_run,
526 false,
527 MAX_REGULAR_SEQ_CANDIDATES,
528 deadline,
529 )?
530 } else {
531 self::seq_match::select_seq_candidates(
532 index,
533 &query_run,
534 false,
535 MAX_REGULAR_SEQ_CANDIDATES,
536 )
537 };
538 if !candidates.is_empty() {
539 let matches = if deadline.is_some() {
540 seq_match_with_candidates_and_deadline(index, &query_run, &candidates, deadline)?
541 } else {
542 self::seq_match::seq_match_with_candidates(index, &query_run, &candidates)
543 };
544 seq_all_matches.extend(matches);
545 }
546 }
547
548 let merged_seq = merge_overlapping_matches(&seq_all_matches);
549 let filtered_same_expression =
550 filter_redundant_same_expression_seq_containers(merged_seq, candidate_contained_matches);
551 Ok(filter_redundant_low_coverage_composite_seq_wrappers(
552 filtered_same_expression,
553 candidate_contained_matches,
554 ))
555}
556
557pub type ColdBuildNotify<'a> = &'a dyn Fn();
562
563impl LicenseDetectionEngine {
564 fn from_index(
569 index: index::LicenseIndex,
570 spdx_license_list_version: Option<String>,
571 license_index_provenance: Option<LicenseIndexProvenance>,
572 ) -> Result<Self> {
573 let mut license_vec: Vec<_> = index.licenses_by_key.values().cloned().collect();
574 license_vec.sort_by(|a, b| a.key.cmp(&b.key));
575 let spdx_mapping = build_spdx_mapping(&license_vec);
576
577 Ok(Self {
578 index: Arc::new(index),
579 spdx_mapping,
580 spdx_license_list_version,
581 license_index_provenance,
582 })
583 }
584
585 #[cfg(test)]
586 pub(crate) fn from_test_index(index: index::LicenseIndex) -> Self {
587 Self::from_index(index, None, None).expect("test index should build license engine")
588 }
589
590 #[cfg(test)]
591 pub(crate) fn from_test_index_with_provenance(
592 index: index::LicenseIndex,
593 license_index_provenance: LicenseIndexProvenance,
594 ) -> Self {
595 Self::from_index(index, None, Some(license_index_provenance))
596 .expect("test index should build license engine")
597 }
598
599 pub fn from_embedded() -> Result<Self> {
604 let cache_config =
605 LicenseCacheConfig::new(LicenseCacheConfig::default_root_dir(), false, true);
606 Self::from_embedded_with_cache(&cache_config, None)
607 }
608
609 pub fn from_embedded_with_cache(
626 cache_config: &LicenseCacheConfig,
627 on_cold_build: Option<ColdBuildNotify<'_>>,
628 ) -> Result<Self> {
629 let artifact_bytes = include_bytes!("../../resources/license_detection/license_index.zst");
630 let fingerprint = compute_artifact_fingerprint(artifact_bytes);
631 let artifact_metadata = load_embedded_artifact_metadata_from_bytes(artifact_bytes)
632 .map_err(|e| {
633 anyhow::anyhow!("Failed to load embedded license artifact metadata: {}", e)
634 })?;
635 debug_assert_eq!(
636 artifact_metadata.license_index_provenance.source,
637 EMBEDDED_LICENSE_INDEX_SOURCE
638 );
639 let spdx_version = Some(artifact_metadata.spdx_license_list_version.clone());
640 let provenance = Some(artifact_metadata.license_index_provenance.clone());
641
642 if !cache_config.reindex {
643 let start = Instant::now();
644 if let Some(cached) =
645 load_cached_index(cache_config, LicenseCacheNamespace::Embedded, &fingerprint)?
646 {
647 log::debug!(
648 "License index loaded from rkyv cache in {:.2}s",
649 start.elapsed().as_secs_f64()
650 );
651 return Self::from_index(cached, spdx_version, provenance);
652 }
653 } else {
654 delete_cache(cache_config, LicenseCacheNamespace::Embedded, &fingerprint)?;
655 }
656
657 if let Some(notify) = on_cold_build {
659 notify();
660 }
661
662 let snapshot = load_loader_snapshot_from_bytes(artifact_bytes)
663 .map_err(|e| anyhow::anyhow!("Failed to load embedded license index: {}", e))?;
664 let spdx_version = Some(snapshot.metadata.spdx_license_list_version.clone());
665 let provenance = Some(snapshot.metadata.license_index_provenance.clone());
666
667 let start = Instant::now();
668 let index = build_index_from_loaded(snapshot.rules, snapshot.licenses, false);
669 log::debug!(
670 "License index built from embedded artifact in {:.2}s",
671 start.elapsed().as_secs_f64()
672 );
673
674 let mut index = index;
675 index.spdx_license_list_version = spdx_version.clone();
676 if let Err(e) = save_cached_index(
677 cache_config,
678 LicenseCacheNamespace::Embedded,
679 &index,
680 &fingerprint,
681 ) {
682 log::warn!("Failed to save license index cache: {e}");
683 } else if let Some(size) =
684 cache_file_size(cache_config, LicenseCacheNamespace::Embedded, &fingerprint)
685 {
686 log::debug!(
687 "License index cache saved ({:.1} MB)",
688 size as f64 / 1_048_576.0
689 );
690 }
691
692 Self::from_index(index, spdx_version, provenance)
693 }
694
695 pub fn from_directory(rules_path: &Path) -> Result<Self> {
700 let cache_config =
701 LicenseCacheConfig::new(LicenseCacheConfig::default_root_dir(), false, true);
702 Self::from_directory_with_cache(rules_path, &cache_config, None)
703 }
704
705 pub fn from_directory_with_cache(
719 rules_path: &Path,
720 cache_config: &LicenseCacheConfig,
721 on_cold_build: Option<ColdBuildNotify<'_>>,
722 ) -> Result<Self> {
723 let LoadedLicenseDataset {
724 manifest,
725 rules: loaded_rules,
726 licenses: loaded_licenses,
727 } = load_license_dataset_from_root(rules_path)?;
728
729 let fingerprint = compute_rules_fingerprint(&loaded_rules, &loaded_licenses)?;
730 let provenance = Some(LicenseIndexProvenance {
731 source: CUSTOM_LICENSE_DATASET_SOURCE.to_string(),
732 dataset_fingerprint: compute_dataset_fingerprint_string(
733 &loaded_rules,
734 &loaded_licenses,
735 )?,
736 ignored_rules: vec![],
737 ignored_licenses: vec![],
738 ignored_rules_due_to_licenses: vec![],
739 added_rules: vec![],
740 replaced_rules: vec![],
741 added_licenses: vec![],
742 replaced_licenses: vec![],
743 });
744
745 if !cache_config.reindex {
746 let start = Instant::now();
747 if let Some(cached) = load_cached_index(
748 cache_config,
749 LicenseCacheNamespace::CustomRules,
750 &fingerprint,
751 )? {
752 log::debug!(
753 "License index loaded from rkyv cache in {:.2}s",
754 start.elapsed().as_secs_f64()
755 );
756 return Self::from_index(
757 cached,
758 Some(manifest.spdx_license_list_version),
759 provenance,
760 );
761 }
762 } else {
763 delete_cache(
764 cache_config,
765 LicenseCacheNamespace::CustomRules,
766 &fingerprint,
767 )?;
768 }
769
770 if let Some(notify) = on_cold_build {
772 notify();
773 }
774
775 let start = Instant::now();
776 let index = build_index_from_loaded(loaded_rules, loaded_licenses, false);
777 log::debug!(
778 "License index built from custom dataset in {:.2}s",
779 start.elapsed().as_secs_f64()
780 );
781
782 if let Err(e) = save_cached_index(
783 cache_config,
784 LicenseCacheNamespace::CustomRules,
785 &index,
786 &fingerprint,
787 ) {
788 log::warn!("Failed to save license index cache: {e}");
789 } else if let Some(size) = cache_file_size(
790 cache_config,
791 LicenseCacheNamespace::CustomRules,
792 &fingerprint,
793 ) {
794 log::debug!(
795 "License index cache saved ({:.1} MB)",
796 size as f64 / 1_048_576.0
797 );
798 }
799
800 Self::from_index(index, Some(manifest.spdx_license_list_version), provenance)
801 }
802
803 pub fn embedded_spdx_license_list_version() -> Result<String> {
804 let artifact_bytes = include_bytes!("../../resources/license_detection/license_index.zst");
805 Ok(load_embedded_artifact_metadata_from_bytes(artifact_bytes)
806 .map_err(|e| {
807 anyhow::anyhow!("Failed to load embedded license artifact metadata: {}", e)
808 })?
809 .spdx_license_list_version)
810 }
811
812 pub fn detect_with_kind(
813 &self,
814 text: &str,
815 unknown_licenses: bool,
816 binary_derived: bool,
817 ) -> Result<Vec<LicenseDetection>> {
818 self.detect_with_kind_with_score_and_deadline_with_options(
819 text,
820 unknown_licenses,
821 binary_derived,
822 true,
823 0.0,
824 None,
825 )
826 .map_err(Into::into)
827 }
828
829 pub fn detect_with_kind_with_score(
830 &self,
831 text: &str,
832 unknown_licenses: bool,
833 binary_derived: bool,
834 min_score: f32,
835 ) -> Result<Vec<LicenseDetection>> {
836 self.detect_with_kind_with_score_and_deadline_with_options(
837 text,
838 unknown_licenses,
839 binary_derived,
840 true,
841 min_score,
842 None,
843 )
844 .map_err(Into::into)
845 }
846
847 pub(crate) fn detect_with_kind_with_score_and_deadline_with_options(
848 &self,
849 text: &str,
850 unknown_licenses: bool,
851 binary_derived: bool,
852 enable_sequence_matching: bool,
853 min_score: f32,
854 deadline: Option<Instant>,
855 ) -> Result<Vec<LicenseDetection>, LicenseDetectionError> {
856 ensure_within_deadline(deadline)?;
857 let clean_text = strip_utf8_bom_str(text);
858
859 let content = truncate_detection_text(clean_text);
860
861 ensure_within_deadline(deadline)?;
862 let mut query = if deadline.is_some() {
863 Query::from_extracted_text_with_deadline(
864 content,
865 &self.index,
866 binary_derived,
867 deadline,
868 )?
869 } else {
870 Query::from_extracted_text(content, &self.index, binary_derived)?
871 };
872 let whole_query_run = query.whole_query_run();
873
874 let mut all_matches = Vec::new();
875 let mut candidate_contained_matches = Vec::new();
876 let mut aho_extra_matchables = PositionSet::new();
877 let mut matched_qspans: Vec<models::PositionSpan> = Vec::new();
878
879 {
882 ensure_within_deadline(deadline)?;
883 let hash_matches = hash_match(&self.index, &whole_query_run);
884
885 if !hash_matches.is_empty() {
886 let mut matches = hash_matches;
887 sort_matches_by_line(&mut matches);
888
889 let groups = split_groups_across_frontmatter_boundary(
890 group_matches_by_region(&matches),
891 Some(content),
892 );
893 let detections: Vec<LicenseDetection> = groups
894 .iter()
895 .map(|group| {
896 let mut detection = empty_detection();
897 populate_detection_from_group_with_spdx(
898 &mut detection,
899 group,
900 &self.spdx_mapping,
901 Some(content),
902 );
903 detection
904 })
905 .collect();
906
907 return Ok(post_process_detections(detections, min_score));
908 }
909 }
910
911 {
913 ensure_within_deadline(deadline)?;
914 let spdx_matches = spdx_lid_match(&self.index, &query);
915 subtract_spdx_match_qspans(
916 &mut query,
917 &mut matched_qspans,
918 &mut aho_extra_matchables,
919 &spdx_matches,
920 );
921 all_matches.extend(spdx_matches);
922 }
923
924 {
926 ensure_within_deadline(deadline)?;
927 let aho_matches = if aho_extra_matchables.is_empty() {
928 if deadline.is_some() {
929 aho_match::aho_match_with_deadline(&self.index, &whole_query_run, deadline)?
930 } else {
931 aho_match(&self.index, &whole_query_run)
932 }
933 } else {
934 if deadline.is_some() {
935 aho_match::aho_match_with_extra_matchables(
936 &self.index,
937 &whole_query_run,
938 Some(&aho_extra_matchables),
939 deadline,
940 )?
941 } else {
942 aho_match::aho_match_with_extra_matchables(
943 &self.index,
944 &whole_query_run,
945 Some(&aho_extra_matchables),
946 None,
947 )?
948 }
949 };
950
951 let refined_aho = match_refine::refine_aho_matches(&self.index, aho_matches, &query);
954 candidate_contained_matches.extend(refined_aho.clone());
955 let (merged_aho, _) = merge_and_prepare_aho_matches(
956 &self.index,
957 &mut query,
958 &mut matched_qspans,
959 &refined_aho,
960 );
961 all_matches.extend(merged_aho);
962
963 let whole_query_followup = collect_whole_query_exact_followup_matches(
964 &self.index,
965 &mut query,
966 &mut matched_qspans,
967 &whole_query_run,
968 enable_sequence_matching,
969 deadline,
970 )?;
971 all_matches.extend(whole_query_followup);
972
973 if enable_sequence_matching {
974 let merged_seq = collect_regular_seq_matches(
975 &self.index,
976 &query,
977 &matched_qspans,
978 &candidate_contained_matches,
979 deadline,
980 )?;
981 all_matches.extend(merged_seq);
982 }
983 }
984
985 ensure_within_deadline(deadline)?;
988 let merged_matches =
989 refine_matches_without_false_positive_filter(&self.index, all_matches, &query);
990
991 let refined_matches = if unknown_licenses {
994 let (good_matches, weak_matches) = split_weak_matches(&self.index, &merged_matches);
996
997 let unknown_matches = unknown_match(&self.index, &query, &good_matches);
999 let filtered_unknown =
1000 filter_invalid_contained_unknown_matches(&unknown_matches, &good_matches);
1001
1002 let mut all_matches = good_matches;
1003 all_matches.extend(filtered_unknown);
1004 all_matches.extend(weak_matches);
1007 all_matches
1008 } else {
1009 merged_matches
1010 };
1011
1012 ensure_within_deadline(deadline)?;
1014 let refined = refine_matches(&self.index, refined_matches, &query);
1015
1016 let mut sorted = refined;
1017 sort_matches_by_line(&mut sorted);
1018
1019 let groups = split_groups_across_frontmatter_boundary(
1020 group_matches_by_region(&sorted),
1021 Some(content),
1022 );
1023
1024 let detections: Vec<LicenseDetection> = groups
1025 .iter()
1026 .map(|group| {
1027 let mut detection = empty_detection();
1028 populate_detection_from_group_with_spdx(
1029 &mut detection,
1030 group,
1031 &self.spdx_mapping,
1032 Some(content),
1033 );
1034 detection
1035 })
1036 .collect();
1037
1038 let detections = post_process_detections(detections, min_score);
1039
1040 ensure_within_deadline(deadline)?;
1041 Ok(detections)
1042 }
1043
1044 pub fn detect_with_kind_and_source(
1045 &self,
1046 text: &str,
1047 unknown_licenses: bool,
1048 binary_derived: bool,
1049 source_path: &str,
1050 ) -> Result<Vec<LicenseDetection>> {
1051 self.detect_with_kind_and_source_with_deadline_and_options(
1052 text,
1053 unknown_licenses,
1054 binary_derived,
1055 source_path,
1056 true,
1057 None,
1058 )
1059 .map_err(Into::into)
1060 }
1061
1062 pub(crate) fn detect_with_kind_and_source_with_deadline_and_options(
1063 &self,
1064 text: &str,
1065 unknown_licenses: bool,
1066 binary_derived: bool,
1067 source_path: &str,
1068 enable_sequence_matching: bool,
1069 deadline: Option<Instant>,
1070 ) -> Result<Vec<LicenseDetection>, LicenseDetectionError> {
1071 let mut detections = self.detect_with_kind_with_score_and_deadline_with_options(
1072 text,
1073 unknown_licenses,
1074 binary_derived,
1075 enable_sequence_matching,
1076 0.0,
1077 deadline,
1078 )?;
1079 attach_source_path_to_detections(&mut detections, source_path);
1080 Ok(detections)
1081 }
1082
1083 pub fn detect_with_kind_and_source_with_score(
1084 &self,
1085 text: &str,
1086 unknown_licenses: bool,
1087 binary_derived: bool,
1088 source_path: &str,
1089 min_score: f32,
1090 ) -> Result<Vec<LicenseDetection>> {
1091 self.detect_with_kind_and_source_with_score_options(
1092 text,
1093 unknown_licenses,
1094 binary_derived,
1095 source_path,
1096 true,
1097 min_score,
1098 )
1099 }
1100
1101 pub fn detect_with_kind_and_source_with_options(
1102 &self,
1103 text: &str,
1104 unknown_licenses: bool,
1105 binary_derived: bool,
1106 source_path: &str,
1107 enable_sequence_matching: bool,
1108 ) -> Result<Vec<LicenseDetection>> {
1109 self.detect_with_kind_and_source_with_score_options(
1110 text,
1111 unknown_licenses,
1112 binary_derived,
1113 source_path,
1114 enable_sequence_matching,
1115 0.0,
1116 )
1117 }
1118
1119 pub fn detect_with_kind_and_source_with_score_options(
1120 &self,
1121 text: &str,
1122 unknown_licenses: bool,
1123 binary_derived: bool,
1124 source_path: &str,
1125 enable_sequence_matching: bool,
1126 min_score: f32,
1127 ) -> Result<Vec<LicenseDetection>> {
1128 let mut detections = self.detect_with_kind_with_score_and_deadline_with_options(
1129 text,
1130 unknown_licenses,
1131 binary_derived,
1132 enable_sequence_matching,
1133 min_score,
1134 None,
1135 )?;
1136 attach_source_path_to_detections(&mut detections, source_path);
1137 Ok(detections)
1138 }
1139
1140 #[cfg(any(test, feature = "golden-tests"))]
1145 pub fn detect_matches_with_kind(
1146 &self,
1147 text: &str,
1148 unknown_licenses: bool,
1149 binary_derived: bool,
1150 ) -> Result<Vec<LicenseMatch>> {
1151 let clean_text = strip_utf8_bom_str(text);
1152
1153 let content = truncate_detection_text(clean_text);
1154
1155 let mut query = Query::from_extracted_text(content, &self.index, binary_derived)?;
1156 let whole_query_run = query.whole_query_run();
1157
1158 let mut all_matches = Vec::new();
1159 let mut candidate_contained_matches = Vec::new();
1160 let mut aho_extra_matchables = PositionSet::new();
1161 let mut matched_qspans: Vec<models::PositionSpan> = Vec::new();
1162
1163 {
1165 let hash_matches = hash_match(&self.index, &whole_query_run);
1166
1167 if !hash_matches.is_empty() {
1168 let mut matches = hash_matches;
1169 sort_matches_by_line(&mut matches);
1170 return Ok(matches);
1171 }
1172 }
1173
1174 {
1176 let spdx_matches = spdx_lid_match(&self.index, &query);
1177 subtract_spdx_match_qspans(
1178 &mut query,
1179 &mut matched_qspans,
1180 &mut aho_extra_matchables,
1181 &spdx_matches,
1182 );
1183 all_matches.extend(spdx_matches);
1184 }
1185
1186 {
1188 let aho_matches = if aho_extra_matchables.is_empty() {
1189 aho_match(&self.index, &whole_query_run)
1190 } else {
1191 aho_match::aho_match_with_extra_matchables(
1192 &self.index,
1193 &whole_query_run,
1194 Some(&aho_extra_matchables),
1195 None,
1196 )?
1197 };
1198 let refined_aho = match_refine::refine_aho_matches(&self.index, aho_matches, &query);
1199 candidate_contained_matches.extend(refined_aho.clone());
1200 let (merged_aho, _) = merge_and_prepare_aho_matches(
1201 &self.index,
1202 &mut query,
1203 &mut matched_qspans,
1204 &refined_aho,
1205 );
1206 all_matches.extend(merged_aho);
1207
1208 let whole_query_followup = collect_whole_query_exact_followup_matches(
1209 &self.index,
1210 &mut query,
1211 &mut matched_qspans,
1212 &whole_query_run,
1213 true,
1214 None,
1215 )?;
1216 all_matches.extend(whole_query_followup);
1217
1218 let merged_seq = collect_regular_seq_matches(
1219 &self.index,
1220 &query,
1221 &matched_qspans,
1222 &candidate_contained_matches,
1223 None,
1224 )?;
1225 all_matches.extend(merged_seq);
1226 }
1227
1228 let merged_matches =
1230 refine_matches_without_false_positive_filter(&self.index, all_matches, &query);
1231
1232 let refined_matches = if unknown_licenses {
1234 let (good_matches, weak_matches) = split_weak_matches(&self.index, &merged_matches);
1235 let unknown_matches = unknown_match(&self.index, &query, &good_matches);
1236 let filtered_unknown =
1237 filter_invalid_contained_unknown_matches(&unknown_matches, &good_matches);
1238
1239 let mut all_matches = good_matches;
1240 all_matches.extend(filtered_unknown);
1241 all_matches.extend(weak_matches);
1242 all_matches
1243 } else {
1244 merged_matches
1245 };
1246
1247 let refined = refine_matches(&self.index, refined_matches, &query);
1249
1250 let mut sorted = refined;
1251 sort_matches_by_line(&mut sorted);
1252
1253 Ok(sorted)
1255 }
1256
1257 pub fn index(&self) -> &index::LicenseIndex {
1259 &self.index
1260 }
1261
1262 pub fn spdx_license_list_version(&self) -> Option<&str> {
1263 self.spdx_license_list_version.as_deref()
1264 }
1265
1266 pub fn license_index_provenance(&self) -> Option<&LicenseIndexProvenance> {
1267 self.license_index_provenance.as_ref()
1268 }
1269
1270 #[cfg(test)]
1272 pub fn spdx_mapping(&self) -> &SpdxMapping {
1273 &self.spdx_mapping
1274 }
1275}
1276
1277pub fn detect_scancode_spdx_license_list_version(search_path: &Path) -> Result<Option<String>> {
1278 for ancestor in search_path.ancestors() {
1279 let candidate = ancestor.join("scancode_config.py");
1280 if candidate.is_file() {
1281 let config = fs::read_to_string(&candidate)?;
1282 return Ok(parse_scancode_spdx_license_list_version(&config));
1283 }
1284 }
1285
1286 Ok(None)
1287}
1288
1289fn parse_scancode_spdx_license_list_version(config: &str) -> Option<String> {
1290 config.lines().find_map(|line| {
1291 let trimmed = line.trim();
1292 let (_, value) = trimmed.split_once('=')?;
1293 (trimmed.starts_with("spdx_license_list_version")).then(|| {
1294 value
1295 .trim()
1296 .trim_matches('"')
1297 .trim_matches('\'')
1298 .to_string()
1299 })
1300 })
1301}
1302
1303#[cfg(test)]
1304mod tests;