use crate::parser::{read_u32, read_u8};
use crate::Error;
pub const META_VERSION_1: u32 = 1;
pub const META_HEADER_LEN: usize = 16;
pub const META_DATA_MAP_LEN: usize = 12;
pub const META_TABLE_TAG: [u8; 4] = *b"meta";
const MAX_DATA_MAPS: u32 = 1024;
pub const META_TAG_DLNG: [u8; 4] = *b"dlng";
pub const META_TAG_SLNG: [u8; 4] = *b"slng";
pub const META_TAG_APPL: [u8; 4] = *b"appl";
pub const META_TAG_BILD: [u8; 4] = *b"bild";
#[derive(Debug, Clone, Copy)]
pub struct MetaRecord<'a> {
pub tag: [u8; 4],
pub payload: &'a [u8],
}
impl<'a> MetaRecord<'a> {
pub fn payload_as_str(&self) -> Option<&'a str> {
std::str::from_utf8(self.payload).ok()
}
}
#[derive(Debug, Clone)]
pub struct MetaTable<'a> {
version: u32,
flags: u32,
reserved: u32,
records: Vec<MetaRecord<'a>>,
}
impl<'a> MetaTable<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
if bytes.len() < META_HEADER_LEN {
return Err(Error::UnexpectedEof);
}
let version = read_u32(bytes, 0)?;
if version != META_VERSION_1 {
return Err(Error::BadStructure("meta: version != 1"));
}
let flags = read_u32(bytes, 4)?;
if flags != 0 {
return Err(Error::BadStructure("meta: flags != 0"));
}
let reserved = read_u32(bytes, 8)?;
let count = read_u32(bytes, 12)?;
if count > MAX_DATA_MAPS {
return Err(Error::BadStructure("meta: dataMapsCount cap"));
}
let count_usize = count as usize;
let body_end = META_HEADER_LEN
.checked_add(
count_usize
.checked_mul(META_DATA_MAP_LEN)
.ok_or(Error::BadStructure("meta: dataMaps overflow"))?,
)
.ok_or(Error::BadStructure("meta: dataMaps overflow"))?;
if bytes.len() < body_end {
return Err(Error::UnexpectedEof);
}
let total_len = bytes.len();
let mut records: Vec<MetaRecord<'a>> = Vec::with_capacity(count_usize);
for i in 0..count_usize {
let off = META_HEADER_LEN + i * META_DATA_MAP_LEN;
let tag = [
read_u8(bytes, off)?,
read_u8(bytes, off + 1)?,
read_u8(bytes, off + 2)?,
read_u8(bytes, off + 3)?,
];
if !is_valid_meta_tag(&tag) {
return Err(Error::BadStructure("meta: tag not §5.7.6.2-conformant"));
}
let data_offset = read_u32(bytes, off + 4)? as usize;
let data_length = read_u32(bytes, off + 8)? as usize;
let data_end = data_offset
.checked_add(data_length)
.ok_or(Error::BadStructure(
"meta: dataOffset + dataLength overflow",
))?;
if data_end > total_len {
return Err(Error::BadStructure(
"meta: DataMap payload past end of table",
));
}
let payload = &bytes[data_offset..data_end];
records.push(MetaRecord { tag, payload });
}
Ok(Self {
version,
flags,
reserved,
records,
})
}
pub fn version(&self) -> u32 {
self.version
}
pub fn flags(&self) -> u32 {
self.flags
}
pub fn reserved(&self) -> u32 {
self.reserved
}
pub fn records(&self) -> &[MetaRecord<'a>] {
&self.records
}
pub fn record(&self, tag: &[u8; 4]) -> Option<MetaRecord<'a>> {
self.records.iter().copied().find(|r| &r.tag == tag)
}
pub fn design_languages(&self) -> Option<&'a str> {
self.record(&META_TAG_DLNG)?.payload_as_str()
}
pub fn supported_languages(&self) -> Option<&'a str> {
self.record(&META_TAG_SLNG)?.payload_as_str()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScriptLangTag<'a> {
pub raw: &'a str,
}
impl<'a> ScriptLangTag<'a> {
pub fn subtags(&self) -> impl Iterator<Item = &'a str> {
self.raw.split('-')
}
pub fn subtag_count(&self) -> usize {
self.subtags().count()
}
}
pub fn script_lang_tags(payload: &str) -> impl Iterator<Item = ScriptLangTag<'_>> {
payload.split(',').filter_map(|raw| {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
if !trimmed.is_ascii() {
return None;
}
if trimmed.starts_with('-') || trimmed.ends_with('-') || trimmed.contains("--") {
return None;
}
Some(ScriptLangTag { raw: trimmed })
})
}
pub fn is_valid_meta_tag(tag: &[u8; 4]) -> bool {
if !is_meta_tag_letter(tag[0]) {
return false;
}
let mut seen_space = false;
for &b in tag {
if b == b' ' {
seen_space = true;
continue;
}
if seen_space {
return false;
}
if !(is_meta_tag_letter(b) || b.is_ascii_digit()) {
return false;
}
}
true
}
#[inline]
fn is_meta_tag_letter(b: u8) -> bool {
matches!(b, 0x41..=0x5A | 0x61..=0x7A)
}
#[cfg(test)]
mod tests {
use super::*;
fn build(records: &[(&[u8; 4], &[u8])]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&META_VERSION_1.to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&(records.len() as u32).to_be_bytes());
let payload_base = META_HEADER_LEN + records.len() * META_DATA_MAP_LEN;
let mut cur = payload_base;
for (tag, payload) in records {
b.extend_from_slice(*tag);
b.extend_from_slice(&(cur as u32).to_be_bytes());
b.extend_from_slice(&(payload.len() as u32).to_be_bytes());
cur += payload.len();
}
for (_, payload) in records {
b.extend_from_slice(payload);
}
b
}
#[test]
fn parses_minimal_empty_table() {
let bytes = build(&[]);
assert_eq!(bytes.len(), META_HEADER_LEN);
let meta = MetaTable::parse(&bytes).expect("parse");
assert_eq!(meta.version(), META_VERSION_1);
assert_eq!(meta.flags(), 0);
assert_eq!(meta.reserved(), 0);
assert_eq!(meta.records().len(), 0);
assert!(meta.design_languages().is_none());
assert!(meta.supported_languages().is_none());
}
#[test]
fn parses_dlng_and_slng_records() {
let dlng = b"Latn";
let slng = b"Latn, Cyrl, Grek";
let bytes = build(&[(b"dlng", dlng), (b"slng", slng)]);
let meta = MetaTable::parse(&bytes).expect("parse");
assert_eq!(meta.records().len(), 2);
assert_eq!(meta.design_languages(), Some("Latn"));
assert_eq!(meta.supported_languages(), Some("Latn, Cyrl, Grek"));
assert!(meta.record(&META_TAG_DLNG).is_some());
assert!(meta.record(&META_TAG_SLNG).is_some());
assert!(meta.record(&META_TAG_APPL).is_none());
}
#[test]
fn reserved_tags_appl_and_bild_pass_the_tag_grammar() {
assert!(is_valid_meta_tag(&META_TAG_APPL));
assert!(is_valid_meta_tag(&META_TAG_BILD));
assert!(is_valid_meta_tag(&META_TAG_DLNG));
assert!(is_valid_meta_tag(&META_TAG_SLNG));
}
#[test]
fn rejects_short_header() {
let b = vec![0u8; META_HEADER_LEN - 1];
assert!(matches!(MetaTable::parse(&b), Err(Error::UnexpectedEof)));
}
#[test]
fn rejects_wrong_version() {
let mut b = build(&[]);
b[0..4].copy_from_slice(&2u32.to_be_bytes());
assert!(matches!(MetaTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_nonzero_flags() {
let mut b = build(&[]);
b[4..8].copy_from_slice(&1u32.to_be_bytes());
assert!(matches!(MetaTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn tolerates_nonzero_reserved_field() {
let mut b = build(&[]);
b[8..12].copy_from_slice(&42u32.to_be_bytes());
let meta = MetaTable::parse(&b).expect("parse");
assert_eq!(meta.reserved(), 42);
}
#[test]
fn rejects_truncated_data_maps_array() {
let mut b = build(&[(b"dlng", b"Latn")]);
b[12..16].copy_from_slice(&2u32.to_be_bytes());
assert!(matches!(MetaTable::parse(&b), Err(Error::UnexpectedEof)));
}
#[test]
fn rejects_data_payload_past_table_end() {
let mut b = build(&[(b"dlng", b"Latn")]);
let map_off = META_HEADER_LEN;
let bogus_offset = (b.len() + 10) as u32;
b[map_off + 4..map_off + 8].copy_from_slice(&bogus_offset.to_be_bytes());
assert!(matches!(MetaTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_data_offset_plus_length_overflow() {
let mut b = build(&[(b"dlng", b"Latn")]);
let map_off = META_HEADER_LEN;
b[map_off + 4..map_off + 8].copy_from_slice(&u32::MAX.to_be_bytes());
b[map_off + 8..map_off + 12].copy_from_slice(&u32::MAX.to_be_bytes());
assert!(matches!(MetaTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_data_maps_count_cap() {
let mut b = vec![0u8; META_HEADER_LEN];
b[0..4].copy_from_slice(&META_VERSION_1.to_be_bytes());
b[12..16].copy_from_slice(&(MAX_DATA_MAPS + 1).to_be_bytes());
assert!(matches!(MetaTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_tag_starting_with_digit() {
let b = build(&[(b"1lng", b"Latn")]);
assert!(matches!(MetaTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_tag_with_inner_space() {
let b = build(&[(b"d ng", b"Latn")]);
assert!(matches!(MetaTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn rejects_tag_with_non_alphanumeric() {
let b = build(&[(b"dl-g", b"Latn")]);
assert!(matches!(MetaTable::parse(&b), Err(Error::BadStructure(_))));
}
#[test]
fn accepts_short_tag_padded_with_trailing_space() {
assert!(is_valid_meta_tag(b"ab "));
assert!(is_valid_meta_tag(b"a "));
}
#[test]
fn rejects_all_space_tag() {
assert!(!is_valid_meta_tag(b" "));
}
#[test]
fn parses_vendor_private_tag() {
let b = build(&[(b"XYZ9", b"private blob")]);
let meta = MetaTable::parse(&b).expect("parse");
let rec = meta.record(b"XYZ9").expect("vendor tag visible");
assert_eq!(rec.payload, b"private blob");
assert_eq!(rec.payload_as_str(), Some("private blob"));
}
#[test]
fn payload_as_str_returns_none_for_non_utf8_bytes() {
let bytes = build(&[(b"BINS", &[0xFF, 0xFE, 0xFD])]);
let meta = MetaTable::parse(&bytes).expect("parse");
let rec = meta.record(b"BINS").expect("record");
assert!(rec.payload_as_str().is_none());
}
#[test]
fn script_lang_tag_splitter_handles_single_value() {
let tags: Vec<_> = script_lang_tags("Latn").map(|t| t.raw).collect();
assert_eq!(tags, vec!["Latn"]);
}
#[test]
fn script_lang_tag_splitter_handles_multiple_values() {
let tags: Vec<_> = script_lang_tags("Latn, Cyrl, Grek")
.map(|t| t.raw)
.collect();
assert_eq!(tags, vec!["Latn", "Cyrl", "Grek"]);
}
#[test]
fn script_lang_tag_splitter_handles_extended_subtags() {
let tags: Vec<_> = script_lang_tags("sr-Cyrl, en-Dsrt, Hant-HK")
.map(|t| (t.raw, t.subtag_count()))
.collect();
assert_eq!(tags, vec![("sr-Cyrl", 2), ("en-Dsrt", 2), ("Hant-HK", 2)]);
}
#[test]
fn script_lang_tag_splitter_discards_empty_fragments() {
let tags: Vec<_> = script_lang_tags("Latn, , Cyrl, ").map(|t| t.raw).collect();
assert_eq!(tags, vec!["Latn", "Cyrl"]);
}
#[test]
fn script_lang_tag_splitter_rejects_leading_or_trailing_hyphen() {
let tags: Vec<_> = script_lang_tags("-Latn, Cyrl-, ja--Jpan, ok-Latn")
.map(|t| t.raw)
.collect();
assert_eq!(tags, vec!["ok-Latn"]);
}
#[test]
fn script_lang_tag_splitter_rejects_non_ascii_fragment() {
let tags: Vec<_> = script_lang_tags("Latn, Lаtn") .map(|t| t.raw)
.collect();
assert_eq!(tags, vec!["Latn"]);
}
#[test]
fn shared_data_payload_between_two_records_round_trips() {
let mut b = Vec::new();
b.extend_from_slice(&META_VERSION_1.to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes());
b.extend_from_slice(&2u32.to_be_bytes()); let payload_off = META_HEADER_LEN + 2 * META_DATA_MAP_LEN;
let payload = b"shared";
b.extend_from_slice(b"dlng");
b.extend_from_slice(&(payload_off as u32).to_be_bytes());
b.extend_from_slice(&(payload.len() as u32).to_be_bytes());
b.extend_from_slice(b"slng");
b.extend_from_slice(&(payload_off as u32).to_be_bytes());
b.extend_from_slice(&(payload.len() as u32).to_be_bytes());
b.extend_from_slice(payload);
let meta = MetaTable::parse(&b).expect("parse");
assert_eq!(meta.design_languages(), Some("shared"));
assert_eq!(meta.supported_languages(), Some("shared"));
}
#[test]
fn records_accessor_preserves_document_order() {
let b = build(&[
(b"slng", b"Latn, Cyrl"),
(b"dlng", b"Latn"),
(b"XYZ1", b"blob"),
]);
let meta = MetaTable::parse(&b).expect("parse");
let tags: Vec<_> = meta.records().iter().map(|r| r.tag).collect();
assert_eq!(tags, vec![*b"slng", *b"dlng", *b"XYZ1"]);
}
}