• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vigna / webgraph-rs / 22350210654

24 Feb 2026 12:10PM UTC coverage: 71.503% (-0.2%) from 71.724%
22350210654

push

github

vigna
code_to_str now returns None instead of panicking

10 of 33 new or added lines in 6 files covered. (30.3%)

5 existing lines in 1 file now uncovered.

6278 of 8780 relevant lines covered (71.5%)

51288774.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.32
/webgraph/src/graphs/bvgraph/comp/flags.rs
1
/*
2
 * SPDX-FileCopyrightText: 2023 Inria
3
 * SPDX-FileCopyrightText: 2023 Sebastiano Vigna
4
 *
5
 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
6
 */
7

8
use anyhow::{Context, Result, bail, ensure};
9
use dsi_bitstream::dispatch::Codes;
10
use dsi_bitstream::traits::{BigEndian, Endianness, LittleEndian};
11
use std::collections::HashMap;
12

13
#[derive(Clone, Copy, Debug)]
14
#[cfg_attr(feature = "fuzz", derive(arbitrary::Arbitrary))]
15
/// The compression flags for reading or compressing a graph.
16
///
17
/// As documented, one code might sets multiple values. This is done for
18
/// compatibility with the previous Java version of the library.
19
/// But the codes optimizers will return the optimal codes for each of them,
20
/// so if it identify some big save from using different codes, we can consider
21
/// splitting them.
22
pub struct CompFlags {
23
    /// The instantaneous code to use to encode the `outdegrees`
24
    pub outdegrees: Codes,
25
    /// The instantaneous code to use to encode the `reference_offset`
26
    pub references: Codes,
27
    /// The instantaneous code to use to encode the `block_count` and `blocks`
28
    pub blocks: Codes,
29
    /// The instantaneous code to use to encode the `interval_count`, `interval_start`, and `interval_len`.
30
    pub intervals: Codes,
31
    /// The instantaneous code to use to encode the `first_residual` and `residual`
32
    pub residuals: Codes,
33
    /// The minimum length of an interval to be compressed as (start, len)
34
    pub min_interval_length: usize,
35
    /// The number of previous nodes to use for reference compression
36
    pub compression_window: usize,
37
    /// The maximum recursion depth during decoding, this modulates the tradeoff
38
    /// between compression ratio and decoding speed
39
    pub max_ref_count: usize,
40
}
41

42
impl core::default::Default for CompFlags {
43
    fn default() -> Self {
2,333,968✔
44
        CompFlags {
45
            outdegrees: Codes::Gamma,
46
            references: Codes::Unary,
47
            blocks: Codes::Gamma,
48
            intervals: Codes::Gamma,
49
            residuals: Codes::Zeta(3),
2,333,968✔
50
            min_interval_length: 4,
51
            compression_window: 7,
52
            max_ref_count: 3,
53
        }
54
    }
55
}
56

57
const OLD_CODES: [Codes; 10] = [
58
    Codes::Unary,
59
    Codes::Gamma,
60
    Codes::Delta,
61
    Codes::Zeta(1),
62
    Codes::Zeta(2),
63
    Codes::Zeta(3),
64
    Codes::Zeta(4),
65
    Codes::Zeta(5),
66
    Codes::Zeta(6),
67
    Codes::Zeta(7),
68
];
69

70
impl CompFlags {
71
    /// Convert a string from the `compflags` field from the `.properties` file
72
    /// into which code to use.
73
    ///
74
    /// Note that ζ codes are supported both with parameterless names (e.g.,
75
    /// "ZETA") and with the parameter in the name (e.g., "ZETA3"), for
76
    /// compatibility with previous versions of the library.
77
    ///
78
    /// For CLI ergonomics and compatibility, this codes must be the same as
79
    /// those appearing in the `PrivCode` enum of the `webgraph-cli` crate.
80
    ///
81
    /// Returns `None` if the string is not recognized.
82
    pub fn code_from_str(s: &str, k: usize) -> Option<Codes> {
4,251,150✔
83
        match s.to_uppercase().as_str() {
4,251,150✔
84
            "UNARY" => Some(Codes::Unary),
5,495,335✔
85
            "GAMMA" => Some(Codes::Gamma),
3,732,740✔
86
            "DELTA" => Some(Codes::Delta),
4,251,210✔
87
            "ZETA" => Some(Codes::Zeta(k)),
466,735✔
88
            "PI1" => Some(Codes::Pi(1)),
155,615✔
89
            "PI2" => Some(Codes::Pi(2)),
155,610✔
90
            "PI3" => Some(Codes::Pi(3)),
155,590✔
91
            "PI4" => Some(Codes::Pi(4)),
155,580✔
92
            "ZETA1" => Some(Codes::Zeta(1)),
155,565✔
93
            "ZETA2" => Some(Codes::Zeta(2)),
311,080✔
94
            "ZETA3" => Some(Codes::Zeta(3)),
35✔
95
            "ZETA4" => Some(Codes::Zeta(4)),
30✔
96
            "ZETA5" => Some(Codes::Zeta(5)),
25✔
97
            "ZETA6" => Some(Codes::Zeta(6)),
20✔
98
            "ZETA7" => Some(Codes::Zeta(7)),
15✔
99
            _ => None,
5✔
100
        }
101
    }
102

103
    pub fn code_to_str(c: Codes, version: usize) -> Option<&'static str> {
4,251,175✔
104
        if version == 0 {
4,251,175✔
105
            match c {
2,125,630✔
106
                Codes::Unary => Some("UNARY"),
622,105✔
107
                Codes::Gamma => Some("GAMMA"),
362,895✔
108
                Codes::Delta => Some("DELTA"),
985,055✔
109
                Codes::Zeta(_) => Some("ZETA"),
155,570✔
110
                _ => None,
5✔
111
            }
112
        } else {
113
            match c {
2,125,545✔
114
                Codes::Unary => Some("UNARY"),
622,085✔
115
                Codes::Gamma => Some("GAMMA"),
362,885✔
116
                Codes::Delta => Some("DELTA"),
984,965✔
117
                Codes::Zeta(1) => Some("ZETA1"),
5✔
118
                Codes::Zeta(2) => Some("ZETA2"),
155,525✔
119
                Codes::Zeta(3) => Some("ZETA3"),
5✔
120
                Codes::Zeta(4) => Some("ZETA4"),
5✔
121
                Codes::Zeta(5) => Some("ZETA5"),
5✔
122
                Codes::Zeta(6) => Some("ZETA6"),
5✔
123
                Codes::Zeta(7) => Some("ZETA7"),
5✔
124
                Codes::Pi(1) => Some("PI1"),
10✔
125
                Codes::Pi(2) => Some("PI2"),
15✔
126
                Codes::Pi(3) => Some("PI3"),
10✔
127
                Codes::Pi(4) => Some("PI4"),
10✔
128
                _ => None,
10✔
129
            }
130
        }
131
    }
132

133
    fn contains_new_codes(&self) -> bool {
622,457✔
134
        !OLD_CODES.contains(&self.outdegrees)
1,244,914✔
135
            || !OLD_CODES.contains(&self.references)
1,244,914✔
136
            || !OLD_CODES.contains(&self.blocks)
1,244,914✔
137
            || !OLD_CODES.contains(&self.intervals)
1,244,914✔
138
            || !OLD_CODES.contains(&self.residuals)
1,244,914✔
139
    }
140

141
    pub fn to_properties<E: Endianness>(
746,881✔
142
        &self,
143
        num_nodes: usize,
144
        num_arcs: u64,
145
        bitstream_len: u64,
146
    ) -> Result<String> {
147
        let mut s = String::new();
1,493,762✔
148
        s.push_str("#BVGraph properties\n");
2,240,643✔
149
        s.push_str("graphclass=it.unimi.dsi.webgraph.BVGraph\n");
2,240,643✔
150

151
        // Version 1 if we have big-endian or new codes
152
        let version = (core::any::TypeId::of::<E>() != core::any::TypeId::of::<BigEndian>()
1,493,762✔
153
            || self.contains_new_codes()) as usize;
1,244,914✔
154

155
        s.push_str(&format!("version={version}\n"));
2,240,643✔
156
        s.push_str(&format!("endianness={}\n", E::NAME));
2,240,643✔
157

158
        s.push_str(&format!("nodes={num_nodes}\n"));
2,240,643✔
159
        s.push_str(&format!("arcs={num_arcs}\n"));
2,240,643✔
160
        s.push_str(&format!("minintervallength={}\n", self.min_interval_length));
2,240,643✔
161
        s.push_str(&format!("maxrefcount={}\n", self.max_ref_count));
2,240,643✔
162
        s.push_str(&format!("windowsize={}\n", self.compression_window));
2,240,643✔
163
        s.push_str(&format!(
2,987,524✔
164
            "bitsperlink={}\n",
746,881✔
165
            bitstream_len as f64 / num_arcs as f64
746,881✔
166
        ));
167
        s.push_str(&format!(
2,987,524✔
168
            "bitspernode={}\n",
746,881✔
169
            bitstream_len as f64 / num_nodes as f64
746,881✔
170
        ));
171
        s.push_str(&format!("length={bitstream_len}\n"));
2,240,643✔
172

173
        fn stirling(n: u64) -> f64 {
3,733,731✔
174
            let n = n as f64;
7,467,462✔
175
            n * (n.ln() - 1.0) + 0.5 * (2.0 * std::f64::consts::PI * n).ln()
11,201,193✔
176
        }
177

178
        let n_squared = (num_nodes * num_nodes) as u64;
1,493,762✔
179
        let theoretical_bound =
746,881✔
180
            (stirling(n_squared) - stirling(num_arcs) - stirling(n_squared - num_arcs))
3,734,405✔
181
                / 2.0_f64.ln();
746,881✔
182
        s.push_str(&format!(
2,987,524✔
183
            "compratio={:.3}\n",
746,881✔
184
            bitstream_len as f64 / theoretical_bound
746,881✔
185
        ));
186

187
        s.push_str("compressionflags=");
2,240,643✔
188
        let mut comp_flags = false;
1,493,762✔
189
        if self.outdegrees != Codes::Gamma {
746,881✔
190
            s.push_str(&format!(
1,493,115✔
UNCOV
191
                "OUTDEGREES_{}|",
×
192
                Self::code_to_str(self.outdegrees, version).with_context(|| format!(
1,990,820✔
NEW
193
                    "Code {:?} is not supported for outdegrees in version {version}",
×
NEW
194
                    self.outdegrees
×
195
                ))?
196
            ));
197
            comp_flags = true;
497,705✔
198
        }
199
        if self.references != Codes::Unary {
746,881✔
200
            s.push_str(&format!(
1,493,070✔
UNCOV
201
                "REFERENCES_{}|",
×
202
                Self::code_to_str(self.references, version).with_context(|| format!(
1,990,760✔
NEW
203
                    "Code {:?} is not supported for references in version {version}",
×
NEW
204
                    self.references
×
205
                ))?
206
            ));
207
            comp_flags = true;
497,690✔
208
        }
209
        if self.blocks != Codes::Gamma {
746,881✔
210
            s.push_str(&format!(
1,493,085✔
UNCOV
211
                "BLOCKS_{}|",
×
212
                Self::code_to_str(self.blocks, version).with_context(|| format!(
1,990,780✔
NEW
213
                    "Code {:?} is not supported for blocks in version {version}",
×
NEW
214
                    self.blocks
×
215
                ))?
216
            ));
217
            comp_flags = true;
497,695✔
218
        }
219
        if self.intervals != Codes::Gamma {
746,881✔
220
            s.push_str(&format!(
1,493,085✔
UNCOV
221
                "INTERVALS_{}|",
×
222
                Self::code_to_str(self.intervals, version).with_context(|| format!(
1,990,780✔
NEW
223
                    "Code {:?} is not supported for intervals in version {version}",
×
NEW
224
                    self.intervals
×
225
                ))?
226
            ));
227
            comp_flags = true;
497,695✔
228
        }
229
        if (version == 0 && !matches!(self.residuals, Codes::Zeta(_)))
1,680,403✔
230
            || self.residuals != (Codes::Zeta(3))
435,816✔
231
        {
232
            s.push_str(&format!(
1,679,724✔
UNCOV
233
                "RESIDUALS_{}|",
×
234
                Self::code_to_str(self.residuals, version).with_context(|| format!(
2,239,632✔
NEW
235
                    "Code {:?} is not supported for residuals in version {version}",
×
NEW
236
                    self.residuals
×
237
                ))?
238
            ));
239
            comp_flags = true;
559,908✔
240
        }
241
        if comp_flags {
1,491,114✔
242
            s.pop();
744,233✔
243
        }
244
        s.push('\n');
1,493,762✔
245
        if version == 0 {
746,881✔
246
            // check that if a k is specified, it is the same for all codes
247
            let mut k = None;
1,244,914✔
248
            macro_rules! check_and_set_k {
×
249
                ($code:expr) => {
×
250
                    match $code {
×
251
                        Codes::Zeta(new_k) => {
×
252
                            if let Some(old_k) = k {
×
NEW
253
                                ensure!(
×
NEW
254
                                    old_k == new_k,
×
NEW
255
                                    "Only one value of k is supported in version 0"
×
256
                                )
257
                            }
258
                            k = Some(new_k)
×
259
                        }
260
                        _ => {}
×
261
                    }
262
                };
263
            }
264
            check_and_set_k!(self.outdegrees);
1,244,904✔
265
            check_and_set_k!(self.references);
1,244,909✔
266
            check_and_set_k!(self.blocks);
1,244,909✔
267
            check_and_set_k!(self.intervals);
1,244,904✔
268
            check_and_set_k!(self.residuals);
933,522✔
269
            // if no k was specified, use the default one (3)
270
            s.push_str(&format!("zetak={}\n", k.unwrap_or(3)));
2,489,828✔
271
        }
272
        Ok(s)
746,881✔
273
    }
274

275
    /// Convert the decoded `.properties` file into a `CompFlags` struct.
276
    /// Also check that the endianness is correct.
277
    pub fn from_properties<E: Endianness>(map: &HashMap<String, String>) -> Result<Self> {
747,085✔
278
        // Default values, same as the Java class
279
        let endianness = map
1,494,170✔
280
            .get("endianness")
281
            .map(|x| x.to_string())
2,240,855✔
282
            .unwrap_or_else(|| BigEndian::NAME.to_string());
747,485✔
283

284
        anyhow::ensure!(
747,085✔
285
            endianness == E::NAME,
747,085✔
286
            "Wrong endianness, got {} while expected {}",
×
287
            endianness,
×
288
            E::NAME
×
289
        );
290
        // check that the version was properly set for LE
291
        if core::any::TypeId::of::<E>() == core::any::TypeId::of::<LittleEndian>() {
747,084✔
292
            anyhow::ensure!(
124,422✔
293
                map.get("version").map(|x| x.parse::<u32>().unwrap()) == Some(1),
870,954✔
294
                "Wrong version, got {} while expected 1",
×
295
                map.get("version").unwrap_or(&"None".to_string())
×
296
            );
297
        }
298

299
        let mut cf = CompFlags::default();
1,494,168✔
300
        let mut k = 3;
1,494,168✔
301
        if let Some(spec_k) = map.get("zetak") {
2,116,825✔
302
            let spec_k = spec_k.parse::<usize>()?;
1,245,314✔
303
            if !(1..=7).contains(&spec_k) {
1,245,314✔
304
                bail!("Only ζ₁-ζ₇ are supported");
×
305
            }
306
            k = spec_k;
622,657✔
307
        }
308
        if let Some(comp_flags) = map.get("compressionflags") {
2,241,252✔
309
            if !comp_flags.is_empty() {
747,084✔
310
                for flag in comp_flags.split('|') {
3,294,926✔
311
                    let s: Vec<_> = flag.split('_').collect();
12,753,465✔
312
                    let code = CompFlags::code_from_str(s[1], k).unwrap();
12,753,465✔
313
                    match s[0] {
2,550,693✔
314
                        "OUTDEGREES" => cf.outdegrees = code,
3,048,398✔
315
                        "REFERENCES" => cf.references = code,
2,550,678✔
316
                        "BLOCKS" => cf.blocks = code,
2,052,993✔
317
                        "INTERVALS" => cf.intervals = code,
1,555,298✔
318
                        "RESIDUALS" => cf.residuals = code,
1,119,816✔
319
                        "OFFSETS" => {
×
320
                            ensure!(code == Codes::Gamma, "Only γ code is supported for offsets")
×
321
                        }
322
                        _ => bail!("Unknown compression flag {}", flag),
×
323
                    }
324
                }
325
            }
326
        }
327
        if let Some(compression_window) = map.get("windowsize") {
2,241,247✔
328
            cf.compression_window = compression_window.parse()?;
747,079✔
329
        }
330
        if let Some(min_interval_length) = map.get("minintervallength") {
2,241,247✔
331
            cf.min_interval_length = min_interval_length.parse()?;
747,079✔
332
        }
333
        if let Some(max_ref_count) = map.get("maxrefcount") {
2,241,247✔
334
            cf.max_ref_count = max_ref_count.parse()?;
747,079✔
335
        }
336
        Ok(cf)
747,084✔
337
    }
338
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc