Skip to main content

saf_core/
summary.rs

1//! Function summaries for compositional analysis.
2//!
3//! A [`FunctionSummary`] is a compact, serializable description of a function's
4//! pointer and memory behavior. Summaries serve as the universal exchange format
5//! between:
6//!
7//! - **YAML specs** (hand-written for external/library functions)
8//! - **Analysis results** (computed by PTA, value-flow, etc.)
9//! - **Compositional analysis** (bottom-up summary composition)
10//!
11//! Access paths ([`AccessPath`]) describe memory locations relative to function
12//! parameters and globals, enabling field-sensitive and context-sensitive
13//! summaries without concrete `ValueId` bindings.
14//!
15//! # Persistence
16//!
17//! Summaries support JSON serialization to disk via [`FunctionSummary::save`] and
18//! [`FunctionSummary::load`], stored at `{cache_dir}/summaries/{function_id_hex}.json`.
19
20use 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// ---------------------------------------------------------------------------
32// AccessPath
33// ---------------------------------------------------------------------------
34
35/// Describes a memory access location relative to function parameters or globals.
36///
37/// Access paths form a tree rooted at a base (`Param`, `Global`, or `Return`)
38/// with `Deref` and `Field` steps descending into pointed-to memory.
39///
40/// # Examples
41///
42/// - `Param(0)` -- the first parameter itself
43/// - `Deref(Param(0))` -- what the first parameter points to
44/// - `Field(Deref(Param(0)), 2)` -- field 2 of the struct pointed to by param 0
45/// - `Return` -- the return value
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
47pub enum AccessPath {
48    /// A function parameter by zero-based index.
49    Param(u32),
50    /// A global variable identified by its `ValueId`.
51    Global(ValueId),
52    /// Dereference of an inner access path (one level of pointer indirection).
53    Deref(Box<AccessPath>),
54    /// Field access at a given byte offset within a struct.
55    Field(Box<AccessPath>, u32),
56    /// The function return value.
57    Return,
58}
59
60/// Errors that can occur when parsing an [`AccessPath`] from a string.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum AccessPathParseError {
63    /// The input string was empty.
64    Empty,
65    /// An unrecognized base token was encountered.
66    UnknownBase(String),
67    /// A `param.N` token had a non-integer index.
68    InvalidParamIndex(String),
69    /// A `global(0x...)` token had an invalid hex ID.
70    InvalidGlobalId(String),
71    /// A `field(N)` step had a non-integer offset.
72    InvalidFieldOffset(String),
73    /// An unrecognized step after `->` was encountered.
74    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    /// Parse an access path from its string representation.
94    ///
95    /// Supported formats:
96    /// - `"return"` -- [`AccessPath::Return`]
97    /// - `"param.N"` -- [`AccessPath::Param`] with index N
98    /// - `"global(0x...)"` -- [`AccessPath::Global`] with hex ID
99    /// - `"param.0->deref"` -- [`AccessPath::Deref`] wrapping `Param(0)`
100    /// - `"param.0->field(8)"` -- [`AccessPath::Field`] with offset 8
101    /// - Steps can be chained: `"param.0->deref->field(4)->deref"`
102    ///
103    /// # Errors
104    ///
105    /// Returns [`AccessPathParseError`] if the string is empty, contains an
106    /// unrecognized base or step token, or has invalid numeric indices.
107    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        // Split into base and steps on "->"
114        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    /// Parse the base token (before any `->` steps).
128    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    /// Parse a single step token and wrap the current path.
151    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    /// Compute the depth of this access path (number of `Deref`/`Field` steps).
170    ///
171    /// Base paths (`Param`, `Global`, `Return`) have depth 0.
172    #[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    /// Truncate this access path to at most `k` levels of `Deref`/`Field`.
181    ///
182    /// If the path is already within the limit, returns a clone. Otherwise,
183    /// the deepest steps beyond `k` are dropped, and the path is terminated
184    /// at the k-th level.
185    #[must_use]
186    pub fn truncate(&self, k: u32) -> Self {
187        self.truncate_inner(k as usize)
188    }
189
190    /// Recursive truncation helper.
191    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                    // At the limit -- return the inner base without this Deref
197                    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// ---------------------------------------------------------------------------
226// Supporting enums
227// ---------------------------------------------------------------------------
228
229/// How a function summary was produced.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
231#[serde(rename_all = "snake_case")]
232pub enum SummarySource {
233    /// From a YAML spec file (hand-authored or shipped).
234    Spec,
235    /// Computed by static analysis.
236    Analysis,
237    /// Default conservative assumption.
238    Default,
239}
240
241/// Precision guarantee of a function summary.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
243#[serde(rename_all = "snake_case")]
244pub enum SummaryPrecision {
245    /// Sound over-approximation (no false negatives).
246    Sound,
247    /// Best-effort approximation (may miss behaviors).
248    BestEffort,
249}
250
251/// Nullness classification for a pointer value.
252#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254pub enum SummaryNullness {
255    /// Guaranteed non-null.
256    NonNull,
257    /// May be null.
258    MaybeNull,
259    /// Always null.
260    AlwaysNull,
261}
262
263/// High-level role of a function in the summary system.
264///
265/// Mirrors [`Role`] but is self-contained for summary serialization
266/// without depending on the spec module's enum variants.
267#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
268#[serde(rename_all = "snake_case")]
269pub enum SummaryRole {
270    /// Memory allocator (e.g., `malloc`, `new`).
271    Allocator,
272    /// Memory reallocator (e.g., `realloc`).
273    Reallocator,
274    /// Memory deallocator (e.g., `free`, `delete`).
275    Deallocator,
276    /// Taint source (e.g., `getenv`, `recv`).
277    Source,
278    /// Taint sink (e.g., `system`, `exec`).
279    Sink,
280    /// Taint sanitizer.
281    Sanitizer,
282    /// String operation (e.g., `strlen`, `strcpy`).
283    StringOperation,
284    /// I/O operation (e.g., `read`, `write`).
285    Io,
286    /// Custom role for domain-specific analyses.
287    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    /// Convert from the spec system's [`Nullness`] to [`SummaryNullness`].
324    ///
325    /// The spec system has four variants (`NotNull`, `MaybeNull`,
326    /// `RequiredNonnull`, `Nullable`). For summary purposes, `NotNull` and
327    /// `RequiredNonnull` both map to `NonNull`, and `Nullable` maps to
328    /// `MaybeNull`.
329    #[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    /// Convert to the spec system's [`Nullness`].
338    ///
339    /// This is lossy: `AlwaysNull` has no direct spec equivalent and maps to
340    /// `MaybeNull` (the conservative choice).
341    #[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// ---------------------------------------------------------------------------
351// Effect types
352// ---------------------------------------------------------------------------
353
354/// Describes the effect of a function on its return value.
355#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
356pub struct ReturnEffect {
357    /// What the return value aliases (e.g., `Param(0)` means "returns param 0").
358    pub aliases: Option<AccessPath>,
359    /// Whether the return value is freshly allocated.
360    pub fresh_allocation: bool,
361}
362
363/// Describes a memory side-effect of a function.
364#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
365pub struct MemoryEffect {
366    /// The access path being read or written.
367    pub path: AccessPath,
368    /// Whether this effect is a read.
369    pub reads: bool,
370    /// Whether this effect is a write.
371    pub writes: bool,
372}
373
374/// Describes an allocation performed by a function.
375#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
376pub struct AllocationEffect {
377    /// Where the allocated pointer flows to (usually `Return` or a parameter).
378    pub target: AccessPath,
379    /// Whether the allocation is heap-based (vs. stack/static).
380    pub heap: bool,
381}
382
383/// Reference to a callee from within a function body.
384#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
385pub enum CalleeRef {
386    /// Direct call to a known function.
387    Direct(FunctionId),
388    /// Indirect call through a function pointer at an access path.
389    Indirect(AccessPath),
390}
391
392/// Taint propagation rule within a function summary.
393#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
394pub struct SummaryTaintPropagation {
395    /// Where taint originates (e.g., `Param(0)`).
396    pub from: AccessPath,
397    /// Where taint flows to (e.g., `Return`).
398    pub to: AccessPath,
399}
400
401/// How a return value is bounded by a parameter property.
402///
403/// Mirrors [`crate::spec::derived::BoundMode`] with serde support for
404/// summary persistence.
405#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
406#[serde(rename_all = "snake_case")]
407pub enum SummaryBoundMode {
408    /// Return in `[0, alloc_size(param) - 1]`.
409    AllocSizeMinusOne,
410    /// Return in `[0, alloc_size(param)]`.
411    AllocSize,
412    /// Return in `[-1, param_value - 1]`.
413    ParamValueMinusOne,
414}
415
416/// A computed return bound: return interval depends on a parameter property.
417///
418/// Mirrors [`crate::spec::derived::ComputedBound`] with serde support for
419/// summary persistence.
420#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
421pub struct SummaryComputedBound {
422    /// Which parameter's property bounds the return value.
423    pub param_index: u32,
424    /// How the return is bounded.
425    pub mode: SummaryBoundMode,
426}
427
428// ---------------------------------------------------------------------------
429// FunctionSummary
430// ---------------------------------------------------------------------------
431
432/// A compact, serializable description of a function's pointer and memory behavior.
433///
434/// This is the universal exchange format for function behavior information.
435/// Summaries can originate from YAML specs, static analysis, or default
436/// conservative assumptions, and are used by incremental and compositional
437/// analyses to avoid re-analyzing unchanged functions.
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct FunctionSummary {
440    /// The function this summary describes.
441    pub function_id: FunctionId,
442
443    /// Monotonically increasing version for cache invalidation.
444    pub version: u64,
445
446    /// Effects on the return value (aliasing, fresh allocation).
447    #[serde(default, skip_serializing_if = "Vec::is_empty")]
448    pub return_effects: Vec<ReturnEffect>,
449
450    /// Memory read/write side effects.
451    #[serde(default, skip_serializing_if = "Vec::is_empty")]
452    pub memory_effects: Vec<MemoryEffect>,
453
454    /// Allocation effects (heap/stack objects created by this function).
455    #[serde(default, skip_serializing_if = "Vec::is_empty")]
456    pub allocation_effects: Vec<AllocationEffect>,
457
458    /// Known callees (direct and indirect).
459    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
460    pub callees: BTreeSet<CalleeRef>,
461
462    /// High-level role (allocator, source, sink, etc.).
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub role: Option<SummaryRole>,
465
466    /// Whether the function is pure (no side effects, result depends only on inputs).
467    #[serde(default)]
468    pub pure: bool,
469
470    /// Whether the function never returns (e.g., `exit`, `abort`).
471    #[serde(default)]
472    pub noreturn: bool,
473
474    /// Per-parameter nullness classification.
475    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
476    pub param_nullness: BTreeMap<u32, SummaryNullness>,
477
478    /// Return value nullness classification.
479    #[serde(default, skip_serializing_if = "Option::is_none")]
480    pub return_nullness: Option<SummaryNullness>,
481
482    /// Taint propagation rules.
483    #[serde(default, skip_serializing_if = "Vec::is_empty")]
484    pub taint_propagation: Vec<SummaryTaintPropagation>,
485
486    /// Computed return value bound relative to a parameter property.
487    #[serde(default, skip_serializing_if = "Option::is_none")]
488    pub return_bound: Option<SummaryComputedBound>,
489
490    /// Per-parameter: whether the callee frees this parameter.
491    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
492    pub param_freed: BTreeMap<u32, bool>,
493
494    /// Per-parameter: whether the callee dereferences this parameter.
495    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
496    pub param_dereferenced: BTreeMap<u32, bool>,
497
498    /// How this summary was produced.
499    pub source: SummarySource,
500
501    /// Precision guarantee.
502    pub precision: SummaryPrecision,
503}
504
505impl FunctionSummary {
506    /// Create a new default (conservative) summary for the given function.
507    #[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    /// Save this summary to `{cache_dir}/summaries/{function_id_hex}.json`.
531    ///
532    /// Creates the `summaries/` subdirectory if it does not exist.
533    ///
534    /// # Errors
535    ///
536    /// Returns an I/O error if the directory cannot be created or the file
537    /// cannot be written.
538    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    /// Load a summary from `{cache_dir}/summaries/{function_id_hex}.json`.
551    ///
552    /// Returns `Ok(None)` if the file does not exist.
553    ///
554    /// # Errors
555    ///
556    /// Returns an I/O error if the file exists but cannot be read or parsed.
557    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    // -- Spec conversion --
575
576    /// Convert a [`FunctionSpec`] into a [`FunctionSummary`].
577    ///
578    /// This conversion is lossless: every spec field maps to a corresponding
579    /// summary field. The resulting summary has `source = Spec` and
580    /// `precision = Sound` (specs are assumed to be authoritative).
581    #[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        // Role
587        summary.role = spec.role.as_ref().map(SummaryRole::from);
588
589        // Flags
590        summary.pure = spec.pure.unwrap_or(false);
591        summary.noreturn = spec.noreturn.unwrap_or(false);
592
593        // Per-parameter effects
594        for param in &spec.params {
595            // Memory effects from reads/modifies
596            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            // Nullness
607            if let Some(ref nullness) = param.nullness {
608                summary
609                    .param_nullness
610                    .insert(param.index, SummaryNullness::from_spec_nullness(nullness));
611            }
612
613            // Callback → indirect callee
614            if param.callback.unwrap_or(false) {
615                summary
616                    .callees
617                    .insert(CalleeRef::Indirect(AccessPath::Param(param.index)));
618            }
619        }
620
621        // Return effects
622        if let Some(ref ret) = spec.returns {
623            Self::convert_return_spec(ret, &mut summary);
624        }
625
626        // Taint propagation
627        if let Some(ref taint) = spec.taint {
628            Self::convert_taint_spec(taint, &mut summary);
629        }
630
631        summary
632    }
633
634    /// Helper: convert a [`ReturnSpec`] into summary return/allocation effects.
635    fn convert_return_spec(ret: &ReturnSpec, summary: &mut Self) {
636        // Nullness
637        if let Some(ref nullness) = ret.nullness {
638            summary.return_nullness = Some(SummaryNullness::from_spec_nullness(nullness));
639        }
640
641        // Fresh allocation
642        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        // Alias
657        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    /// Helper: convert a [`TaintSpec`] into summary taint propagation rules.
666    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    /// Convert a [`TaintLocation`] to an [`AccessPath`].
685    ///
686    /// Returns `None` for `TaintLocation::Unknown`.
687    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    /// Convert this summary to a simple [`FunctionSpec`] (lossy).
696    ///
697    /// Access paths are collapsed to parameter-level reads/modifies. Complex
698    /// effects (field-sensitive access, deep dereferences) are simplified.
699    /// The resulting spec preserves role, pure, noreturn, nullness, taint,
700    /// and parameter-level read/write information.
701    #[must_use]
702    pub fn to_simple_spec(&self, name: &str) -> FunctionSpec {
703        let mut spec = FunctionSpec::new(name);
704
705        // Role
706        spec.role = self.role.as_ref().map(Role::from);
707
708        // Flags
709        if self.pure {
710            spec.pure = Some(true);
711        }
712        if self.noreturn {
713            spec.noreturn = Some(true);
714        }
715
716        // Collect per-parameter info from memory effects
717        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        // Build ParamSpecs from all param indices we know about
732        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        // Return spec
752        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            // Check for fresh allocation targeting Return
764            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            // Check for return alias
776            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        // Taint propagation
787        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                // Group by `from` to produce multi-target rules
797                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    /// Extract the root parameter index from an access path, if it is
815    /// rooted at a `Param`.
816    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    /// Convert an [`AccessPath`] to a [`TaintLocation`] (lossy).
827    ///
828    /// Only `Param` and `Return` bases are representable; other paths
829    /// return `None`.
830    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            // Complex paths (Deref, Field, Global) cannot be represented
835            // in the simple TaintLocation system.
836            _ => None,
837        }
838    }
839}
840
841// ---------------------------------------------------------------------------
842// Tests
843// ---------------------------------------------------------------------------
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848    use crate::spec::{TaintPropagation as SpecTaintPropagation, TaintSpec};
849
850    // -- AccessPath parsing --
851
852    #[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    // -- AccessPath depth --
924
925    #[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    // -- AccessPath truncation --
945
946    #[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        // param.0->deref->field(4) (depth 2) truncated to k=1
967        // The outermost step (Field) is kept, but the inner Deref is stripped,
968        // resulting in Field(Param(0), 4) with depth 1.
969        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        // param.0->deref->field(4)->deref (depth 3) truncated to k=2
984        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    // -- FunctionSummary serialization --
994
995    #[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    // -- Disk persistence --
1079
1080    #[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    // -- Supporting type serde --
1114
1115    #[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    // -- Role conversion --
1165
1166    #[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    // -- Nullness conversion --
1187
1188    #[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        // Roundtrip (lossy for AlwaysNull)
1207        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    // -- from_spec --
1222
1223    #[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        // Memory effects
1288        assert_eq!(summary.memory_effects.len(), 2);
1289        // Param 0 writes
1290        assert!(summary.memory_effects.iter().any(|e| {
1291            e.path == AccessPath::Deref(Box::new(AccessPath::Param(0))) && e.writes && !e.reads
1292        }));
1293        // Param 1 reads
1294        assert!(summary.memory_effects.iter().any(|e| {
1295            e.path == AccessPath::Deref(Box::new(AccessPath::Param(1))) && e.reads && !e.writes
1296        }));
1297        // Nullness
1298        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        // Return alias
1307        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    // -- to_simple_spec --
1355
1356    #[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}