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