1use std::collections::BTreeSet;
8use std::error::Error;
9use std::fmt::{Display, Formatter};
10
11pub const RELATION_INTERCHANGE_SCHEMA_VERSION: u32 = 1;
13
14const RELATION_INTERCHANGE_MAGIC: [u8; 4] = *b"WRIF";
15
16#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
18pub struct RelationEndpoint {
19 pub id: u32,
21 pub node_id: u32,
23 pub proc_id: u32,
25 pub block_id: u32,
27 pub fact_id: u32,
29 pub file_id: u32,
31 pub start_byte: u32,
33 pub end_byte: u32,
35}
36
37#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
39pub struct RelationEdge {
40 pub id: u32,
42 pub src_node: u32,
44 pub dst_node: u32,
46 pub edge_kind: u32,
48}
49
50#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
52pub struct RelationCallString {
53 pub id: u32,
55 pub caller_node: u32,
57 pub callee_node: u32,
59 pub return_node: u32,
61 pub depth: u32,
63}
64
65#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
67pub struct RelationFact {
68 pub id: u32,
70 pub domain_id: u32,
72 pub tag: u32,
74}
75
76#[derive(Debug, Clone, Eq, PartialEq)]
78pub struct RelationOutputWitness {
79 pub source_id: u32,
81 pub sink_id: u32,
83 pub path_nodes: Vec<u32>,
85}
86
87#[derive(Debug, Clone, Eq, PartialEq, Default)]
89pub struct RelationInterchange {
90 pub sources: Vec<RelationEndpoint>,
92 pub sinks: Vec<RelationEndpoint>,
94 pub edges: Vec<RelationEdge>,
96 pub call_strings: Vec<RelationCallString>,
98 pub facts: Vec<RelationFact>,
100 pub output_witnesses: Vec<RelationOutputWitness>,
102}
103
104#[derive(Debug, Clone, Eq, PartialEq)]
106pub struct ReconstructedRelationWitness {
107 pub source: RelationEndpoint,
109 pub sink: RelationEndpoint,
111 pub path_nodes: Vec<u32>,
113}
114
115#[derive(Debug, Clone, Eq, PartialEq)]
117pub enum RelationInterchangeError {
118 BadMagic {
120 observed: [u8; 4],
122 },
123 UnsupportedVersion {
125 version: u32,
127 },
128 Truncated {
130 field: &'static str,
132 },
133 TrailingBytes {
135 trailing: usize,
137 },
138 EmptyRelation {
140 relation: &'static str,
142 },
143 DuplicateId {
145 relation: &'static str,
147 id: u32,
149 },
150 InvalidEndpointSpan {
152 id: u32,
154 start_byte: u32,
156 end_byte: u32,
158 },
159 MissingWitnessEndpoint {
161 relation: &'static str,
163 id: u32,
165 },
166 InvalidWitnessPath {
168 source_id: u32,
170 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
224pub 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
278pub 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
363pub 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
391pub 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}