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::load::Load;
18use crate::nfc_map::NfcMap;
19use crate::plural;
20use crate::view::{MsgView, Names};
21
22#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
27pub struct StrRef(pub(crate) u32);
28
29#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
31pub struct CldrVersion {
32 pub major: u16,
34 pub minor: u8,
36 pub patch: u8,
38}
39
40impl CldrVersion {
41 #[doc(hidden)]
43 pub const fn to_u32(self) -> u32 {
44 (self.major as u32) << 16 | (self.minor as u32) << 8 | self.patch as u32
45 }
46
47 #[allow(clippy::cast_possible_truncation)] #[doc(hidden)]
50 pub const fn from_u32(v: u32) -> Option<Self> {
51 if v == 0 {
52 return None;
53 }
54 Some(CldrVersion {
55 major: (v >> 16) as u16,
56 minor: (v >> 8) as u8,
57 patch: v as u8,
58 })
59 }
60}
61
62#[derive(Clone, Copy, Debug)]
64pub enum Entry<'a> {
65 Simple(StrRef),
68 Pattern(MsgView<'a>),
70 Select(MsgView<'a>),
72 Absent,
74}
75
76#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
78pub(crate) struct Span {
79 pub(crate) off: usize,
80 pub(crate) len: usize,
81}
82
83impl Span {
84 #[inline]
85 pub(crate) fn of<'a>(&self, b: &'a [u8]) -> &'a [u8] {
86 match self.off.checked_add(self.len) {
87 Some(end) => b.get(self.off..end).unwrap_or(&[]),
88 None => &[],
89 }
90 }
91}
92
93pub struct Catalog {
96 bytes: Bytes,
97 version: u16,
98 flags: u16,
99 hash: u64,
100 count: u32,
101 locale: u32,
102 cldr: u32,
103 chunk: u8,
104 dir: Dir,
105 pub(crate) index: Span,
106 pub(crate) messages: Span,
107 #[cfg_attr(not(feature = "decode"), allow(dead_code))]
109 pub(crate) cold: Option<Span>,
110 pub(crate) names: Span,
111 fallback: Option<Fallback>,
112 locale_sec: Span,
113 plural: [Option<Span>; 2],
114 funcs: Span,
115 ids: Option<Span>,
116 nfc: Option<Span>,
117 pub(crate) strings: Span,
118 #[cfg(feature = "server-data")]
121 server: Option<ServerData>,
122 #[allow(dead_code)]
126 load: Load,
127}
128
129#[cfg(feature = "server-data")]
133struct ServerData {
134 bytes: &'static [u8],
135 plural: [Option<Span>; 2],
136}
137
138#[cfg(feature = "server-data")]
139impl ServerData {
140 fn entry(&self, key: u32) -> Option<&'static [u8]> {
141 let bytes = self.bytes;
142 match key {
143 locale_key::PLURAL_CARDINAL => self.plural[0].map(|s| s.of(bytes)),
144 locale_key::PLURAL_ORDINAL => self.plural[1].map(|s| s.of(bytes)),
145 _ => find_entry(bytes, key),
146 }
147 }
148}
149
150#[cfg(not(feature = "static-bytes"))]
156type Bytes = Vec<u8>;
157
158#[cfg(feature = "static-bytes")]
162enum Bytes {
163 Owned(Vec<u8>),
164 Static(&'static [u8]),
165}
166
167#[cfg(feature = "static-bytes")]
168impl Bytes {
169 fn as_slice(&self) -> &[u8] {
170 match self {
171 Bytes::Owned(v) => v,
172 Bytes::Static(b) => b,
173 }
174 }
175}
176
177#[cfg(feature = "static-bytes")]
178impl core::ops::Deref for Bytes {
179 type Target = [u8];
180
181 fn deref(&self) -> &[u8] {
182 self.as_slice()
183 }
184}
185
186#[derive(Clone, Copy)]
188struct Fallback {
189 locales: Span,
190 entries: Span,
191}
192
193impl core::fmt::Debug for Catalog {
194 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
195 f.debug_struct("Catalog")
196 .field("locale", &self.locale())
197 .field("messages", &self.count)
198 .field("bytes", &self.bytes.len())
199 .finish_non_exhaustive()
200 }
201}
202
203#[derive(Default)]
205struct Found {
206 index: Option<Span>,
207 messages: Option<Span>,
208 cold: Option<Span>,
209 names: Option<Span>,
210 fallback: Option<Span>,
211 locale: Option<Span>,
212 funcs: Option<Span>,
213 ids: Option<Span>,
214 nfc: Option<Span>,
215 strings: Option<Span>,
216}
217
218impl Found {
219 fn slot(&mut self, kind: u16) -> Option<&mut Option<Span>> {
220 Some(match kind {
221 section::INDEX => &mut self.index,
222 section::MESSAGES => &mut self.messages,
223 section::COLD => &mut self.cold,
224 section::NAMES => &mut self.names,
225 section::FALLBACK => &mut self.fallback,
226 section::LOCALE => &mut self.locale,
227 section::FUNCS => &mut self.funcs,
228 section::IDS => &mut self.ids,
229 section::NFC => &mut self.nfc,
230 section::STRINGS => &mut self.strings,
231 _ => return None,
232 })
233 }
234}
235
236impl Catalog {
237 pub fn new(bytes: Vec<u8>, expect_manifest: u64) -> Result<Catalog, CatalogError> {
243 #[cfg(feature = "static-bytes")]
244 let bytes = Bytes::Owned(bytes);
245 Catalog::validate(bytes, expect_manifest)
246 }
247
248 #[cfg(feature = "static-bytes")]
252 pub fn from_static(
253 bytes: &'static [u8],
254 expect_manifest: u64,
255 ) -> Result<Catalog, CatalogError> {
256 Catalog::validate(Bytes::Static(bytes), expect_manifest)
257 }
258
259 #[inline(always)]
262 #[allow(clippy::inline_always)]
263 fn validate(bytes: Bytes, expect_manifest: u64) -> Result<Catalog, CatalogError> {
264 let b = bytes.as_slice();
265 if b.get(..4) != Some(&MAGIC[..]) {
266 return Err(CatalogError::Magic);
267 }
268 let version = u16_at(b, header::VERSION).ok_or(CatalogError::Truncated)?;
269 if version >> 8 != VERSION_MAJOR {
270 return Err(CatalogError::Version);
271 }
272 if b.len() < HEADER_LEN {
273 return Err(CatalogError::Truncated);
274 }
275 let hash = u64_at(b, header::MANIFEST_HASH).ok_or(CatalogError::Truncated)?;
276 if hash != expect_manifest {
277 return Err(CatalogError::ManifestMismatch);
278 }
279 let get16 = |at| u16_at(b, at).ok_or(CatalogError::Truncated);
280 let get32 = |at| u32_at(b, at).ok_or(CatalogError::Truncated);
281 let flags = get16(header::FLAGS)?;
282 let count = get32(header::MESSAGE_COUNT)?;
283 let locale = get32(header::LOCALE)?;
284 let cldr = get32(header::CLDR_VERSION)?;
285 let chunk = *b.get(header::CHUNK).ok_or(CatalogError::Truncated)?;
286 let dir = match b.get(header::DIR) {
287 Some(0) => Dir::Ltr,
288 Some(1) => Dir::Rtl,
289 _ => return Err(CatalogError::Header),
290 };
291 if count > MAX_MESSAGES {
292 return Err(CatalogError::Header);
293 }
294 let found = sections(b)?;
295 let need = |s: Option<Span>| s.ok_or(CatalogError::MissingSection);
296 let index = need(found.index)?;
297 let messages = need(found.messages)?;
298 let names = need(found.names)?;
299 let locale_sec = need(found.locale)?;
300 let funcs = need(found.funcs)?;
301 let strings = need(found.strings)?;
302 let pool = strings.of(b);
303 if pool.last() != Some(&0) {
304 return Err(CatalogError::Strings);
305 }
306 let in_pool = |r: u32| (r as usize) < pool.len();
307 if !in_pool(locale) {
308 return Err(CatalogError::Header);
309 }
310 check_index(index.of(b), count as usize, messages.len, pool.len())?;
311 check_names(names.of(b), pool.len()).ok_or(CatalogError::Names)?;
312 check_funcs(funcs.of(b), pool.len()).ok_or(CatalogError::Funcs)?;
313 let fallback = match found.fallback {
314 Some(s) => Some(check_fallback(b, s, count, pool.len()).ok_or(CatalogError::Fallback)?),
315 None => None,
316 };
317 let plural = check_locale(b, locale_sec).ok_or(CatalogError::Locale)?;
318 if let Some(ids) = found.ids {
319 check_ids(ids.of(b), count as usize).ok_or(CatalogError::Ids)?;
320 }
321 if let Some(nfc) = found.nfc {
322 NfcMap::from_bytes(nfc.of(b)).ok_or(CatalogError::Nfc)?;
323 }
324 Ok(Catalog {
325 version,
326 flags,
327 hash,
328 count,
329 locale,
330 cldr,
331 chunk,
332 dir,
333 index,
334 messages,
335 cold: found.cold,
336 names,
337 fallback,
338 locale_sec,
339 plural,
340 funcs,
341 ids: found.ids,
342 nfc: found.nfc,
343 strings,
344 bytes,
345 #[cfg(feature = "server-data")]
346 server: None,
347 load: Load::next(),
348 })
349 }
350
351 #[cfg(feature = "server-data")]
357 pub fn with_server_data(mut self, table: &'static [u8]) -> Result<Catalog, CatalogError> {
358 if table.is_empty() {
359 return Ok(self);
360 }
361 let all = Span {
362 off: 0,
363 len: table.len(),
364 };
365 let plural = check_locale(table, all).ok_or(CatalogError::Locale)?;
366 self.server = Some(ServerData {
367 bytes: table,
368 plural,
369 });
370 self.load = Load::next();
372 Ok(self)
373 }
374
375 #[cfg(any(
381 all(
382 feature = "std-load-id",
383 not(all(target_arch = "wasm32", target_os = "unknown"))
384 ),
385 all(feature = "web-load-id", target_arch = "wasm32", target_os = "unknown")
386 ))]
387 #[doc(hidden)]
388 pub fn load_id(&self) -> u64 {
389 self.load.id()
390 }
391
392 #[doc(hidden)]
394 pub fn format_version(&self) -> u16 {
395 self.version
396 }
397
398 pub fn manifest_hash(&self) -> u64 {
400 self.hash
401 }
402
403 pub fn locale(&self) -> &str {
405 self.text(StrRef(self.locale)).unwrap_or("")
406 }
407
408 pub fn dir(&self) -> Dir {
410 self.dir
411 }
412
413 #[doc(hidden)]
415 pub fn chunk(&self) -> u8 {
416 self.chunk
417 }
418
419 pub fn cldr_version(&self) -> Option<CldrVersion> {
421 CldrVersion::from_u32(self.cldr)
422 }
423
424 #[doc(hidden)]
426 pub fn cold_stripped(&self) -> bool {
427 self.flags & flags::COLD_STRIPPED != 0
428 }
429
430 #[doc(hidden)]
432 pub fn ids_stripped(&self) -> bool {
433 self.flags & flags::IDS_STRIPPED != 0
434 }
435
436 pub fn message_count(&self) -> u32 {
438 self.count
439 }
440
441 pub fn as_bytes(&self) -> &[u8] {
443 &self.bytes
444 }
445
446 pub fn into_bytes(self) -> Vec<u8> {
448 #[cfg(not(feature = "static-bytes"))]
449 return self.bytes;
450 #[cfg(feature = "static-bytes")]
451 match self.bytes {
452 Bytes::Owned(v) => v,
453 Bytes::Static(b) => b.to_vec(),
454 }
455 }
456
457 #[doc(hidden)]
460 pub fn sections(&self) -> impl Iterator<Item = (u16, u32, u32)> + '_ {
461 let n = u16_at(&self.bytes, header::SECTION_COUNT).unwrap_or(0);
462 (0..usize::from(n)).filter_map(move |i| {
463 let at = HEADER_LEN.checked_add(i.checked_mul(SECTION_ENTRY_LEN)?)?;
464 Some((
465 u16_at(&self.bytes, at)?,
466 u32_at(&self.bytes, at.checked_add(2)?)?,
467 u32_at(&self.bytes, at.checked_add(6)?)?,
468 ))
469 })
470 }
471
472 #[inline]
475 #[doc(hidden)]
476 pub fn get(&self, id: MsgId) -> Entry<'_> {
477 if id.chunk() != self.chunk || id.index() >= self.count {
478 return Entry::Absent;
479 }
480 let Some(e) = plane_entry(
481 self.index.of(&self.bytes),
482 self.count as usize,
483 id.index() as usize,
484 ) else {
485 return Entry::Absent;
486 };
487 let off = e & kind::OFFSET_MASK;
488 match e >> kind::SHIFT {
489 kind::SIMPLE => Entry::Simple(StrRef(off)),
490 kind::PATTERN => Entry::Pattern(MsgView::new(self, off as usize, false)),
491 kind::SELECT => Entry::Select(MsgView::new(self, off as usize, true)),
492 _ => Entry::Absent,
493 }
494 }
495
496 #[inline]
500 #[doc(hidden)]
501 pub fn text(&self, r: StrRef) -> Option<&str> {
502 let rest = self.strings.of(&self.bytes).get(r.0 as usize..)?;
503 let end = nul_pos(rest)?;
504 core::str::from_utf8(rest.get(..end)?).ok()
505 }
506
507 #[doc(hidden)]
510 pub fn fallback_locale(&self, id: MsgId) -> Option<&str> {
511 let fb = self.fallback?;
512 if id.chunk() != self.chunk {
513 return None;
514 }
515 let entries = fb.entries.of(&self.bytes);
516 let target = id.index();
517 let (mut lo, mut hi) = (0usize, entries.len() / 4);
518 while lo < hi {
519 let mid = lo + (hi - lo) / 2;
520 let e = u32_at(entries, mid.checked_mul(4)?)?;
521 match (e & 0x00ff_ffff).cmp(&target) {
522 core::cmp::Ordering::Less => lo = mid + 1,
523 core::cmp::Ordering::Greater => hi = mid,
524 core::cmp::Ordering::Equal => {
525 let loc = u32_at(fb.locales.of(&self.bytes), ((e >> 24) as usize) * 4)?;
526 return self.text(StrRef(loc));
527 }
528 }
529 }
530 None
531 }
532
533 #[doc(hidden)]
535 pub fn function(&self, index: u32) -> Option<&str> {
536 let at = (index as usize).checked_mul(4)?;
537 self.text(StrRef(u32_at(self.funcs.of(&self.bytes), at)?))
538 }
539
540 pub fn nfc_map(&self) -> NfcMap<'_> {
545 match self.nfc {
546 Some(s) => NfcMap::from_bytes(s.of(&self.bytes)).unwrap_or(NfcMap::EMPTY),
548 None => NfcMap::EMPTY,
549 }
550 }
551
552 #[doc(hidden)]
554 pub fn function_count(&self) -> u32 {
555 u32::try_from(self.funcs.len / 4).unwrap_or(u32::MAX)
556 }
557
558 pub fn names_function(&self, pick: impl Fn(&str) -> bool) -> bool {
564 (0..self.function_count()).any(|i| self.function(i).is_some_and(&pick))
565 }
566
567 pub fn calls(&self, id: MsgId, pick: impl Fn(&str) -> bool) -> bool {
575 match self.get(id) {
576 Entry::Pattern(m) | Entry::Select(m) => {
577 m.calls(&|index| self.function(index).is_some_and(&pick))
578 }
579 Entry::Simple(_) | Entry::Absent => false,
580 }
581 }
582
583 #[cfg(not(feature = "server-data"))]
585 #[doc(hidden)]
586 pub fn locale_entry(&self, key: u32) -> Option<&[u8]> {
587 self.own_locale_entry(key)
588 }
589
590 #[cfg(feature = "server-data")]
593 #[doc(hidden)]
594 pub fn locale_entry(&self, key: u32) -> Option<&[u8]> {
595 match self.own_locale_entry(key) {
596 Some(payload) => Some(payload),
597 None => self.server.as_ref()?.entry(key),
598 }
599 }
600
601 #[inline(always)]
604 #[allow(clippy::inline_always)]
605 fn own_locale_entry(&self, key: u32) -> Option<&[u8]> {
606 match key {
607 locale_key::PLURAL_CARDINAL => return self.plural[0].map(|s| s.of(&self.bytes)),
608 locale_key::PLURAL_ORDINAL => return self.plural[1].map(|s| s.of(&self.bytes)),
609 _ => {}
610 }
611 find_entry(self.locale_sec.of(&self.bytes), key)
612 }
613
614 #[doc(hidden)]
617 pub fn names(&self, id: MsgId) -> Names<'_> {
618 match self.get(id) {
619 Entry::Pattern(m) | Entry::Select(m) => m.names(),
620 Entry::Simple(_) | Entry::Absent => Names::EMPTY,
621 }
622 }
623
624 #[cfg(feature = "decode")]
631 #[doc(hidden)]
632 pub fn id_of(&self, id: MsgId) -> Option<alloc::string::String> {
633 let index = id.index() as usize;
634 let count = self.count as usize;
635 if index >= count {
636 return None;
637 }
638 let ids = self.ids?.of(&self.bytes);
639 let blocks = count.div_ceil(IDS_RESTART);
640 let table_len = blocks.checked_mul(4)?;
641 let entries = ids.get(table_len..)?;
642 let block = index / IDS_RESTART;
643 let at = u32_at(ids, block.checked_mul(4)?)? as usize;
644 let mut c = Cur::new(entries, at);
645 let mut current: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
648 for i in (block * IDS_RESTART)..=index {
649 let shared = c.len()?;
650 let len = c.len()?;
651 let suffix = c.take(len)?;
652 if shared > current.len() {
653 return None;
654 }
655 current.truncate(shared);
656 current.extend_from_slice(suffix);
657 if i == index {
658 return alloc::string::String::from_utf8(current).ok();
659 }
660 }
661 None
662 }
663
664 pub fn lookup(&self, id: &str) -> Option<MsgId> {
667 let ids = self.ids?.of(&self.bytes);
668 let count = self.count as usize;
669 let blocks = count.div_ceil(IDS_RESTART);
670 let table_len = blocks.checked_mul(4)?;
671 let entries = ids.get(table_len..)?;
672 let key = id.as_bytes();
673 let (mut lo, mut hi) = (0usize, blocks);
675 while lo < hi {
676 let mid = lo + (hi - lo) / 2;
677 let at = u32_at(ids, mid.checked_mul(4)?)? as usize;
678 let mut c = Cur::new(entries, at);
679 let _shared = c.varint()?;
680 let len = c.len()?;
681 if c.take(len)? <= key {
682 lo = mid + 1;
683 } else {
684 hi = mid;
685 }
686 }
687 let block = lo.checked_sub(1)?;
688 let at = u32_at(ids, block.checked_mul(4)?)? as usize;
689 let mut c = Cur::new(entries, at);
690 let mut matched = 0usize;
694 let first = block.checked_mul(IDS_RESTART)?;
695 for i in first..count.min(first.checked_add(IDS_RESTART)?) {
696 let shared = c.len()?;
697 let len = c.len()?;
698 let suffix = c.take(len)?;
699 if shared > matched {
700 continue;
703 }
704 let rest = key.get(shared..)?;
709 let common = rest.iter().zip(suffix).take_while(|(a, b)| a == b).count();
710 if common == rest.len() && common == suffix.len() {
711 return MsgId::new(self.chunk, u32::try_from(i).ok()?);
712 }
713 matched = shared.checked_add(common)?;
714 }
715 None
716 }
717}
718
719#[inline(always)]
722#[allow(clippy::inline_always)]
723fn find_entry(sec: &[u8], key: u32) -> Option<&[u8]> {
724 let mut c = Cur::new(sec, 0);
725 let n = c.varint()?;
726 for _ in 0..n {
727 let k = c.varint()?;
728 let len = c.len()?;
729 let payload = c.take(len)?;
730 if k == key {
731 return Some(payload);
732 }
733 if k > key {
734 return None;
735 }
736 }
737 None
738}
739
740fn sections(b: &[u8]) -> Result<Found, CatalogError> {
742 let n = usize::from(u16_at(b, header::SECTION_COUNT).ok_or(CatalogError::Truncated)?);
743 let table_end = n
744 .checked_mul(SECTION_ENTRY_LEN)
745 .and_then(|t| t.checked_add(HEADER_LEN))
746 .ok_or(CatalogError::Truncated)?;
747 if table_end > b.len() {
748 return Err(CatalogError::Truncated);
749 }
750 let mut found = Found::default();
751 let mut prev_end = table_end;
752 let mut last = None;
753 for i in 0..n {
754 let at = HEADER_LEN + i * SECTION_ENTRY_LEN;
755 let (Some(kind), Some(off), Some(len)) =
756 (u16_at(b, at), u32_at(b, at + 2), u32_at(b, at + 6))
757 else {
758 return Err(CatalogError::Truncated);
759 };
760 let (off, len) = (off as usize, len as usize);
761 let end = off.checked_add(len).ok_or(CatalogError::SectionTable)?;
762 if off < prev_end || end > b.len() || found.strings.is_some() {
763 return Err(CatalogError::SectionTable);
764 }
765 prev_end = end;
766 last = Some(end);
767 if let Some(slot) = found.slot(kind) {
768 if slot.is_some() {
769 return Err(CatalogError::SectionTable);
770 }
771 *slot = Some(Span { off, len });
772 }
773 }
774 if found.strings.is_some() && last != Some(b.len()) {
775 return Err(CatalogError::SectionTable);
776 }
777 Ok(found)
778}
779
780fn check_index(
782 index: &[u8],
783 count: usize,
784 messages_len: usize,
785 pool_len: usize,
786) -> Result<(), CatalogError> {
787 if Some(index.len()) != count.checked_mul(4) {
788 return Err(CatalogError::Index);
789 }
790 let mut prev: Option<u32> = None;
791 for i in 0..count {
792 let e = plane_entry(index, count, i).ok_or(CatalogError::Index)?;
793 let off = e & kind::OFFSET_MASK;
794 match e >> kind::SHIFT {
795 kind::SIMPLE => {
796 if off as usize >= pool_len {
797 return Err(CatalogError::Index);
798 }
799 }
800 kind::PATTERN | kind::SELECT => {
801 if off as usize >= messages_len || prev.is_some_and(|p| off <= p) {
802 return Err(CatalogError::Index);
803 }
804 prev = Some(off);
805 }
806 _ => {}
807 }
808 }
809 Ok(())
810}
811
812fn check_names(names: &[u8], pool_len: usize) -> Option<()> {
814 let mut c = Cur::new(names, 0);
815 while !c.at_end() {
816 let n = c.len()?.checked_add(c.len()?)?;
817 if n > c.remaining() / 4 {
818 return None;
819 }
820 for _ in 0..n {
821 if c.u32()? as usize >= pool_len {
822 return None;
823 }
824 }
825 }
826 Some(())
827}
828
829fn check_funcs(funcs: &[u8], pool_len: usize) -> Option<()> {
831 if !funcs.len().is_multiple_of(4) {
832 return None;
833 }
834 let mut c = Cur::new(funcs, 0);
835 while !c.at_end() {
836 if c.u32()? as usize >= pool_len {
837 return None;
838 }
839 }
840 Some(())
841}
842
843fn check_fallback(buf: &[u8], sec: Span, count: u32, pool_len: usize) -> Option<Fallback> {
845 let mut c = Cur::new(sec.of(buf), 0);
846 let n_locales = c.len()?;
847 if n_locales > MAX_FALLBACK_LOCALES || n_locales > c.remaining() / 4 {
848 return None;
849 }
850 let locales = Span {
851 off: sec.off.checked_add(c.pos())?,
852 len: n_locales * 4,
853 };
854 for _ in 0..n_locales {
855 if c.u32()? as usize >= pool_len {
856 return None;
857 }
858 }
859 let entries = Span {
860 off: sec.off.checked_add(c.pos())?,
861 len: c.remaining(),
862 };
863 if !entries.len.is_multiple_of(4) {
864 return None;
865 }
866 let mut prev: Option<u32> = None;
867 while !c.at_end() {
868 let e = c.u32()?;
869 let (msg, loc) = (e & 0x00ff_ffff, (e >> 24) as usize);
870 if msg >= count || loc >= n_locales || prev.is_some_and(|p| msg <= p) {
871 return None;
872 }
873 prev = Some(msg);
874 }
875 Some(Fallback { locales, entries })
876}
877
878fn check_locale(b: &[u8], s: Span) -> Option<[Option<Span>; 2]> {
881 let mut c = Cur::new(s.of(b), 0);
882 let n = c.varint()?;
883 let mut plural = [None, None];
884 let mut prev: Option<u32> = None;
885 for _ in 0..n {
886 let key = c.varint()?;
887 if prev.is_some_and(|p| key <= p) {
888 return None;
889 }
890 prev = Some(key);
891 let len = c.len()?;
892 let at = c.pos();
893 let payload = c.take(len)?;
894 let span = Span {
895 off: s.off.checked_add(at)?,
896 len,
897 };
898 match key {
899 locale_key::PLURAL_CARDINAL | locale_key::PLURAL_ORDINAL => {
900 if !plural::valid(payload) {
901 return None;
902 }
903 if let Some(slot) = plural.get_mut(key as usize - 1) {
904 *slot = Some(span);
905 }
906 }
907 _ => {}
908 }
909 }
910 c.at_end().then_some(plural)
911}
912
913fn check_ids(ids: &[u8], count: usize) -> Option<()> {
916 let blocks = count.div_ceil(IDS_RESTART);
917 let table_len = blocks.checked_mul(4)?;
918 let table = ids.get(..table_len)?;
919 let mut c = Cur::new(ids.get(table_len..)?, 0);
920 let mut prev_len = 0usize;
921 for i in 0..count {
922 let at = c.pos();
923 let shared = c.len()?;
924 let len = c.len()?;
925 if i % IDS_RESTART == 0 {
926 if shared != 0 || u32_at(table, (i / IDS_RESTART) * 4)? as usize != at {
927 return None;
928 }
929 } else if shared > prev_len {
930 return None;
931 }
932 c.skip(len)?;
933 prev_len = shared.checked_add(len)?;
934 }
935 c.at_end().then_some(())
936}