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
557impl LicenseDetectionEngine {
558 fn from_index(
563 index: index::LicenseIndex,
564 spdx_license_list_version: Option<String>,
565 license_index_provenance: Option<LicenseIndexProvenance>,
566 ) -> Result<Self> {
567 let mut license_vec: Vec<_> = index.licenses_by_key.values().cloned().collect();
568 license_vec.sort_by(|a, b| a.key.cmp(&b.key));
569 let spdx_mapping = build_spdx_mapping(&license_vec);
570
571 Ok(Self {
572 index: Arc::new(index),
573 spdx_mapping,
574 spdx_license_list_version,
575 license_index_provenance,
576 })
577 }
578
579 #[cfg(test)]
580 pub(crate) fn from_test_index(index: index::LicenseIndex) -> Self {
581 Self::from_index(index, None, None).expect("test index should build license engine")
582 }
583
584 #[cfg(test)]
585 pub(crate) fn from_test_index_with_provenance(
586 index: index::LicenseIndex,
587 license_index_provenance: LicenseIndexProvenance,
588 ) -> Self {
589 Self::from_index(index, None, Some(license_index_provenance))
590 .expect("test index should build license engine")
591 }
592
593 pub fn from_embedded() -> Result<Self> {
598 let cache_config =
599 LicenseCacheConfig::new(LicenseCacheConfig::default_root_dir(), false, true);
600 Self::from_embedded_with_cache(&cache_config)
601 }
602
603 pub fn from_embedded_with_cache(cache_config: &LicenseCacheConfig) -> Result<Self> {
618 let artifact_bytes = include_bytes!("../../resources/license_detection/license_index.zst");
619 let fingerprint = compute_artifact_fingerprint(artifact_bytes);
620 let artifact_metadata = load_embedded_artifact_metadata_from_bytes(artifact_bytes)
621 .map_err(|e| {
622 anyhow::anyhow!("Failed to load embedded license artifact metadata: {}", e)
623 })?;
624 debug_assert_eq!(
625 artifact_metadata.license_index_provenance.source,
626 EMBEDDED_LICENSE_INDEX_SOURCE
627 );
628 let spdx_version = Some(artifact_metadata.spdx_license_list_version.clone());
629 let provenance = Some(artifact_metadata.license_index_provenance.clone());
630
631 if !cache_config.reindex {
632 let start = Instant::now();
633 if let Some(cached) =
634 load_cached_index(cache_config, LicenseCacheNamespace::Embedded, &fingerprint)?
635 {
636 eprintln!(
637 "License index loaded from rkyv cache in {:.2}s",
638 start.elapsed().as_secs_f64()
639 );
640 return Self::from_index(cached, spdx_version, provenance);
641 }
642 } else {
643 delete_cache(cache_config, LicenseCacheNamespace::Embedded, &fingerprint)?;
644 }
645
646 let snapshot = load_loader_snapshot_from_bytes(artifact_bytes)
647 .map_err(|e| anyhow::anyhow!("Failed to load embedded license index: {}", e))?;
648 let spdx_version = Some(snapshot.metadata.spdx_license_list_version.clone());
649 let provenance = Some(snapshot.metadata.license_index_provenance.clone());
650
651 let start = Instant::now();
652 let index = build_index_from_loaded(snapshot.rules, snapshot.licenses, false);
653 eprintln!(
654 "License index built from embedded artifact in {:.2}s",
655 start.elapsed().as_secs_f64()
656 );
657
658 let mut index = index;
659 index.spdx_license_list_version = spdx_version.clone();
660 if let Err(e) = save_cached_index(
661 cache_config,
662 LicenseCacheNamespace::Embedded,
663 &index,
664 &fingerprint,
665 ) {
666 eprintln!("Warning: failed to save license index cache: {}", e);
667 } else if let Some(size) =
668 cache_file_size(cache_config, LicenseCacheNamespace::Embedded, &fingerprint)
669 {
670 eprintln!(
671 "License index cache saved ({:.1} MB)",
672 size as f64 / 1_048_576.0
673 );
674 }
675
676 Self::from_index(index, spdx_version, provenance)
677 }
678
679 pub fn from_directory(rules_path: &Path) -> Result<Self> {
684 let cache_config =
685 LicenseCacheConfig::new(LicenseCacheConfig::default_root_dir(), false, true);
686 Self::from_directory_with_cache(rules_path, &cache_config)
687 }
688
689 pub fn from_directory_with_cache(
701 rules_path: &Path,
702 cache_config: &LicenseCacheConfig,
703 ) -> Result<Self> {
704 let LoadedLicenseDataset {
705 manifest,
706 rules: loaded_rules,
707 licenses: loaded_licenses,
708 } = load_license_dataset_from_root(rules_path)?;
709
710 let fingerprint = compute_rules_fingerprint(&loaded_rules, &loaded_licenses)?;
711 let provenance = Some(LicenseIndexProvenance {
712 source: CUSTOM_LICENSE_DATASET_SOURCE.to_string(),
713 dataset_fingerprint: compute_dataset_fingerprint_string(
714 &loaded_rules,
715 &loaded_licenses,
716 )?,
717 ignored_rules: vec![],
718 ignored_licenses: vec![],
719 ignored_rules_due_to_licenses: vec![],
720 added_rules: vec![],
721 replaced_rules: vec![],
722 added_licenses: vec![],
723 replaced_licenses: vec![],
724 });
725
726 if !cache_config.reindex {
727 if let Some(cached) = load_cached_index(
728 cache_config,
729 LicenseCacheNamespace::CustomRules,
730 &fingerprint,
731 )? {
732 let start = Instant::now();
733 eprintln!(
734 "License index loaded from rkyv cache in {:.2}s",
735 start.elapsed().as_secs_f64()
736 );
737 return Self::from_index(
738 cached,
739 Some(manifest.spdx_license_list_version),
740 provenance,
741 );
742 }
743 } else {
744 delete_cache(
745 cache_config,
746 LicenseCacheNamespace::CustomRules,
747 &fingerprint,
748 )?;
749 }
750
751 let start = Instant::now();
752 let index = build_index_from_loaded(loaded_rules, loaded_licenses, false);
753 eprintln!(
754 "License index built from custom dataset in {:.2}s",
755 start.elapsed().as_secs_f64()
756 );
757
758 if let Err(e) = save_cached_index(
759 cache_config,
760 LicenseCacheNamespace::CustomRules,
761 &index,
762 &fingerprint,
763 ) {
764 eprintln!("Warning: failed to save license index cache: {}", e);
765 } else if let Some(size) = cache_file_size(
766 cache_config,
767 LicenseCacheNamespace::CustomRules,
768 &fingerprint,
769 ) {
770 eprintln!(
771 "License index cache saved ({:.1} MB)",
772 size as f64 / 1_048_576.0
773 );
774 }
775
776 Self::from_index(index, Some(manifest.spdx_license_list_version), provenance)
777 }
778
779 pub fn embedded_spdx_license_list_version() -> Result<String> {
780 let artifact_bytes = include_bytes!("../../resources/license_detection/license_index.zst");
781 Ok(load_embedded_artifact_metadata_from_bytes(artifact_bytes)
782 .map_err(|e| {
783 anyhow::anyhow!("Failed to load embedded license artifact metadata: {}", e)
784 })?
785 .spdx_license_list_version)
786 }
787
788 pub fn detect_with_kind(
789 &self,
790 text: &str,
791 unknown_licenses: bool,
792 binary_derived: bool,
793 ) -> Result<Vec<LicenseDetection>> {
794 self.detect_with_kind_with_score_and_deadline_with_options(
795 text,
796 unknown_licenses,
797 binary_derived,
798 true,
799 0.0,
800 None,
801 )
802 .map_err(Into::into)
803 }
804
805 pub fn detect_with_kind_with_score(
806 &self,
807 text: &str,
808 unknown_licenses: bool,
809 binary_derived: bool,
810 min_score: f32,
811 ) -> Result<Vec<LicenseDetection>> {
812 self.detect_with_kind_with_score_and_deadline_with_options(
813 text,
814 unknown_licenses,
815 binary_derived,
816 true,
817 min_score,
818 None,
819 )
820 .map_err(Into::into)
821 }
822
823 pub(crate) fn detect_with_kind_with_score_and_deadline_with_options(
824 &self,
825 text: &str,
826 unknown_licenses: bool,
827 binary_derived: bool,
828 enable_sequence_matching: bool,
829 min_score: f32,
830 deadline: Option<Instant>,
831 ) -> Result<Vec<LicenseDetection>, LicenseDetectionError> {
832 ensure_within_deadline(deadline)?;
833 let clean_text = strip_utf8_bom_str(text);
834
835 let content = truncate_detection_text(clean_text);
836
837 ensure_within_deadline(deadline)?;
838 let mut query = if deadline.is_some() {
839 Query::from_extracted_text_with_deadline(
840 content,
841 &self.index,
842 binary_derived,
843 deadline,
844 )?
845 } else {
846 Query::from_extracted_text(content, &self.index, binary_derived)?
847 };
848 let whole_query_run = query.whole_query_run();
849
850 let mut all_matches = Vec::new();
851 let mut candidate_contained_matches = Vec::new();
852 let mut aho_extra_matchables = PositionSet::new();
853 let mut matched_qspans: Vec<models::PositionSpan> = Vec::new();
854
855 {
858 ensure_within_deadline(deadline)?;
859 let hash_matches = hash_match(&self.index, &whole_query_run);
860
861 if !hash_matches.is_empty() {
862 let mut matches = hash_matches;
863 sort_matches_by_line(&mut matches);
864
865 let groups = split_groups_across_frontmatter_boundary(
866 group_matches_by_region(&matches),
867 Some(content),
868 );
869 let detections: Vec<LicenseDetection> = groups
870 .iter()
871 .map(|group| {
872 let mut detection = empty_detection();
873 populate_detection_from_group_with_spdx(
874 &mut detection,
875 group,
876 &self.spdx_mapping,
877 Some(content),
878 );
879 detection
880 })
881 .collect();
882
883 return Ok(post_process_detections(detections, min_score));
884 }
885 }
886
887 {
889 ensure_within_deadline(deadline)?;
890 let spdx_matches = spdx_lid_match(&self.index, &query);
891 subtract_spdx_match_qspans(
892 &mut query,
893 &mut matched_qspans,
894 &mut aho_extra_matchables,
895 &spdx_matches,
896 );
897 all_matches.extend(spdx_matches);
898 }
899
900 {
902 ensure_within_deadline(deadline)?;
903 let aho_matches = if aho_extra_matchables.is_empty() {
904 if deadline.is_some() {
905 aho_match::aho_match_with_deadline(&self.index, &whole_query_run, deadline)?
906 } else {
907 aho_match(&self.index, &whole_query_run)
908 }
909 } else {
910 if deadline.is_some() {
911 aho_match::aho_match_with_extra_matchables(
912 &self.index,
913 &whole_query_run,
914 Some(&aho_extra_matchables),
915 deadline,
916 )?
917 } else {
918 aho_match::aho_match_with_extra_matchables(
919 &self.index,
920 &whole_query_run,
921 Some(&aho_extra_matchables),
922 None,
923 )?
924 }
925 };
926
927 let refined_aho = match_refine::refine_aho_matches(&self.index, aho_matches, &query);
930 candidate_contained_matches.extend(refined_aho.clone());
931 let (merged_aho, _) = merge_and_prepare_aho_matches(
932 &self.index,
933 &mut query,
934 &mut matched_qspans,
935 &refined_aho,
936 );
937 all_matches.extend(merged_aho);
938
939 let whole_query_followup = collect_whole_query_exact_followup_matches(
940 &self.index,
941 &mut query,
942 &mut matched_qspans,
943 &whole_query_run,
944 enable_sequence_matching,
945 deadline,
946 )?;
947 all_matches.extend(whole_query_followup);
948
949 if enable_sequence_matching {
950 let merged_seq = collect_regular_seq_matches(
951 &self.index,
952 &query,
953 &matched_qspans,
954 &candidate_contained_matches,
955 deadline,
956 )?;
957 all_matches.extend(merged_seq);
958 }
959 }
960
961 ensure_within_deadline(deadline)?;
964 let merged_matches =
965 refine_matches_without_false_positive_filter(&self.index, all_matches, &query);
966
967 let refined_matches = if unknown_licenses {
970 let (good_matches, weak_matches) = split_weak_matches(&self.index, &merged_matches);
972
973 let unknown_matches = unknown_match(&self.index, &query, &good_matches);
975 let filtered_unknown =
976 filter_invalid_contained_unknown_matches(&unknown_matches, &good_matches);
977
978 let mut all_matches = good_matches;
979 all_matches.extend(filtered_unknown);
980 all_matches.extend(weak_matches);
983 all_matches
984 } else {
985 merged_matches
986 };
987
988 ensure_within_deadline(deadline)?;
990 let refined = refine_matches(&self.index, refined_matches, &query);
991
992 let mut sorted = refined;
993 sort_matches_by_line(&mut sorted);
994
995 let groups = split_groups_across_frontmatter_boundary(
996 group_matches_by_region(&sorted),
997 Some(content),
998 );
999
1000 let detections: Vec<LicenseDetection> = groups
1001 .iter()
1002 .map(|group| {
1003 let mut detection = empty_detection();
1004 populate_detection_from_group_with_spdx(
1005 &mut detection,
1006 group,
1007 &self.spdx_mapping,
1008 Some(content),
1009 );
1010 detection
1011 })
1012 .collect();
1013
1014 let detections = post_process_detections(detections, min_score);
1015
1016 ensure_within_deadline(deadline)?;
1017 Ok(detections)
1018 }
1019
1020 pub fn detect_with_kind_and_source(
1021 &self,
1022 text: &str,
1023 unknown_licenses: bool,
1024 binary_derived: bool,
1025 source_path: &str,
1026 ) -> Result<Vec<LicenseDetection>> {
1027 self.detect_with_kind_and_source_with_deadline_and_options(
1028 text,
1029 unknown_licenses,
1030 binary_derived,
1031 source_path,
1032 true,
1033 None,
1034 )
1035 .map_err(Into::into)
1036 }
1037
1038 pub(crate) fn detect_with_kind_and_source_with_deadline_and_options(
1039 &self,
1040 text: &str,
1041 unknown_licenses: bool,
1042 binary_derived: bool,
1043 source_path: &str,
1044 enable_sequence_matching: bool,
1045 deadline: Option<Instant>,
1046 ) -> Result<Vec<LicenseDetection>, LicenseDetectionError> {
1047 let mut detections = self.detect_with_kind_with_score_and_deadline_with_options(
1048 text,
1049 unknown_licenses,
1050 binary_derived,
1051 enable_sequence_matching,
1052 0.0,
1053 deadline,
1054 )?;
1055 attach_source_path_to_detections(&mut detections, source_path);
1056 Ok(detections)
1057 }
1058
1059 pub fn detect_with_kind_and_source_with_score(
1060 &self,
1061 text: &str,
1062 unknown_licenses: bool,
1063 binary_derived: bool,
1064 source_path: &str,
1065 min_score: f32,
1066 ) -> Result<Vec<LicenseDetection>> {
1067 self.detect_with_kind_and_source_with_score_options(
1068 text,
1069 unknown_licenses,
1070 binary_derived,
1071 source_path,
1072 true,
1073 min_score,
1074 )
1075 }
1076
1077 pub fn detect_with_kind_and_source_with_options(
1078 &self,
1079 text: &str,
1080 unknown_licenses: bool,
1081 binary_derived: bool,
1082 source_path: &str,
1083 enable_sequence_matching: bool,
1084 ) -> Result<Vec<LicenseDetection>> {
1085 self.detect_with_kind_and_source_with_score_options(
1086 text,
1087 unknown_licenses,
1088 binary_derived,
1089 source_path,
1090 enable_sequence_matching,
1091 0.0,
1092 )
1093 }
1094
1095 pub fn detect_with_kind_and_source_with_score_options(
1096 &self,
1097 text: &str,
1098 unknown_licenses: bool,
1099 binary_derived: bool,
1100 source_path: &str,
1101 enable_sequence_matching: bool,
1102 min_score: f32,
1103 ) -> Result<Vec<LicenseDetection>> {
1104 let mut detections = self.detect_with_kind_with_score_and_deadline_with_options(
1105 text,
1106 unknown_licenses,
1107 binary_derived,
1108 enable_sequence_matching,
1109 min_score,
1110 None,
1111 )?;
1112 attach_source_path_to_detections(&mut detections, source_path);
1113 Ok(detections)
1114 }
1115
1116 #[cfg(any(test, feature = "golden-tests"))]
1121 pub fn detect_matches_with_kind(
1122 &self,
1123 text: &str,
1124 unknown_licenses: bool,
1125 binary_derived: bool,
1126 ) -> Result<Vec<LicenseMatch>> {
1127 let clean_text = strip_utf8_bom_str(text);
1128
1129 let content = truncate_detection_text(clean_text);
1130
1131 let mut query = Query::from_extracted_text(content, &self.index, binary_derived)?;
1132 let whole_query_run = query.whole_query_run();
1133
1134 let mut all_matches = Vec::new();
1135 let mut candidate_contained_matches = Vec::new();
1136 let mut aho_extra_matchables = PositionSet::new();
1137 let mut matched_qspans: Vec<models::PositionSpan> = Vec::new();
1138
1139 {
1141 let hash_matches = hash_match(&self.index, &whole_query_run);
1142
1143 if !hash_matches.is_empty() {
1144 let mut matches = hash_matches;
1145 sort_matches_by_line(&mut matches);
1146 return Ok(matches);
1147 }
1148 }
1149
1150 {
1152 let spdx_matches = spdx_lid_match(&self.index, &query);
1153 subtract_spdx_match_qspans(
1154 &mut query,
1155 &mut matched_qspans,
1156 &mut aho_extra_matchables,
1157 &spdx_matches,
1158 );
1159 all_matches.extend(spdx_matches);
1160 }
1161
1162 {
1164 let aho_matches = if aho_extra_matchables.is_empty() {
1165 aho_match(&self.index, &whole_query_run)
1166 } else {
1167 aho_match::aho_match_with_extra_matchables(
1168 &self.index,
1169 &whole_query_run,
1170 Some(&aho_extra_matchables),
1171 None,
1172 )?
1173 };
1174 let refined_aho = match_refine::refine_aho_matches(&self.index, aho_matches, &query);
1175 candidate_contained_matches.extend(refined_aho.clone());
1176 let (merged_aho, _) = merge_and_prepare_aho_matches(
1177 &self.index,
1178 &mut query,
1179 &mut matched_qspans,
1180 &refined_aho,
1181 );
1182 all_matches.extend(merged_aho);
1183
1184 let whole_query_followup = collect_whole_query_exact_followup_matches(
1185 &self.index,
1186 &mut query,
1187 &mut matched_qspans,
1188 &whole_query_run,
1189 true,
1190 None,
1191 )?;
1192 all_matches.extend(whole_query_followup);
1193
1194 let merged_seq = collect_regular_seq_matches(
1195 &self.index,
1196 &query,
1197 &matched_qspans,
1198 &candidate_contained_matches,
1199 None,
1200 )?;
1201 all_matches.extend(merged_seq);
1202 }
1203
1204 let merged_matches =
1206 refine_matches_without_false_positive_filter(&self.index, all_matches, &query);
1207
1208 let refined_matches = if unknown_licenses {
1210 let (good_matches, weak_matches) = split_weak_matches(&self.index, &merged_matches);
1211 let unknown_matches = unknown_match(&self.index, &query, &good_matches);
1212 let filtered_unknown =
1213 filter_invalid_contained_unknown_matches(&unknown_matches, &good_matches);
1214
1215 let mut all_matches = good_matches;
1216 all_matches.extend(filtered_unknown);
1217 all_matches.extend(weak_matches);
1218 all_matches
1219 } else {
1220 merged_matches
1221 };
1222
1223 let refined = refine_matches(&self.index, refined_matches, &query);
1225
1226 let mut sorted = refined;
1227 sort_matches_by_line(&mut sorted);
1228
1229 Ok(sorted)
1231 }
1232
1233 pub fn index(&self) -> &index::LicenseIndex {
1235 &self.index
1236 }
1237
1238 pub fn spdx_license_list_version(&self) -> Option<&str> {
1239 self.spdx_license_list_version.as_deref()
1240 }
1241
1242 pub fn license_index_provenance(&self) -> Option<&LicenseIndexProvenance> {
1243 self.license_index_provenance.as_ref()
1244 }
1245
1246 #[cfg(test)]
1248 pub fn spdx_mapping(&self) -> &SpdxMapping {
1249 &self.spdx_mapping
1250 }
1251}
1252
1253pub fn detect_scancode_spdx_license_list_version(search_path: &Path) -> Result<Option<String>> {
1254 for ancestor in search_path.ancestors() {
1255 let candidate = ancestor.join("scancode_config.py");
1256 if candidate.is_file() {
1257 let config = fs::read_to_string(&candidate)?;
1258 return Ok(parse_scancode_spdx_license_list_version(&config));
1259 }
1260 }
1261
1262 Ok(None)
1263}
1264
1265fn parse_scancode_spdx_license_list_version(config: &str) -> Option<String> {
1266 config.lines().find_map(|line| {
1267 let trimmed = line.trim();
1268 let (_, value) = trimmed.split_once('=')?;
1269 (trimmed.starts_with("spdx_license_list_version")).then(|| {
1270 value
1271 .trim()
1272 .trim_matches('"')
1273 .trim_matches('\'')
1274 .to_string()
1275 })
1276 })
1277}
1278
1279#[cfg(test)]
1280mod tests;