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