1use alloc::vec::Vec;
8
9use mf2_model::{Dir, MsgId};
10
11use crate::bytes::{Cur, nul_pos, plane_entry, u16_at, u32_at, u64_at};
12use crate::error::CatalogError;
13use crate::format::{
14 HEADER_LEN, IDS_RESTART, MAGIC, MAX_FALLBACK_LOCALES, MAX_MESSAGES, SECTION_ENTRY_LEN,
15 VERSION_MAJOR, flags, header, kind, locale_key, section,
16};
17use crate::plural;
18use crate::view::{MsgView, Names};
19
20#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
25pub struct StrRef(pub(crate) u32);
26
27#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
29pub struct CldrVersion {
30 pub major: u16,
32 pub minor: u8,
34 pub patch: u8,
36}
37
38impl CldrVersion {
39 #[doc(hidden)]
41 pub const fn to_u32(self) -> u32 {
42 (self.major as u32) << 16 | (self.minor as u32) << 8 | self.patch as u32
43 }
44
45 #[allow(clippy::cast_possible_truncation)] #[doc(hidden)]
48 pub const fn from_u32(v: u32) -> Option<Self> {
49 if v == 0 {
50 return None;
51 }
52 Some(CldrVersion {
53 major: (v >> 16) as u16,
54 minor: (v >> 8) as u8,
55 patch: v as u8,
56 })
57 }
58}
59
60#[derive(Clone, Copy)]
62pub enum Entry<'a> {
63 Simple(StrRef),
66 Pattern(MsgView<'a>),
68 Select(MsgView<'a>),
70 Absent,
72}
73
74#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
76pub(crate) struct Span {
77 pub(crate) off: usize,
78 pub(crate) len: usize,
79}
80
81impl Span {
82 #[inline]
83 pub(crate) fn of<'a>(&self, b: &'a [u8]) -> &'a [u8] {
84 match self.off.checked_add(self.len) {
85 Some(end) => b.get(self.off..end).unwrap_or(&[]),
86 None => &[],
87 }
88 }
89}
90
91pub struct Catalog {
94 bytes: Vec<u8>,
95 version: u16,
96 flags: u16,
97 hash: u64,
98 count: u32,
99 locale: u32,
100 cldr: u32,
101 chunk: u8,
102 dir: Dir,
103 pub(crate) index: Span,
104 pub(crate) messages: Span,
105 #[cfg_attr(not(feature = "decode"), allow(dead_code))]
107 pub(crate) cold: Option<Span>,
108 pub(crate) names: Span,
109 fallback: Option<Fallback>,
110 locale_sec: Span,
111 plural: [Option<Span>; 2],
112 funcs: Span,
113 ids: Option<Span>,
114 pub(crate) strings: Span,
115}
116
117#[derive(Clone, Copy)]
119struct Fallback {
120 locales: Span,
121 entries: Span,
122}
123
124impl core::fmt::Debug for Catalog {
125 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126 f.debug_struct("Catalog")
127 .field("locale", &self.locale())
128 .field("messages", &self.count)
129 .field("bytes", &self.bytes.len())
130 .finish_non_exhaustive()
131 }
132}
133
134#[derive(Default)]
136struct Found {
137 index: Option<Span>,
138 messages: Option<Span>,
139 cold: Option<Span>,
140 names: Option<Span>,
141 fallback: Option<Span>,
142 locale: Option<Span>,
143 funcs: Option<Span>,
144 ids: Option<Span>,
145 strings: Option<Span>,
146}
147
148impl Found {
149 fn slot(&mut self, kind: u16) -> Option<&mut Option<Span>> {
150 Some(match kind {
151 section::INDEX => &mut self.index,
152 section::MESSAGES => &mut self.messages,
153 section::COLD => &mut self.cold,
154 section::NAMES => &mut self.names,
155 section::FALLBACK => &mut self.fallback,
156 section::LOCALE => &mut self.locale,
157 section::FUNCS => &mut self.funcs,
158 section::IDS => &mut self.ids,
159 section::STRINGS => &mut self.strings,
160 _ => return None,
161 })
162 }
163}
164
165impl Catalog {
166 pub fn new(bytes: Vec<u8>, expect_manifest: u64) -> Result<Catalog, CatalogError> {
172 let b = bytes.as_slice();
173 if b.get(..4) != Some(&MAGIC[..]) {
174 return Err(CatalogError::Magic);
175 }
176 let version = u16_at(b, header::VERSION).ok_or(CatalogError::Truncated)?;
177 if version >> 8 != VERSION_MAJOR {
178 return Err(CatalogError::Version);
179 }
180 if b.len() < HEADER_LEN {
181 return Err(CatalogError::Truncated);
182 }
183 let hash = u64_at(b, header::MANIFEST_HASH).ok_or(CatalogError::Truncated)?;
184 if hash != expect_manifest {
185 return Err(CatalogError::ManifestMismatch);
186 }
187 let get16 = |at| u16_at(b, at).ok_or(CatalogError::Truncated);
188 let get32 = |at| u32_at(b, at).ok_or(CatalogError::Truncated);
189 let flags = get16(header::FLAGS)?;
190 let count = get32(header::MESSAGE_COUNT)?;
191 let locale = get32(header::LOCALE)?;
192 let cldr = get32(header::CLDR_VERSION)?;
193 let chunk = *b.get(header::CHUNK).ok_or(CatalogError::Truncated)?;
194 let dir = match b.get(header::DIR) {
195 Some(0) => Dir::Ltr,
196 Some(1) => Dir::Rtl,
197 _ => return Err(CatalogError::Header),
198 };
199 if count > MAX_MESSAGES {
200 return Err(CatalogError::Header);
201 }
202 let found = sections(b)?;
203 let need = |s: Option<Span>| s.ok_or(CatalogError::MissingSection);
204 let index = need(found.index)?;
205 let messages = need(found.messages)?;
206 let names = need(found.names)?;
207 let locale_sec = need(found.locale)?;
208 let funcs = need(found.funcs)?;
209 let strings = need(found.strings)?;
210 let pool = strings.of(b);
211 if pool.last() != Some(&0) {
212 return Err(CatalogError::Strings);
213 }
214 let in_pool = |r: u32| (r as usize) < pool.len();
215 if !in_pool(locale) {
216 return Err(CatalogError::Header);
217 }
218 check_index(index.of(b), count as usize, messages.len, pool.len())?;
219 check_names(names.of(b), pool.len()).ok_or(CatalogError::Names)?;
220 check_funcs(funcs.of(b), pool.len()).ok_or(CatalogError::Funcs)?;
221 let fallback = match found.fallback {
222 Some(s) => Some(check_fallback(b, s, count, pool.len()).ok_or(CatalogError::Fallback)?),
223 None => None,
224 };
225 let plural = check_locale(b, locale_sec).ok_or(CatalogError::Locale)?;
226 if let Some(ids) = found.ids {
227 check_ids(ids.of(b), count as usize).ok_or(CatalogError::Ids)?;
228 }
229 Ok(Catalog {
230 version,
231 flags,
232 hash,
233 count,
234 locale,
235 cldr,
236 chunk,
237 dir,
238 index,
239 messages,
240 cold: found.cold,
241 names,
242 fallback,
243 locale_sec,
244 plural,
245 funcs,
246 ids: found.ids,
247 strings,
248 bytes,
249 })
250 }
251
252 #[doc(hidden)]
254 pub fn format_version(&self) -> u16 {
255 self.version
256 }
257
258 pub fn manifest_hash(&self) -> u64 {
260 self.hash
261 }
262
263 pub fn locale(&self) -> &str {
265 self.text(StrRef(self.locale)).unwrap_or("")
266 }
267
268 pub fn dir(&self) -> Dir {
270 self.dir
271 }
272
273 #[doc(hidden)]
275 pub fn chunk(&self) -> u8 {
276 self.chunk
277 }
278
279 pub fn cldr_version(&self) -> Option<CldrVersion> {
281 CldrVersion::from_u32(self.cldr)
282 }
283
284 #[doc(hidden)]
286 pub fn cold_stripped(&self) -> bool {
287 self.flags & flags::COLD_STRIPPED != 0
288 }
289
290 #[doc(hidden)]
292 pub fn ids_stripped(&self) -> bool {
293 self.flags & flags::IDS_STRIPPED != 0
294 }
295
296 pub fn message_count(&self) -> u32 {
298 self.count
299 }
300
301 pub fn as_bytes(&self) -> &[u8] {
303 &self.bytes
304 }
305
306 pub fn into_bytes(self) -> Vec<u8> {
308 self.bytes
309 }
310
311 #[doc(hidden)]
314 pub fn sections(&self) -> impl Iterator<Item = (u16, u32, u32)> + '_ {
315 let n = u16_at(&self.bytes, header::SECTION_COUNT).unwrap_or(0);
316 (0..usize::from(n)).filter_map(move |i| {
317 let at = HEADER_LEN.checked_add(i.checked_mul(SECTION_ENTRY_LEN)?)?;
318 Some((
319 u16_at(&self.bytes, at)?,
320 u32_at(&self.bytes, at.checked_add(2)?)?,
321 u32_at(&self.bytes, at.checked_add(6)?)?,
322 ))
323 })
324 }
325
326 #[inline]
329 #[doc(hidden)]
330 pub fn get(&self, id: MsgId) -> Entry<'_> {
331 if id.chunk() != self.chunk || id.index() >= self.count {
332 return Entry::Absent;
333 }
334 let Some(e) = plane_entry(
335 self.index.of(&self.bytes),
336 self.count as usize,
337 id.index() as usize,
338 ) else {
339 return Entry::Absent;
340 };
341 let off = e & kind::OFFSET_MASK;
342 match e >> kind::SHIFT {
343 kind::SIMPLE => Entry::Simple(StrRef(off)),
344 kind::PATTERN => Entry::Pattern(MsgView::new(self, off as usize, false)),
345 kind::SELECT => Entry::Select(MsgView::new(self, off as usize, true)),
346 _ => Entry::Absent,
347 }
348 }
349
350 #[inline]
354 #[doc(hidden)]
355 pub fn text(&self, r: StrRef) -> Option<&str> {
356 let rest = self.strings.of(&self.bytes).get(r.0 as usize..)?;
357 let end = nul_pos(rest)?;
358 core::str::from_utf8(rest.get(..end)?).ok()
359 }
360
361 #[doc(hidden)]
364 pub fn fallback_locale(&self, id: MsgId) -> Option<&str> {
365 let fb = self.fallback?;
366 if id.chunk() != self.chunk {
367 return None;
368 }
369 let entries = fb.entries.of(&self.bytes);
370 let target = id.index();
371 let (mut lo, mut hi) = (0usize, entries.len() / 4);
372 while lo < hi {
373 let mid = lo + (hi - lo) / 2;
374 let e = u32_at(entries, mid.checked_mul(4)?)?;
375 match (e & 0x00ff_ffff).cmp(&target) {
376 core::cmp::Ordering::Less => lo = mid + 1,
377 core::cmp::Ordering::Greater => hi = mid,
378 core::cmp::Ordering::Equal => {
379 let loc = u32_at(fb.locales.of(&self.bytes), ((e >> 24) as usize) * 4)?;
380 return self.text(StrRef(loc));
381 }
382 }
383 }
384 None
385 }
386
387 #[doc(hidden)]
389 pub fn function(&self, index: u32) -> Option<&str> {
390 let at = (index as usize).checked_mul(4)?;
391 self.text(StrRef(u32_at(self.funcs.of(&self.bytes), at)?))
392 }
393
394 #[doc(hidden)]
396 pub fn function_count(&self) -> u32 {
397 u32::try_from(self.funcs.len / 4).unwrap_or(u32::MAX)
398 }
399
400 #[doc(hidden)]
402 pub fn locale_entry(&self, key: u32) -> Option<&[u8]> {
403 match key {
404 locale_key::PLURAL_CARDINAL => return self.plural[0].map(|s| s.of(&self.bytes)),
405 locale_key::PLURAL_ORDINAL => return self.plural[1].map(|s| s.of(&self.bytes)),
406 _ => {}
407 }
408 let mut c = Cur::new(self.locale_sec.of(&self.bytes), 0);
409 let n = c.varint()?;
410 for _ in 0..n {
411 let k = c.varint()?;
412 let len = c.len()?;
413 let payload = c.take(len)?;
414 if k == key {
415 return Some(payload);
416 }
417 if k > key {
418 return None;
419 }
420 }
421 None
422 }
423
424 #[doc(hidden)]
427 pub fn names(&self, id: MsgId) -> Names<'_> {
428 match self.get(id) {
429 Entry::Pattern(m) | Entry::Select(m) => m.names(),
430 Entry::Simple(_) | Entry::Absent => Names::EMPTY,
431 }
432 }
433
434 #[cfg(feature = "decode")]
441 #[doc(hidden)]
442 pub fn id_of(&self, id: MsgId) -> Option<alloc::string::String> {
443 let index = id.index() as usize;
444 let count = self.count as usize;
445 if index >= count {
446 return None;
447 }
448 let ids = self.ids?.of(&self.bytes);
449 let blocks = count.div_ceil(IDS_RESTART);
450 let table_len = blocks.checked_mul(4)?;
451 let entries = ids.get(table_len..)?;
452 let block = index / IDS_RESTART;
453 let at = u32_at(ids, block.checked_mul(4)?)? as usize;
454 let mut c = Cur::new(entries, at);
455 let mut current: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
458 for i in (block * IDS_RESTART)..=index {
459 let shared = c.len()?;
460 let len = c.len()?;
461 let suffix = c.take(len)?;
462 if shared > current.len() {
463 return None;
464 }
465 current.truncate(shared);
466 current.extend_from_slice(suffix);
467 if i == index {
468 return alloc::string::String::from_utf8(current).ok();
469 }
470 }
471 None
472 }
473
474 pub fn lookup(&self, id: &str) -> Option<MsgId> {
477 let ids = self.ids?.of(&self.bytes);
478 let count = self.count as usize;
479 let blocks = count.div_ceil(IDS_RESTART);
480 let table_len = blocks.checked_mul(4)?;
481 let entries = ids.get(table_len..)?;
482 let key = id.as_bytes();
483 let (mut lo, mut hi) = (0usize, blocks);
485 while lo < hi {
486 let mid = lo + (hi - lo) / 2;
487 let at = u32_at(ids, mid.checked_mul(4)?)? as usize;
488 let mut c = Cur::new(entries, at);
489 let _shared = c.varint()?;
490 let len = c.len()?;
491 if c.take(len)? <= key {
492 lo = mid + 1;
493 } else {
494 hi = mid;
495 }
496 }
497 let block = lo.checked_sub(1)?;
498 let at = u32_at(ids, block.checked_mul(4)?)? as usize;
499 let mut c = Cur::new(entries, at);
500 let mut matched = 0usize;
504 let first = block.checked_mul(IDS_RESTART)?;
505 for i in first..count.min(first.checked_add(IDS_RESTART)?) {
506 let shared = c.len()?;
507 let len = c.len()?;
508 let suffix = c.take(len)?;
509 if shared > matched {
510 continue;
513 }
514 let rest = key.get(shared..)?;
519 let common = rest.iter().zip(suffix).take_while(|(a, b)| a == b).count();
520 if common == rest.len() && common == suffix.len() {
521 return MsgId::new(self.chunk, u32::try_from(i).ok()?);
522 }
523 matched = shared.checked_add(common)?;
524 }
525 None
526 }
527}
528
529fn sections(b: &[u8]) -> Result<Found, CatalogError> {
531 let n = usize::from(u16_at(b, header::SECTION_COUNT).ok_or(CatalogError::Truncated)?);
532 let table_end = n
533 .checked_mul(SECTION_ENTRY_LEN)
534 .and_then(|t| t.checked_add(HEADER_LEN))
535 .ok_or(CatalogError::Truncated)?;
536 if table_end > b.len() {
537 return Err(CatalogError::Truncated);
538 }
539 let mut found = Found::default();
540 let mut prev_end = table_end;
541 let mut last = None;
542 for i in 0..n {
543 let at = HEADER_LEN + i * SECTION_ENTRY_LEN;
544 let (Some(kind), Some(off), Some(len)) =
545 (u16_at(b, at), u32_at(b, at + 2), u32_at(b, at + 6))
546 else {
547 return Err(CatalogError::Truncated);
548 };
549 let (off, len) = (off as usize, len as usize);
550 let end = off.checked_add(len).ok_or(CatalogError::SectionTable)?;
551 if off < prev_end || end > b.len() || found.strings.is_some() {
552 return Err(CatalogError::SectionTable);
553 }
554 prev_end = end;
555 last = Some(end);
556 if let Some(slot) = found.slot(kind) {
557 if slot.is_some() {
558 return Err(CatalogError::SectionTable);
559 }
560 *slot = Some(Span { off, len });
561 }
562 }
563 if found.strings.is_some() && last != Some(b.len()) {
564 return Err(CatalogError::SectionTable);
565 }
566 Ok(found)
567}
568
569fn check_index(
571 index: &[u8],
572 count: usize,
573 messages_len: usize,
574 pool_len: usize,
575) -> Result<(), CatalogError> {
576 if Some(index.len()) != count.checked_mul(4) {
577 return Err(CatalogError::Index);
578 }
579 let mut prev: Option<u32> = None;
580 for i in 0..count {
581 let e = plane_entry(index, count, i).ok_or(CatalogError::Index)?;
582 let off = e & kind::OFFSET_MASK;
583 match e >> kind::SHIFT {
584 kind::SIMPLE => {
585 if off as usize >= pool_len {
586 return Err(CatalogError::Index);
587 }
588 }
589 kind::PATTERN | kind::SELECT => {
590 if off as usize >= messages_len || prev.is_some_and(|p| off <= p) {
591 return Err(CatalogError::Index);
592 }
593 prev = Some(off);
594 }
595 _ => {}
596 }
597 }
598 Ok(())
599}
600
601fn check_names(names: &[u8], pool_len: usize) -> Option<()> {
603 let mut c = Cur::new(names, 0);
604 while !c.at_end() {
605 let n = c.len()?.checked_add(c.len()?)?;
606 if n > c.remaining() / 4 {
607 return None;
608 }
609 for _ in 0..n {
610 if c.u32()? as usize >= pool_len {
611 return None;
612 }
613 }
614 }
615 Some(())
616}
617
618fn check_funcs(funcs: &[u8], pool_len: usize) -> Option<()> {
620 if !funcs.len().is_multiple_of(4) {
621 return None;
622 }
623 let mut c = Cur::new(funcs, 0);
624 while !c.at_end() {
625 if c.u32()? as usize >= pool_len {
626 return None;
627 }
628 }
629 Some(())
630}
631
632fn check_fallback(buf: &[u8], sec: Span, count: u32, pool_len: usize) -> Option<Fallback> {
634 let mut c = Cur::new(sec.of(buf), 0);
635 let n_locales = c.len()?;
636 if n_locales > MAX_FALLBACK_LOCALES || n_locales > c.remaining() / 4 {
637 return None;
638 }
639 let locales = Span {
640 off: sec.off.checked_add(c.pos())?,
641 len: n_locales * 4,
642 };
643 for _ in 0..n_locales {
644 if c.u32()? as usize >= pool_len {
645 return None;
646 }
647 }
648 let entries = Span {
649 off: sec.off.checked_add(c.pos())?,
650 len: c.remaining(),
651 };
652 if !entries.len.is_multiple_of(4) {
653 return None;
654 }
655 let mut prev: Option<u32> = None;
656 while !c.at_end() {
657 let e = c.u32()?;
658 let (msg, loc) = (e & 0x00ff_ffff, (e >> 24) as usize);
659 if msg >= count || loc >= n_locales || prev.is_some_and(|p| msg <= p) {
660 return None;
661 }
662 prev = Some(msg);
663 }
664 Some(Fallback { locales, entries })
665}
666
667fn check_locale(b: &[u8], s: Span) -> Option<[Option<Span>; 2]> {
670 let mut c = Cur::new(s.of(b), 0);
671 let n = c.varint()?;
672 let mut plural = [None, None];
673 let mut prev: Option<u32> = None;
674 for _ in 0..n {
675 let key = c.varint()?;
676 if prev.is_some_and(|p| key <= p) {
677 return None;
678 }
679 prev = Some(key);
680 let len = c.len()?;
681 let at = c.pos();
682 let payload = c.take(len)?;
683 let span = Span {
684 off: s.off.checked_add(at)?,
685 len,
686 };
687 match key {
688 locale_key::PLURAL_CARDINAL | locale_key::PLURAL_ORDINAL => {
689 if !plural::valid(payload) {
690 return None;
691 }
692 if let Some(slot) = plural.get_mut(key as usize - 1) {
693 *slot = Some(span);
694 }
695 }
696 _ => {}
697 }
698 }
699 c.at_end().then_some(plural)
700}
701
702fn check_ids(ids: &[u8], count: usize) -> Option<()> {
705 let blocks = count.div_ceil(IDS_RESTART);
706 let table_len = blocks.checked_mul(4)?;
707 let table = ids.get(..table_len)?;
708 let mut c = Cur::new(ids.get(table_len..)?, 0);
709 let mut prev_len = 0usize;
710 for i in 0..count {
711 let at = c.pos();
712 let shared = c.len()?;
713 let len = c.len()?;
714 if i % IDS_RESTART == 0 {
715 if shared != 0 || u32_at(table, (i / IDS_RESTART) * 4)? as usize != at {
716 return None;
717 }
718 } else if shared > prev_len {
719 return None;
720 }
721 c.skip(len)?;
722 prev_len = shared.checked_add(len)?;
723 }
724 c.at_end().then_some(())
725}