provenant/license_detection/models/
rule.rs1use std::collections::HashMap;
10use std::ops::Range;
11
12use rkyv::Archive;
13use serde::{Deserialize, Serialize};
14
15use crate::license_detection::index::dictionary::TokenId;
16
17const SCANCODE_LICENSE_URL_BASE: &str =
18 "https://github.com/aboutcode-org/scancode-toolkit/tree/develop/src/licensedcode/data/licenses";
19const SCANCODE_RULE_URL_BASE: &str =
20 "https://github.com/aboutcode-org/scancode-toolkit/tree/develop/src/licensedcode/data/rules";
21
22mod range_serde {
23 use serde::{Deserialize, Deserializer, Serialize, Serializer};
24 use std::ops::Range;
25
26 pub fn serialize<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
27 where
28 S: Serializer,
29 {
30 let tuples: Vec<(usize, usize)> = ranges.iter().map(|r| (r.start, r.end)).collect();
31 tuples.serialize(serializer)
32 }
33
34 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Range<usize>>, D::Error>
35 where
36 D: Deserializer<'de>,
37 {
38 let tuples: Vec<(usize, usize)> = Vec::deserialize(deserializer)?;
39 Ok(tuples
40 .into_iter()
41 .map(|(start, end)| Range { start, end })
42 .collect())
43 }
44}
45
46mod stopwords_serde {
47 use serde::{Deserialize, Deserializer, Serialize, Serializer};
48 use std::collections::HashMap;
49
50 pub fn serialize<S>(
51 map: &HashMap<Option<usize>, usize>,
52 serializer: S,
53 ) -> Result<S::Ok, S::Error>
54 where
55 S: Serializer,
56 {
57 let mut entries: Vec<(Option<usize>, usize)> = map.iter().map(|(k, v)| (*k, *v)).collect();
58 entries.sort_by_key(|(k, _)| *k);
59 entries.serialize(serializer)
60 }
61
62 pub fn deserialize<'de, D>(deserializer: D) -> Result<HashMap<Option<usize>, usize>, D::Error>
63 where
64 D: Deserializer<'de>,
65 {
66 let entries: Vec<(Option<usize>, usize)> = Vec::deserialize(deserializer)?;
67 Ok(entries.into_iter().collect())
68 }
69}
70
71#[derive(
72 Debug,
73 Clone,
74 Copy,
75 PartialEq,
76 Eq,
77 Hash,
78 PartialOrd,
79 Ord,
80 Default,
81 Serialize,
82 Deserialize,
83 Archive,
84 rkyv::Serialize,
85 rkyv::Deserialize,
86)]
87#[rkyv(derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord))]
88pub enum RuleKind {
89 #[default]
90 None,
91 Text,
92 Notice,
93 Reference,
94 Tag,
95 Intro,
96 Clue,
97}
98
99impl RuleKind {
100 pub fn from_rule_flags(
101 is_license_text: bool,
102 is_license_notice: bool,
103 is_license_reference: bool,
104 is_license_tag: bool,
105 is_license_intro: bool,
106 is_license_clue: bool,
107 ) -> Result<Self, &'static str> {
108 let mut active = None;
109
110 for (enabled, kind) in [
111 (is_license_text, Self::Text),
112 (is_license_notice, Self::Notice),
113 (is_license_reference, Self::Reference),
114 (is_license_tag, Self::Tag),
115 (is_license_intro, Self::Intro),
116 (is_license_clue, Self::Clue),
117 ] {
118 if !enabled {
119 continue;
120 }
121
122 if active.replace(kind).is_some() {
123 return Err("rule has multiple rule kinds set");
124 }
125 }
126
127 Ok(active.unwrap_or(Self::None))
128 }
129
130 pub const fn is_license_text(self) -> bool {
131 matches!(self, Self::Text)
132 }
133
134 pub const fn is_license_notice(self) -> bool {
135 matches!(self, Self::Notice)
136 }
137
138 pub const fn is_license_reference(self) -> bool {
139 matches!(self, Self::Reference)
140 }
141
142 pub const fn is_license_tag(self) -> bool {
143 matches!(self, Self::Tag)
144 }
145
146 pub const fn is_license_intro(self) -> bool {
147 matches!(self, Self::Intro)
148 }
149
150 pub const fn is_license_clue(self) -> bool {
151 matches!(self, Self::Clue)
152 }
153}
154
155#[derive(
157 Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize,
158)]
159pub struct Rule {
160 pub identifier: String,
164
165 pub license_expression: String,
167
168 pub text: String,
170
171 #[serde(
173 serialize_with = "serialize_token_ids",
174 deserialize_with = "deserialize_token_ids"
175 )]
176 pub tokens: Vec<TokenId>,
177
178 pub rule_kind: RuleKind,
180
181 pub is_false_positive: bool,
183
184 pub is_required_phrase: bool,
188
189 pub is_from_license: bool,
191
192 pub relevance: u8,
194
195 pub minimum_coverage: Option<u8>,
197
198 pub has_stored_minimum_coverage: bool,
200
201 pub is_continuous: bool,
203
204 #[serde(with = "range_serde", default)]
207 pub required_phrase_spans: Vec<Range<usize>>,
208
209 #[serde(with = "stopwords_serde", default)]
212 pub stopwords_by_pos: HashMap<Option<usize>, usize>,
213
214 pub referenced_filenames: Option<Vec<String>>,
216
217 pub ignorable_urls: Option<Vec<String>>,
219
220 pub ignorable_emails: Option<Vec<String>>,
222
223 pub ignorable_copyrights: Option<Vec<String>>,
225
226 pub ignorable_holders: Option<Vec<String>>,
228
229 pub ignorable_authors: Option<Vec<String>>,
231
232 pub language: Option<String>,
234
235 pub notes: Option<String>,
237
238 pub length_unique: usize,
240
241 pub high_length_unique: usize,
243
244 pub high_length: usize,
246
247 pub min_matched_length: usize,
249
250 pub min_high_matched_length: usize,
252
253 pub min_matched_length_unique: usize,
255
256 pub min_high_matched_length_unique: usize,
258
259 pub is_small: bool,
261
262 pub is_tiny: bool,
264
265 pub starts_with_license: bool,
267
268 pub ends_with_license: bool,
270
271 pub is_deprecated: bool,
273
274 pub spdx_license_key: Option<String>,
276
277 pub other_spdx_license_keys: Vec<String>,
279}
280
281fn serialize_token_ids<S>(token_ids: &[TokenId], serializer: S) -> Result<S::Ok, S::Error>
282where
283 S: serde::Serializer,
284{
285 let raw_ids: Vec<u16> = token_ids.iter().map(|id| id.raw()).collect();
286 <Vec<u16> as serde::Serialize>::serialize(&raw_ids, serializer)
287}
288
289fn deserialize_token_ids<'de, D>(deserializer: D) -> Result<Vec<TokenId>, D::Error>
290where
291 D: serde::Deserializer<'de>,
292{
293 let raw_ids: Vec<u16> = Vec::deserialize(deserializer)?;
294 Ok(raw_ids.into_iter().map(TokenId::new).collect())
295}
296
297impl PartialOrd for Rule {
298 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
299 Some(self.cmp(other))
300 }
301}
302
303impl Ord for Rule {
304 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
305 self.identifier.cmp(&other.identifier)
306 }
307}
308
309impl Rule {
310 pub fn rule_url(&self) -> Option<String> {
311 if self.is_from_license {
312 return (!self.license_expression.is_empty()).then(|| {
313 format!(
314 "{SCANCODE_LICENSE_URL_BASE}/{}.LICENSE",
315 self.license_expression
316 )
317 });
318 }
319
320 (!self.identifier.is_empty())
321 .then(|| format!("{SCANCODE_RULE_URL_BASE}/{}", self.identifier))
322 }
323
324 pub const fn kind(&self) -> RuleKind {
325 self.rule_kind
326 }
327
328 pub const fn is_license_text(&self) -> bool {
329 self.rule_kind.is_license_text()
330 }
331
332 #[allow(dead_code)]
337 pub const fn is_license_notice(&self) -> bool {
338 self.rule_kind.is_license_notice()
339 }
340
341 pub const fn is_license_reference(&self) -> bool {
342 self.rule_kind.is_license_reference()
343 }
344
345 pub const fn is_license_tag(&self) -> bool {
346 self.rule_kind.is_license_tag()
347 }
348
349 #[allow(dead_code)]
353 pub const fn is_license_intro(&self) -> bool {
354 self.rule_kind.is_license_intro()
355 }
356
357 pub const fn is_license_clue(&self) -> bool {
358 self.rule_kind.is_license_clue()
359 }
360}