1use core::fmt;
27
28use crate::clone_class::CloneClass;
29use crate::discovery::{BuildVariant, Language};
30use crate::engine::normalize::{self, LiteralNorm, NormAtom, Resolution};
31use crate::engine::{EngineReport, InputFile};
32use crate::frontend::Token;
33use crate::semantic::{SOG_SCHEMA_VERSION, SemanticOperationGraph};
34
35pub const FP_SCHEMA_VERSION: &str = "fp-schema-v1";
38
39pub const HASH_ALGORITHM: &str = "blake3-128";
42
43macro_rules! stable_id {
44 ($(#[$doc:meta])* $name:ident) => {
45 $(#[$doc])*
46 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
47 pub struct $name([u8; 16]);
48
49 impl $name {
50 #[must_use]
53 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
54 Self(bytes)
55 }
56
57 #[must_use]
59 pub const fn as_bytes(&self) -> &[u8; 16] {
60 &self.0
61 }
62
63 #[must_use]
65 pub fn to_hex(&self) -> String {
66 self.to_string()
67 }
68 }
69
70 impl fmt::Display for $name {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 for byte in self.0 {
73 write!(f, "{byte:02x}")?;
74 }
75 Ok(())
76 }
77 }
78 };
79}
80
81stable_id!(
82 UnitFingerprint
85);
86stable_id!(
87 FragmentFingerprint
91);
92stable_id!(
93 CloneGroupFingerprint
96);
97stable_id!(
98 GroupLineageId
103);
104stable_id!(
105 FindingId
109);
110stable_id!(
111 CrossVariantComparisonId
116);
117stable_id!(
118 CrossVariantGroupId
120);
121stable_id!(
122 CrossVariantMemberId
124);
125stable_id!(
126 CrossLanguageComparisonId
132);
133stable_id!(
134 CrossLanguageGroupId
136);
137stable_id!(
138 CrossLanguageMemberId
140);
141
142pub const CROSS_VARIANT_POLICY_VERSION: &str = "cross-variant-exact-v1";
144
145pub const CROSS_LANGUAGE_POLICY_VERSION: &str = "cross-language-semantic-v1";
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum ContentNorm {
151 Raw,
153 Normalized(LiteralNorm),
156 ResolvedNormalized(LiteralNorm),
163}
164
165impl ContentNorm {
166 #[must_use]
169 pub const fn label(self) -> &'static str {
170 match self {
171 Self::Raw => "raw",
172 Self::Normalized(LiteralNorm::Preserve) => "alpha-lit-preserve",
173 Self::Normalized(LiteralNorm::Category) => "alpha-lit-category",
174 Self::Normalized(LiteralNorm::Full) => "alpha-lit-full",
175 Self::ResolvedNormalized(LiteralNorm::Preserve) => "alpha-resolved-lit-preserve",
176 Self::ResolvedNormalized(LiteralNorm::Category) => "alpha-resolved-lit-category",
177 Self::ResolvedNormalized(LiteralNorm::Full) => "alpha-resolved-lit-full",
178 }
179 }
180}
181
182#[derive(Debug, Clone, Copy)]
188pub struct FileContext<'a> {
189 pub frontend_version: &'a str,
191 pub language: Language,
193}
194
195struct IdHasher {
197 hasher: blake3::Hasher,
198}
199
200impl IdHasher {
201 fn new(domain: &str) -> Self {
202 let mut this = Self {
203 hasher: blake3::Hasher::new(),
204 };
205 this.write_bytes(domain.as_bytes());
206 this
207 }
208
209 fn write_bytes(&mut self, bytes: &[u8]) {
210 let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
211 self.hasher.update(&len.to_le_bytes());
212 self.hasher.update(bytes);
213 }
214
215 fn write_str(&mut self, text: &str) {
216 self.write_bytes(text.as_bytes());
217 }
218
219 fn write_u8(&mut self, value: u8) {
220 self.hasher.update(&[value]);
221 }
222
223 fn write_u32(&mut self, value: u32) {
224 self.hasher.update(&value.to_le_bytes());
225 }
226
227 fn write_context(&mut self, variant: &BuildVariant, file: &FileContext<'_>, norm: ContentNorm) {
230 self.write_str(FP_SCHEMA_VERSION);
231 self.write_str(HASH_ALGORITHM);
232 self.write_str(norm.label());
233 self.write_u32(variant.normalization_version);
234 self.write_str(file.frontend_version);
235 self.write_str(variant.mode.name());
236 self.write_str(file.language.name());
237 self.write_str(&variant.canonical());
238 }
239
240 fn write_content(
243 &mut self,
244 tokens: &[Token],
245 norm: ContentNorm,
246 resolution: Option<&Resolution>,
247 ) {
248 match norm {
249 ContentNorm::Raw => {
250 for token in tokens {
251 self.write_u8(token.kind.tag());
252 self.write_bytes(token.text.as_bytes());
253 }
254 }
255 ContentNorm::Normalized(literals) | ContentNorm::ResolvedNormalized(literals) => {
256 let mut normalized = Vec::new();
257 normalize::normalize_resolved_into(
258 tokens,
259 literals,
260 matches!(norm, ContentNorm::ResolvedNormalized(_))
261 .then_some(resolution)
262 .flatten(),
263 &mut normalized,
264 );
265 for norm_token in normalized {
266 self.write_u8(norm_token.tag);
267 match norm_token.atom {
268 NormAtom::Renamed(n) => {
269 self.write_u8(1);
270 self.write_u32(n);
271 }
272 NormAtom::Text(text) => {
273 self.write_u8(2);
274 self.write_bytes(text.as_bytes());
275 }
276 NormAtom::Literal(class) => {
277 self.write_u8(3);
278 self.write_u8(class);
279 }
280 }
281 }
282 }
283 }
284 }
285
286 fn finish(self) -> [u8; 16] {
287 let digest = self.hasher.finalize();
288 let mut out = [0u8; 16];
289 out.copy_from_slice(&digest.as_bytes()[..16]);
290 out
291 }
292}
293
294#[must_use]
299pub fn unit_fingerprint(
300 variant: &BuildVariant,
301 file: &FileContext<'_>,
302 tokens: &[Token],
303 norm: ContentNorm,
304) -> UnitFingerprint {
305 let mut hasher = IdHasher::new("unit");
306 hasher.write_context(variant, file, norm);
307 hasher.write_content(tokens, norm, None);
308 UnitFingerprint(hasher.finish())
309}
310
311#[must_use]
318pub fn fragment_fingerprint(
319 variant: &BuildVariant,
320 file: &FileContext<'_>,
321 kind: &str,
322 tokens: &[Token],
323 norm: ContentNorm,
324) -> FragmentFingerprint {
325 let mut hasher = IdHasher::new("fragment");
326 hasher.write_context(variant, file, norm);
327 hasher.write_str(kind);
328 hasher.write_content(tokens, norm, None);
329 FragmentFingerprint(hasher.finish())
330}
331
332#[must_use]
339pub fn resolved_fragment_fingerprint(
340 variant: &BuildVariant,
341 file: &FileContext<'_>,
342 kind: &str,
343 tokens: &[Token],
344 norm: ContentNorm,
345 resolution: Option<&Resolution>,
346) -> FragmentFingerprint {
347 let mut hasher = IdHasher::new("fragment");
348 hasher.write_context(variant, file, norm);
349 hasher.write_str(kind);
350 hasher.write_content(tokens, norm, resolution);
351 FragmentFingerprint(hasher.finish())
352}
353
354#[must_use]
362pub fn semantic_fragment_fingerprint(
363 variant: &BuildVariant,
364 graph: &SemanticOperationGraph,
365) -> FragmentFingerprint {
366 let mut hasher = IdHasher::new("fragment-semantic");
367 hasher.write_context(
368 variant,
369 &FileContext {
370 frontend_version: SOG_SCHEMA_VERSION,
371 language: graph.language,
372 },
373 ContentNorm::Raw,
374 );
375 hasher.write_str(&graph.schema_version);
376 hasher.write_u32(u32::try_from(graph.nodes.len()).unwrap_or(u32::MAX));
377 for node in &graph.nodes {
378 hasher.write_str(node.kind.name());
379 match node.attributes.type_tag {
380 Some(tag) => {
381 hasher.write_u8(1);
382 hasher.write_str(tag.name());
383 }
384 None => hasher.write_u8(0),
385 }
386 hasher.write_u32(u32::try_from(node.attributes.api_names.len()).unwrap_or(u32::MAX));
387 for api_name in &node.attributes.api_names {
388 hasher.write_str(api_name);
389 }
390 match &node.attributes.resource_kind {
391 Some(resource_kind) => {
392 hasher.write_u8(1);
393 hasher.write_str(resource_kind);
394 }
395 None => hasher.write_u8(0),
396 }
397 match node.attributes.fallible_kind {
398 Some(kind) => {
399 hasher.write_u8(1);
400 hasher.write_str(kind.name());
401 }
402 None => hasher.write_u8(0),
403 }
404 match node.attributes.direct_propagation {
405 Some(kind) => {
406 hasher.write_u8(1);
407 hasher.write_str(kind.name());
408 }
409 None => hasher.write_u8(0),
410 }
411 match node.attributes.structure_fingerprint {
412 Some(fingerprint) => {
413 hasher.write_u8(1);
414 hasher.write_bytes(&fingerprint);
415 }
416 None => hasher.write_u8(0),
417 }
418 }
419 hasher.write_u32(u32::try_from(graph.edges.len()).unwrap_or(u32::MAX));
420 for edge in &graph.edges {
421 hasher.write_u32(edge.from);
422 hasher.write_u32(edge.to);
423 hasher.write_str(edge.kind.name());
424 }
425 FragmentFingerprint(hasher.finish())
426}
427
428#[must_use]
436pub fn semantic_structure_fingerprint(
437 variant: &BuildVariant,
438 file: &FileContext<'_>,
439 tokens: &[Token],
440) -> [u8; 16] {
441 let mut hasher = IdHasher::new("semantic-source-structure-v1");
442 hasher.write_context(variant, file, ContentNorm::Raw);
443 hasher.write_content(tokens, ContentNorm::Raw, None);
444 hasher.finish()
445}
446
447#[must_use]
454pub fn semantic_occurrence_fingerprint(
455 content: FragmentFingerprint,
456 host: &UnitFingerprint,
457 occurrence_rank: u32,
458) -> FragmentFingerprint {
459 let mut hasher = IdHasher::new("semantic-occurrence-v1");
460 hasher.write_str(FP_SCHEMA_VERSION);
461 hasher.write_bytes(content.as_bytes());
462 hasher.write_bytes(host.as_bytes());
463 hasher.write_u32(occurrence_rank);
464 FragmentFingerprint(hasher.finish())
465}
466
467#[must_use]
474pub fn clone_group_fingerprint(
475 variant: &BuildVariant,
476 clone_type: CloneClass,
477 members: &[FragmentFingerprint],
478) -> CloneGroupFingerprint {
479 let mut distinct: Vec<[u8; 16]> = members.iter().map(|m| m.0).collect();
480 distinct.sort_unstable();
481 distinct.dedup();
482
483 let mut hasher = IdHasher::new("group");
484 hasher.write_str(FP_SCHEMA_VERSION);
485 hasher.write_str(HASH_ALGORITHM);
486 hasher.write_str(variant.mode.name());
487 hasher.write_str(&variant.canonical());
488 hasher.write_str(clone_type.name());
489 hasher.write_u32(u32::try_from(distinct.len()).unwrap_or(u32::MAX));
490 for bytes in &distinct {
491 hasher.write_bytes(bytes);
492 }
493 CloneGroupFingerprint(hasher.finish())
494}
495
496#[must_use]
501pub fn group_lineage_id(group: &CloneGroupFingerprint) -> GroupLineageId {
502 let mut hasher = IdHasher::new("group-lineage");
503 hasher.write_str(FP_SCHEMA_VERSION);
504 hasher.write_str(HASH_ALGORITHM);
505 hasher.write_bytes(group.as_bytes());
506 GroupLineageId(hasher.finish())
507}
508
509#[must_use]
515pub fn semantic_clone_group_fingerprint(
516 variant: &BuildVariant,
517 rule_id: &str,
518 rule_version: u32,
519 members: &[FragmentFingerprint],
520) -> CloneGroupFingerprint {
521 let mut occurrences: Vec<[u8; 16]> = members.iter().map(|member| member.0).collect();
522 occurrences.sort_unstable();
523
524 let mut hasher = IdHasher::new("group-semantic");
525 hasher.write_str(FP_SCHEMA_VERSION);
526 hasher.write_str(HASH_ALGORITHM);
527 hasher.write_str(variant.mode.name());
528 hasher.write_str(&variant.canonical());
529 hasher.write_str(CloneClass::RestrictedSemantic.name());
530 hasher.write_str(SOG_SCHEMA_VERSION);
531 hasher.write_str(rule_id);
532 hasher.write_u32(rule_version);
533 hasher.write_u32(u32::try_from(occurrences.len()).unwrap_or(u32::MAX));
534 for bytes in &occurrences {
535 hasher.write_bytes(bytes);
536 }
537 CloneGroupFingerprint(hasher.finish())
538}
539
540#[must_use]
557pub fn structural_clone_group_fingerprint(
558 variant: &BuildVariant,
559 class: CloneClass,
560 canonical: &FragmentFingerprint,
561 members: &[FragmentFingerprint],
562) -> CloneGroupFingerprint {
563 let mut distinct: Vec<[u8; 16]> = members.iter().map(|m| m.0).collect();
564 distinct.sort_unstable();
565 distinct.dedup();
566
567 let mut hasher = IdHasher::new("group-structural");
568 hasher.write_str(FP_SCHEMA_VERSION);
569 hasher.write_str(HASH_ALGORITHM);
570 hasher.write_str(variant.mode.name());
571 hasher.write_str(&variant.canonical());
572 hasher.write_str(class.name());
573 hasher.write_bytes(&canonical.0);
575 hasher.write_u32(u32::try_from(distinct.len()).unwrap_or(u32::MAX));
576 for bytes in &distinct {
577 hasher.write_bytes(bytes);
578 }
579 CloneGroupFingerprint(hasher.finish())
580}
581
582#[must_use]
591pub fn finding_id(
592 group: &CloneGroupFingerprint,
593 host: Option<&UnitFingerprint>,
594 rank_in_host: u32,
595) -> FindingId {
596 let mut hasher = IdHasher::new("finding");
597 hasher.write_str(FP_SCHEMA_VERSION);
598 hasher.write_bytes(&group.0);
599 match host {
600 Some(unit) => {
601 hasher.write_u8(1);
602 hasher.write_bytes(&unit.0);
603 }
604 None => hasher.write_u8(0),
605 }
606 hasher.write_u32(rank_in_host);
607 FindingId(hasher.finish())
608}
609
610#[must_use]
616pub fn cross_variant_comparison_id(origins: &[String]) -> CrossVariantComparisonId {
617 let mut origins = origins.to_vec();
618 origins.sort_unstable();
619 origins.dedup();
620 let mut hasher = IdHasher::new("cross-variant-comparison");
621 hasher.write_str(FP_SCHEMA_VERSION);
622 hasher.write_str(CROSS_VARIANT_POLICY_VERSION);
623 hasher.write_u32(u32::try_from(origins.len()).unwrap_or(u32::MAX));
624 for origin in origins {
625 hasher.write_str(&origin);
626 }
627 CrossVariantComparisonId(hasher.finish())
628}
629
630#[must_use]
636pub fn cross_language_comparison_id(origins: &[String]) -> CrossLanguageComparisonId {
637 let mut origins = origins.to_vec();
638 origins.sort_unstable();
639 origins.dedup();
640 let mut hasher = IdHasher::new("cross-language-comparison");
641 hasher.write_str(FP_SCHEMA_VERSION);
642 hasher.write_str(CROSS_LANGUAGE_POLICY_VERSION);
643 hasher.write_u32(u32::try_from(origins.len()).unwrap_or(u32::MAX));
644 for origin in origins {
645 hasher.write_str(&origin);
646 }
647 CrossLanguageComparisonId(hasher.finish())
648}
649
650#[must_use]
657pub fn cross_language_group_id(
658 comparison: &CrossLanguageComparisonId,
659 rule_id: &str,
660 rule_version: u32,
661 members: &[FragmentFingerprint],
662) -> CrossLanguageGroupId {
663 let mut members = members.to_vec();
664 members.sort_unstable();
665 let mut hasher = IdHasher::new("cross-language-group");
666 hasher.write_str(FP_SCHEMA_VERSION);
667 hasher.write_str(CROSS_LANGUAGE_POLICY_VERSION);
668 hasher.write_bytes(comparison.as_bytes());
669 hasher.write_str(rule_id);
670 hasher.write_u32(rule_version);
671 hasher.write_u32(u32::try_from(members.len()).unwrap_or(u32::MAX));
672 for member in members {
673 hasher.write_bytes(member.as_bytes());
674 }
675 CrossLanguageGroupId(hasher.finish())
676}
677
678#[must_use]
684pub fn cross_variant_group_id(
685 comparison: &CrossVariantComparisonId,
686 class: CloneClass,
687 language: Language,
688 content: &[u8; 16],
689) -> CrossVariantGroupId {
690 let mut hasher = IdHasher::new("cross-variant-group");
691 hasher.write_str(FP_SCHEMA_VERSION);
692 hasher.write_str(CROSS_VARIANT_POLICY_VERSION);
693 hasher.write_bytes(comparison.as_bytes());
694 hasher.write_str(class.name());
695 hasher.write_str(language.name());
696 hasher.write_bytes(content);
697 CrossVariantGroupId(hasher.finish())
698}
699
700#[must_use]
705pub fn cross_variant_member_id(
706 group: &CrossVariantGroupId,
707 origin_variant: &str,
708 language: Language,
709 occurrence_rank: u32,
710) -> CrossVariantMemberId {
711 let mut hasher = IdHasher::new("cross-variant-member");
712 hasher.write_str(FP_SCHEMA_VERSION);
713 hasher.write_str(CROSS_VARIANT_POLICY_VERSION);
714 hasher.write_bytes(group.as_bytes());
715 hasher.write_str(origin_variant);
716 hasher.write_str(language.name());
717 hasher.write_u32(occurrence_rank);
718 CrossVariantMemberId(hasher.finish())
719}
720
721#[must_use]
723pub fn cross_language_member_id(
724 group: &CrossLanguageGroupId,
725 origin_variant: &str,
726 occurrence: &FragmentFingerprint,
727) -> CrossLanguageMemberId {
728 let mut hasher = IdHasher::new("cross-language-member");
729 hasher.write_str(FP_SCHEMA_VERSION);
730 hasher.write_str(CROSS_LANGUAGE_POLICY_VERSION);
731 hasher.write_bytes(group.as_bytes());
732 hasher.write_str(origin_variant);
733 hasher.write_bytes(occurrence.as_bytes());
734 CrossLanguageMemberId(hasher.finish())
735}
736
737#[derive(Debug, Clone, PartialEq, Eq)]
740pub struct MemberIds {
741 pub content: FragmentFingerprint,
743 pub finding: FindingId,
745}
746
747#[derive(Debug, Clone, PartialEq, Eq)]
749pub struct GroupIds {
750 pub fingerprint: CloneGroupFingerprint,
752 pub members: Vec<MemberIds>,
754}
755
756#[must_use]
767pub fn report_ids(
768 files: &[InputFile<'_>],
769 contexts: &[FileContext<'_>],
770 variant: &BuildVariant,
771 report: &EngineReport,
772 literals: LiteralNorm,
773) -> Vec<GroupIds> {
774 report
775 .groups
776 .iter()
777 .map(|group| {
778 let norm = match group.clone_type {
779 CloneClass::Type1 => ContentNorm::Raw,
780 CloneClass::Type2 | CloneClass::Type3 | CloneClass::RestrictedSemantic => {
781 ContentNorm::Normalized(literals)
782 }
783 };
784 let member_fps: Vec<FragmentFingerprint> = group
785 .members
786 .iter()
787 .map(|member| {
788 let file = &files[member.file];
789 let start = member.token_start.min(file.tokens.len());
790 let end = member.token_end.min(file.tokens.len()).max(start);
791 let tokens = &file.tokens[start..end];
792 fragment_fingerprint(variant, &contexts[member.file], "member", tokens, norm)
793 })
794 .collect();
795 let fingerprint = clone_group_fingerprint(variant, group.clone_type, &member_fps);
796
797 let hosts: Vec<Option<UnitFingerprint>> = group
800 .members
801 .iter()
802 .map(|member| {
803 member.unit.map(|unit_idx| {
804 let unit = &files[member.file].units[unit_idx];
805 let file = &files[member.file];
806 let start = unit.token_start.min(file.tokens.len());
807 let end = unit.token_end.min(file.tokens.len()).max(start);
808 let tokens = &file.tokens[start..end];
809 unit_fingerprint(variant, &contexts[member.file], tokens, ContentNorm::Raw)
810 })
811 })
812 .collect();
813 let members = member_fps
814 .iter()
815 .zip(hosts.iter())
816 .enumerate()
817 .map(|(i, (content, host))| {
818 let rank = hosts[..i].iter().filter(|h| *h == host).count();
819 MemberIds {
820 content: *content,
821 finding: finding_id(
822 &fingerprint,
823 host.as_ref(),
824 u32::try_from(rank).unwrap_or(u32::MAX),
825 ),
826 }
827 })
828 .collect();
829 GroupIds {
830 fingerprint,
831 members,
832 }
833 })
834 .collect()
835}
836
837#[cfg(test)]
838#[allow(clippy::expect_used, clippy::unwrap_used)]
839mod tests;