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