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, Debug)]
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: Bytes,
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#[cfg(not(feature = "static-bytes"))]
123type Bytes = Vec<u8>;
124
125#[cfg(feature = "static-bytes")]
129enum Bytes {
130 Owned(Vec<u8>),
131 Static(&'static [u8]),
132}
133
134#[cfg(feature = "static-bytes")]
135impl Bytes {
136 fn as_slice(&self) -> &[u8] {
137 match self {
138 Bytes::Owned(v) => v,
139 Bytes::Static(b) => b,
140 }
141 }
142}
143
144#[cfg(feature = "static-bytes")]
145impl core::ops::Deref for Bytes {
146 type Target = [u8];
147
148 fn deref(&self) -> &[u8] {
149 self.as_slice()
150 }
151}
152
153#[derive(Clone, Copy)]
155struct Fallback {
156 locales: Span,
157 entries: Span,
158}
159
160impl core::fmt::Debug for Catalog {
161 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
162 f.debug_struct("Catalog")
163 .field("locale", &self.locale())
164 .field("messages", &self.count)
165 .field("bytes", &self.bytes.len())
166 .finish_non_exhaustive()
167 }
168}
169
170#[derive(Default)]
172struct Found {
173 index: Option<Span>,
174 messages: Option<Span>,
175 cold: Option<Span>,
176 names: Option<Span>,
177 fallback: Option<Span>,
178 locale: Option<Span>,
179 funcs: Option<Span>,
180 ids: Option<Span>,
181 strings: Option<Span>,
182}
183
184impl Found {
185 fn slot(&mut self, kind: u16) -> Option<&mut Option<Span>> {
186 Some(match kind {
187 section::INDEX => &mut self.index,
188 section::MESSAGES => &mut self.messages,
189 section::COLD => &mut self.cold,
190 section::NAMES => &mut self.names,
191 section::FALLBACK => &mut self.fallback,
192 section::LOCALE => &mut self.locale,
193 section::FUNCS => &mut self.funcs,
194 section::IDS => &mut self.ids,
195 section::STRINGS => &mut self.strings,
196 _ => return None,
197 })
198 }
199}
200
201impl Catalog {
202 pub fn new(bytes: Vec<u8>, expect_manifest: u64) -> Result<Catalog, CatalogError> {
208 #[cfg(feature = "static-bytes")]
209 let bytes = Bytes::Owned(bytes);
210 Catalog::validate(bytes, expect_manifest)
211 }
212
213 #[cfg(feature = "static-bytes")]
217 pub fn from_static(
218 bytes: &'static [u8],
219 expect_manifest: u64,
220 ) -> Result<Catalog, CatalogError> {
221 Catalog::validate(Bytes::Static(bytes), expect_manifest)
222 }
223
224 #[inline(always)]
227 #[allow(clippy::inline_always)]
228 fn validate(bytes: Bytes, expect_manifest: u64) -> Result<Catalog, CatalogError> {
229 let b = bytes.as_slice();
230 if b.get(..4) != Some(&MAGIC[..]) {
231 return Err(CatalogError::Magic);
232 }
233 let version = u16_at(b, header::VERSION).ok_or(CatalogError::Truncated)?;
234 if version >> 8 != VERSION_MAJOR {
235 return Err(CatalogError::Version);
236 }
237 if b.len() < HEADER_LEN {
238 return Err(CatalogError::Truncated);
239 }
240 let hash = u64_at(b, header::MANIFEST_HASH).ok_or(CatalogError::Truncated)?;
241 if hash != expect_manifest {
242 return Err(CatalogError::ManifestMismatch);
243 }
244 let get16 = |at| u16_at(b, at).ok_or(CatalogError::Truncated);
245 let get32 = |at| u32_at(b, at).ok_or(CatalogError::Truncated);
246 let flags = get16(header::FLAGS)?;
247 let count = get32(header::MESSAGE_COUNT)?;
248 let locale = get32(header::LOCALE)?;
249 let cldr = get32(header::CLDR_VERSION)?;
250 let chunk = *b.get(header::CHUNK).ok_or(CatalogError::Truncated)?;
251 let dir = match b.get(header::DIR) {
252 Some(0) => Dir::Ltr,
253 Some(1) => Dir::Rtl,
254 _ => return Err(CatalogError::Header),
255 };
256 if count > MAX_MESSAGES {
257 return Err(CatalogError::Header);
258 }
259 let found = sections(b)?;
260 let need = |s: Option<Span>| s.ok_or(CatalogError::MissingSection);
261 let index = need(found.index)?;
262 let messages = need(found.messages)?;
263 let names = need(found.names)?;
264 let locale_sec = need(found.locale)?;
265 let funcs = need(found.funcs)?;
266 let strings = need(found.strings)?;
267 let pool = strings.of(b);
268 if pool.last() != Some(&0) {
269 return Err(CatalogError::Strings);
270 }
271 let in_pool = |r: u32| (r as usize) < pool.len();
272 if !in_pool(locale) {
273 return Err(CatalogError::Header);
274 }
275 check_index(index.of(b), count as usize, messages.len, pool.len())?;
276 check_names(names.of(b), pool.len()).ok_or(CatalogError::Names)?;
277 check_funcs(funcs.of(b), pool.len()).ok_or(CatalogError::Funcs)?;
278 let fallback = match found.fallback {
279 Some(s) => Some(check_fallback(b, s, count, pool.len()).ok_or(CatalogError::Fallback)?),
280 None => None,
281 };
282 let plural = check_locale(b, locale_sec).ok_or(CatalogError::Locale)?;
283 if let Some(ids) = found.ids {
284 check_ids(ids.of(b), count as usize).ok_or(CatalogError::Ids)?;
285 }
286 Ok(Catalog {
287 version,
288 flags,
289 hash,
290 count,
291 locale,
292 cldr,
293 chunk,
294 dir,
295 index,
296 messages,
297 cold: found.cold,
298 names,
299 fallback,
300 locale_sec,
301 plural,
302 funcs,
303 ids: found.ids,
304 strings,
305 bytes,
306 })
307 }
308
309 #[doc(hidden)]
311 pub fn format_version(&self) -> u16 {
312 self.version
313 }
314
315 pub fn manifest_hash(&self) -> u64 {
317 self.hash
318 }
319
320 pub fn locale(&self) -> &str {
322 self.text(StrRef(self.locale)).unwrap_or("")
323 }
324
325 pub fn dir(&self) -> Dir {
327 self.dir
328 }
329
330 #[doc(hidden)]
332 pub fn chunk(&self) -> u8 {
333 self.chunk
334 }
335
336 pub fn cldr_version(&self) -> Option<CldrVersion> {
338 CldrVersion::from_u32(self.cldr)
339 }
340
341 #[doc(hidden)]
343 pub fn cold_stripped(&self) -> bool {
344 self.flags & flags::COLD_STRIPPED != 0
345 }
346
347 #[doc(hidden)]
349 pub fn ids_stripped(&self) -> bool {
350 self.flags & flags::IDS_STRIPPED != 0
351 }
352
353 pub fn message_count(&self) -> u32 {
355 self.count
356 }
357
358 pub fn as_bytes(&self) -> &[u8] {
360 &self.bytes
361 }
362
363 pub fn into_bytes(self) -> Vec<u8> {
365 #[cfg(not(feature = "static-bytes"))]
366 return self.bytes;
367 #[cfg(feature = "static-bytes")]
368 match self.bytes {
369 Bytes::Owned(v) => v,
370 Bytes::Static(b) => b.to_vec(),
371 }
372 }
373
374 #[doc(hidden)]
377 pub fn sections(&self) -> impl Iterator<Item = (u16, u32, u32)> + '_ {
378 let n = u16_at(&self.bytes, header::SECTION_COUNT).unwrap_or(0);
379 (0..usize::from(n)).filter_map(move |i| {
380 let at = HEADER_LEN.checked_add(i.checked_mul(SECTION_ENTRY_LEN)?)?;
381 Some((
382 u16_at(&self.bytes, at)?,
383 u32_at(&self.bytes, at.checked_add(2)?)?,
384 u32_at(&self.bytes, at.checked_add(6)?)?,
385 ))
386 })
387 }
388
389 #[inline]
392 #[doc(hidden)]
393 pub fn get(&self, id: MsgId) -> Entry<'_> {
394 if id.chunk() != self.chunk || id.index() >= self.count {
395 return Entry::Absent;
396 }
397 let Some(e) = plane_entry(
398 self.index.of(&self.bytes),
399 self.count as usize,
400 id.index() as usize,
401 ) else {
402 return Entry::Absent;
403 };
404 let off = e & kind::OFFSET_MASK;
405 match e >> kind::SHIFT {
406 kind::SIMPLE => Entry::Simple(StrRef(off)),
407 kind::PATTERN => Entry::Pattern(MsgView::new(self, off as usize, false)),
408 kind::SELECT => Entry::Select(MsgView::new(self, off as usize, true)),
409 _ => Entry::Absent,
410 }
411 }
412
413 #[inline]
417 #[doc(hidden)]
418 pub fn text(&self, r: StrRef) -> Option<&str> {
419 let rest = self.strings.of(&self.bytes).get(r.0 as usize..)?;
420 let end = nul_pos(rest)?;
421 core::str::from_utf8(rest.get(..end)?).ok()
422 }
423
424 #[doc(hidden)]
427 pub fn fallback_locale(&self, id: MsgId) -> Option<&str> {
428 let fb = self.fallback?;
429 if id.chunk() != self.chunk {
430 return None;
431 }
432 let entries = fb.entries.of(&self.bytes);
433 let target = id.index();
434 let (mut lo, mut hi) = (0usize, entries.len() / 4);
435 while lo < hi {
436 let mid = lo + (hi - lo) / 2;
437 let e = u32_at(entries, mid.checked_mul(4)?)?;
438 match (e & 0x00ff_ffff).cmp(&target) {
439 core::cmp::Ordering::Less => lo = mid + 1,
440 core::cmp::Ordering::Greater => hi = mid,
441 core::cmp::Ordering::Equal => {
442 let loc = u32_at(fb.locales.of(&self.bytes), ((e >> 24) as usize) * 4)?;
443 return self.text(StrRef(loc));
444 }
445 }
446 }
447 None
448 }
449
450 #[doc(hidden)]
452 pub fn function(&self, index: u32) -> Option<&str> {
453 let at = (index as usize).checked_mul(4)?;
454 self.text(StrRef(u32_at(self.funcs.of(&self.bytes), at)?))
455 }
456
457 #[doc(hidden)]
459 pub fn function_count(&self) -> u32 {
460 u32::try_from(self.funcs.len / 4).unwrap_or(u32::MAX)
461 }
462
463 #[doc(hidden)]
465 pub fn locale_entry(&self, key: u32) -> Option<&[u8]> {
466 match key {
467 locale_key::PLURAL_CARDINAL => return self.plural[0].map(|s| s.of(&self.bytes)),
468 locale_key::PLURAL_ORDINAL => return self.plural[1].map(|s| s.of(&self.bytes)),
469 _ => {}
470 }
471 let mut c = Cur::new(self.locale_sec.of(&self.bytes), 0);
472 let n = c.varint()?;
473 for _ in 0..n {
474 let k = c.varint()?;
475 let len = c.len()?;
476 let payload = c.take(len)?;
477 if k == key {
478 return Some(payload);
479 }
480 if k > key {
481 return None;
482 }
483 }
484 None
485 }
486
487 #[doc(hidden)]
490 pub fn names(&self, id: MsgId) -> Names<'_> {
491 match self.get(id) {
492 Entry::Pattern(m) | Entry::Select(m) => m.names(),
493 Entry::Simple(_) | Entry::Absent => Names::EMPTY,
494 }
495 }
496
497 #[cfg(feature = "decode")]
504 #[doc(hidden)]
505 pub fn id_of(&self, id: MsgId) -> Option<alloc::string::String> {
506 let index = id.index() as usize;
507 let count = self.count as usize;
508 if index >= count {
509 return None;
510 }
511 let ids = self.ids?.of(&self.bytes);
512 let blocks = count.div_ceil(IDS_RESTART);
513 let table_len = blocks.checked_mul(4)?;
514 let entries = ids.get(table_len..)?;
515 let block = index / IDS_RESTART;
516 let at = u32_at(ids, block.checked_mul(4)?)? as usize;
517 let mut c = Cur::new(entries, at);
518 let mut current: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
521 for i in (block * IDS_RESTART)..=index {
522 let shared = c.len()?;
523 let len = c.len()?;
524 let suffix = c.take(len)?;
525 if shared > current.len() {
526 return None;
527 }
528 current.truncate(shared);
529 current.extend_from_slice(suffix);
530 if i == index {
531 return alloc::string::String::from_utf8(current).ok();
532 }
533 }
534 None
535 }
536
537 pub fn lookup(&self, id: &str) -> Option<MsgId> {
540 let ids = self.ids?.of(&self.bytes);
541 let count = self.count as usize;
542 let blocks = count.div_ceil(IDS_RESTART);
543 let table_len = blocks.checked_mul(4)?;
544 let entries = ids.get(table_len..)?;
545 let key = id.as_bytes();
546 let (mut lo, mut hi) = (0usize, blocks);
548 while lo < hi {
549 let mid = lo + (hi - lo) / 2;
550 let at = u32_at(ids, mid.checked_mul(4)?)? as usize;
551 let mut c = Cur::new(entries, at);
552 let _shared = c.varint()?;
553 let len = c.len()?;
554 if c.take(len)? <= key {
555 lo = mid + 1;
556 } else {
557 hi = mid;
558 }
559 }
560 let block = lo.checked_sub(1)?;
561 let at = u32_at(ids, block.checked_mul(4)?)? as usize;
562 let mut c = Cur::new(entries, at);
563 let mut matched = 0usize;
567 let first = block.checked_mul(IDS_RESTART)?;
568 for i in first..count.min(first.checked_add(IDS_RESTART)?) {
569 let shared = c.len()?;
570 let len = c.len()?;
571 let suffix = c.take(len)?;
572 if shared > matched {
573 continue;
576 }
577 let rest = key.get(shared..)?;
582 let common = rest.iter().zip(suffix).take_while(|(a, b)| a == b).count();
583 if common == rest.len() && common == suffix.len() {
584 return MsgId::new(self.chunk, u32::try_from(i).ok()?);
585 }
586 matched = shared.checked_add(common)?;
587 }
588 None
589 }
590}
591
592fn sections(b: &[u8]) -> Result<Found, CatalogError> {
594 let n = usize::from(u16_at(b, header::SECTION_COUNT).ok_or(CatalogError::Truncated)?);
595 let table_end = n
596 .checked_mul(SECTION_ENTRY_LEN)
597 .and_then(|t| t.checked_add(HEADER_LEN))
598 .ok_or(CatalogError::Truncated)?;
599 if table_end > b.len() {
600 return Err(CatalogError::Truncated);
601 }
602 let mut found = Found::default();
603 let mut prev_end = table_end;
604 let mut last = None;
605 for i in 0..n {
606 let at = HEADER_LEN + i * SECTION_ENTRY_LEN;
607 let (Some(kind), Some(off), Some(len)) =
608 (u16_at(b, at), u32_at(b, at + 2), u32_at(b, at + 6))
609 else {
610 return Err(CatalogError::Truncated);
611 };
612 let (off, len) = (off as usize, len as usize);
613 let end = off.checked_add(len).ok_or(CatalogError::SectionTable)?;
614 if off < prev_end || end > b.len() || found.strings.is_some() {
615 return Err(CatalogError::SectionTable);
616 }
617 prev_end = end;
618 last = Some(end);
619 if let Some(slot) = found.slot(kind) {
620 if slot.is_some() {
621 return Err(CatalogError::SectionTable);
622 }
623 *slot = Some(Span { off, len });
624 }
625 }
626 if found.strings.is_some() && last != Some(b.len()) {
627 return Err(CatalogError::SectionTable);
628 }
629 Ok(found)
630}
631
632fn check_index(
634 index: &[u8],
635 count: usize,
636 messages_len: usize,
637 pool_len: usize,
638) -> Result<(), CatalogError> {
639 if Some(index.len()) != count.checked_mul(4) {
640 return Err(CatalogError::Index);
641 }
642 let mut prev: Option<u32> = None;
643 for i in 0..count {
644 let e = plane_entry(index, count, i).ok_or(CatalogError::Index)?;
645 let off = e & kind::OFFSET_MASK;
646 match e >> kind::SHIFT {
647 kind::SIMPLE => {
648 if off as usize >= pool_len {
649 return Err(CatalogError::Index);
650 }
651 }
652 kind::PATTERN | kind::SELECT => {
653 if off as usize >= messages_len || prev.is_some_and(|p| off <= p) {
654 return Err(CatalogError::Index);
655 }
656 prev = Some(off);
657 }
658 _ => {}
659 }
660 }
661 Ok(())
662}
663
664fn check_names(names: &[u8], pool_len: usize) -> Option<()> {
666 let mut c = Cur::new(names, 0);
667 while !c.at_end() {
668 let n = c.len()?.checked_add(c.len()?)?;
669 if n > c.remaining() / 4 {
670 return None;
671 }
672 for _ in 0..n {
673 if c.u32()? as usize >= pool_len {
674 return None;
675 }
676 }
677 }
678 Some(())
679}
680
681fn check_funcs(funcs: &[u8], pool_len: usize) -> Option<()> {
683 if !funcs.len().is_multiple_of(4) {
684 return None;
685 }
686 let mut c = Cur::new(funcs, 0);
687 while !c.at_end() {
688 if c.u32()? as usize >= pool_len {
689 return None;
690 }
691 }
692 Some(())
693}
694
695fn check_fallback(buf: &[u8], sec: Span, count: u32, pool_len: usize) -> Option<Fallback> {
697 let mut c = Cur::new(sec.of(buf), 0);
698 let n_locales = c.len()?;
699 if n_locales > MAX_FALLBACK_LOCALES || n_locales > c.remaining() / 4 {
700 return None;
701 }
702 let locales = Span {
703 off: sec.off.checked_add(c.pos())?,
704 len: n_locales * 4,
705 };
706 for _ in 0..n_locales {
707 if c.u32()? as usize >= pool_len {
708 return None;
709 }
710 }
711 let entries = Span {
712 off: sec.off.checked_add(c.pos())?,
713 len: c.remaining(),
714 };
715 if !entries.len.is_multiple_of(4) {
716 return None;
717 }
718 let mut prev: Option<u32> = None;
719 while !c.at_end() {
720 let e = c.u32()?;
721 let (msg, loc) = (e & 0x00ff_ffff, (e >> 24) as usize);
722 if msg >= count || loc >= n_locales || prev.is_some_and(|p| msg <= p) {
723 return None;
724 }
725 prev = Some(msg);
726 }
727 Some(Fallback { locales, entries })
728}
729
730fn check_locale(b: &[u8], s: Span) -> Option<[Option<Span>; 2]> {
733 let mut c = Cur::new(s.of(b), 0);
734 let n = c.varint()?;
735 let mut plural = [None, None];
736 let mut prev: Option<u32> = None;
737 for _ in 0..n {
738 let key = c.varint()?;
739 if prev.is_some_and(|p| key <= p) {
740 return None;
741 }
742 prev = Some(key);
743 let len = c.len()?;
744 let at = c.pos();
745 let payload = c.take(len)?;
746 let span = Span {
747 off: s.off.checked_add(at)?,
748 len,
749 };
750 match key {
751 locale_key::PLURAL_CARDINAL | locale_key::PLURAL_ORDINAL => {
752 if !plural::valid(payload) {
753 return None;
754 }
755 if let Some(slot) = plural.get_mut(key as usize - 1) {
756 *slot = Some(span);
757 }
758 }
759 _ => {}
760 }
761 }
762 c.at_end().then_some(plural)
763}
764
765fn check_ids(ids: &[u8], count: usize) -> Option<()> {
768 let blocks = count.div_ceil(IDS_RESTART);
769 let table_len = blocks.checked_mul(4)?;
770 let table = ids.get(..table_len)?;
771 let mut c = Cur::new(ids.get(table_len..)?, 0);
772 let mut prev_len = 0usize;
773 for i in 0..count {
774 let at = c.pos();
775 let shared = c.len()?;
776 let len = c.len()?;
777 if i % IDS_RESTART == 0 {
778 if shared != 0 || u32_at(table, (i / IDS_RESTART) * 4)? as usize != at {
779 return None;
780 }
781 } else if shared > prev_len {
782 return None;
783 }
784 c.skip(len)?;
785 prev_len = shared.checked_add(len)?;
786 }
787 c.at_end().then_some(())
788}