netindex 2.1.0

Immutable IP-range and ASN indexes with borrowed payloads
Documentation
//! Shared-value identity, borrowed access, fixed bytes, limits, and failed output.

#![cfg(feature = "shared-values")]

use std::io;

use netindex::{
    Error, Limits,
    values::{StringPool, ValuePool, ValuePoolBuilder},
};

#[test]
fn shared_values_preserve_binary_strings_empty_values_and_ids() {
    let mut builder = ValuePoolBuilder::new(Limits::default());
    let text = builder.intern("Montréal".as_bytes()).unwrap();
    let binary = builder.intern(&[0xff, 0, 0x80]).unwrap();
    let empty = builder.intern(&[]).unwrap();

    assert_eq!(builder.intern("Montréal".as_bytes()).unwrap(), text);
    assert_eq!(builder.intern(&[]).unwrap(), empty);
    assert_eq!(builder.len(), 3);

    let bytes = builder.into_bytes().unwrap();
    let pool = ValuePool::open(&bytes, Limits::default()).unwrap();

    assert_eq!(pool.len(), 3);
    assert!(!pool.is_empty());
    assert_eq!(pool.get(text).unwrap(), "Montréal".as_bytes());
    assert_eq!(pool.get(binary).unwrap(), &[0xff, 0, 0x80]);
    assert_eq!(pool.get(empty).unwrap(), &[]);
    assert!(
        bytes
            .as_ptr_range()
            .contains(&pool.get(text).unwrap().as_ptr())
    );
    assert!(matches!(pool.get(3), Err(Error::Invalid("value ID"))));
    assert!(pool.get(u32::MAX).is_err());
}

#[test]
fn independent_pool_fixture_rejects_invalid_offsets_and_truncation() {
    let bytes = [
        b'N', b'I', b'V', b'A', b'L', b'U', b'E', b'S', 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0,
        0, 0, b'a', b'b', b'c',
    ];
    let pool = ValuePool::open(&bytes, Limits::default()).unwrap();

    assert_eq!(pool.get(0).unwrap(), b"a");
    assert_eq!(pool.get(1).unwrap(), b"bc");

    for length in 0..bytes.len() {
        assert!(ValuePool::open(&bytes[..length], Limits::default()).is_err());
    }

    for (offset, value) in [(0, 0), (8, 3), (12, 1), (16, 4), (20, 2)] {
        let mut invalid = bytes;
        invalid[offset] = value;
        assert!(ValuePool::open(&invalid, Limits::default()).is_err());
    }

    let mut trailing = bytes.to_vec();
    trailing.push(0);

    assert!(ValuePool::open(&trailing, Limits::default()).is_err());
}

#[test]
fn limits_and_io_failures_leave_existing_ids_intact() {
    let mut builder = ValuePoolBuilder::new(Limits {
        records: 1,
        bytes: 21,
    });
    assert_eq!(builder.intern(b"a").unwrap(), 0);
    assert!(builder.intern(b"b").is_err());
    assert_eq!(builder.intern(b"a").unwrap(), 0);

    let bytes = builder.into_bytes().unwrap();

    assert!(
        ValuePool::open(
            &bytes,
            Limits {
                records: 0,
                ..Limits::default()
            }
        )
        .is_err()
    );
    assert!(
        ValuePool::open(
            &bytes,
            Limits {
                bytes: bytes.len() - 1,
                ..Limits::default()
            }
        )
        .is_err()
    );

    let mut bounded = ValuePoolBuilder::new(Limits {
        records: 10,
        bytes: 21,
    });
    assert!(bounded.intern(b"too large").is_err());
    assert!(bounded.is_empty());
    assert_eq!(bounded.intern(b"a").unwrap(), 0);

    let empty = ValuePoolBuilder::new(Limits::default())
        .into_bytes()
        .unwrap();
    assert!(
        ValuePool::open(&empty, Limits::default())
            .unwrap()
            .is_empty()
    );

    assert!(matches!(bounded.write_to(Fail), Err(Error::Io(_))));
}

#[test]
fn typed_strings_validate_once_and_borrow_only_valid_utf8() {
    let mut builder = ValuePoolBuilder::new(Limits::default());
    let empty = builder.intern(b"").unwrap();
    let unicode = builder.intern("🦀 Montréal".as_bytes()).unwrap();
    let bytes = builder.into_bytes().unwrap();
    let strings = StringPool::open(&bytes, Limits::default()).unwrap();

    assert_eq!(strings.len(), 2);
    assert!(!strings.is_empty());
    assert_eq!(strings.get(empty).unwrap(), "");
    assert_eq!(strings.get(unicode).unwrap(), "🦀 Montréal");
    assert!(
        bytes
            .as_ptr_range()
            .contains(&strings.get(unicode).unwrap().as_ptr())
    );
    assert!(strings.get(u32::MAX).is_err());

    let mut invalid = ValuePoolBuilder::new(Limits::default());
    invalid.intern(&[0xff]).unwrap();
    let invalid = invalid.into_bytes().unwrap();

    assert!(matches!(
        StringPool::open(&invalid, Limits::default()),
        Err(Error::Utf8(_))
    ));
}

#[test]
fn string_pools_reject_split_code_points_and_preserve_value_relative_errors() {
    assert!(std::str::from_utf8(b"prefix\xc3\xa9").is_ok());

    for values in [
        vec![b"prefix".as_slice(), b"\xc3", b"\xa9"],
        vec![b"prefix".as_slice(), b"ab\xff"],
        vec![b"prefix".as_slice(), b"ab\xc3"],
    ] {
        let expected = std::str::from_utf8(values[1]).unwrap_err();
        let mut builder = ValuePoolBuilder::new(Limits::default());
        for value in values {
            builder.intern(value).unwrap();
        }
        let bytes = builder.into_bytes().unwrap();

        match StringPool::open(&bytes, Limits::default()) {
            Err(Error::Utf8(actual)) => assert_eq!(actual, expected),
            result => panic!("expected a value-relative UTF-8 error, got {result:?}"),
        }
    }
}

struct Fail;

impl io::Write for Fail {
    fn write(&mut self, _: &[u8]) -> io::Result<usize> {
        Err(io::Error::other("test write failure"))
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}