1use std::collections::{BTreeMap, BTreeSet};
21use std::fmt;
22use std::path::Path;
23
24use serde::{Deserialize, Serialize};
25
26use crate::ids::{FunctionId, ValueId};
27use crate::spec::{
28 FunctionSpec, Nullness, ParamSpec, Pointer, ReturnSpec, Role, TaintLocation, TaintSpec,
29};
30
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
47pub enum AccessPath {
48 Param(u32),
50 Global(ValueId),
52 Deref(Box<AccessPath>),
54 Field(Box<AccessPath>, u32),
56 Return,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum AccessPathParseError {
63 Empty,
65 UnknownBase(String),
67 InvalidParamIndex(String),
69 InvalidGlobalId(String),
71 InvalidFieldOffset(String),
73 UnknownStep(String),
75}
76
77impl fmt::Display for AccessPathParseError {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self {
80 Self::Empty => write!(f, "empty access path string"),
81 Self::UnknownBase(s) => write!(f, "unknown access path base: '{s}'"),
82 Self::InvalidParamIndex(s) => write!(f, "invalid param index: '{s}'"),
83 Self::InvalidGlobalId(s) => write!(f, "invalid global ID: '{s}'"),
84 Self::InvalidFieldOffset(s) => write!(f, "invalid field offset: '{s}'"),
85 Self::UnknownStep(s) => write!(f, "unknown access path step: '{s}'"),
86 }
87 }
88}
89
90impl std::error::Error for AccessPathParseError {}
91
92impl AccessPath {
93 pub fn parse(s: &str) -> Result<Self, AccessPathParseError> {
108 let s = s.trim();
109 if s.is_empty() {
110 return Err(AccessPathParseError::Empty);
111 }
112
113 let mut parts = s.split("->");
115 let base_str = parts.next().ok_or(AccessPathParseError::Empty)?.trim();
116
117 let mut path = Self::parse_base(base_str)?;
118
119 for step_str in parts {
120 let step = step_str.trim();
121 path = Self::parse_step(path, step)?;
122 }
123
124 Ok(path)
125 }
126
127 fn parse_base(s: &str) -> Result<Self, AccessPathParseError> {
129 if s == "return" {
130 return Ok(AccessPath::Return);
131 }
132
133 if let Some(idx_str) = s.strip_prefix("param.") {
134 let idx = idx_str
135 .parse::<u32>()
136 .map_err(|_| AccessPathParseError::InvalidParamIndex(idx_str.to_string()))?;
137 return Ok(AccessPath::Param(idx));
138 }
139
140 if let Some(inner) = s.strip_prefix("global(").and_then(|r| r.strip_suffix(')')) {
141 let hex_str = inner.strip_prefix("0x").unwrap_or(inner);
142 let id = u128::from_str_radix(hex_str, 16)
143 .map_err(|_| AccessPathParseError::InvalidGlobalId(inner.to_string()))?;
144 return Ok(AccessPath::Global(ValueId::new(id)));
145 }
146
147 Err(AccessPathParseError::UnknownBase(s.to_string()))
148 }
149
150 fn parse_step(base: Self, step: &str) -> Result<Self, AccessPathParseError> {
152 if step == "deref" {
153 return Ok(AccessPath::Deref(Box::new(base)));
154 }
155
156 if let Some(inner) = step
157 .strip_prefix("field(")
158 .and_then(|r| r.strip_suffix(')'))
159 {
160 let offset = inner
161 .parse::<u32>()
162 .map_err(|_| AccessPathParseError::InvalidFieldOffset(inner.to_string()))?;
163 return Ok(AccessPath::Field(Box::new(base), offset));
164 }
165
166 Err(AccessPathParseError::UnknownStep(step.to_string()))
167 }
168
169 #[must_use]
173 pub fn depth(&self) -> usize {
174 match self {
175 AccessPath::Param(_) | AccessPath::Global(_) | AccessPath::Return => 0,
176 AccessPath::Deref(inner) | AccessPath::Field(inner, _) => 1 + inner.depth(),
177 }
178 }
179
180 #[must_use]
186 pub fn truncate(&self, k: u32) -> Self {
187 self.truncate_inner(k as usize)
188 }
189
190 fn truncate_inner(&self, remaining: usize) -> Self {
192 match self {
193 AccessPath::Param(_) | AccessPath::Global(_) | AccessPath::Return => self.clone(),
194 AccessPath::Deref(inner) => {
195 if remaining == 0 {
196 inner.truncate_inner(0)
198 } else {
199 AccessPath::Deref(Box::new(inner.truncate_inner(remaining - 1)))
200 }
201 }
202 AccessPath::Field(inner, offset) => {
203 if remaining == 0 {
204 inner.truncate_inner(0)
205 } else {
206 AccessPath::Field(Box::new(inner.truncate_inner(remaining - 1)), *offset)
207 }
208 }
209 }
210 }
211}
212
213impl fmt::Display for AccessPath {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 match self {
216 AccessPath::Return => write!(f, "return"),
217 AccessPath::Param(idx) => write!(f, "param.{idx}"),
218 AccessPath::Global(id) => write!(f, "global({id})"),
219 AccessPath::Deref(inner) => write!(f, "{inner}->deref"),
220 AccessPath::Field(inner, offset) => write!(f, "{inner}->field({offset})"),
221 }
222 }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
231#[serde(rename_all = "snake_case")]
232pub enum SummarySource {
233 Spec,
235 Analysis,
237 Default,
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
243#[serde(rename_all = "snake_case")]
244pub enum SummaryPrecision {
245 Sound,
247 BestEffort,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254pub enum SummaryNullness {
255 NonNull,
257 MaybeNull,
259 AlwaysNull,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
268#[serde(rename_all = "snake_case")]
269pub enum SummaryRole {
270 Allocator,
272 Reallocator,
274 Deallocator,
276 Source,
278 Sink,
280 Sanitizer,
282 StringOperation,
284 Io,
286 Custom(String),
288}
289
290impl From<&Role> for SummaryRole {
291 fn from(role: &Role) -> Self {
292 match role {
293 Role::Allocator => SummaryRole::Allocator,
294 Role::Reallocator => SummaryRole::Reallocator,
295 Role::Deallocator => SummaryRole::Deallocator,
296 Role::Source => SummaryRole::Source,
297 Role::Sink => SummaryRole::Sink,
298 Role::Sanitizer => SummaryRole::Sanitizer,
299 Role::StringOperation => SummaryRole::StringOperation,
300 Role::Io => SummaryRole::Io,
301 Role::Custom(s) => SummaryRole::Custom(s.clone()),
302 }
303 }
304}
305
306impl From<&SummaryRole> for Role {
307 fn from(role: &SummaryRole) -> Self {
308 match role {
309 SummaryRole::Allocator => Role::Allocator,
310 SummaryRole::Reallocator => Role::Reallocator,
311 SummaryRole::Deallocator => Role::Deallocator,
312 SummaryRole::Source => Role::Source,
313 SummaryRole::Sink => Role::Sink,
314 SummaryRole::Sanitizer => Role::Sanitizer,
315 SummaryRole::StringOperation => Role::StringOperation,
316 SummaryRole::Io => Role::Io,
317 SummaryRole::Custom(s) => Role::Custom(s.clone()),
318 }
319 }
320}
321
322impl SummaryNullness {
323 #[must_use]
330 pub fn from_spec_nullness(n: &Nullness) -> Self {
331 match n {
332 Nullness::NotNull | Nullness::RequiredNonnull => SummaryNullness::NonNull,
333 Nullness::MaybeNull | Nullness::Nullable => SummaryNullness::MaybeNull,
334 }
335 }
336
337 #[must_use]
342 pub fn to_spec_nullness(self) -> Nullness {
343 match self {
344 SummaryNullness::NonNull => Nullness::NotNull,
345 SummaryNullness::MaybeNull | SummaryNullness::AlwaysNull => Nullness::MaybeNull,
346 }
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
356pub struct ReturnEffect {
357 pub aliases: Option<AccessPath>,
359 pub fresh_allocation: bool,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
365pub struct MemoryEffect {
366 pub path: AccessPath,
368 pub reads: bool,
370 pub writes: bool,
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
376pub struct AllocationEffect {
377 pub target: AccessPath,
379 pub heap: bool,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
385pub enum CalleeRef {
386 Direct(FunctionId),
388 Indirect(AccessPath),
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
394pub struct SummaryTaintPropagation {
395 pub from: AccessPath,
397 pub to: AccessPath,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
406#[serde(rename_all = "snake_case")]
407pub enum SummaryBoundMode {
408 AllocSizeMinusOne,
410 AllocSize,
412 ParamValueMinusOne,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
421pub struct SummaryComputedBound {
422 pub param_index: u32,
424 pub mode: SummaryBoundMode,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct FunctionSummary {
440 pub function_id: FunctionId,
442
443 pub version: u64,
445
446 #[serde(default, skip_serializing_if = "Vec::is_empty")]
448 pub return_effects: Vec<ReturnEffect>,
449
450 #[serde(default, skip_serializing_if = "Vec::is_empty")]
452 pub memory_effects: Vec<MemoryEffect>,
453
454 #[serde(default, skip_serializing_if = "Vec::is_empty")]
456 pub allocation_effects: Vec<AllocationEffect>,
457
458 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
460 pub callees: BTreeSet<CalleeRef>,
461
462 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub role: Option<SummaryRole>,
465
466 #[serde(default)]
468 pub pure: bool,
469
470 #[serde(default)]
472 pub noreturn: bool,
473
474 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
476 pub param_nullness: BTreeMap<u32, SummaryNullness>,
477
478 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub return_nullness: Option<SummaryNullness>,
481
482 #[serde(default, skip_serializing_if = "Vec::is_empty")]
484 pub taint_propagation: Vec<SummaryTaintPropagation>,
485
486 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub return_bound: Option<SummaryComputedBound>,
489
490 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
492 pub param_freed: BTreeMap<u32, bool>,
493
494 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
496 pub param_dereferenced: BTreeMap<u32, bool>,
497
498 pub source: SummarySource,
500
501 pub precision: SummaryPrecision,
503}
504
505impl FunctionSummary {
506 #[must_use]
508 pub fn default_for(function_id: FunctionId) -> Self {
509 Self {
510 function_id,
511 version: 0,
512 return_effects: Vec::new(),
513 memory_effects: Vec::new(),
514 allocation_effects: Vec::new(),
515 callees: BTreeSet::new(),
516 role: None,
517 pure: false,
518 noreturn: false,
519 param_nullness: BTreeMap::new(),
520 return_nullness: None,
521 taint_propagation: Vec::new(),
522 return_bound: None,
523 param_freed: BTreeMap::new(),
524 param_dereferenced: BTreeMap::new(),
525 source: SummarySource::Default,
526 precision: SummaryPrecision::Sound,
527 }
528 }
529
530 pub fn save(&self, cache_dir: &Path) -> Result<(), std::io::Error> {
539 let dir = cache_dir.join("summaries");
540 std::fs::create_dir_all(&dir)?;
541
542 let filename = format!("{}.json", self.function_id.to_hex());
543 let path = dir.join(filename);
544
545 let json = serde_json::to_string_pretty(self)
546 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
547 std::fs::write(path, json)
548 }
549
550 pub fn load(
558 cache_dir: &Path,
559 function_id: &FunctionId,
560 ) -> Result<Option<Self>, std::io::Error> {
561 let filename = format!("{}.json", function_id.to_hex());
562 let path = cache_dir.join("summaries").join(filename);
563
564 if !path.exists() {
565 return Ok(None);
566 }
567
568 let json = std::fs::read_to_string(&path)?;
569 let summary: Self = serde_json::from_str(&json)
570 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
571 Ok(Some(summary))
572 }
573
574 #[must_use]
582 pub fn from_spec(spec: &FunctionSpec, function_id: FunctionId) -> Self {
583 let mut summary = Self::default_for(function_id);
584 summary.source = SummarySource::Spec;
585
586 summary.role = spec.role.as_ref().map(SummaryRole::from);
588
589 summary.pure = spec.pure.unwrap_or(false);
591 summary.noreturn = spec.noreturn.unwrap_or(false);
592
593 for param in &spec.params {
595 let reads = param.reads.unwrap_or(false);
597 let writes = param.modifies.unwrap_or(false);
598 if reads || writes {
599 summary.memory_effects.push(MemoryEffect {
600 path: AccessPath::Deref(Box::new(AccessPath::Param(param.index))),
601 reads,
602 writes,
603 });
604 }
605
606 if let Some(ref nullness) = param.nullness {
608 summary
609 .param_nullness
610 .insert(param.index, SummaryNullness::from_spec_nullness(nullness));
611 }
612
613 if param.callback.unwrap_or(false) {
615 summary
616 .callees
617 .insert(CalleeRef::Indirect(AccessPath::Param(param.index)));
618 }
619 }
620
621 if let Some(ref ret) = spec.returns {
623 Self::convert_return_spec(ret, &mut summary);
624 }
625
626 if let Some(ref taint) = spec.taint {
628 Self::convert_taint_spec(taint, &mut summary);
629 }
630
631 summary
632 }
633
634 fn convert_return_spec(ret: &ReturnSpec, summary: &mut Self) {
636 if let Some(ref nullness) = ret.nullness {
638 summary.return_nullness = Some(SummaryNullness::from_spec_nullness(nullness));
639 }
640
641 let is_fresh = matches!(ret.pointer, Some(Pointer::FreshHeap | Pointer::FreshStack));
643 let is_heap = matches!(ret.pointer, Some(Pointer::FreshHeap));
644
645 if is_fresh {
646 summary.allocation_effects.push(AllocationEffect {
647 target: AccessPath::Return,
648 heap: is_heap,
649 });
650 summary.return_effects.push(ReturnEffect {
651 aliases: None,
652 fresh_allocation: true,
653 });
654 }
655
656 if let Some(param_idx) = ret.alias_param_index() {
658 summary.return_effects.push(ReturnEffect {
659 aliases: Some(AccessPath::Param(param_idx)),
660 fresh_allocation: false,
661 });
662 }
663 }
664
665 fn convert_taint_spec(taint: &TaintSpec, summary: &mut Self) {
667 for prop in &taint.propagates {
668 let from = Self::taint_location_to_access_path(prop.from);
669 let Some(from) = from else {
670 continue;
671 };
672 for to_loc in &prop.to {
673 let Some(to) = Self::taint_location_to_access_path(*to_loc) else {
674 continue;
675 };
676 summary.taint_propagation.push(SummaryTaintPropagation {
677 from: from.clone(),
678 to,
679 });
680 }
681 }
682 }
683
684 fn taint_location_to_access_path(loc: TaintLocation) -> Option<AccessPath> {
688 match loc {
689 TaintLocation::Return => Some(AccessPath::Return),
690 TaintLocation::Param(idx) => Some(AccessPath::Param(idx)),
691 TaintLocation::Unknown => None,
692 }
693 }
694
695 #[must_use]
702 pub fn to_simple_spec(&self, name: &str) -> FunctionSpec {
703 let mut spec = FunctionSpec::new(name);
704
705 spec.role = self.role.as_ref().map(Role::from);
707
708 if self.pure {
710 spec.pure = Some(true);
711 }
712 if self.noreturn {
713 spec.noreturn = Some(true);
714 }
715
716 let mut param_reads: BTreeMap<u32, bool> = BTreeMap::new();
718 let mut param_writes: BTreeMap<u32, bool> = BTreeMap::new();
719
720 for effect in &self.memory_effects {
721 if let Some(idx) = Self::access_path_root_param(&effect.path) {
722 if effect.reads {
723 param_reads.insert(idx, true);
724 }
725 if effect.writes {
726 param_writes.insert(idx, true);
727 }
728 }
729 }
730
731 let mut param_indices: BTreeSet<u32> = BTreeSet::new();
733 param_indices.extend(param_reads.keys());
734 param_indices.extend(param_writes.keys());
735 param_indices.extend(self.param_nullness.keys());
736
737 for idx in param_indices {
738 let mut ps = ParamSpec::new(idx);
739 if param_reads.contains_key(&idx) {
740 ps.reads = Some(true);
741 }
742 if param_writes.contains_key(&idx) {
743 ps.modifies = Some(true);
744 }
745 if let Some(nullness) = self.param_nullness.get(&idx) {
746 ps.nullness = Some(nullness.to_spec_nullness());
747 }
748 spec.params.push(ps);
749 }
750
751 let has_return_info = self.return_nullness.is_some()
753 || !self.return_effects.is_empty()
754 || !self.allocation_effects.is_empty();
755
756 if has_return_info {
757 let mut ret = ReturnSpec::new();
758
759 if let Some(nullness) = self.return_nullness {
760 ret.nullness = Some(nullness.to_spec_nullness());
761 }
762
763 for alloc in &self.allocation_effects {
765 if alloc.target == AccessPath::Return {
766 ret.pointer = Some(if alloc.heap {
767 Pointer::FreshHeap
768 } else {
769 Pointer::FreshStack
770 });
771 break;
772 }
773 }
774
775 for re in &self.return_effects {
777 if let Some(AccessPath::Param(idx)) = &re.aliases {
778 ret.aliases = Some(format!("param.{idx}"));
779 break;
780 }
781 }
782
783 spec.returns = Some(ret);
784 }
785
786 if !self.taint_propagation.is_empty() {
788 let mut propagates = Vec::new();
789 for tp in &self.taint_propagation {
790 let Some(from) = Self::access_path_to_taint_location(&tp.from) else {
791 continue;
792 };
793 let Some(to) = Self::access_path_to_taint_location(&tp.to) else {
794 continue;
795 };
796 if let Some(existing) = propagates
798 .iter_mut()
799 .find(|p: &&mut crate::spec::TaintPropagation| p.from == from)
800 {
801 existing.to.push(to);
802 } else {
803 propagates.push(crate::spec::TaintPropagation { from, to: vec![to] });
804 }
805 }
806 if !propagates.is_empty() {
807 spec.taint = Some(TaintSpec { propagates });
808 }
809 }
810
811 spec
812 }
813
814 fn access_path_root_param(path: &AccessPath) -> Option<u32> {
817 match path {
818 AccessPath::Param(idx) => Some(*idx),
819 AccessPath::Deref(inner) | AccessPath::Field(inner, _) => {
820 Self::access_path_root_param(inner)
821 }
822 AccessPath::Global(_) | AccessPath::Return => None,
823 }
824 }
825
826 fn access_path_to_taint_location(path: &AccessPath) -> Option<TaintLocation> {
831 match path {
832 AccessPath::Return => Some(TaintLocation::Return),
833 AccessPath::Param(idx) => Some(TaintLocation::Param(*idx)),
834 _ => None,
837 }
838 }
839}
840
841#[cfg(test)]
846mod tests {
847 use super::*;
848 use crate::spec::{TaintPropagation as SpecTaintPropagation, TaintSpec};
849
850 #[test]
853 fn parse_return() {
854 let path = AccessPath::parse("return").expect("parse return");
855 assert_eq!(path, AccessPath::Return);
856 }
857
858 #[test]
859 fn parse_param() {
860 let path = AccessPath::parse("param.0").expect("parse param.0");
861 assert_eq!(path, AccessPath::Param(0));
862
863 let path = AccessPath::parse("param.42").expect("parse param.42");
864 assert_eq!(path, AccessPath::Param(42));
865 }
866
867 #[test]
868 fn parse_global() {
869 let path =
870 AccessPath::parse("global(0x00000000000000000000000000000abc)").expect("parse global");
871 assert_eq!(path, AccessPath::Global(ValueId::new(0xabc)));
872 }
873
874 #[test]
875 fn parse_deref() {
876 let path = AccessPath::parse("param.0->deref").expect("parse deref");
877 assert_eq!(path, AccessPath::Deref(Box::new(AccessPath::Param(0))));
878 }
879
880 #[test]
881 fn parse_field() {
882 let path = AccessPath::parse("param.0->field(8)").expect("parse field");
883 assert_eq!(path, AccessPath::Field(Box::new(AccessPath::Param(0)), 8));
884 }
885
886 #[test]
887 fn parse_chained_steps() {
888 let path =
889 AccessPath::parse("param.1->deref->field(4)->deref").expect("parse chained steps");
890 let expected = AccessPath::Deref(Box::new(AccessPath::Field(
891 Box::new(AccessPath::Deref(Box::new(AccessPath::Param(1)))),
892 4,
893 )));
894 assert_eq!(path, expected);
895 }
896
897 #[test]
898 fn parse_display_roundtrip() {
899 let cases = [
900 "return",
901 "param.0",
902 "param.3->deref",
903 "param.1->field(8)",
904 "param.0->deref->field(4)->deref",
905 ];
906 for input in &cases {
907 let parsed = AccessPath::parse(input).expect("parse");
908 let displayed = parsed.to_string();
909 let reparsed = AccessPath::parse(&displayed).expect("reparse");
910 assert_eq!(parsed, reparsed, "roundtrip failed for '{input}'");
911 }
912 }
913
914 #[test]
915 fn parse_errors() {
916 assert!(AccessPath::parse("").is_err());
917 assert!(AccessPath::parse("unknown").is_err());
918 assert!(AccessPath::parse("param.abc").is_err());
919 assert!(AccessPath::parse("param.0->bogus").is_err());
920 assert!(AccessPath::parse("param.0->field(abc)").is_err());
921 }
922
923 #[test]
926 fn depth_base_paths() {
927 assert_eq!(AccessPath::Return.depth(), 0);
928 assert_eq!(AccessPath::Param(0).depth(), 0);
929 assert_eq!(AccessPath::Global(ValueId::new(1)).depth(), 0);
930 }
931
932 #[test]
933 fn depth_nested() {
934 let one = AccessPath::Deref(Box::new(AccessPath::Param(0)));
935 assert_eq!(one.depth(), 1);
936
937 let two = AccessPath::Field(Box::new(one.clone()), 4);
938 assert_eq!(two.depth(), 2);
939
940 let three = AccessPath::Deref(Box::new(two));
941 assert_eq!(three.depth(), 3);
942 }
943
944 #[test]
947 fn truncate_within_limit() {
948 let path = AccessPath::Deref(Box::new(AccessPath::Param(0)));
949 let truncated = path.truncate(5);
950 assert_eq!(truncated, path);
951 }
952
953 #[test]
954 fn truncate_at_zero() {
955 let path = AccessPath::Deref(Box::new(AccessPath::Field(
956 Box::new(AccessPath::Param(0)),
957 8,
958 )));
959 let truncated = path.truncate(0);
960 assert_eq!(truncated, AccessPath::Param(0));
961 assert_eq!(truncated.depth(), 0);
962 }
963
964 #[test]
965 fn truncate_at_one() {
966 let path = AccessPath::Field(
970 Box::new(AccessPath::Deref(Box::new(AccessPath::Param(0)))),
971 4,
972 );
973 let truncated = path.truncate(1);
974 assert_eq!(truncated.depth(), 1);
975 assert_eq!(
976 truncated,
977 AccessPath::Field(Box::new(AccessPath::Param(0)), 4)
978 );
979 }
980
981 #[test]
982 fn truncate_deep_chain() {
983 let path = AccessPath::Deref(Box::new(AccessPath::Field(
985 Box::new(AccessPath::Deref(Box::new(AccessPath::Param(0)))),
986 4,
987 )));
988 assert_eq!(path.depth(), 3);
989 let truncated = path.truncate(2);
990 assert_eq!(truncated.depth(), 2);
991 }
992
993 #[test]
996 fn summary_serde_roundtrip() {
997 let fid = FunctionId::derive(b"test_function");
998 let mut summary = FunctionSummary::default_for(fid);
999 summary.version = 3;
1000 summary.pure = true;
1001 summary.role = Some(SummaryRole::Allocator);
1002 summary.return_nullness = Some(SummaryNullness::MaybeNull);
1003 summary.param_nullness.insert(0, SummaryNullness::NonNull);
1004 summary.return_effects.push(ReturnEffect {
1005 aliases: Some(AccessPath::Param(0)),
1006 fresh_allocation: false,
1007 });
1008 summary.memory_effects.push(MemoryEffect {
1009 path: AccessPath::Deref(Box::new(AccessPath::Param(1))),
1010 reads: true,
1011 writes: false,
1012 });
1013 summary.allocation_effects.push(AllocationEffect {
1014 target: AccessPath::Return,
1015 heap: true,
1016 });
1017 summary
1018 .callees
1019 .insert(CalleeRef::Direct(FunctionId::derive(b"helper")));
1020 summary
1021 .callees
1022 .insert(CalleeRef::Indirect(AccessPath::Param(2)));
1023 summary.taint_propagation.push(SummaryTaintPropagation {
1024 from: AccessPath::Param(0),
1025 to: AccessPath::Return,
1026 });
1027 summary.return_bound = Some(SummaryComputedBound {
1028 param_index: 0,
1029 mode: SummaryBoundMode::AllocSizeMinusOne,
1030 });
1031 summary.param_freed.insert(0, true);
1032 summary.param_dereferenced.insert(1, true);
1033 summary.source = SummarySource::Analysis;
1034 summary.precision = SummaryPrecision::BestEffort;
1035
1036 let json = serde_json::to_string_pretty(&summary).expect("serialize");
1037 let deserialized: FunctionSummary = serde_json::from_str(&json).expect("deserialize");
1038
1039 assert_eq!(deserialized.function_id, fid);
1040 assert_eq!(deserialized.version, 3);
1041 assert!(deserialized.pure);
1042 assert_eq!(deserialized.role, Some(SummaryRole::Allocator));
1043 assert_eq!(
1044 deserialized.return_nullness,
1045 Some(SummaryNullness::MaybeNull)
1046 );
1047 assert_eq!(deserialized.return_effects.len(), 1);
1048 assert_eq!(deserialized.memory_effects.len(), 1);
1049 assert_eq!(deserialized.allocation_effects.len(), 1);
1050 assert_eq!(deserialized.callees.len(), 2);
1051 assert_eq!(deserialized.taint_propagation.len(), 1);
1052 assert!(deserialized.return_bound.is_some());
1053 assert_eq!(deserialized.param_freed.get(&0), Some(&true));
1054 assert_eq!(deserialized.param_dereferenced.get(&1), Some(&true));
1055 assert_eq!(deserialized.source, SummarySource::Analysis);
1056 assert_eq!(deserialized.precision, SummaryPrecision::BestEffort);
1057 }
1058
1059 #[test]
1060 fn summary_default_for_is_conservative() {
1061 let fid = FunctionId::derive(b"unknown");
1062 let summary = FunctionSummary::default_for(fid);
1063
1064 assert_eq!(summary.function_id, fid);
1065 assert_eq!(summary.version, 0);
1066 assert!(!summary.pure);
1067 assert!(!summary.noreturn);
1068 assert!(summary.return_effects.is_empty());
1069 assert!(summary.memory_effects.is_empty());
1070 assert!(summary.allocation_effects.is_empty());
1071 assert!(summary.callees.is_empty());
1072 assert!(summary.role.is_none());
1073 assert!(summary.return_nullness.is_none());
1074 assert_eq!(summary.source, SummarySource::Default);
1075 assert_eq!(summary.precision, SummaryPrecision::Sound);
1076 }
1077
1078 #[test]
1081 fn save_load_roundtrip() {
1082 let tmp_dir = tempfile::tempdir().expect("create temp dir");
1083 let fid = FunctionId::derive(b"save_load_test");
1084 let mut summary = FunctionSummary::default_for(fid);
1085 summary.version = 7;
1086 summary.pure = true;
1087 summary.noreturn = true;
1088 summary.role = Some(SummaryRole::Deallocator);
1089 summary.source = SummarySource::Spec;
1090
1091 summary.save(tmp_dir.path()).expect("save");
1092
1093 let loaded = FunctionSummary::load(tmp_dir.path(), &fid).expect("load");
1094 let loaded = loaded.expect("should exist");
1095
1096 assert_eq!(loaded.function_id, fid);
1097 assert_eq!(loaded.version, 7);
1098 assert!(loaded.pure);
1099 assert!(loaded.noreturn);
1100 assert_eq!(loaded.role, Some(SummaryRole::Deallocator));
1101 assert_eq!(loaded.source, SummarySource::Spec);
1102 }
1103
1104 #[test]
1105 fn load_missing_returns_none() {
1106 let tmp_dir = tempfile::tempdir().expect("create temp dir");
1107 let fid = FunctionId::derive(b"nonexistent");
1108
1109 let result = FunctionSummary::load(tmp_dir.path(), &fid).expect("load should succeed");
1110 assert!(result.is_none());
1111 }
1112
1113 #[test]
1116 fn summary_source_serde() {
1117 let json = serde_json::to_string(&SummarySource::Analysis).expect("serialize");
1118 assert_eq!(json, "\"analysis\"");
1119
1120 let parsed: SummarySource = serde_json::from_str("\"spec\"").expect("deserialize");
1121 assert_eq!(parsed, SummarySource::Spec);
1122 }
1123
1124 #[test]
1125 fn summary_precision_serde() {
1126 let json = serde_json::to_string(&SummaryPrecision::Sound).expect("serialize");
1127 assert_eq!(json, "\"sound\"");
1128
1129 let parsed: SummaryPrecision =
1130 serde_json::from_str("\"best_effort\"").expect("deserialize");
1131 assert_eq!(parsed, SummaryPrecision::BestEffort);
1132 }
1133
1134 #[test]
1135 fn summary_nullness_serde() {
1136 let json = serde_json::to_string(&SummaryNullness::AlwaysNull).expect("serialize");
1137 assert_eq!(json, "\"always_null\"");
1138 }
1139
1140 #[test]
1141 fn summary_role_serde() {
1142 let json = serde_json::to_string(&SummaryRole::Allocator).expect("serialize");
1143 assert_eq!(json, "\"allocator\"");
1144
1145 let custom = SummaryRole::Custom("validator".to_string());
1146 let json = serde_json::to_string(&custom).expect("serialize custom");
1147 let parsed: SummaryRole = serde_json::from_str(&json).expect("deserialize custom");
1148 assert_eq!(parsed, custom);
1149 }
1150
1151 #[test]
1152 fn callee_ref_serde() {
1153 let direct = CalleeRef::Direct(FunctionId::derive(b"target"));
1154 let json = serde_json::to_string(&direct).expect("serialize");
1155 let parsed: CalleeRef = serde_json::from_str(&json).expect("deserialize");
1156 assert_eq!(parsed, direct);
1157
1158 let indirect = CalleeRef::Indirect(AccessPath::Param(0));
1159 let json = serde_json::to_string(&indirect).expect("serialize");
1160 let parsed: CalleeRef = serde_json::from_str(&json).expect("deserialize");
1161 assert_eq!(parsed, indirect);
1162 }
1163
1164 #[test]
1167 fn role_conversion_roundtrip() {
1168 let roles = [
1169 Role::Allocator,
1170 Role::Reallocator,
1171 Role::Deallocator,
1172 Role::Source,
1173 Role::Sink,
1174 Role::Sanitizer,
1175 Role::StringOperation,
1176 Role::Io,
1177 Role::Custom("validator".to_string()),
1178 ];
1179 for role in &roles {
1180 let summary_role = SummaryRole::from(role);
1181 let back = Role::from(&summary_role);
1182 assert_eq!(&back, role);
1183 }
1184 }
1185
1186 #[test]
1189 fn nullness_conversion() {
1190 assert_eq!(
1191 SummaryNullness::from_spec_nullness(&Nullness::NotNull),
1192 SummaryNullness::NonNull
1193 );
1194 assert_eq!(
1195 SummaryNullness::from_spec_nullness(&Nullness::RequiredNonnull),
1196 SummaryNullness::NonNull
1197 );
1198 assert_eq!(
1199 SummaryNullness::from_spec_nullness(&Nullness::MaybeNull),
1200 SummaryNullness::MaybeNull
1201 );
1202 assert_eq!(
1203 SummaryNullness::from_spec_nullness(&Nullness::Nullable),
1204 SummaryNullness::MaybeNull
1205 );
1206 assert_eq!(
1208 SummaryNullness::NonNull.to_spec_nullness(),
1209 Nullness::NotNull
1210 );
1211 assert_eq!(
1212 SummaryNullness::MaybeNull.to_spec_nullness(),
1213 Nullness::MaybeNull
1214 );
1215 assert_eq!(
1216 SummaryNullness::AlwaysNull.to_spec_nullness(),
1217 Nullness::MaybeNull
1218 );
1219 }
1220
1221 #[test]
1224 fn from_spec_minimal() {
1225 let spec = FunctionSpec::new("test_func");
1226 let fid = FunctionId::derive(b"test_func");
1227 let summary = FunctionSummary::from_spec(&spec, fid);
1228
1229 assert_eq!(summary.function_id, fid);
1230 assert_eq!(summary.source, SummarySource::Spec);
1231 assert_eq!(summary.precision, SummaryPrecision::Sound);
1232 assert!(!summary.pure);
1233 assert!(!summary.noreturn);
1234 assert!(summary.role.is_none());
1235 }
1236
1237 #[test]
1238 fn from_spec_allocator() {
1239 let mut spec = FunctionSpec::new("malloc");
1240 spec.role = Some(Role::Allocator);
1241 spec.params.push({
1242 let mut p = ParamSpec::new(0);
1243 p.semantic = Some("allocation_size".to_string());
1244 p
1245 });
1246 spec.returns = Some(ReturnSpec {
1247 pointer: Some(Pointer::FreshHeap),
1248 nullness: Some(Nullness::MaybeNull),
1249 ..ReturnSpec::default()
1250 });
1251
1252 let fid = FunctionId::derive(b"malloc");
1253 let summary = FunctionSummary::from_spec(&spec, fid);
1254
1255 assert_eq!(summary.role, Some(SummaryRole::Allocator));
1256 assert_eq!(summary.return_nullness, Some(SummaryNullness::MaybeNull));
1257 assert_eq!(summary.allocation_effects.len(), 1);
1258 assert!(summary.allocation_effects[0].heap);
1259 assert_eq!(summary.allocation_effects[0].target, AccessPath::Return);
1260 assert_eq!(summary.return_effects.len(), 1);
1261 assert!(summary.return_effects[0].fresh_allocation);
1262 }
1263
1264 #[test]
1265 fn from_spec_with_params() {
1266 let mut spec = FunctionSpec::new("memcpy");
1267 spec.params.push({
1268 let mut p = ParamSpec::new(0);
1269 p.modifies = Some(true);
1270 p.nullness = Some(Nullness::RequiredNonnull);
1271 p
1272 });
1273 spec.params.push({
1274 let mut p = ParamSpec::new(1);
1275 p.reads = Some(true);
1276 p.nullness = Some(Nullness::RequiredNonnull);
1277 p
1278 });
1279 spec.returns = Some(ReturnSpec {
1280 aliases: Some("param.0".to_string()),
1281 ..ReturnSpec::default()
1282 });
1283
1284 let fid = FunctionId::derive(b"memcpy");
1285 let summary = FunctionSummary::from_spec(&spec, fid);
1286
1287 assert_eq!(summary.memory_effects.len(), 2);
1289 assert!(summary.memory_effects.iter().any(|e| {
1291 e.path == AccessPath::Deref(Box::new(AccessPath::Param(0))) && e.writes && !e.reads
1292 }));
1293 assert!(summary.memory_effects.iter().any(|e| {
1295 e.path == AccessPath::Deref(Box::new(AccessPath::Param(1))) && e.reads && !e.writes
1296 }));
1297 assert_eq!(
1299 summary.param_nullness.get(&0),
1300 Some(&SummaryNullness::NonNull)
1301 );
1302 assert_eq!(
1303 summary.param_nullness.get(&1),
1304 Some(&SummaryNullness::NonNull)
1305 );
1306 assert!(
1308 summary
1309 .return_effects
1310 .iter()
1311 .any(|e| e.aliases == Some(AccessPath::Param(0)) && !e.fresh_allocation)
1312 );
1313 }
1314
1315 #[test]
1316 fn from_spec_with_taint() {
1317 let mut spec = FunctionSpec::new("getenv");
1318 spec.role = Some(Role::Source);
1319 spec.taint = Some(TaintSpec {
1320 propagates: vec![SpecTaintPropagation {
1321 from: TaintLocation::Param(0),
1322 to: vec![TaintLocation::Return],
1323 }],
1324 });
1325
1326 let fid = FunctionId::derive(b"getenv");
1327 let summary = FunctionSummary::from_spec(&spec, fid);
1328
1329 assert_eq!(summary.role, Some(SummaryRole::Source));
1330 assert_eq!(summary.taint_propagation.len(), 1);
1331 assert_eq!(summary.taint_propagation[0].from, AccessPath::Param(0));
1332 assert_eq!(summary.taint_propagation[0].to, AccessPath::Return);
1333 }
1334
1335 #[test]
1336 fn from_spec_callback_becomes_indirect_callee() {
1337 let mut spec = FunctionSpec::new("qsort");
1338 spec.params.push({
1339 let mut p = ParamSpec::new(3);
1340 p.callback = Some(true);
1341 p
1342 });
1343
1344 let fid = FunctionId::derive(b"qsort");
1345 let summary = FunctionSummary::from_spec(&spec, fid);
1346
1347 assert!(
1348 summary
1349 .callees
1350 .contains(&CalleeRef::Indirect(AccessPath::Param(3)))
1351 );
1352 }
1353
1354 #[test]
1357 fn to_simple_spec_roundtrip_preserves_key_fields() {
1358 let mut spec = FunctionSpec::new("malloc");
1359 spec.role = Some(Role::Allocator);
1360 spec.pure = Some(true);
1361 spec.returns = Some(ReturnSpec {
1362 pointer: Some(Pointer::FreshHeap),
1363 nullness: Some(Nullness::MaybeNull),
1364 ..ReturnSpec::default()
1365 });
1366
1367 let fid = FunctionId::derive(b"malloc");
1368 let summary = FunctionSummary::from_spec(&spec, fid);
1369 let back = summary.to_simple_spec("malloc");
1370
1371 assert_eq!(back.name, "malloc");
1372 assert_eq!(back.role, Some(Role::Allocator));
1373 assert_eq!(back.pure, Some(true));
1374 let ret = back.returns.as_ref().expect("returns");
1375 assert_eq!(ret.pointer, Some(Pointer::FreshHeap));
1376 assert_eq!(ret.nullness, Some(Nullness::MaybeNull));
1377 }
1378
1379 #[test]
1380 fn to_simple_spec_preserves_param_effects() {
1381 let mut spec = FunctionSpec::new("memcpy");
1382 spec.params.push({
1383 let mut p = ParamSpec::new(0);
1384 p.modifies = Some(true);
1385 p.nullness = Some(Nullness::NotNull);
1386 p
1387 });
1388 spec.params.push({
1389 let mut p = ParamSpec::new(1);
1390 p.reads = Some(true);
1391 p
1392 });
1393
1394 let fid = FunctionId::derive(b"memcpy");
1395 let summary = FunctionSummary::from_spec(&spec, fid);
1396 let back = summary.to_simple_spec("memcpy");
1397
1398 let p0 = back.param(0).expect("param 0");
1399 assert_eq!(p0.modifies, Some(true));
1400 assert_eq!(p0.nullness, Some(Nullness::NotNull));
1401
1402 let p1 = back.param(1).expect("param 1");
1403 assert_eq!(p1.reads, Some(true));
1404 }
1405
1406 #[test]
1407 fn to_simple_spec_preserves_taint() {
1408 let mut spec = FunctionSpec::new("getenv");
1409 spec.taint = Some(TaintSpec {
1410 propagates: vec![SpecTaintPropagation {
1411 from: TaintLocation::Param(0),
1412 to: vec![TaintLocation::Return, TaintLocation::Param(1)],
1413 }],
1414 });
1415
1416 let fid = FunctionId::derive(b"getenv");
1417 let summary = FunctionSummary::from_spec(&spec, fid);
1418 let back = summary.to_simple_spec("getenv");
1419
1420 let taint = back.taint.as_ref().expect("taint");
1421 assert_eq!(taint.propagates.len(), 1);
1422 assert_eq!(taint.propagates[0].from, TaintLocation::Param(0));
1423 assert_eq!(taint.propagates[0].to.len(), 2);
1424 assert!(taint.propagates[0].to.contains(&TaintLocation::Return));
1425 assert!(taint.propagates[0].to.contains(&TaintLocation::Param(1)));
1426 }
1427
1428 #[test]
1429 fn to_simple_spec_alias_roundtrip() {
1430 let mut spec = FunctionSpec::new("realloc");
1431 spec.returns = Some(ReturnSpec {
1432 aliases: Some("param.0".to_string()),
1433 ..ReturnSpec::default()
1434 });
1435
1436 let fid = FunctionId::derive(b"realloc");
1437 let summary = FunctionSummary::from_spec(&spec, fid);
1438 let back = summary.to_simple_spec("realloc");
1439
1440 let ret = back.returns.as_ref().expect("returns");
1441 assert_eq!(ret.aliases, Some("param.0".to_string()));
1442 }
1443}