weavatrix_graph/format/
graph6.rs1use crate::{
2 BitMatrix, EdgeEndpoints, GraphError, IndexUndirectedGraphView, NodeIndex, Result,
3 UndirectedTopology,
4};
5use crate::{String, Vec};
6
7pub fn graph6_encode<G>(graph: &G) -> Result<String>
14where
15 G: IndexUndirectedGraphView,
16{
17 if graph.node_bound() != graph.node_count() {
18 return Err(unsupported("non-compact node indexes"));
19 }
20 let mut seen = BitMatrix::try_new(graph.node_count())?;
21 for node in graph.node_indices() {
22 if G::node_slot(node) >= graph.node_count() {
23 return Err(unsupported("non-compact node indexes"));
24 }
25 }
26 for (_, endpoints) in graph
27 .edge_indices()
28 .filter_map(|edge| graph.edge_endpoints(edge).map(|ends| (edge, ends)))
29 {
30 let mut left = G::node_slot(endpoints.source());
31 let mut right = G::node_slot(endpoints.target());
32 if left == right {
33 return Err(unsupported("self-loops"));
34 }
35 if left > right {
36 core::mem::swap(&mut left, &mut right);
37 }
38 if !seen.insert(node(left), node(right))? {
39 return Err(unsupported("parallel edges"));
40 }
41 }
42 let mut encoded = encode_node_count(graph.node_count())?;
43 let mut value = 0_u8;
44 let mut width = 0_u8;
45 for right in 1..graph.node_count() {
46 for left in 0..right {
47 value = (value << 1) | u8::from(seen.contains(node(left), node(right)));
48 width += 1;
49 if width == 6 {
50 encoded.push(char::from(value + 63));
51 value = 0;
52 width = 0;
53 }
54 }
55 }
56 if width != 0 {
57 encoded.push(char::from((value << (6 - width)) + 63));
58 }
59 Ok(encoded)
60}
61
62pub fn graph6_decode(input: &str) -> Result<UndirectedTopology> {
68 let record = input.trim();
69 let record = record.strip_prefix(">>graph6<<").unwrap_or(record);
70 let bytes = record.as_bytes();
71 let (node_count, offset) = decode_node_count(bytes)?;
72 let bits = node_count
73 .checked_mul(node_count.saturating_sub(1))
74 .ok_or_else(|| invalid("node count overflows adjacency bits"))?
75 / 2;
76 let encoded_len = bits
77 .checked_add(5)
78 .ok_or_else(|| invalid("adjacency length overflow"))?
79 / 6;
80 if bytes.len() != offset + encoded_len {
81 return Err(invalid("adjacency payload has the wrong length"));
82 }
83 let values = bytes[offset..]
84 .iter()
85 .map(|byte| six(*byte))
86 .collect::<Result<Vec<_>>>()?;
87 if let Some(last) = values.last() {
88 let used = bits % 6;
89 if used != 0 && last & ((1_u8 << (6 - used)) - 1) != 0 {
90 return Err(invalid("nonzero Graph6 padding bits"));
91 }
92 }
93 let mut edges = Vec::new();
94 let mut position = 0_usize;
95 for right in 1..node_count {
96 for left in 0..right {
97 let value = values[position / 6];
98 let bit = 5 - position % 6;
99 if value & (1_u8 << bit) != 0 {
100 edges.push(EdgeEndpoints::new(node(left), node(right)));
101 }
102 position += 1;
103 }
104 }
105 UndirectedTopology::try_from_edges(node_count, edges)
106}
107
108fn encode_node_count(node_count: usize) -> Result<String> {
109 let count = u64::try_from(node_count).map_err(|_| GraphError::IndexCapacityExceeded {
110 category: "Graph6 nodes",
111 count: node_count,
112 })?;
113 let mut output = String::new();
114 if count <= 62 {
115 push_six(&mut output, count)?;
116 } else if count <= 258_047 {
117 output.push('~');
118 for shift in [12, 6, 0] {
119 push_six(&mut output, (count >> shift) & 0x3f)?;
120 }
121 } else if count <= 0xffff_fffff {
122 output.push_str("~~");
123 for shift in [30, 24, 18, 12, 6, 0] {
124 push_six(&mut output, (count >> shift) & 0x3f)?;
125 }
126 } else {
127 return Err(GraphError::IndexCapacityExceeded {
128 category: "Graph6 nodes",
129 count: node_count,
130 });
131 }
132 Ok(output)
133}
134
135fn decode_node_count(bytes: &[u8]) -> Result<(usize, usize)> {
136 let Some(&first) = bytes.first() else {
137 return Err(invalid("empty record"));
138 };
139 if first != b'~' {
140 return Ok((usize::from(six(first)?), 1));
141 }
142 if bytes.get(1) != Some(&b'~') {
143 return Ok((decode_groups(bytes, 1, 3)?, 4));
144 }
145 Ok((decode_groups(bytes, 2, 6)?, 8))
146}
147
148fn decode_groups(bytes: &[u8], offset: usize, count: usize) -> Result<usize> {
149 let slice = bytes
150 .get(offset..offset + count)
151 .ok_or_else(|| invalid("truncated node-count header"))?;
152 slice.iter().try_fold(0_usize, |value, byte| {
153 Ok((value << 6) | usize::from(six(*byte)?))
154 })
155}
156
157fn push_six(output: &mut String, value: u64) -> Result<()> {
158 let value = u8::try_from(value).map_err(|_| invalid("invalid six-bit value"))?;
159 output.push(char::from(value + 63));
160 Ok(())
161}
162
163fn six(byte: u8) -> Result<u8> {
164 byte.checked_sub(63)
165 .filter(|value| *value < 64)
166 .ok_or_else(|| invalid("character is outside the Graph6 alphabet"))
167}
168
169fn node(index: usize) -> NodeIndex {
170 NodeIndex::new(u32::try_from(index).unwrap_or(u32::MAX))
171}
172
173fn invalid(reason: &str) -> GraphError {
174 GraphError::InvalidFormat {
175 format: "Graph6",
176 reason: String::from(reason),
177 }
178}
179
180fn unsupported(feature: &'static str) -> GraphError {
181 GraphError::UnsupportedGraphFeature {
182 format: "Graph6",
183 feature,
184 }
185}