Skip to main content

weir/
relation_interchange.rs

1//! Relation interchange rows for IFDS fixtures and differential evidence.
2//!
3//! The schema is deliberately byte-oriented and dataflow-generic: it records
4//! source rows, sink rows, graph edges, call-string rows, fact domains, and
5//! output witnesses without depending on a downstream reporter.
6
7use std::collections::BTreeSet;
8use std::error::Error;
9use std::fmt::{Display, Formatter};
10
11/// Relation interchange schema version.
12pub const RELATION_INTERCHANGE_SCHEMA_VERSION: u32 = 1;
13
14const RELATION_INTERCHANGE_MAGIC: [u8; 4] = *b"WRIF";
15
16/// Source or sink relation row.
17#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
18pub struct RelationEndpoint {
19    /// Stable endpoint tuple id.
20    pub id: u32,
21    /// Statement or exploded-node id.
22    pub node_id: u32,
23    /// Procedure id.
24    pub proc_id: u32,
25    /// Block id within the procedure.
26    pub block_id: u32,
27    /// Dataflow fact id.
28    pub fact_id: u32,
29    /// Source file id.
30    pub file_id: u32,
31    /// Source byte start.
32    pub start_byte: u32,
33    /// Source byte end.
34    pub end_byte: u32,
35}
36
37/// Graph or summary edge relation row.
38#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
39pub struct RelationEdge {
40    /// Stable edge tuple id.
41    pub id: u32,
42    /// Source statement or exploded-node id.
43    pub src_node: u32,
44    /// Destination statement or exploded-node id.
45    pub dst_node: u32,
46    /// Edge-kind bit mask.
47    pub edge_kind: u32,
48}
49
50/// Call-string relation row.
51#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
52pub struct RelationCallString {
53    /// Stable call-string tuple id.
54    pub id: u32,
55    /// Caller node id.
56    pub caller_node: u32,
57    /// Callee entry node id.
58    pub callee_node: u32,
59    /// Return-site node id.
60    pub return_node: u32,
61    /// Call-string stack depth represented by this row.
62    pub depth: u32,
63}
64
65/// Fact-domain relation row.
66#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
67pub struct RelationFact {
68    /// Stable fact tuple id.
69    pub id: u32,
70    /// Domain id, such as taint, alias, or lifetime.
71    pub domain_id: u32,
72    /// Stable producer-local fact tag.
73    pub tag: u32,
74}
75
76/// Output witness relation row.
77#[derive(Debug, Clone, Eq, PartialEq)]
78pub struct RelationOutputWitness {
79    /// Source endpoint tuple id.
80    pub source_id: u32,
81    /// Sink endpoint tuple id.
82    pub sink_id: u32,
83    /// Ordered statement or exploded-node path.
84    pub path_nodes: Vec<u32>,
85}
86
87/// Owned relation interchange fixture.
88#[derive(Debug, Clone, Eq, PartialEq, Default)]
89pub struct RelationInterchange {
90    /// Source relation rows.
91    pub sources: Vec<RelationEndpoint>,
92    /// Sink relation rows.
93    pub sinks: Vec<RelationEndpoint>,
94    /// Graph and summary edge rows.
95    pub edges: Vec<RelationEdge>,
96    /// Call-string rows.
97    pub call_strings: Vec<RelationCallString>,
98    /// Fact-domain rows.
99    pub facts: Vec<RelationFact>,
100    /// Output witness rows.
101    pub output_witnesses: Vec<RelationOutputWitness>,
102}
103
104/// Reconstructed source-to-sink witness.
105#[derive(Debug, Clone, Eq, PartialEq)]
106pub struct ReconstructedRelationWitness {
107    /// Source endpoint row.
108    pub source: RelationEndpoint,
109    /// Sink endpoint row.
110    pub sink: RelationEndpoint,
111    /// Ordered witness path.
112    pub path_nodes: Vec<u32>,
113}
114
115/// Relation interchange validation and decode failures.
116#[derive(Debug, Clone, Eq, PartialEq)]
117pub enum RelationInterchangeError {
118    /// Buffer did not start with the interchange magic.
119    BadMagic {
120        /// Observed first four bytes.
121        observed: [u8; 4],
122    },
123    /// Unsupported schema version.
124    UnsupportedVersion {
125        /// Observed version.
126        version: u32,
127    },
128    /// Input bytes ended before a required field.
129    Truncated {
130        /// Field being decoded.
131        field: &'static str,
132    },
133    /// Decode consumed fewer bytes than supplied.
134    TrailingBytes {
135        /// Number of unconsumed bytes.
136        trailing: usize,
137    },
138    /// Required relation family was empty.
139    EmptyRelation {
140        /// Relation family name.
141        relation: &'static str,
142    },
143    /// Duplicate tuple id in a relation family.
144    DuplicateId {
145        /// Relation family name.
146        relation: &'static str,
147        /// Duplicate id.
148        id: u32,
149    },
150    /// Endpoint span is reversed.
151    InvalidEndpointSpan {
152        /// Endpoint id.
153        id: u32,
154        /// Start byte.
155        start_byte: u32,
156        /// End byte.
157        end_byte: u32,
158    },
159    /// Witness references a missing source or sink endpoint.
160    MissingWitnessEndpoint {
161        /// Missing relation family.
162        relation: &'static str,
163        /// Missing endpoint id.
164        id: u32,
165    },
166    /// Witness path is empty or does not connect source and sink nodes.
167    InvalidWitnessPath {
168        /// Source endpoint id.
169        source_id: u32,
170        /// Sink endpoint id.
171        sink_id: u32,
172    },
173}
174
175impl Display for RelationInterchangeError {
176    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
177        match self {
178            Self::BadMagic { observed } => write!(
179                f,
180                "relation interchange magic was {observed:?}. Fix: decode only WRIF relation fixtures."
181            ),
182            Self::UnsupportedVersion { version } => write!(
183                f,
184                "relation interchange schema version {version} is unsupported. Fix: regenerate the fixture with the current schema."
185            ),
186            Self::Truncated { field } => write!(
187                f,
188                "relation interchange bytes ended while reading `{field}`. Fix: reject truncated relation fixtures."
189            ),
190            Self::TrailingBytes { trailing } => write!(
191                f,
192                "relation interchange decode left {trailing} trailing bytes. Fix: write canonical tuple bytes only."
193            ),
194            Self::EmptyRelation { relation } => write!(
195                f,
196                "relation interchange `{relation}` relation is empty. Fix: include complete source, sink, fact, and witness fixtures."
197            ),
198            Self::DuplicateId { relation, id } => write!(
199                f,
200                "relation interchange `{relation}` relation has duplicate id {id}. Fix: deduplicate tuple ids before export."
201            ),
202            Self::InvalidEndpointSpan {
203                id,
204                start_byte,
205                end_byte,
206            } => write!(
207                f,
208                "relation endpoint {id} has invalid span {start_byte}..{end_byte}. Fix: normalize source spans before export."
209            ),
210            Self::MissingWitnessEndpoint { relation, id } => write!(
211                f,
212                "relation witness references missing `{relation}` endpoint {id}. Fix: include every witness endpoint tuple."
213            ),
214            Self::InvalidWitnessPath { source_id, sink_id } => write!(
215                f,
216                "relation witness {source_id}->{sink_id} has an empty or disconnected path. Fix: record source-to-sink path nodes exactly."
217            ),
218        }
219    }
220}
221
222impl Error for RelationInterchangeError {}
223
224/// Serialize relation interchange rows to canonical little-endian bytes.
225///
226/// # Errors
227///
228/// Returns [`RelationInterchangeError`] when the fixture violates tuple
229/// uniqueness, endpoint span, or witness consistency requirements.
230pub fn export_relation_interchange_bytes(
231    interchange: &RelationInterchange,
232) -> Result<Vec<u8>, RelationInterchangeError> {
233    validate_relation_interchange(interchange)?;
234    let mut out = Vec::new();
235    out.extend_from_slice(&RELATION_INTERCHANGE_MAGIC);
236    write_u32(&mut out, RELATION_INTERCHANGE_SCHEMA_VERSION);
237    write_u32(&mut out, interchange.sources.len() as u32);
238    write_u32(&mut out, interchange.sinks.len() as u32);
239    write_u32(&mut out, interchange.edges.len() as u32);
240    write_u32(&mut out, interchange.call_strings.len() as u32);
241    write_u32(&mut out, interchange.facts.len() as u32);
242    write_u32(&mut out, interchange.output_witnesses.len() as u32);
243    for endpoint in &interchange.sources {
244        write_endpoint(&mut out, *endpoint);
245    }
246    for endpoint in &interchange.sinks {
247        write_endpoint(&mut out, *endpoint);
248    }
249    for edge in &interchange.edges {
250        write_u32(&mut out, edge.id);
251        write_u32(&mut out, edge.src_node);
252        write_u32(&mut out, edge.dst_node);
253        write_u32(&mut out, edge.edge_kind);
254    }
255    for call_string in &interchange.call_strings {
256        write_u32(&mut out, call_string.id);
257        write_u32(&mut out, call_string.caller_node);
258        write_u32(&mut out, call_string.callee_node);
259        write_u32(&mut out, call_string.return_node);
260        write_u32(&mut out, call_string.depth);
261    }
262    for fact in &interchange.facts {
263        write_u32(&mut out, fact.id);
264        write_u32(&mut out, fact.domain_id);
265        write_u32(&mut out, fact.tag);
266    }
267    for witness in &interchange.output_witnesses {
268        write_u32(&mut out, witness.source_id);
269        write_u32(&mut out, witness.sink_id);
270        write_u32(&mut out, witness.path_nodes.len() as u32);
271        for node in &witness.path_nodes {
272            write_u32(&mut out, *node);
273        }
274    }
275    Ok(out)
276}
277
278/// Decode canonical relation interchange bytes.
279///
280/// # Errors
281///
282/// Returns [`RelationInterchangeError`] when the bytes are malformed or the
283/// decoded fixture violates relation consistency.
284pub fn import_relation_interchange_bytes(
285    bytes: &[u8],
286) -> Result<RelationInterchange, RelationInterchangeError> {
287    let mut reader = RelationReader { bytes, offset: 0 };
288    let magic = reader.read_magic()?;
289    if magic != RELATION_INTERCHANGE_MAGIC {
290        return Err(RelationInterchangeError::BadMagic { observed: magic });
291    }
292    let version = reader.read_u32("schema_version")?;
293    if version != RELATION_INTERCHANGE_SCHEMA_VERSION {
294        return Err(RelationInterchangeError::UnsupportedVersion { version });
295    }
296    let source_count = reader.read_u32("source_count")? as usize;
297    let sink_count = reader.read_u32("sink_count")? as usize;
298    let edge_count = reader.read_u32("edge_count")? as usize;
299    let call_string_count = reader.read_u32("call_string_count")? as usize;
300    let fact_count = reader.read_u32("fact_count")? as usize;
301    let witness_count = reader.read_u32("witness_count")? as usize;
302    let mut interchange = RelationInterchange {
303        sources: Vec::with_capacity(source_count),
304        sinks: Vec::with_capacity(sink_count),
305        edges: Vec::with_capacity(edge_count),
306        call_strings: Vec::with_capacity(call_string_count),
307        facts: Vec::with_capacity(fact_count),
308        output_witnesses: Vec::with_capacity(witness_count),
309    };
310    for _ in 0..source_count {
311        interchange.sources.push(reader.read_endpoint()?);
312    }
313    for _ in 0..sink_count {
314        interchange.sinks.push(reader.read_endpoint()?);
315    }
316    for _ in 0..edge_count {
317        interchange.edges.push(RelationEdge {
318            id: reader.read_u32("edge.id")?,
319            src_node: reader.read_u32("edge.src_node")?,
320            dst_node: reader.read_u32("edge.dst_node")?,
321            edge_kind: reader.read_u32("edge.edge_kind")?,
322        });
323    }
324    for _ in 0..call_string_count {
325        interchange.call_strings.push(RelationCallString {
326            id: reader.read_u32("call_string.id")?,
327            caller_node: reader.read_u32("call_string.caller_node")?,
328            callee_node: reader.read_u32("call_string.callee_node")?,
329            return_node: reader.read_u32("call_string.return_node")?,
330            depth: reader.read_u32("call_string.depth")?,
331        });
332    }
333    for _ in 0..fact_count {
334        interchange.facts.push(RelationFact {
335            id: reader.read_u32("fact.id")?,
336            domain_id: reader.read_u32("fact.domain_id")?,
337            tag: reader.read_u32("fact.tag")?,
338        });
339    }
340    for _ in 0..witness_count {
341        let source_id = reader.read_u32("witness.source_id")?;
342        let sink_id = reader.read_u32("witness.sink_id")?;
343        let path_len = reader.read_u32("witness.path_len")? as usize;
344        let mut path_nodes = Vec::with_capacity(path_len);
345        for _ in 0..path_len {
346            path_nodes.push(reader.read_u32("witness.path_node")?);
347        }
348        interchange.output_witnesses.push(RelationOutputWitness {
349            source_id,
350            sink_id,
351            path_nodes,
352        });
353    }
354    if reader.offset != bytes.len() {
355        return Err(RelationInterchangeError::TrailingBytes {
356            trailing: bytes.len() - reader.offset,
357        });
358    }
359    validate_relation_interchange(&interchange)?;
360    Ok(interchange)
361}
362
363/// Validate relation interchange tuple identities and witness consistency.
364///
365/// # Errors
366///
367/// Returns [`RelationInterchangeError`] when required relations are empty,
368/// tuple ids duplicate, endpoint spans are invalid, or witnesses are
369/// disconnected from source/sink endpoints.
370pub fn validate_relation_interchange(
371    interchange: &RelationInterchange,
372) -> Result<(), RelationInterchangeError> {
373    require_non_empty("sources", interchange.sources.len())?;
374    require_non_empty("sinks", interchange.sinks.len())?;
375    require_non_empty("facts", interchange.facts.len())?;
376    require_non_empty("output_witnesses", interchange.output_witnesses.len())?;
377    validate_endpoint_ids("sources", &interchange.sources)?;
378    validate_endpoint_ids("sinks", &interchange.sinks)?;
379    validate_ids("edges", interchange.edges.iter().map(|edge| edge.id))?;
380    validate_ids(
381        "call_strings",
382        interchange.call_strings.iter().map(|call| call.id),
383    )?;
384    validate_ids("facts", interchange.facts.iter().map(|fact| fact.id))?;
385    for witness in &interchange.output_witnesses {
386        reconstruct_relation_witness(interchange, witness.source_id, witness.sink_id)?;
387    }
388    Ok(())
389}
390
391/// Reconstruct one source-to-sink witness from relation interchange rows.
392///
393/// # Errors
394///
395/// Returns [`RelationInterchangeError`] when the source, sink, or witness path
396/// is missing or inconsistent.
397pub fn reconstruct_relation_witness(
398    interchange: &RelationInterchange,
399    source_id: u32,
400    sink_id: u32,
401) -> Result<ReconstructedRelationWitness, RelationInterchangeError> {
402    let source = endpoint_by_id(&interchange.sources, source_id, "sources")?;
403    let sink = endpoint_by_id(&interchange.sinks, sink_id, "sinks")?;
404    let witness = interchange
405        .output_witnesses
406        .iter()
407        .find(|witness| witness.source_id == source_id && witness.sink_id == sink_id)
408        .ok_or(RelationInterchangeError::MissingWitnessEndpoint {
409            relation: "output_witnesses",
410            id: source_id,
411        })?;
412    if witness.path_nodes.first() != Some(&source.node_id)
413        || witness.path_nodes.last() != Some(&sink.node_id)
414    {
415        return Err(RelationInterchangeError::InvalidWitnessPath { source_id, sink_id });
416    }
417    Ok(ReconstructedRelationWitness {
418        source,
419        sink,
420        path_nodes: witness.path_nodes.clone(),
421    })
422}
423
424fn require_non_empty(relation: &'static str, len: usize) -> Result<(), RelationInterchangeError> {
425    if len == 0 {
426        return Err(RelationInterchangeError::EmptyRelation { relation });
427    }
428    Ok(())
429}
430
431fn validate_endpoint_ids(
432    relation: &'static str,
433    endpoints: &[RelationEndpoint],
434) -> Result<(), RelationInterchangeError> {
435    validate_ids(relation, endpoints.iter().map(|endpoint| endpoint.id))?;
436    for endpoint in endpoints {
437        if endpoint.end_byte < endpoint.start_byte {
438            return Err(RelationInterchangeError::InvalidEndpointSpan {
439                id: endpoint.id,
440                start_byte: endpoint.start_byte,
441                end_byte: endpoint.end_byte,
442            });
443        }
444    }
445    Ok(())
446}
447
448fn validate_ids(
449    relation: &'static str,
450    ids: impl Iterator<Item = u32>,
451) -> Result<(), RelationInterchangeError> {
452    let mut seen = BTreeSet::new();
453    for id in ids {
454        if !seen.insert(id) {
455            return Err(RelationInterchangeError::DuplicateId { relation, id });
456        }
457    }
458    Ok(())
459}
460
461fn endpoint_by_id(
462    endpoints: &[RelationEndpoint],
463    id: u32,
464    relation: &'static str,
465) -> Result<RelationEndpoint, RelationInterchangeError> {
466    endpoints
467        .iter()
468        .copied()
469        .find(|endpoint| endpoint.id == id)
470        .ok_or(RelationInterchangeError::MissingWitnessEndpoint { relation, id })
471}
472
473fn write_endpoint(out: &mut Vec<u8>, endpoint: RelationEndpoint) {
474    write_u32(out, endpoint.id);
475    write_u32(out, endpoint.node_id);
476    write_u32(out, endpoint.proc_id);
477    write_u32(out, endpoint.block_id);
478    write_u32(out, endpoint.fact_id);
479    write_u32(out, endpoint.file_id);
480    write_u32(out, endpoint.start_byte);
481    write_u32(out, endpoint.end_byte);
482}
483
484fn write_u32(out: &mut Vec<u8>, value: u32) {
485    out.extend_from_slice(&value.to_le_bytes());
486}
487
488struct RelationReader<'a> {
489    bytes: &'a [u8],
490    offset: usize,
491}
492
493impl<'a> RelationReader<'a> {
494    fn read_magic(&mut self) -> Result<[u8; 4], RelationInterchangeError> {
495        let Some(bytes) = self.bytes.get(self.offset..self.offset + 4) else {
496            return Err(RelationInterchangeError::Truncated { field: "magic" });
497        };
498        self.offset += 4;
499        Ok([bytes[0], bytes[1], bytes[2], bytes[3]])
500    }
501
502    fn read_endpoint(&mut self) -> Result<RelationEndpoint, RelationInterchangeError> {
503        Ok(RelationEndpoint {
504            id: self.read_u32("endpoint.id")?,
505            node_id: self.read_u32("endpoint.node_id")?,
506            proc_id: self.read_u32("endpoint.proc_id")?,
507            block_id: self.read_u32("endpoint.block_id")?,
508            fact_id: self.read_u32("endpoint.fact_id")?,
509            file_id: self.read_u32("endpoint.file_id")?,
510            start_byte: self.read_u32("endpoint.start_byte")?,
511            end_byte: self.read_u32("endpoint.end_byte")?,
512        })
513    }
514
515    fn read_u32(&mut self, field: &'static str) -> Result<u32, RelationInterchangeError> {
516        let Some(bytes) = self.bytes.get(self.offset..self.offset + 4) else {
517            return Err(RelationInterchangeError::Truncated { field });
518        };
519        self.offset += 4;
520        Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    fn fixture() -> RelationInterchange {
529        RelationInterchange {
530            sources: vec![RelationEndpoint {
531                id: 1,
532                node_id: 10,
533                proc_id: 0,
534                block_id: 1,
535                fact_id: 7,
536                file_id: 3,
537                start_byte: 100,
538                end_byte: 110,
539            }],
540            sinks: vec![RelationEndpoint {
541                id: 2,
542                node_id: 30,
543                proc_id: 1,
544                block_id: 4,
545                fact_id: 9,
546                file_id: 3,
547                start_byte: 200,
548                end_byte: 210,
549            }],
550            edges: vec![
551                RelationEdge {
552                    id: 1,
553                    src_node: 10,
554                    dst_node: 20,
555                    edge_kind: 1,
556                },
557                RelationEdge {
558                    id: 2,
559                    src_node: 20,
560                    dst_node: 30,
561                    edge_kind: 2,
562                },
563            ],
564            call_strings: vec![RelationCallString {
565                id: 1,
566                caller_node: 10,
567                callee_node: 20,
568                return_node: 30,
569                depth: 1,
570            }],
571            facts: vec![
572                RelationFact {
573                    id: 7,
574                    domain_id: 1,
575                    tag: 11,
576                },
577                RelationFact {
578                    id: 9,
579                    domain_id: 1,
580                    tag: 13,
581                },
582            ],
583            output_witnesses: vec![RelationOutputWitness {
584                source_id: 1,
585                sink_id: 2,
586                path_nodes: vec![10, 20, 30],
587            }],
588        }
589    }
590
591    #[test]
592    fn relation_interchange_exports_exact_tuple_bytes_and_imports_them() {
593        let interchange = fixture();
594
595        let bytes = export_relation_interchange_bytes(&interchange).unwrap();
596
597        assert_eq!(&bytes[0..4], b"WRIF");
598        assert_eq!(u32::from_le_bytes(bytes[8..12].try_into().unwrap()), 1);
599        assert_eq!(u32::from_le_bytes(bytes[12..16].try_into().unwrap()), 1);
600        assert_eq!(u32::from_le_bytes(bytes[16..20].try_into().unwrap()), 2);
601        assert_eq!(import_relation_interchange_bytes(&bytes).unwrap(), interchange);
602    }
603
604    #[test]
605    fn relation_interchange_reconstructs_output_witness() {
606        let interchange = fixture();
607
608        let witness = reconstruct_relation_witness(&interchange, 1, 2).unwrap();
609
610        assert_eq!(witness.source.node_id, 10);
611        assert_eq!(witness.sink.node_id, 30);
612        assert_eq!(witness.path_nodes, vec![10, 20, 30]);
613    }
614
615    #[test]
616    fn relation_interchange_rejects_disconnected_witness() {
617        let mut interchange = fixture();
618        interchange.output_witnesses[0].path_nodes = vec![10, 20];
619
620        let error = validate_relation_interchange(&interchange).unwrap_err();
621
622        assert!(matches!(
623            error,
624            RelationInterchangeError::InvalidWitnessPath {
625                source_id: 1,
626                sink_id: 2
627            }
628        ));
629    }
630}