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