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