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