use std::io::{ErrorKind, Read};
use std::ops::Range;
use crate::Error;
const MAGIC: &[u8; 8] = b"RUMORPH\0";
pub(crate) const MAX_SPELLINGS: usize = 8;
pub(crate) const MAX_LIST: usize = 255;
pub(crate) const ENTRY_KEYS: u32 = 1 << 31;
const VERSION: u32 = 7;
const OLDEST_VERSION: u32 = 5;
const HEADER: usize = 16;
const ALIGN: usize = 8;
const fn sections(version: u32) -> usize {
if version >= 7 { 17 } else { 16 }
}
#[cfg(any(test, feature = "build"))]
#[derive(Clone, Default)]
pub(crate) struct Sections {
pub meta: String,
pub tags: Vec<String>,
pub ud_strings: Vec<String>,
pub ud_tags: Vec<u16>,
pub alphabet: String,
pub prefixes: Vec<String>,
pub suffixes: Vec<String>,
pub paradigm_offsets: Vec<u32>,
pub paradigms: Vec<u16>,
pub tables: Vec<u32>,
pub words: Vec<u8>,
pub records: Vec<u32>,
pub variants: Vec<u32>,
pub probability_records: Vec<u32>,
pub guesses: Vec<u8>,
pub guess_records: Vec<u32>,
pub context: Vec<u8>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WordValue {
Single {
yo: u64,
records: u32,
probabilities: Option<u32>,
},
Many(u32),
}
const RECORDS_BITS: u32 = 24;
const PROBABILITIES_BITS: u32 = 20;
const YO_BITS: u32 = 19;
const MANY: u64 = 1 << 63;
impl WordValue {
#[cfg(any(test, feature = "build"))]
pub fn pack(self) -> Option<u64> {
let fits = |value: u64, bits: u32| (value >> bits == 0).then_some(value);
match self {
WordValue::Single {
yo,
records,
probabilities,
} => {
let probabilities = probabilities.map_or(0, |offset| u64::from(offset) + 1);
Some(
fits(u64::from(records), RECORDS_BITS)?
| fits(probabilities, PROBABILITIES_BITS)? << RECORDS_BITS
| fits(yo, YO_BITS)? << (RECORDS_BITS + PROBABILITIES_BITS),
)
}
WordValue::Many(offset) => Some(MANY | u64::from(offset)),
}
}
pub fn unpack(value: u64) -> Self {
if value & MANY != 0 {
return WordValue::Many(value as u32);
}
let field = |shift: u32, bits: u32| (value >> shift) & ((1 << bits) - 1);
let probabilities = field(RECORDS_BITS, PROBABILITIES_BITS);
WordValue::Single {
yo: field(RECORDS_BITS + PROBABILITIES_BITS, YO_BITS),
records: field(0, RECORDS_BITS) as u32,
probabilities: probabilities.checked_sub(1).map(|offset| offset as u32),
}
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct Layout {
pub meta: Range<usize>,
pub tags: Range<usize>,
pub ud_strings: Range<usize>,
pub ud_tags: Range<usize>,
pub alphabet: Range<usize>,
pub prefixes: Range<usize>,
pub suffixes: Range<usize>,
pub paradigm_offsets: Range<usize>,
pub paradigms: Range<usize>,
pub tables: Range<usize>,
pub words: Range<usize>,
pub records: Range<usize>,
pub variants: Range<usize>,
pub probability_records: Range<usize>,
pub guesses: Range<usize>,
pub guess_records: Range<usize>,
pub context: Range<usize>,
}
impl Layout {
pub fn numbers(&self) -> [(&Range<usize>, usize); 7] {
[
(&self.paradigm_offsets, 4),
(&self.paradigms, 2),
(&self.tables, 4),
(&self.records, 4),
(&self.variants, 4),
(&self.probability_records, 4),
(&self.guess_records, 4),
]
}
}
#[cfg(any(test, feature = "build"))]
pub(crate) fn write(sections: &Sections) -> Vec<u8> {
let mut out = MAGIC.to_vec();
out.extend_from_slice(&VERSION.to_le_bytes());
out.extend_from_slice(&[0; 4]);
let mut section = |bytes: &[u8]| {
out.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
out.extend_from_slice(bytes);
out.resize(out.len().next_multiple_of(ALIGN), 0);
};
section(sections.meta.as_bytes());
section(&strings_bytes(§ions.tags));
section(&strings_bytes(§ions.ud_strings));
section(&u16_bytes(§ions.ud_tags));
section(sections.alphabet.as_bytes());
section(&strings_bytes(§ions.prefixes));
section(&strings_bytes(§ions.suffixes));
section(&u32_bytes(§ions.paradigm_offsets));
section(&u16_bytes(§ions.paradigms));
section(&u32_bytes(§ions.tables));
section(§ions.words);
section(&u32_bytes(§ions.records));
section(&u32_bytes(§ions.variants));
section(&u32_bytes(§ions.probability_records));
section(§ions.guesses);
section(&u32_bytes(§ions.guess_records));
section(§ions.context);
let checksum = crc32fast::hash(&out[HEADER..]);
out[12..HEADER].copy_from_slice(&checksum.to_le_bytes());
out
}
fn check_header(data: &[u8]) -> Result<u32, Error> {
if data.get(..MAGIC.len()) != Some(MAGIC.as_slice()) {
return Err(Error::Format("нет сигнатуры словаря ruMorph".into()));
}
let Some(header) = data.get(..HEADER) else {
return Err(truncated());
};
let version = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
if !(OLDEST_VERSION..=VERSION).contains(&version) {
return Err(Error::Format(format!(
"версия формата {version}, поддерживаются {OLDEST_VERSION}-{VERSION}"
)));
}
Ok(version)
}
pub(crate) fn layout(data: &[u8]) -> Result<Layout, Error> {
let version = check_header(data)?;
let mut at = HEADER;
let mut section = |width: usize| -> Result<Range<usize>, Error> {
let len = data.get(at..at + 8).ok_or_else(truncated)?;
let len = u64::from_le_bytes(len.try_into().map_err(|_| truncated())?);
let start = at + 8;
let end = usize::try_from(len)
.ok()
.and_then(|len| start.checked_add(len))
.filter(|&end| end <= data.len())
.ok_or_else(truncated)?;
if !(end - start).is_multiple_of(width) {
return Err(Error::Format(format!("длина секции не кратна {width}")));
}
at = end.next_multiple_of(ALIGN);
if at > data.len() {
return Err(truncated());
}
Ok(start..end)
};
let layout = Layout {
meta: section(1)?,
tags: section(1)?,
ud_strings: section(1)?,
ud_tags: section(2)?,
alphabet: section(1)?,
prefixes: section(1)?,
suffixes: section(1)?,
paradigm_offsets: section(4)?,
paradigms: section(2)?,
tables: section(4)?,
words: section(1)?,
records: section(4)?,
variants: section(4)?,
probability_records: section(4)?,
guesses: section(1)?,
guess_records: section(4)?,
context: if sections(version) > 16 {
section(1)?
} else {
0..0
},
};
if at != data.len() {
return Err(extra_data());
}
let checksum = u32::from_le_bytes(data[12..HEADER].try_into().map_err(|_| truncated())?);
if crc32fast::hash(&data[HEADER..]) != checksum {
return Err(Error::Format(
"контрольная сумма не совпадает: файл поврежден".into(),
));
}
Ok(layout)
}
pub(crate) fn read(mut input: impl Read) -> Result<Storage, Error> {
let mut buffer = Buffer::default();
buffer.fill(&mut input, HEADER as u64)?;
let version = check_header(buffer.bytes())?;
for _ in 0..sections(version) {
if buffer.fill(&mut input, 8)? < 8 {
return Err(truncated());
}
let bytes = buffer.bytes();
let len = u64::from_le_bytes(
bytes[bytes.len() - 8..]
.try_into()
.map_err(|_| truncated())?,
);
let padded = len
.checked_next_multiple_of(ALIGN as u64)
.ok_or_else(truncated)?;
if buffer.fill(&mut input, padded)? < padded {
return Err(truncated());
}
}
if buffer.fill(&mut input, 1)? > 0 {
return Err(extra_data());
}
buffer.words.truncate(buffer.len.div_ceil(ALIGN));
Ok(Storage::Owned(buffer.words, buffer.len))
}
#[derive(Default)]
struct Buffer {
words: Vec<u64>,
len: usize,
}
impl Buffer {
fn bytes(&self) -> &[u8] {
&bytemuck::cast_slice(&self.words)[..self.len]
}
fn fill(&mut self, input: &mut impl Read, count: u64) -> Result<u64, Error> {
const CHUNK: u64 = 1 << 16;
let mut done = 0;
while done < count {
let want = (count - done).min(CHUNK) as usize;
self.words.resize((self.len + want).div_ceil(ALIGN), 0);
let space = &mut bytemuck::cast_slice_mut::<u64, u8>(&mut self.words)
[self.len..self.len + want];
match input.read(space) {
Ok(0) => break,
Ok(read) => {
self.len += read;
done += read as u64;
}
Err(err) if err.kind() == ErrorKind::Interrupted => {}
Err(err) => return Err(err.into()),
}
}
Ok(done)
}
}
fn truncated() -> Error {
Error::Format("файл обрезан".into())
}
fn extra_data() -> Error {
Error::Format("лишние данные после последней секции".into())
}
pub(crate) enum Storage {
Owned(Vec<u64>, usize),
Mapped(memmap2::Mmap),
Static(&'static [u8]),
}
impl Storage {
pub fn copy(data: &[u8]) -> Self {
let mut words = vec![0u64; data.len().div_ceil(ALIGN)];
bytemuck::cast_slice_mut::<u64, u8>(&mut words)[..data.len()].copy_from_slice(data);
Storage::Owned(words, data.len())
}
pub fn of_static(data: &'static [u8]) -> Self {
if (data.as_ptr() as usize).is_multiple_of(ALIGN) {
Storage::Static(data)
} else {
Storage::copy(data)
}
}
pub fn bytes(&self) -> &[u8] {
match self {
Storage::Owned(words, len) => &bytemuck::cast_slice(words)[..*len],
Storage::Mapped(map) => map,
Storage::Static(data) => data,
}
}
pub fn native(self, layout: &Layout) -> Self {
if cfg!(target_endian = "little") {
self
} else {
self.swapped(layout)
}
}
fn swapped(&self, layout: &Layout) -> Self {
let mut copy = Storage::copy(self.bytes());
if let Storage::Owned(words, _) = &mut copy {
let bytes = bytemuck::cast_slice_mut::<u64, u8>(words);
for (range, width) in layout.numbers() {
for number in bytes[range.clone()].chunks_exact_mut(width) {
number.reverse();
}
}
}
copy
}
}
#[derive(Clone)]
pub(crate) struct Slice {
pub storage: std::sync::Arc<Storage>,
pub range: Range<usize>,
}
impl AsRef<[u8]> for Slice {
fn as_ref(&self) -> &[u8] {
&self.storage.bytes()[self.range.clone()]
}
}
pub(crate) fn numbers<T: bytemuck::Pod>(bytes: &[u8]) -> &[T] {
bytemuck::try_cast_slice(bytes).unwrap_or_default()
}
#[derive(Clone, Debug, Default)]
pub(crate) struct Strings {
text: String,
ends: Vec<u32>,
}
impl Strings {
pub fn read(bytes: &[u8]) -> Result<Self, Error> {
let invalid = |msg: &str| Error::Format(msg.into());
let number = |at: usize| -> Option<u32> {
Some(u32::from_le_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
};
let count = number(0).ok_or_else(|| invalid("таблица строк обрезана"))? as usize;
let start = count
.checked_mul(4)
.and_then(|len| len.checked_add(4))
.filter(|&start| start <= bytes.len())
.ok_or_else(|| invalid("число строк больше размера таблицы"))?;
let text = std::str::from_utf8(&bytes[start..])
.map_err(|_| invalid("строка не в UTF-8"))?
.to_owned();
let ends: Vec<u32> = (0..count)
.map(|i| number(4 + 4 * i).unwrap_or(u32::MAX))
.collect();
let mut previous = 0;
for &end in &ends {
let end = end as usize;
if end < previous || !text.is_char_boundary(end) {
return Err(invalid("неверные границы строк в таблице"));
}
previous = end;
}
if previous != text.len() {
return Err(invalid("лишние данные в таблице строк"));
}
Ok(Strings { text, ends })
}
pub fn len(&self) -> usize {
self.ends.len()
}
pub fn get(&self, index: usize) -> Option<&str> {
let end = *self.ends.get(index)? as usize;
let start = index.checked_sub(1).map_or(0, |i| self.ends[i] as usize);
self.text.get(start..end)
}
pub fn at(&self, index: usize) -> &str {
self.get(index).unwrap_or_default()
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = &str> {
(0..self.len()).map(|index| self.at(index))
}
}
#[cfg(any(test, feature = "build"))]
fn strings_bytes(values: &[String]) -> Vec<u8> {
let mut out = (values.len() as u32).to_le_bytes().to_vec();
let mut end = 0;
for value in values {
end += value.len() as u32;
out.extend_from_slice(&end.to_le_bytes());
}
for value in values {
out.extend_from_slice(value.as_bytes());
}
out
}
#[cfg(any(test, feature = "build"))]
fn u16_bytes(values: &[u16]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
#[cfg(any(test, feature = "build"))]
fn u32_bytes(values: &[u32]) -> Vec<u8> {
values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect()
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) fn read(data: &[u8]) -> Sections {
let layout = layout(data).unwrap();
let bytes = |range: &Range<usize>| &data[range.clone()];
let text = |range: &Range<usize>| String::from_utf8(bytes(range).to_vec()).unwrap();
let strings = |range: &Range<usize>| -> Vec<String> {
Strings::read(bytes(range))
.unwrap()
.iter()
.map(str::to_owned)
.collect()
};
let u16s = |range: &Range<usize>| -> Vec<u16> {
bytes(range)
.as_chunks::<2>()
.0
.iter()
.map(|&item| u16::from_le_bytes(item))
.collect()
};
let u32s = |range: &Range<usize>| -> Vec<u32> {
bytes(range)
.as_chunks::<4>()
.0
.iter()
.map(|&item| u32::from_le_bytes(item))
.collect()
};
Sections {
meta: text(&layout.meta),
tags: strings(&layout.tags),
ud_strings: strings(&layout.ud_strings),
ud_tags: u16s(&layout.ud_tags),
alphabet: text(&layout.alphabet),
prefixes: strings(&layout.prefixes),
suffixes: strings(&layout.suffixes),
paradigm_offsets: u32s(&layout.paradigm_offsets),
paradigms: u16s(&layout.paradigms),
tables: u32s(&layout.tables),
words: bytes(&layout.words).to_vec(),
records: u32s(&layout.records),
variants: u32s(&layout.variants),
probability_records: u32s(&layout.probability_records),
guesses: bytes(&layout.guesses).to_vec(),
guess_records: u32s(&layout.guess_records),
context: bytes(&layout.context).to_vec(),
}
}
pub(crate) fn with_checksum(mut data: Vec<u8>) -> Vec<u8> {
if data.len() >= HEADER {
let checksum = crc32fast::hash(&data[HEADER..]);
data[12..HEADER].copy_from_slice(&checksum.to_le_bytes());
}
data
}
fn sample() -> Sections {
Sections {
meta: "{}".into(),
tags: vec!["NOUN".into(), "VERB".into()],
ud_strings: vec!["NOUN".into(), "Case".into(), "Nom".into()],
ud_tags: vec![0, 1, 1, 2, 0],
alphabet: "аб".into(),
prefixes: vec![String::new(), "по".into()],
suffixes: vec![String::new(), "а".into()],
paradigm_offsets: vec![0, 3],
paradigms: vec![0, 0, 0],
tables: vec![0, 0],
words: vec![1, 2, 3],
records: vec![1, 0],
..Sections::default()
}
}
#[test]
fn word_values() {
let values = [
WordValue::Single {
yo: 0,
records: 0,
probabilities: None,
},
WordValue::Single {
yo: (1 << YO_BITS) - 1,
records: (1 << RECORDS_BITS) - 1,
probabilities: Some((1 << PROBABILITIES_BITS) - 2),
},
WordValue::Many(0),
WordValue::Many(u32::MAX),
];
for value in values {
assert_eq!(WordValue::unpack(value.pack().unwrap()), value);
}
for value in [
WordValue::Single {
yo: 1 << YO_BITS,
records: 0,
probabilities: None,
},
WordValue::Single {
yo: 0,
records: 1 << RECORDS_BITS,
probabilities: None,
},
WordValue::Single {
yo: 0,
records: 0,
probabilities: Some((1 << PROBABILITIES_BITS) - 1),
},
] {
assert_eq!(value.pack(), None);
}
}
#[test]
fn roundtrip() {
let data = write(&sample());
let back = read(&data);
assert_eq!(write(&back), data);
assert_eq!(back.ud_strings, sample().ud_strings);
assert_eq!(back.ud_tags, sample().ud_tags);
assert_eq!(back.paradigms, sample().paradigms);
assert_eq!(back.words, sample().words);
}
#[test]
fn sections_are_aligned() {
let data = write(&sample());
let layout = layout(&data).unwrap();
for (range, _) in layout.numbers() {
assert_eq!(range.start % ALIGN, 0);
}
assert_eq!(data.len() % ALIGN, 0);
let storage = Storage::copy(&data);
assert_eq!(storage.bytes(), data);
assert_eq!(storage.bytes().as_ptr() as usize % ALIGN, 0);
let storage = storage.native(&layout);
assert_eq!(numbers::<u32>(&storage.bytes()[layout.records]), [1, 0]);
}
#[test]
fn swaps_numbers_for_big_endian() {
let mut sections = sample();
sections.paradigms = vec![1, 0x0102, 0];
let data = write(§ions);
let layout = layout(&data).unwrap();
let storage = Storage::copy(&data);
let swapped = storage.swapped(&layout);
assert_eq!(
numbers::<u16>(&swapped.bytes()[layout.paradigms.clone()]),
[1u16, 0x0102, 0].map(|number| u16::from_ne_bytes(number.to_be_bytes()))
);
assert_eq!(
numbers::<u32>(&swapped.bytes()[layout.records.clone()]),
[1u32, 0].map(|number| u32::from_ne_bytes(number.to_be_bytes()))
);
assert_eq!(
swapped.bytes()[layout.words.clone()],
storage.bytes()[layout.words.clone()]
);
let native = storage.native(&layout);
assert_eq!(
numbers::<u16>(&native.bytes()[layout.paradigms.clone()]),
[1, 0x0102, 0]
);
}
#[test]
fn rejects_damaged_files() {
let data = write(&sample());
for at in HEADER..data.len() {
let mut damaged = data.clone();
damaged[at] ^= 0x10;
assert!(layout(&damaged).is_err(), "{at}");
}
let mut damaged = data.clone();
damaged[HEADER + 8] ^= 0x10;
let err = layout(&damaged).err().unwrap();
assert!(err.to_string().contains("контрольная сумма"), "{err}");
for len in 0..data.len() {
assert!(
layout(&with_checksum(data[..len].to_vec())).is_err(),
"{len}"
);
}
let mut longer = data.clone();
longer.extend_from_slice(&[0; 8]);
let err = layout(&with_checksum(longer)).err().unwrap();
assert!(err.to_string().contains("лишние данные"), "{err}");
}
#[test]
fn checks_sections_before_checksum() {
let mut data = write(&sample())[..HEADER].to_vec();
data.resize(1 << 20, 0);
let err = layout(&data).err().unwrap();
assert!(err.to_string().contains("лишние данные"), "{err}");
}
struct Endless {
head: Vec<u8>,
at: usize,
}
impl Read for Endless {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let rest = self.head.get(self.at..).unwrap_or_default();
let len = rest.len().min(buf.len());
buf[..len].copy_from_slice(&rest[..len]);
buf[len..].fill(0);
self.at += buf.len();
Ok(buf.len())
}
}
#[test]
fn reads_stream_by_sections() {
let data = write(&sample());
let storage = super::read(data.as_slice()).unwrap();
assert_eq!(storage.bytes(), data);
assert_eq!(storage.bytes().as_ptr() as usize % ALIGN, 0);
for len in 0..data.len() {
assert!(
matches!(super::read(&data[..len]), Err(Error::Format(_))),
"{len}"
);
}
let mut longer = data.clone();
longer.push(0);
let err = super::read(longer.as_slice()).err().unwrap();
assert!(err.to_string().contains("лишние данные"), "{err}");
let mut huge = data[..HEADER].to_vec();
huge.extend_from_slice(&(u64::MAX - 7).to_le_bytes());
huge.extend_from_slice(b"meta");
let err = super::read(huge.as_slice()).err().unwrap();
assert!(err.to_string().contains("файл обрезан"), "{err}");
let err = super::read(&huge[..HEADER + 4]).err().unwrap();
assert!(err.to_string().contains("файл обрезан"), "{err}");
}
#[test]
fn stops_reading_endless_stream() {
let err = super::read(Endless {
head: Vec::new(),
at: 0,
})
.err()
.unwrap();
assert!(err.to_string().contains("нет сигнатуры"), "{err}");
let head = write(&sample())[..HEADER].to_vec();
let err = super::read(Endless { head, at: 0 }).err().unwrap();
assert!(err.to_string().contains("лишние данные"), "{err}");
}
#[test]
fn reports_stream_errors() {
struct Broken;
impl Read for Broken {
fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("диск"))
}
}
assert!(matches!(super::read(Broken), Err(Error::Io(_))));
struct Interrupted(Vec<u8>, bool);
impl Read for Interrupted {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.1 = !self.1;
if self.1 {
return Err(ErrorKind::Interrupted.into());
}
let len = self.0.len().min(buf.len()).min(3);
buf[..len].copy_from_slice(&self.0[..len]);
self.0.drain(..len);
Ok(len)
}
}
let data = write(&sample());
let storage = super::read(Interrupted(data.clone(), false)).unwrap();
assert_eq!(storage.bytes(), data);
}
#[test]
fn rejects_section_lengths() {
let data = write(&sample());
for huge in [u64::MAX, u64::MAX - 1, data.len() as u64] {
let mut broken = data.clone();
broken[HEADER..HEADER + 8].copy_from_slice(&huge.to_le_bytes());
let err = layout(&with_checksum(broken)).err().unwrap();
assert!(err.to_string().contains("файл обрезан"), "{huge}: {err}");
}
let mut broken = data.clone();
let at = layout(&data).unwrap().ud_tags.start - 8;
let len = u64::from_le_bytes(broken[at..at + 8].try_into().unwrap());
assert_eq!(len, 10);
broken[at..at + 8].copy_from_slice(&3u64.to_le_bytes());
let err = layout(&with_checksum(broken)).err().unwrap();
assert!(err.to_string().contains("не кратна 2"), "{err}");
}
#[test]
fn rejects_foreign_data_and_other_version() {
assert!(matches!(layout(b"not a dictionary"), Err(Error::Format(_))));
assert!(matches!(layout(b""), Err(Error::Format(_))));
assert!(matches!(layout(b"RUMORPH\0"), Err(Error::Format(_))));
let mut data = write(&sample());
data[MAGIC.len()..MAGIC.len() + 4].copy_from_slice(&4u32.to_le_bytes());
let err = layout(&data).err().unwrap();
assert!(err.to_string().contains("версия формата 4"), "{err}");
data.truncate(data.len() - 8);
for version in [5u32, 6] {
data[MAGIC.len()..MAGIC.len() + 4].copy_from_slice(&version.to_le_bytes());
let data = with_checksum(data.clone());
assert!(layout(&data).unwrap().context.is_empty());
assert_eq!(super::read(data.as_slice()).unwrap().bytes(), data);
}
}
#[test]
fn context_section() {
let sections = Sections {
context: b"weights".to_vec(),
..sample()
};
let data = write(§ions);
let layout = layout(&data).unwrap();
assert_eq!(&data[layout.context], b"weights");
assert_eq!(super::read(data.as_slice()).unwrap().bytes(), data);
}
#[test]
fn string_tables() {
let table = Strings::read(&strings_bytes(&["".into(), "по".into(), "а".into()])).unwrap();
assert_eq!(table.iter().collect::<Vec<_>>(), ["", "по", "а"]);
assert_eq!(table.get(3), None);
let mut huge = u32::MAX.to_le_bytes().to_vec();
huge.extend_from_slice(&[0; 8]);
assert!(Strings::read(&huge).is_err());
for (ends, text) in [(&[1u32][..], "п"), (&[2, 1][..], "по"), (&[1][..], "ab")] {
let mut bytes = (ends.len() as u32).to_le_bytes().to_vec();
for end in ends {
bytes.extend_from_slice(&end.to_le_bytes());
}
bytes.extend_from_slice(text.as_bytes());
assert!(Strings::read(&bytes).is_err(), "{ends:?} {text}");
}
assert!(Strings::read(&[0, 0, 0]).is_err());
assert!(Strings::read(&[0, 0, 0, 0, 0xFF]).is_err());
}
}