Skip to main content

rust_igraph/algorithms/io/
graphdb.rs

1//! `GraphDB` binary format reader (ALGO-IO-008).
2//!
3//! Reads graphs in the binary format used by the ARG Graph Database
4//! for isomorphism testing. This is a read-only format.
5//!
6//! The file is composed of 16-bit little-endian words. The first word
7//! is the number of vertices. Then for each vertex, the file contains
8//! a word giving the number of outgoing edges, followed by a word for
9//! each destination vertex (0-based).
10//!
11//! Reference: M. De Santo, P. Foggia, C. Sansone, M. Vento (2003).
12//! A large database of graphs and its use for benchmarking graph
13//! isomorphism algorithms. Pattern Recognition Letters, 24(8).
14//!
15//! Counterpart of `igraph_read_graph_graphdb`.
16
17use std::io::Read;
18
19use crate::core::{Graph, IgraphError, IgraphResult};
20
21/// Read a graph from `GraphDB` binary format.
22///
23/// The format stores adjacency lists in packed little-endian 16-bit
24/// words. Creates a directed or undirected graph depending on the
25/// `directed` parameter.
26///
27/// # Examples
28///
29/// ```
30/// use rust_igraph::read_graphdb;
31///
32/// // 2 vertices; vertex 0 has 1 edge to vertex 1; vertex 1 has 0 edges
33/// let data: Vec<u8> = vec![
34///     2, 0,  // n_vertices = 2
35///     1, 0,  // vertex 0: 1 neighbor
36///     1, 0,  // neighbor: vertex 1
37///     0, 0,  // vertex 1: 0 neighbors
38/// ];
39/// let g = read_graphdb(&data[..], true).unwrap();
40/// assert_eq!(g.vcount(), 2);
41/// assert_eq!(g.ecount(), 1);
42/// assert!(g.is_directed());
43/// ```
44pub fn read_graphdb<R: Read>(mut input: R, directed: bool) -> IgraphResult<Graph> {
45    let n_vertices = read_u16_le(&mut input, "vertex count")?;
46
47    let mut edges: Vec<(u32, u32)> = Vec::new();
48
49    for v in 0..u32::from(n_vertices) {
50        let degree = read_u16_le(&mut input, "edge count")?;
51
52        for _ in 0..degree {
53            let target = read_u16_le(&mut input, "target vertex")?;
54            let target_u32 = u32::from(target);
55
56            if target_u32 >= u32::from(n_vertices) {
57                return Err(IgraphError::Parse {
58                    line: 0,
59                    message: format!(
60                        "invalid vertex ID {target_u32} in graphdb file (n_vertices = {n_vertices})"
61                    ),
62                });
63            }
64
65            edges.push((v, target_u32));
66        }
67    }
68
69    let mut graph = Graph::new(u32::from(n_vertices), directed)?;
70    graph.add_edges(edges)?;
71
72    Ok(graph)
73}
74
75fn read_u16_le<R: Read>(reader: &mut R, context: &str) -> IgraphResult<u16> {
76    let mut buf = [0u8; 2];
77    reader.read_exact(&mut buf).map_err(|e| {
78        if e.kind() == std::io::ErrorKind::UnexpectedEof {
79            IgraphError::Parse {
80                line: 0,
81                message: format!("unexpected end of file while reading {context}"),
82            }
83        } else {
84            IgraphError::Io(e)
85        }
86    })?;
87    Ok(u16::from_le_bytes(buf))
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    fn make_graphdb(n: u16, adj: &[Vec<u16>]) -> Vec<u8> {
95        let mut data = Vec::new();
96        data.extend_from_slice(&n.to_le_bytes());
97        for neighbors in adj {
98            #[allow(clippy::cast_possible_truncation)]
99            let deg = neighbors.len() as u16;
100            data.extend_from_slice(&deg.to_le_bytes());
101            for &tgt in neighbors {
102                data.extend_from_slice(&tgt.to_le_bytes());
103            }
104        }
105        data
106    }
107
108    #[test]
109    fn test_empty_graph() {
110        let data = make_graphdb(0, &[]);
111        let g = read_graphdb(&data[..], false).unwrap();
112        assert_eq!(g.vcount(), 0);
113        assert_eq!(g.ecount(), 0);
114    }
115
116    #[test]
117    fn test_single_vertex_no_edges() {
118        let data = make_graphdb(1, &[vec![]]);
119        let g = read_graphdb(&data[..], false).unwrap();
120        assert_eq!(g.vcount(), 1);
121        assert_eq!(g.ecount(), 0);
122    }
123
124    #[test]
125    fn test_directed_triangle() {
126        let data = make_graphdb(3, &[vec![1], vec![2], vec![0]]);
127        let g = read_graphdb(&data[..], true).unwrap();
128        assert_eq!(g.vcount(), 3);
129        assert_eq!(g.ecount(), 3);
130        assert!(g.is_directed());
131    }
132
133    #[test]
134    fn test_undirected() {
135        let data = make_graphdb(3, &[vec![1, 2], vec![0, 2], vec![0, 1]]);
136        let g = read_graphdb(&data[..], false).unwrap();
137        assert_eq!(g.vcount(), 3);
138        assert_eq!(g.ecount(), 6);
139        assert!(!g.is_directed());
140    }
141
142    #[test]
143    fn test_self_loop() {
144        let data = make_graphdb(2, &[vec![0, 1], vec![]]);
145        let g = read_graphdb(&data[..], true).unwrap();
146        assert_eq!(g.vcount(), 2);
147        assert_eq!(g.ecount(), 2);
148    }
149
150    #[test]
151    fn test_invalid_vertex_id() {
152        let data = make_graphdb(2, &[vec![5], vec![]]);
153        let result = read_graphdb(&data[..], true);
154        assert!(result.is_err());
155    }
156
157    #[test]
158    fn test_truncated_header() {
159        let data = [2u8]; // Only 1 byte, need 2
160        let result = read_graphdb(&data[..], true);
161        assert!(result.is_err());
162    }
163
164    #[test]
165    fn test_truncated_adjacency() {
166        // 2 vertices, vertex 0 says 1 neighbor but no data follows
167        let mut data = Vec::new();
168        data.extend_from_slice(&2u16.to_le_bytes());
169        data.extend_from_slice(&1u16.to_le_bytes());
170        let result = read_graphdb(&data[..], true);
171        assert!(result.is_err());
172    }
173
174    #[test]
175    fn test_large_graph() {
176        // 256 vertex complete-ish graph is too big to enumerate,
177        // just test that 10 vertices with chain edges works
178        let adj: Vec<Vec<u16>> = (0..10u16)
179            .map(|v| if v < 9 { vec![v + 1] } else { vec![] })
180            .collect();
181        let data = make_graphdb(10, &adj);
182        let g = read_graphdb(&data[..], true).unwrap();
183        assert_eq!(g.vcount(), 10);
184        assert_eq!(g.ecount(), 9);
185    }
186
187    #[test]
188    fn test_multiple_edges_from_vertex() {
189        // Star graph: vertex 0 connects to all others
190        let data = make_graphdb(5, &[vec![1, 2, 3, 4], vec![], vec![], vec![], vec![]]);
191        let g = read_graphdb(&data[..], true).unwrap();
192        assert_eq!(g.vcount(), 5);
193        assert_eq!(g.ecount(), 4);
194    }
195}