1
1
//! Implementation of Tor's "subprotocol versioning" feature.
2
//!
3
//! # Overview
4
//!
5
//! The Tor system is built out of numerous "subprotocols" that are
6
//! versioned more or less independently. The `tor-protover` crate
7
//! implements parsing and handling for these subprotocol versions, so
8
//! that different Tor instances know which parts of the protocol
9
//! they support.
10
//!
11
//! Subprotocol versions are also used to determine which versions of
12
//! the protocol are required to connect to the network (or just
13
//! recommended).
14
//!
15
//! For more details, see [tor-spec.txt](https://spec.torproject.org/tor-spec)
16
//! section 9.
17
//!
18
//! This crate is part of
19
//! [Arti](https://gitlab.torproject.org/tpo/core/arti/), a project to
20
//! implement [Tor](https://www.torproject.org/) in Rust.
21
//! It's unlikely to be of general interest
22
//! unless you are writing a Tor implementation, or a program that
23
//! needs to examine fine-grained details of the Tor network.
24
//!
25
//! ## Design notes
26
//!
27
//! We're giving `tor-protover` its own crate within arti because it
28
//! needs to be used to multiple higher level crates that do not
29
//! themselves depend on one another.  (For example, [`tor-proto`]
30
//! needs to know which variant of a subprotocol can be used with a
31
//! given relay, whereas [`tor-netdoc`] needs to parse lists of
32
//! subprotocol versions from directory documents.  Eventually,
33
//! [`arti-client`] will need to check its own list of supported
34
//! protocols against the required list in the consensus.)
35

            
36
#![deny(missing_docs)]
37
#![allow(non_upper_case_globals)]
38
#![warn(noop_method_call)]
39
#![deny(unreachable_pub)]
40
#![warn(clippy::all)]
41
#![deny(clippy::await_holding_lock)]
42
#![deny(clippy::cargo_common_metadata)]
43
#![deny(clippy::cast_lossless)]
44
#![deny(clippy::checked_conversions)]
45
#![warn(clippy::cognitive_complexity)]
46
#![deny(clippy::debug_assert_with_mut_call)]
47
#![deny(clippy::exhaustive_enums)]
48
#![deny(clippy::exhaustive_structs)]
49
#![deny(clippy::expl_impl_clone_on_copy)]
50
#![deny(clippy::fallible_impl_from)]
51
#![deny(clippy::implicit_clone)]
52
#![deny(clippy::large_stack_arrays)]
53
#![warn(clippy::manual_ok_or)]
54
#![deny(clippy::missing_docs_in_private_items)]
55
#![deny(clippy::missing_panics_doc)]
56
#![warn(clippy::needless_borrow)]
57
#![warn(clippy::needless_pass_by_value)]
58
#![warn(clippy::option_option)]
59
#![warn(clippy::rc_buffer)]
60
#![deny(clippy::ref_option_ref)]
61
#![warn(clippy::semicolon_if_nothing_returned)]
62
#![warn(clippy::trait_duplication_in_bounds)]
63
#![deny(clippy::unnecessary_wraps)]
64
#![warn(clippy::unseparated_literal_suffix)]
65
#![deny(clippy::unwrap_used)]
66
#![allow(clippy::upper_case_acronyms)]
67

            
68
use caret::caret_int;
69

            
70
use thiserror::Error;
71

            
72
caret_int! {
73
    /// A recognized subprotocol.
74
    ///
75
    /// These names are kept in sync with the names used in consensus
76
    /// documents; the values are kept in sync with the values in the
77
    /// cbor document format in the walking onions proposal.
78
    ///
79
    /// For the full semantics of each subprotocol, see tor-spec.txt.
80
    pub struct ProtoKind(u16) {
81
        /// Initiating and receiving channels, and getting cells on them.
82
        Link = 0,
83
        /// Different kinds of authenticate cells
84
        LinkAuth = 1,
85
        /// CREATE cells, CREATED cells, and the encryption that they
86
        /// create.
87
        Relay = 2,
88
        /// Serving and fetching network directory documents.
89
        DirCache = 3,
90
        /// Serving onion service descriptors
91
        HSDir = 4,
92
        /// Providing an onion service introduction point
93
        HSIntro = 5,
94
        /// Providing an onion service rendezvous point
95
        HSRend = 6,
96
        /// Describing a relay's functionality using router descriptors.
97
        Desc = 7,
98
        /// Describing a relay's functionality using microdescriptors.
99
        MicroDesc = 8,
100
        /// Describing the network as a consensus directory document.
101
        Cons = 9,
102
        /// Sending and accepting circuit-level padding
103
        Padding = 10,
104
        /// Improved means of flow control on circuits.
105
        FlowCtrl = 11,
106
    }
107
}
108

            
109
/// How many recognized protocols are there?
110
const N_RECOGNIZED: usize = 12;
111

            
112
/// Representation for a known or unknown protocol.
113
#[derive(Eq, PartialEq, Clone, Debug)]
114
enum Protocol {
115
    /// A known protocol; represented by one of ProtoKind.
116
    Proto(ProtoKind),
117
    /// An unknown protocol; represented by its name.
118
    Unrecognized(String),
119
}
120

            
121
impl Protocol {
122
    /// Return true iff `s` is the name of a protocol we do not recognize.
123
6
    fn is_unrecognized(&self, s: &str) -> bool {
124
6
        match self {
125
6
            Protocol::Unrecognized(s2) => s2 == s,
126
            _ => false,
127
        }
128
6
    }
129
    /// Return a string representation of this protocol.
130
2
    fn to_str(&self) -> &str {
131
2
        match self {
132
            Protocol::Proto(k) => k.to_str().unwrap_or("<bug>"),
133
2
            Protocol::Unrecognized(s) => s,
134
        }
135
2
    }
136
}
137

            
138
/// Representation of a set of versions supported by a protocol.
139
///
140
/// For now, we only use this type for unrecognized protocols.
141
#[derive(Debug, Clone)]
142
struct SubprotocolEntry {
143
    /// Which protocol's versions does this describe?
144
    proto: Protocol,
145
    /// A bit-vector defining which versions are supported.  If bit
146
    /// `(1<<i)` is set, then protocol version `i` is supported.
147
    supported: u64,
148
}
149

            
150
/// A set of supported or required subprotocol versions.
151
///
152
/// This type supports both recognized subprotocols (listed in ProtoKind),
153
/// and unrecognized subprotocols (stored by name).
154
///
155
/// To construct an instance, use the FromStr trait:
156
/// ```
157
/// use tor_protover::Protocols;
158
/// let p: Result<Protocols,_> = "Link=1-3 LinkAuth=2-3 Relay=1-2".parse();
159
/// ```
160
99914
#[derive(Debug, Clone, Default)]
161
pub struct Protocols {
162
    /// A mapping from protocols' integer encodings to bit-vectors.
163
    recognized: [u64; N_RECOGNIZED],
164
    /// A vector of unrecognized protocol versions.
165
    unrecognized: Vec<SubprotocolEntry>,
166
}
167

            
168
impl Protocols {
169
    /// Return a new empty set of protocol versions.
170
61924
    pub fn new() -> Self {
171
61924
        Protocols::default()
172
61924
    }
173
    /// Helper: return true iff this protocol set contains the
174
    /// version `ver` of the protocol represented by the integer `proto`.
175
1253108
    fn supports_recognized_ver(&self, proto: usize, ver: u8) -> bool {
176
1253108
        if ver > 63 {
177
1
            return false;
178
1253107
        }
179
1253107
        if proto >= self.recognized.len() {
180
            return false;
181
1253232
        }
182
1253232
        (self.recognized[proto] & (1 << ver)) != 0
183
1253233
    }
184
    /// Helper: return true iff this protocol set contains version
185
    /// `ver` of the unrecognized protocol represented by the string
186
    /// `proto`.
187
    ///
188
    /// Requires that `proto` is not the name of a recognized protocol.
189
5
    fn supports_unrecognized_ver(&self, proto: &str, ver: u8) -> bool {
190
5
        if ver > 63 {
191
1
            return false;
192
4
        }
193
4
        let ent = self
194
4
            .unrecognized
195
4
            .iter()
196
4
            .find(|ent| ent.proto.is_unrecognized(proto));
197
4
        match ent {
198
2
            Some(e) => (e.supported & (1 << ver)) != 0,
199
2
            None => false,
200
        }
201
5
    }
202
    // TODO: Combine these next two functions into one by using a trait.
203
    /// Check whether a known protocol version is supported.
204
    ///
205
    /// ```
206
    /// use tor_protover::*;
207
    /// let protos: Protocols = "Link=1-3 HSDir=2,4-5".parse().unwrap();
208
    ///
209
    /// assert!(protos.supports_known_subver(ProtoKind::Link, 2));
210
    /// assert!(protos.supports_known_subver(ProtoKind::HSDir, 4));
211
    /// assert!(! protos.supports_known_subver(ProtoKind::HSDir, 3));
212
    /// assert!(! protos.supports_known_subver(ProtoKind::LinkAuth, 3));
213
    /// ```
214
1252306
    pub fn supports_known_subver(&self, proto: ProtoKind, ver: u8) -> bool {
215
1252306
        self.supports_recognized_ver(proto.get() as usize, ver)
216
1252306
    }
217
    /// Check whether a protocol version identified by a string is supported.
218
    ///
219
    /// ```
220
    /// use tor_protover::*;
221
    /// let protos: Protocols = "Link=1-3 Foobar=7".parse().unwrap();
222
    ///
223
    /// assert!(protos.supports_subver("Link", 2));
224
    /// assert!(protos.supports_subver("Foobar", 7));
225
    /// assert!(! protos.supports_subver("Link", 5));
226
    /// assert!(! protos.supports_subver("Foobar", 6));
227
    /// assert!(! protos.supports_subver("Wombat", 3));
228
    /// ```
229
57
    pub fn supports_subver(&self, proto: &str, ver: u8) -> bool {
230
57
        match ProtoKind::from_name(proto) {
231
52
            Some(p) => self.supports_recognized_ver(p.get() as usize, ver),
232
5
            None => self.supports_unrecognized_ver(proto, ver),
233
        }
234
57
    }
235

            
236
    /// Parsing helper: Try to add a new entry `ent` to this set of protocols.
237
    ///
238
    /// Uses `foundmask`, a bit mask saying which recognized protocols
239
    /// we've already found entries for.  Returns an error if `ent` is
240
    /// for a protocol we've already added.
241
61192
    fn add(&mut self, foundmask: &mut u64, ent: SubprotocolEntry) -> Result<(), ParseError> {
242
61192
        match ent.proto {
243
57912
            Protocol::Proto(k) => {
244
57912
                let idx = k.get() as usize;
245
57912
                let bit = 1 << u64::from(k.get());
246
57912
                if (*foundmask & bit) != 0 {
247
2
                    return Err(ParseError::Duplicate);
248
57910
                }
249
57910
                *foundmask |= bit;
250
57910
                self.recognized[idx] = ent.supported;
251
            }
252
3280
            Protocol::Unrecognized(ref s) => {
253
3280
                if self
254
3280
                    .unrecognized
255
3280
                    .iter()
256
3280
                    .any(|ent| ent.proto.is_unrecognized(s))
257
                {
258
1
                    return Err(ParseError::Duplicate);
259
3279
                }
260
3279
                self.unrecognized.push(ent);
261
            }
262
        }
263
61189
        Ok(())
264
61192
    }
265
}
266

            
267
/// An error representing a failure to parse a set of protocol versions.
268
18
#[derive(Error, Debug, PartialEq, Eq, Clone)]
269
#[non_exhaustive]
270
pub enum ParseError {
271
    /// A protocol version was not in the range 0..=63.
272
    #[error("protocol version out of range")]
273
    OutOfRange,
274
    /// Some subprotocol or protocol version appeared more than once.
275
    #[error("duplicate protocol entry")]
276
    Duplicate,
277
    /// The list of protocol versions was malformed in some other way.
278
    #[error("malformed protocol entry")]
279
    Malformed,
280
}
281

            
282
/// Helper: return a new u64 in which bits `lo` through `hi` inclusive
283
/// are set to 1, and all the other bits are set to 0.
284
///
285
/// In other words, `bitrange(a,b)` is how we represent the range of
286
/// versions `a-b` in a protocol version bitmask.
287
///
288
/// ```ignore
289
/// # use tor_protover::bitrange;
290
/// assert_eq!(bitrange(0, 5), 0b111111);
291
/// assert_eq!(bitrange(2, 5), 0b111100);
292
/// assert_eq!(bitrange(2, 7), 0b11111100);
293
/// ```
294
63108
fn bitrange(lo: u64, hi: u64) -> u64 {
295
63233
    assert!(lo <= hi && lo <= 63 && hi <= 63);
296
63233
    let mut mask = !0;
297
63233
    mask <<= 63 - hi;
298
63233
    mask >>= 63 - hi + lo;
299
63233
    mask <<= lo;
300
63233
    mask
301
63233
}
302

            
303
/// Helper: return true if the provided string is a valid "integer"
304
/// in the form accepted by the protover spec.  This is stricter than
305
/// rust's integer parsing format.
306
126726
fn is_good_number(n: &str) -> bool {
307
126785
    n.chars().all(|ch| ch.is_ascii_digit()) && !n.starts_with('0')
308
126726
}
309

            
310
/// A single SubprotocolEntry is parsed from a string of the format
311
/// Name=Versions, where Versions is a comma-separated list of
312
/// integers or ranges of integers.
313
impl std::str::FromStr for SubprotocolEntry {
314
    type Err = ParseError;
315

            
316
61282
    fn from_str(s: &str) -> Result<Self, ParseError> {
317
        // split the string on the =.
318
61281
        let (name, versions) = {
319
61282
            let eq_idx = s.find('=').ok_or(ParseError::Malformed)?;
320
61281
            (&s[..eq_idx], &s[eq_idx + 1..])
321
        };
322
        // Look up the protocol by name.
323
61281
        let proto = match ProtoKind::from_name(name) {
324
58325
            Some(p) => Protocol::Proto(p),
325
2956
            None => Protocol::Unrecognized(name.to_string()),
326
        };
327
61281
        if versions.is_empty() {
328
            // We need to handle this case specially, since otherwise
329
            // it would be treated below as a single empty value, which
330
            // would be rejected.
331
1
            return Ok(SubprotocolEntry {
332
1
                proto,
333
1
                supported: 0,
334
1
            });
335
61280
        }
336
61280
        // Construct a bitmask based on the comma-separated versions.
337
61280
        let mut supported = 0_u64;
338
63491
        for ent in versions.split(',') {
339
            // Find and parse lo and hi for a single range of versions.
340
            // (If this is not a range, but rather a single version v,
341
            // treat it as if it were a range v-v.)
342
63491
            let (lo_s, hi_s) = {
343
63491
                match ent.find('-') {
344
19737
                    Some(pos) => (&ent[..pos], &ent[pos + 1..]),
345
43754
                    None => (ent, ent),
346
                }
347
            };
348
63491
            if !is_good_number(lo_s) {
349
5
                return Err(ParseError::Malformed);
350
63486
            }
351
63486
            if !is_good_number(hi_s) {
352
1
                return Err(ParseError::Malformed);
353
63485
            }
354
63485
            let lo: u64 = lo_s.parse().map_err(|_| ParseError::Malformed)?;
355
63484
            let hi: u64 = hi_s.parse().map_err(|_| ParseError::Malformed)?;
356
            // Make sure that lo and hi are in-bounds and consistent.
357
63482
            if lo > 63 || hi > 63 {
358
3
                return Err(ParseError::OutOfRange);
359
63504
            }
360
63504
            if lo > hi {
361
1
                return Err(ParseError::Malformed);
362
63503
            }
363
63503
            let mask = bitrange(lo, hi);
364
63503
            // Make sure that no version is included twice.
365
63503
            if (supported & mask) != 0 {
366
1
                return Err(ParseError::Duplicate);
367
63502
            }
368
63502
            // Add the appropriate bits to the mask.
369
63502
            supported |= mask;
370
        }
371
61516
        Ok(SubprotocolEntry { proto, supported })
372
61532
    }
373
}
374

            
375
/// A Protocols set can be parsed from a string according to the
376
/// format used in Tor consensus documents.
377
///
378
/// A protocols set is represented by a space-separated list of
379
/// entries.  Each entry is of the form `Name=Versions`, where `Name`
380
/// is the name of a protocol, and `Versions` is a comma-separated
381
/// list of version numbers and version ranges.  Each version range is
382
/// a pair of integers separated by `-`.
383
///
384
/// No protocol name may be listed twice.  No version may be listed
385
/// twice for a single protocol.  All versions must be in range 0
386
/// through 63 inclusive.
387
impl std::str::FromStr for Protocols {
388
    type Err = ParseError;
389

            
390
61924
    fn from_str(s: &str) -> Result<Self, ParseError> {
391
61924
        let mut result = Protocols::new();
392
61924
        let mut foundmask = 0_u64;
393
91660
        for ent in s.split(' ') {
394
91660
            if ent.is_empty() {
395
30053
                continue;
396
61607
            }
397

            
398
61607
            let s: SubprotocolEntry = ent.parse()?;
399
61592
            result.add(&mut foundmask, s)?;
400
        }
401
62531
        Ok(result)
402
62549
    }
403
}
404

            
405
/// Given a bitmask, return a list of the bits set in the mask, as a
406
/// String in the format expected by Tor consensus documents.
407
///
408
/// This implementation constructs ranges greedily.  For example, the
409
/// bitmask `0b0111011` will be represented as `0-1,3-5`, and not
410
/// `0,1,3,4,5` or `0,1,3-5`.
411
///
412
/// ```ignore
413
/// # use tor_protover::dumpmask;
414
/// assert_eq!(dumpmask(0b111111), "0-5");
415
/// assert_eq!(dumpmask(0b111100), "2-5");
416
/// assert_eq!(dumpmask(0b11111100), "2-7");
417
/// ```
418
37
fn dumpmask(mut mask: u64) -> String {
419
37
    /// Helper: push a range (which may be a singleton) onto `v`.
420
40
    fn append(v: &mut Vec<String>, lo: u32, hi: u32) {
421
40
        if lo == hi {
422
32
            v.push(lo.to_string());
423
33
        } else {
424
8
            v.push(format!("{}-{}", lo, hi));
425
8
        }
426
40
    }
427
37
    // We'll be building up our result here, then joining it with
428
37
    // commas.
429
37
    let mut result = Vec::new();
430
37
    // This implementation is a little tricky, but it should be more
431
37
    // efficient than a raw search.  Basically, we're using the
432
37
    // function u64::trailing_zeros to count how large each range of
433
37
    // 1s or 0s is, and then shifting by that amount.
434
37

            
435
37
    // How many bits have we already shifted `mask`?
436
37
    let mut shift = 0;
437
76
    while mask != 0 {
438
40
        let zeros = mask.trailing_zeros();
439
40
        mask >>= zeros;
440
40
        shift += zeros;
441
40
        let ones = mask.trailing_ones();
442
40
        append(&mut result, shift, shift + ones - 1);
443
40
        shift += ones;
444
40
        if ones == 64 {
445
            // We have to do this check to avoid overflow when formatting
446
            // the range `0-63`.
447
1
            break;
448
39
        }
449
39
        mask >>= ones;
450
    }
451
37
    result.join(",")
452
37
}
453

            
454
/// The Display trait formats a protocol set in the format expected by Tor
455
/// consensus documents.
456
///
457
/// ```
458
/// use tor_protover::*;
459
/// let protos: Protocols = "Link=1,2,3 Foobar=7 Relay=2".parse().unwrap();
460
/// assert_eq!(format!("{}", protos),
461
///            "Foobar=7 Link=1-3 Relay=2");
462
/// ```
463
impl std::fmt::Display for Protocols {
464
30
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
465
30
        let mut entries = Vec::new();
466
360
        for (idx, mask) in self.recognized.iter().enumerate() {
467
360
            if *mask != 0 {
468
30
                let pk: ProtoKind = (idx as u16).into();
469
30
                entries.push(format!("{}={}", pk, dumpmask(*mask)));
470
330
            }
471
        }
472
32
        for ent in &self.unrecognized {
473
2
            if ent.supported != 0 {
474
2
                entries.push(format!(
475
2
                    "{}={}",
476
2
                    ent.proto.to_str(),
477
2
                    dumpmask(ent.supported)
478
2
                ));
479
2
            }
480
        }
481
        // This sort is required.
482
30
        entries.sort();
483
30
        write!(f, "{}", entries.join(" "))
484
30
    }
485
}
486

            
487
#[cfg(test)]
488
mod test {
489
    #![allow(clippy::unwrap_used)]
490
    use super::*;
491

            
492
    #[test]
493
    fn test_bitrange() {
494
        assert_eq!(0b1, bitrange(0, 0));
495
        assert_eq!(0b10, bitrange(1, 1));
496
        assert_eq!(0b11, bitrange(0, 1));
497
        assert_eq!(0b1111110000000, bitrange(7, 12));
498
        assert_eq!(!0, bitrange(0, 63));
499
    }
500

            
501
    #[test]
502
    fn test_dumpmask() {
503
        assert_eq!("", dumpmask(0));
504
        assert_eq!("0-5", dumpmask(0b111111));
505
        assert_eq!("4-5", dumpmask(0b110000));
506
        assert_eq!("1,4-5", dumpmask(0b110010));
507
        assert_eq!("0-63", dumpmask(!0));
508
    }
509

            
510
    #[test]
511
    fn test_canonical() -> Result<(), ParseError> {
512
        fn t(orig: &str, canonical: &str) -> Result<(), ParseError> {
513
            let protos: Protocols = orig.parse()?;
514
            let enc = format!("{}", protos);
515
            assert_eq!(enc, canonical);
516
            Ok(())
517
        }
518

            
519
        t("", "")?;
520
        t(" ", "")?;
521
        t("Link=5,6,7,9 Relay=4-7,2", "Link=5-7,9 Relay=2,4-7")?;
522
        t("FlowCtrl= Padding=8,7 Desc=1-5,6-8", "Desc=1-8 Padding=7-8")?;
523
        t("Zelda=7 Gannon=3,6 Link=4", "Gannon=3,6 Link=4 Zelda=7")?;
524

            
525
        Ok(())
526
    }
527

            
528
    #[test]
529
    fn test_invalid() {
530
        fn t(s: &str) -> ParseError {
531
            let protos: Result<Protocols, ParseError> = s.parse();
532
            assert!(protos.is_err());
533
            protos.err().unwrap()
534
        }
535

            
536
        assert_eq!(t("Link=1-100"), ParseError::OutOfRange);
537
        assert_eq!(t("Zelda=100"), ParseError::OutOfRange);
538
        assert_eq!(t("Link=100-200"), ParseError::OutOfRange);
539

            
540
        assert_eq!(t("Link=1,1"), ParseError::Duplicate);
541
        assert_eq!(t("Link=1 Link=1"), ParseError::Duplicate);
542
        assert_eq!(t("Link=1 Link=3"), ParseError::Duplicate);
543
        assert_eq!(t("Zelda=1 Zelda=3"), ParseError::Duplicate);
544

            
545
        assert_eq!(t("Link=Zelda"), ParseError::Malformed);
546
        assert_eq!(t("Link=6-2"), ParseError::Malformed);
547
        assert_eq!(t("Link=6-"), ParseError::Malformed);
548
        assert_eq!(t("Link=6-,2"), ParseError::Malformed);
549
        assert_eq!(t("Link=1,,2"), ParseError::Malformed);
550
        assert_eq!(t("Link=6-frog"), ParseError::Malformed);
551
        assert_eq!(t("Link=gannon-9"), ParseError::Malformed);
552
        assert_eq!(t("Link Zelda"), ParseError::Malformed);
553

            
554
        assert_eq!(t("Link=01"), ParseError::Malformed);
555
        assert_eq!(t("Link=waffle"), ParseError::Malformed);
556
        assert_eq!(t("Link=1_1"), ParseError::Malformed);
557
    }
558

            
559
    #[test]
560
    fn test_supports() -> Result<(), ParseError> {
561
        let p: Protocols = "Link=4,5-7 Padding=2 Lonk=1-3,5".parse()?;
562

            
563
        assert!(p.supports_known_subver(ProtoKind::Padding, 2));
564
        assert!(!p.supports_known_subver(ProtoKind::Padding, 1));
565
        assert!(p.supports_known_subver(ProtoKind::Link, 6));
566
        assert!(!p.supports_known_subver(ProtoKind::Link, 255));
567
        assert!(!p.supports_known_subver(ProtoKind::Cons, 1));
568
        assert!(!p.supports_known_subver(ProtoKind::Cons, 0));
569
        assert!(p.supports_subver("Link", 6));
570
        assert!(!p.supports_subver("link", 6));
571
        assert!(!p.supports_subver("Cons", 0));
572
        assert!(p.supports_subver("Lonk", 3));
573
        assert!(!p.supports_subver("Lonk", 4));
574
        assert!(!p.supports_subver("lonk", 3));
575
        assert!(!p.supports_subver("Lonk", 64));
576

            
577
        Ok(())
578
    }
579
}