#![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(())
}
}