1use std::path::Path;
24
25use crate::{
26 common::{arena, numeric_ops::NumericOps, store::Stored},
27 definition::expandable::{Expandable, ExpandableOptions},
28 state::{self, Scope, TableName},
29 token::{Catcode, Token},
30 tokens::Tokens,
31};
32
33pub fn load_native_dump(path: &Path) -> Result<usize, String> {
36 let content = std::fs::read_to_string(path)
37 .map_err(|e| format!("Failed to read dump file {}: {}", path.display(), e))?;
38 let count = load_from_str_internal(&content, &path.display().to_string())?;
39 Ok(count)
40}
41
42pub fn load_from_str(content: &str) -> Result<usize, String> {
46 load_from_str_internal(content, "<embedded:latex>")
49}
50
51pub fn load_from_str_plain(content: &str) -> Result<usize, String> {
56 load_from_str_internal(content, "<embedded:plain>")
57}
58
59pub fn load_from_str_labeled(content: &str, source: &str) -> Result<usize, String> {
65 load_from_str_internal(content, source)
66}
67
68thread_local! {
74 static CURRENT_LOAD_CTX: std::cell::Cell<Option<(arena::SymStr, u32)>> =
75 const { std::cell::Cell::new(None) };
76 static DEFERRED_ALIASES: std::cell::RefCell<Vec<(Token, Token)>> =
80 const { std::cell::RefCell::new(Vec::new()) };
81}
82
83pub fn flush_deferred_aliases() -> (usize, usize) {
88 let pending: Vec<(Token, Token)> =
89 DEFERRED_ALIASES.with(|cell| std::mem::take(&mut *cell.borrow_mut()));
90 let mut applied = 0usize;
91 let mut skipped = 0usize;
92 for (cs_tok, target_tok) in pending {
93 if !state::has_meaning(&target_tok) {
98 skipped += 1;
99 continue;
100 }
101 match state::lookup_meaning(&target_tok) {
104 Some(meaning) => {
105 state::assign_internal(
106 TableName::Meaning,
107 cs_tok.get_cs_name(),
108 meaning,
109 Some(Scope::Global),
110 );
111 applied += 1;
112 },
113 _ => {
114 skipped += 1;
115 },
116 }
117 }
118 (applied, skipped)
119}
120
121fn current_dump_locator() -> crate::common::locator::Locator {
122 if let Some((source, lineno)) = CURRENT_LOAD_CTX.with(|c| c.get()) {
123 crate::common::locator::Locator {
124 source,
125 from_line: lineno,
126 to_line: lineno,
127 from_column: 1,
128 to_column: 1,
129 }
130 } else {
131 crate::common::locator::Locator::default()
132 }
133}
134
135fn load_from_str_internal(content: &str, source_name: &str) -> Result<usize, String> {
136 let mut count = 0;
137 let mut skipped = 0;
138 let mut errors = 0;
139 let source_sym = arena::pin(source_name);
140
141 for (lineno, line) in content.lines().enumerate() {
142 let line = line.trim_end_matches('\r');
147 if line.is_empty() || line.starts_with('#') {
148 continue;
149 }
150
151 CURRENT_LOAD_CTX.with(|c| c.set(Some((source_sym, (lineno + 1) as u32))));
152
153 match parse_and_load(line) {
154 Ok(true) => count += 1,
155 Ok(false) => skipped += 1,
156 Err(e) => {
157 errors += 1;
158 if errors <= 10 {
159 Warn!(
160 "dump_reader",
161 "line",
162 s!(
163 "Line {}: {}: {}",
164 lineno + 1,
165 e,
166 &line[..line.len().min(80)]
167 )
168 );
169 }
170 },
171 }
172 }
173
174 if errors > 10 {
175 Warn!(
176 "dump_reader",
177 "errors",
178 s!("... and {} more errors", errors - 10)
179 );
180 }
181 Info!(
182 "dump_reader",
183 "loaded",
184 s!(
185 "Loaded {} entries from {} ({} skipped, {} errors)",
186 count,
187 source_name,
188 skipped,
189 errors
190 )
191 );
192
193 Ok(count)
194}
195
196pub fn collect_meaning_keys(content: &str) -> rustc_hash::FxHashSet<Box<str>> {
210 let mut keys = rustc_hash::FxHashSet::default();
211 for line in content.lines() {
212 let Some(rest) = line.strip_prefix("M\t") else {
216 continue;
217 };
218 let raw_key = rest.split('\t').next().unwrap_or("");
219 if raw_key.is_empty() {
220 continue;
221 }
222 if raw_key.contains('%') {
223 keys.insert(url_decode(raw_key).into_boxed_str());
224 } else {
225 keys.insert(Box::from(raw_key));
226 }
227 }
228 keys
229}
230
231fn parse_and_load(line: &str) -> Result<bool, String> {
234 let mut it = line.splitn(3, '\t');
237 let table = it.next().ok_or("Too few fields")?;
238 let raw_key = match it.next() {
239 Some(k) => k,
240 None => return Err("Too few fields".into()),
241 };
242 let key_cow: std::borrow::Cow<'_, str> = if raw_key.contains('%') {
246 std::borrow::Cow::Owned(url_decode(raw_key))
247 } else {
248 std::borrow::Cow::Borrowed(raw_key)
249 };
250 let key = key_cow.as_ref();
251 let data = it.next().unwrap_or("");
252
253 match table {
254 "V" if key == "MAX_ERRORS" => Ok(false),
264 "V" => load_value(key, data),
265 "IA" => load_intarray(key, data),
271 "M" => load_meaning(key, data),
279 "LC" => load_lccode(key, data),
281 "UC" => load_uccode(key, data),
282 "SC" => load_sfcode(key, data),
284 "C" => {
287 let ch = decode_char_key(key);
288 if ch.is_some_and(|c| c as u32 > 127) {
289 load_catcode(key, data)
290 } else {
291 Ok(false)
292 }
293 },
294 "MC" => load_mathcode(key, data),
302 "DC" => load_delcode(key, data),
303 _ => Ok(false),
304 }
305}
306
307const SKIP_VALUE_KEYS: &[&str] = &[
309 "INTERPRETING_DEFINITIONS",
310 "if_count",
311 "absorb_count",
312 "if_stack",
313 "INCLUDE_COMMENTS",
314 "INCLUDE_STYLES",
315 "INPUT_ENCODING",
316 "CURRENT_INPUT_ENCODING",
317 "SUPPRESS_UNEXPECTED_ERRORS",
318 "SUPPRESS_UNDEFINED_ERRORS",
319 "DOCUMENT_REWRITE_RULES",
323 "PARAMETER_TYPES",
324 "TAG_PROPERTIES",
325 "MATH_LIGATURES",
326 "TEXT_LIGATURES",
327];
328
329const SKIP_VALUE_PREFIXES: &[&str] = &["input_file:", "output_file:", "texsys"];
331
332const SKIP_VALUE_CONTAINS: &[&str] = &[
351 "_loaded", ];
354
355fn load_value(key: &str, data: &str) -> Result<bool, String> {
360 for skip in SKIP_VALUE_KEYS {
362 if key == *skip {
363 return Ok(false);
364 }
365 }
366 for prefix in SKIP_VALUE_PREFIXES {
368 if key.starts_with(prefix) {
369 return Ok(false);
370 }
371 }
372 for substr in SKIP_VALUE_CONTAINS {
374 if key.contains(substr) {
375 return Ok(false);
376 }
377 }
378
379 let mut top_it = data.splitn(2, '\t');
386 let kind = top_it.next().ok_or("Missing value type")?;
387 let rest = top_it.next().unwrap_or("");
388 let rest_or_zero = if rest.is_empty() { "0" } else { rest };
391
392 let value = match kind {
393 "N" => return Ok(false), "B" => Stored::Bool(rest == "1"),
395 "I" => {
396 let n: i64 = rest_or_zero
397 .parse()
398 .map_err(|e| format!("Bad int: {}", e))?;
399 Stored::Int(n)
400 },
401 "Nm" => {
404 let n: i64 = rest_or_zero
405 .parse()
406 .map_err(|e| format!("Bad number: {}", e))?;
407 Stored::Number(crate::common::number::Number(n))
408 },
409 "S" => Stored::from(url_decode(rest)),
410 "F" => {
411 use std::borrow::Cow;
416
417 use crate::common::font::Font;
418 let mut font = Font::default();
419 for kv in rest.split('\x1f') {
420 if let Some((k, v)) = kv.split_once('=') {
421 let v_dec = url_decode(v);
422 match k {
423 "name" => font.name = Some(Cow::Owned(v_dec)),
424 "family" => font.family = Some(Cow::Owned(v_dec)),
425 "series" => font.series = Some(Cow::Owned(v_dec)),
426 "shape" => font.shape = Some(Cow::Owned(v_dec)),
427 "encoding" => font.encoding = Some(Cow::Owned(v_dec)),
428 "language" => font.language = Some(Cow::Owned(v_dec)),
429 "mathstyle" => font.mathstyle = Some(Cow::Owned(v_dec)),
430 "opacity" => font.opacity = Some(Cow::Owned(v_dec)),
431 "size" => font.size = v_dec.parse().ok(),
432 "scale" => font.scale = v_dec.parse().ok(),
433 "emph" => font.emph = Some(v_dec == "1"),
434 "scripted" => font.scripted = Some(v_dec == "1"),
435 "mathstylestep" => font.mathstylestep = v_dec.parse().ok(),
436 "flags" => font.flags = v_dec.parse().ok(),
437 _ => {},
438 }
439 }
440 }
441 Stored::Font(std::rc::Rc::new(font))
442 },
443 "CH" => {
444 let n: u16 = rest_or_zero
445 .parse()
446 .map_err(|e| format!("Bad charcode: {}", e))?;
447 Stored::Charcode(n)
448 },
449 "CC" => {
450 let n: u8 = rest_or_zero
451 .parse()
452 .map_err(|e| format!("Bad catcode: {}", e))?;
453 Stored::Catcode(Catcode::from(n))
454 },
455 "T" => {
456 let tok = parse_token(rest)?;
457 Stored::Token(tok)
458 },
459 "TK" => {
460 let toks = parse_token_list(rest)?;
461 Stored::Tokens(Tokens::from(toks))
462 },
463 "D" => {
464 let n: i64 = rest_or_zero
465 .parse()
466 .map_err(|e| format!("Bad dimension: {}", e))?;
467 Stored::Dimension(crate::common::dimension::Dimension(n))
468 },
469 "G" => Stored::Glue(parse_glue(rest_or_zero)?),
470 "MD" => {
471 let n: i64 = rest_or_zero
472 .parse()
473 .map_err(|e| format!("Bad mudimension: {}", e))?;
474 Stored::MuDimension(crate::common::mudimension::MuDimension(n))
475 },
476 "MG" => Stored::MuGlue(parse_muglue(rest_or_zero)?),
477 "VD" => return Ok(false), _ => return Ok(false), };
480
481 state::assign_internal(
485 TableName::Value,
486 arena::pin(key),
487 value,
488 Some(Scope::Global),
489 );
490 Ok(true)
491}
492
493fn load_intarray(key: &str, data: &str) -> Result<bool, String> {
500 let mut it = data.splitn(2, '\t');
501 let len_s = it.next().unwrap_or("");
502 let rle = it.next().unwrap_or("");
503 let len: usize = len_s.parse().map_err(|e| format!("Bad IA length: {}", e))?;
504 let values = rle_decode_i64(rle)?;
505 if values.len() != len {
506 return Err(format!(
507 "IA length mismatch for {:?}: declared {} but RLE decoded to {}",
508 key,
509 len,
510 values.len()
511 ));
512 }
513 for (i, val) in values.into_iter().enumerate() {
514 let slot_key = format!("{}_{}", key, i + 1);
515 state::assign_internal(
516 TableName::Value,
517 arena::pin(&slot_key),
518 Stored::Dimension(crate::common::dimension::Dimension(val)),
519 Some(Scope::Global),
520 );
521 }
522 Ok(true)
523}
524
525fn rle_decode_i64(s: &str) -> Result<Vec<i64>, String> {
529 let mut out = Vec::new();
530 if s.is_empty() {
531 return Ok(out);
532 }
533 for tok in s.split(',') {
534 if let Some(xi) = tok.find('x') {
535 let val: i64 = tok[..xi]
536 .parse()
537 .map_err(|e| format!("Bad RLE value in {:?}: {}", tok, e))?;
538 let cnt: usize = tok[xi + 1..]
539 .parse()
540 .map_err(|e| format!("Bad RLE count in {:?}: {}", tok, e))?;
541 for _ in 0..cnt {
542 out.push(val);
543 }
544 } else {
545 let val: i64 = tok
546 .parse()
547 .map_err(|e| format!("Bad RLE value {:?}: {}", tok, e))?;
548 out.push(val);
549 }
550 }
551 Ok(out)
552}
553
554fn load_meaning(key: &str, data: &str) -> Result<bool, String> {
564 let cs_tok = Token {
565 text: arena::pin(key),
566 code: Catcode::CS,
567 #[cfg(feature = "token-locators")]
568 loc: 0,
569 };
570
571 let mut top_it = data.splitn(2, '\t');
579 let kind = top_it.next().ok_or("Missing meaning type")?;
580 let rest = top_it.next().unwrap_or("");
581
582 match kind {
583 "N" => {
584 Ok(false)
586 },
587 "E" => {
588 let mut eit = rest.splitn(6, '\t');
602 let alias_field = eit.next().ok_or("Incomplete Expandable entry")?;
603 let nargs_field = eit.next().ok_or("Incomplete Expandable entry")?;
604 let flags_field = eit.next().ok_or("Incomplete Expandable entry")?;
605 let tok_field = eit.next().ok_or("Incomplete Expandable entry")?;
606 let proto_field = eit.next();
607 let v3_field = eit.next();
608
609 const DEFERRED_NAMES: &[&str] = &["\\unexpanded", "\\the", "\\detokenize", "\\showthe"];
627 let alias_decoded = url_decode(alias_field);
628 let is_alias_diff = cs_tok.with_cs_name(|s| s != alias_decoded.as_str());
629 let alias_for_traits = if is_alias_diff && DEFERRED_NAMES.contains(&alias_decoded.as_str()) {
630 Some(alias_decoded)
631 } else {
632 None
633 };
634 let nargs: usize = nargs_field.parse().unwrap_or(0);
635 let flags = flags_field;
636 let tok_data = tok_field;
637 let proto_opt = proto_field.map(url_decode).filter(|s| !s.is_empty());
638 let v3_opt = v3_field.filter(|s| !s.is_empty());
639
640 let is_long = flags.contains('L');
641 let is_protected = flags.contains('P');
642
643 let expansion = parse_token_list(tok_data)?;
644
645 let paramlist = if let Some(v3) = v3_opt {
650 match parse_parameters_v3(v3) {
651 Ok(pl) => pl,
652 Err(_) => proto_opt
653 .as_ref()
654 .and_then(|p| crate::common::def_parser::parse_parameters(p, &cs_tok, true).ok())
655 .flatten()
656 .or_else(|| {
657 if nargs > 0 {
658 let fallback = "{}".repeat(nargs);
659 crate::common::def_parser::parse_parameters(&fallback, &cs_tok, true)
660 .ok()
661 .flatten()
662 } else {
663 None
664 }
665 }),
666 }
667 } else {
668 match proto_opt {
671 Some(proto) => match crate::common::def_parser::parse_parameters(&proto, &cs_tok, true) {
672 Ok(pl) => pl,
673 Err(_) if nargs > 0 => {
674 let fallback = "{}".repeat(nargs);
675 crate::common::def_parser::parse_parameters(&fallback, &cs_tok, true)
676 .map_err(|e| format!("Param parse fallback: {}", e))?
677 },
678 Err(_) => None,
679 },
680 None if nargs > 0 => {
681 let proto = "{}".repeat(nargs);
682 crate::common::def_parser::parse_parameters(&proto, &cs_tok, true)
683 .map_err(|e| format!("Param parse: {}", e))?
684 },
685 None => None,
686 }
687 };
688
689 let options = Some(ExpandableOptions {
690 long: is_long,
691 protected: is_protected,
692 nopack_parameters: true, alias: alias_for_traits,
694 ..ExpandableOptions::default()
695 });
696
697 let expansion_body = Tokens::from(expansion).into();
698 match Expandable::new(cs_tok, paramlist, Some(expansion_body), options) {
699 Ok(mut exp) => {
700 exp.locator = current_dump_locator();
705 state::assign_internal(
718 TableName::Meaning,
719 cs_tok.get_cs_name(),
720 Stored::from(exp),
721 Some(Scope::Global),
722 );
723 Ok(true)
724 },
725 Err(e) => Err(format!("Expandable creation failed: {}", e)),
726 }
727 },
728 "T" => {
729 let tok = parse_token(rest)?;
734 state::assign_internal(
735 TableName::Meaning,
736 cs_tok.get_cs_name(),
737 Stored::Token(tok),
738 Some(Scope::Global),
739 );
740 Ok(true)
741 },
742 "FD" => {
743 use crate::definition::{BeforeDigestClosure, primitive::Primitive};
754 let font_id_raw = url_decode(rest);
755 let font_id_pin = arena::pin(&font_id_raw);
756 let font_id_str = font_id_raw;
757 let cs_for_fontdef = cs_tok;
758 let merge_closure: BeforeDigestClosure = std::rc::Rc::new(move || {
759 state::assign_value("current_FontDef", Stored::Token(cs_for_fontdef), None);
760 if let Some(Stored::Font(f)) = state::lookup_value(&font_id_str) {
761 crate::binding::content::merge_font((*f).clone());
762 }
763 Ok(Vec::new())
764 });
765 let prim = Primitive {
766 cs: cs_tok,
767 before_digest: vec![merge_closure],
768 font_id: Some(font_id_pin),
769 ..Primitive::default()
770 };
771 state::assign_internal(
772 TableName::Meaning,
773 cs_tok.get_cs_name(),
774 Stored::Primitive(std::rc::Rc::new(prim)),
775 Some(Scope::Global),
776 );
777 Ok(true)
778 },
779 "PA" | "MPA" => {
780 let target_cs_raw = url_decode(rest);
790 if target_cs_raw == key {
791 return Ok(false);
792 }
793 let target_tok = Token {
794 text: arena::pin(&target_cs_raw),
795 code: Catcode::CS,
796 #[cfg(feature = "token-locators")]
797 loc: 0,
798 };
799 if !state::has_meaning(&target_tok) {
810 DEFERRED_ALIASES.with(|cell| {
811 cell.borrow_mut().push((cs_tok, target_tok));
812 });
813 return Ok(false);
814 }
815 let target_meaning = state::lookup_meaning(&target_tok);
816 if let Some(meaning) = target_meaning {
817 state::assign_internal(
818 TableName::Meaning,
819 cs_tok.get_cs_name(),
820 meaning,
821 Some(Scope::Global),
822 );
823 }
824 Ok(true)
825 },
826 "R" => {
827 let mut rit = rest.splitn(5, '\t');
839 let r_cs = rit.next().ok_or("Incomplete Register entry")?;
840 let r_type = rit.next().ok_or("Incomplete Register entry")?;
841 let r_value = rit.next().ok_or("Incomplete Register entry")?;
842 let r_glyph_field = rit.next();
843 let r_addr_field = rit.next();
844 let rtype = r_type;
845 let value_str = r_value;
846 let mathglyph = r_glyph_field
847 .filter(|s| !s.is_empty())
848 .and_then(|s| s.parse::<u32>().ok())
849 .and_then(char::from_u32);
850 let dump_address: Option<String> = r_addr_field.filter(|s| !s.is_empty()).map(url_decode);
851 let internal_cs_decoded = url_decode(r_cs);
862 let dump_address: Option<String> = dump_address.or_else(|| {
863 if internal_cs_decoded != *key && !internal_cs_decoded.is_empty() {
864 Some(internal_cs_decoded)
865 } else {
866 None
867 }
868 });
869
870 use crate::{
871 common::number::Number,
872 definition::register::{Register, RegisterType, RegisterValue},
873 };
874
875 let (reg_type, reg_value) = match rtype {
876 "N" | "CD" => {
877 let n: i64 = value_str.parse().unwrap_or(0);
878 let rt = if rtype == "CD" {
879 RegisterType::CharDef
880 } else {
881 RegisterType::Number
882 };
883 (rt, Some(RegisterValue::Number(Number::new(n))))
884 },
885 "D" => {
886 let n: i64 = value_str.parse().unwrap_or(0);
887 (
888 RegisterType::Dimension,
889 Some(RegisterValue::Dimension(
890 crate::common::dimension::Dimension(n),
891 )),
892 )
893 },
894 "G" => (
895 RegisterType::Glue,
896 Some(RegisterValue::Glue(parse_glue(value_str)?)),
897 ),
898 "MG" => (
899 RegisterType::MuGlue,
900 Some(RegisterValue::MuGlue(parse_muglue(value_str)?)),
901 ),
902 "TK" => {
903 let toks = if value_str == "0" || value_str.is_empty() {
905 Vec::new()
906 } else {
907 parse_token_list(value_str)?
908 };
909 (
910 RegisterType::Tokens,
911 Some(RegisterValue::Tokens(Tokens::from(toks))),
912 )
913 },
914 _ => return Ok(false),
915 };
916
917 let mut reg = Register {
924 cs: cs_tok,
925 register_type: reg_type,
926 value: reg_value.clone(),
927 default: if matches!(reg_type, RegisterType::CharDef) {
928 None
929 } else {
930 reg_value.clone()
931 },
932 mathglyph,
933 locator: current_dump_locator(),
934 ..Register::default()
935 };
936 let has_explicit_address = dump_address.is_some();
939 reg.address = dump_address.unwrap_or_else(|| key.to_string());
940 if reg.parameters.is_none() && reg.address != key {
950 let address_tok = Token {
951 text: arena::pin(®.address),
952 code: Catcode::CS,
953 #[cfg(feature = "token-locators")]
954 loc: 0,
955 };
956 if let Some(base_defn) = state::lookup_register_definition(&address_tok)
957 && let Some(params) = base_defn.parameters.clone()
958 {
959 reg.parameters = Some(params);
960 }
961 }
962 if !matches!(reg_type, RegisterType::CharDef)
963 && let Some(ref rv) = reg_value
964 {
965 let should_assign = !has_explicit_address || !state::has_value(®.address);
977 if should_assign {
978 state::assign_internal(
979 TableName::Value,
980 arena::pin(®.address),
981 rv.clone(),
982 Some(Scope::Global),
983 );
984 }
985 }
986 state::assign_internal(
990 TableName::Meaning,
991 cs_tok.get_cs_name(),
992 Stored::from(reg),
993 Some(Scope::Global),
994 );
995 Ok(true)
996 },
997 _ => Ok(false),
998 }
999}
1000
1001fn decode_char_key(key: &str) -> Option<char> {
1005 let decoded = url_decode(key);
1006 decoded.chars().next()
1007}
1008
1009fn char_key(ch: char) -> arena::SymStr {
1012 let mut buf = [0u8; 4];
1013 arena::pin(ch.encode_utf8(&mut buf))
1014}
1015
1016fn load_catcode(key: &str, data: &str) -> Result<bool, String> {
1019 let ch = decode_char_key(key).ok_or_else(|| format!("Bad catcode char: {}", key))?;
1020 let (tag, val_str) = data
1021 .split_once('\t')
1022 .ok_or_else(|| format!("Bad catcode data: {}", data))?;
1023 if tag != "CC" {
1024 return Err(format!("Bad catcode data: {}", data));
1025 }
1026 let cc: u8 = val_str
1027 .parse()
1028 .map_err(|e| format!("Bad catcode value: {}", e))?;
1029 state::assign_internal(
1030 TableName::Catcode,
1031 char_key(ch),
1032 Stored::Catcode(Catcode::from(cc)),
1033 Some(Scope::Global),
1034 );
1035 Ok(true)
1036}
1037
1038fn load_lccode(key: &str, data: &str) -> Result<bool, String> {
1041 let ch = decode_char_key(key).ok_or_else(|| format!("Bad lccode char: {}", key))?;
1042 let (tag, val_str) = data
1043 .split_once('\t')
1044 .ok_or_else(|| format!("Bad lccode data: {}", data))?;
1045 if tag != "CH" {
1046 return Err(format!("Bad lccode data: {}", data));
1047 }
1048 let val: u16 = val_str
1049 .parse()
1050 .map_err(|e| format!("Bad lccode value: {}", e))?;
1051 state::assign_internal(
1052 TableName::Lccode,
1053 char_key(ch),
1054 Stored::Charcode(val),
1055 Some(Scope::Global),
1056 );
1057 Ok(true)
1058}
1059
1060fn load_uccode(key: &str, data: &str) -> Result<bool, String> {
1063 let ch = decode_char_key(key).ok_or_else(|| format!("Bad uccode char: {}", key))?;
1064 let (tag, val_str) = data
1065 .split_once('\t')
1066 .ok_or_else(|| format!("Bad uccode data: {}", data))?;
1067 if tag != "CH" {
1068 return Err(format!("Bad uccode data: {}", data));
1069 }
1070 let val: u16 = val_str
1071 .parse()
1072 .map_err(|e| format!("Bad uccode value: {}", e))?;
1073 state::assign_internal(
1074 TableName::Uccode,
1075 char_key(ch),
1076 Stored::Charcode(val),
1077 Some(Scope::Global),
1078 );
1079 Ok(true)
1080}
1081
1082fn load_sfcode(key: &str, data: &str) -> Result<bool, String> {
1085 let ch = decode_char_key(key).ok_or_else(|| format!("Bad sfcode char: {}", key))?;
1086 let (tag, val_str) = data
1087 .split_once('\t')
1088 .ok_or_else(|| format!("Bad sfcode data: {}", data))?;
1089 if tag != "CH" {
1090 return Err(format!("Bad sfcode data: {}", data));
1091 }
1092 let val: u16 = val_str
1093 .parse()
1094 .map_err(|e| format!("Bad sfcode value: {}", e))?;
1095 state::assign_internal(
1096 TableName::Sfcode,
1097 char_key(ch),
1098 Stored::Charcode(val),
1099 Some(Scope::Global),
1100 );
1101 Ok(true)
1102}
1103
1104fn load_delcode(key: &str, data: &str) -> Result<bool, String> {
1107 let ch = decode_char_key(key).ok_or_else(|| format!("Bad delcode char: {}", key))?;
1108 let (tag, val_str) = data
1109 .split_once('\t')
1110 .ok_or_else(|| format!("Bad delcode data: {}", data))?;
1111 if tag != "CH" {
1112 return Err(format!("Bad delcode data: {}", data));
1113 }
1114 let val: u16 = val_str
1115 .parse()
1116 .map_err(|e| format!("Bad delcode value: {}", e))?;
1117 state::assign_delcode(ch, val, Some(Scope::Global));
1118 Ok(true)
1119}
1120
1121fn load_mathcode(key: &str, data: &str) -> Result<bool, String> {
1124 let ch = decode_char_key(key).ok_or_else(|| format!("Bad mathcode char: {}", key))?;
1125 let (tag, val_str) = data
1126 .split_once('\t')
1127 .ok_or_else(|| format!("Bad mathcode data: {}", data))?;
1128 if tag != "CH" {
1129 return Err(format!("Bad mathcode data: {}", data));
1130 }
1131 let val: u16 = val_str
1132 .parse()
1133 .map_err(|e| format!("Bad mathcode value: {}", e))?;
1134 state::assign_mathcode(ch, val, Some(Scope::Global));
1135 Ok(true)
1136}
1137
1138fn parse_token(s: &str) -> Result<Token, String> {
1140 let (cc_str, text) = s.split_once(':').ok_or("Missing ':' in token")?;
1141 let cc: u8 = cc_str.parse().map_err(|e| format!("Bad CC: {}", e))?;
1142 let text_sym = if text.contains('%') {
1148 arena::pin(url_decode(text))
1149 } else {
1150 arena::pin(text)
1151 };
1152 Ok(Token {
1153 text: text_sym,
1154 code: Catcode::from(cc),
1155 #[cfg(feature = "token-locators")]
1156 loc: 0,
1157 })
1158}
1159
1160fn parse_token_list(s: &str) -> Result<Vec<Token>, String> {
1162 if s.is_empty() {
1163 return Ok(Vec::new());
1164 }
1165 let n = s.bytes().filter(|b| *b == b',').count() + 1;
1170 let mut out = Vec::with_capacity(n);
1171 for tok in s.split(',') {
1172 out.push(parse_token(tok)?);
1173 }
1174 Ok(out)
1175}
1176
1177pub(crate) fn parse_parameters_v3(
1187 v3: &str,
1188) -> Result<Option<crate::parameter::Parameters>, String> {
1189 if v3.is_empty() {
1190 return Ok(None);
1191 }
1192 let mut params = Vec::new();
1193 for record in v3.split('\x1e') {
1194 let fields: Vec<&str> = record.splitn(4, '\x1f').collect();
1196 if fields.len() != 4 {
1197 return Err(format!(
1198 "v3 Parameter record has {} fields, expected 4",
1199 fields.len()
1200 ));
1201 }
1202 let name = url_decode(fields[0]);
1203 let spec = url_decode(fields[1]);
1204 let flags = fields[2];
1205 let extras_str = fields[3];
1206
1207 let extras = if extras_str.is_empty() {
1208 Vec::new()
1209 } else {
1210 extras_str
1211 .split('\x1d')
1212 .map(|tok_list| parse_token_list(tok_list).map(Tokens::new))
1213 .collect::<Result<Vec<_>, _>>()?
1214 };
1215
1216 let mut param = crate::parameter::Parameter::new(name, spec, Some(extras))
1217 .map_err(|e| format!("Parameter::new failed: {}", e))?;
1218
1219 for flag in flags.split(';').filter(|s| !s.is_empty()) {
1224 match flag {
1225 "n=1" => param.novalue = true,
1226 "o=1" => param.optional = true,
1227 _ => {
1228 },
1231 }
1232 }
1233 params.push(param);
1234 }
1235 if params.is_empty() {
1236 Ok(None)
1237 } else {
1238 Ok(Some(crate::parameter::Parameters::new(params)))
1239 }
1240}
1241
1242pub(crate) fn url_decode(s: &str) -> String {
1243 if !s.contains('%') {
1247 return s.to_owned();
1248 }
1249 let mut result = String::with_capacity(s.len());
1250 let mut chars = s.chars();
1251 while let Some(ch) = chars.next() {
1252 if ch == '%' {
1253 let hex: String = chars.by_ref().take(2).collect();
1254 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
1255 result.push(byte as char);
1256 }
1257 } else {
1258 result.push(ch);
1259 }
1260 }
1261 result
1262}
1263
1264fn parse_glue(s: &str) -> Result<crate::common::glue::Glue, String> {
1266 use crate::common::glue::{FillCode, Glue};
1267 let mut skip = 0i64;
1268 let mut plus = None;
1269 let mut pfill = None;
1270 let mut minus = None;
1271 let mut mfill = None;
1272 for (i, part) in s.split(',').enumerate() {
1273 if i == 0 {
1274 skip = part.parse().map_err(|e| format!("Bad glue skip: {}", e))?;
1275 } else if let Some(rest) = part.strip_prefix("pf") {
1276 pfill = FillCode::new(rest.parse::<usize>().unwrap_or(0));
1277 } else if let Some(rest) = part.strip_prefix('p') {
1278 plus = Some(rest.parse().map_err(|e| format!("Bad glue plus: {}", e))?);
1279 } else if let Some(rest) = part.strip_prefix("mf") {
1280 mfill = FillCode::new(rest.parse::<usize>().unwrap_or(0));
1281 } else if let Some(rest) = part.strip_prefix('m') {
1282 minus = Some(rest.parse().map_err(|e| format!("Bad glue minus: {}", e))?);
1283 }
1284 }
1285 Ok(Glue {
1286 skip,
1287 plus,
1288 pfill,
1289 minus,
1290 mfill,
1291 })
1292}
1293
1294fn parse_muglue(s: &str) -> Result<crate::common::muglue::MuGlue, String> {
1296 use crate::common::{glue::FillCode, muglue::MuGlue};
1297 let mut skip = 0i64;
1298 let mut plus = None;
1299 let mut pfill = None;
1300 let mut minus = None;
1301 let mut mfill = None;
1302 for (i, part) in s.split(',').enumerate() {
1303 if i == 0 {
1304 skip = part
1305 .parse()
1306 .map_err(|e| format!("Bad muglue skip: {}", e))?;
1307 } else if let Some(rest) = part.strip_prefix("pf") {
1308 pfill = FillCode::new(rest.parse::<usize>().unwrap_or(0));
1309 } else if let Some(rest) = part.strip_prefix('p') {
1310 plus = Some(
1311 rest
1312 .parse()
1313 .map_err(|e| format!("Bad muglue plus: {}", e))?,
1314 );
1315 } else if let Some(rest) = part.strip_prefix("mf") {
1316 mfill = FillCode::new(rest.parse::<usize>().unwrap_or(0));
1317 } else if let Some(rest) = part.strip_prefix('m') {
1318 minus = Some(
1319 rest
1320 .parse()
1321 .map_err(|e| format!("Bad muglue minus: {}", e))?,
1322 );
1323 }
1324 }
1325 Ok(MuGlue {
1326 skip,
1327 plus,
1328 pfill,
1329 minus,
1330 mfill,
1331 })
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337
1338 #[test]
1339 fn test_load_native_dump_inline() {
1340 let content = "V\tcount@\tI\t42\nM\t\\mymacro\tE\t\\mymacro\t1\t\t6:1,6:2\n";
1342 let count = load_from_str(content).unwrap();
1343 assert!(
1344 count > 0,
1345 "Expected entries loaded from inline dump, got {}",
1346 count
1347 );
1348 }
1349
1350 #[test]
1351 fn test_catcode_loading_nonascii() {
1352 let content = "C\t\u{00e8}\tCC\t12\n"; let count = load_from_str(content).unwrap();
1355 assert!(count > 0, "Expected non-ASCII catcode entry loaded");
1356 }
1357
1358 #[test]
1359 fn test_lccode_loading() {
1360 let content = "LC\t\u{00c8}\tCH\t232\n"; let count = load_from_str(content).unwrap();
1362 assert!(count > 0, "Expected lccode entry loaded");
1363 }
1364
1365 #[test]
1368 fn rle_decode_empty() {
1369 assert_eq!(rle_decode_i64("").unwrap(), Vec::<i64>::new());
1370 }
1371
1372 #[test]
1373 fn rle_decode_single() {
1374 assert_eq!(rle_decode_i64("5").unwrap(), vec![5]);
1375 }
1376
1377 #[test]
1378 fn rle_decode_single_run() {
1379 assert_eq!(rle_decode_i64("5x3").unwrap(), vec![5, 5, 5]);
1380 }
1381
1382 #[test]
1383 fn rle_decode_mixed() {
1384 assert_eq!(rle_decode_i64("1,2x2,3x3,1").unwrap(), vec![
1385 1, 2, 2, 3, 3, 3, 1
1386 ]);
1387 }
1388
1389 #[test]
1390 fn rle_decode_negative() {
1391 assert_eq!(rle_decode_i64("-5").unwrap(), vec![-5]);
1392 assert_eq!(rle_decode_i64("-5x3").unwrap(), vec![-5, -5, -5]);
1393 }
1394
1395 #[test]
1396 fn rle_decode_long_run() {
1397 let v = rle_decode_i64("218x10000").unwrap();
1398 assert_eq!(v.len(), 10000);
1399 assert!(v.iter().all(|&x| x == 218));
1400 }
1401
1402 #[test]
1403 fn rle_decode_malformed_returns_err() {
1404 assert!(rle_decode_i64("abc").is_err());
1405 assert!(rle_decode_i64("5xabc").is_err());
1406 assert!(rle_decode_i64("5x").is_err());
1407 }
1408
1409 #[test]
1412 fn ia_load_writes_per_slot_values() {
1413 let prefix = "ia_test_prefix";
1416 let content = format!("IA\t{}\t3\t10,20x2\n", prefix);
1417 load_from_str(&content).unwrap();
1418
1419 use crate::{
1420 common::{dimension::Dimension, store::Stored},
1421 state,
1422 };
1423
1424 assert_eq!(
1425 state::lookup_value(&format!("{}_1", prefix)),
1426 Some(Stored::Dimension(Dimension(10)))
1427 );
1428 assert_eq!(
1429 state::lookup_value(&format!("{}_2", prefix)),
1430 Some(Stored::Dimension(Dimension(20)))
1431 );
1432 assert_eq!(
1433 state::lookup_value(&format!("{}_3", prefix)),
1434 Some(Stored::Dimension(Dimension(20)))
1435 );
1436 assert_eq!(state::lookup_value(&format!("{}_4", prefix)), None);
1438 }
1439
1440 #[test]
1441 fn ia_load_length_mismatch_errors() {
1442 let content = "IA\tia_mismatch_prefix\t5\t10,20,30\n";
1444 let count = load_from_str(content).unwrap_or(0);
1447 assert_eq!(count, 0, "Length-mismatch IA should not load");
1448 }
1449
1450 #[test]
1453 fn v_record_dimension_still_loads() {
1454 let prefix = "v_backcompat_prefix";
1458 let content = format!("V\t{}_1\tD\t111\nV\t{}_2\tD\t222\n", prefix, prefix);
1459 load_from_str(&content).unwrap();
1460
1461 use crate::{
1462 common::{dimension::Dimension, store::Stored},
1463 state,
1464 };
1465
1466 assert_eq!(
1467 state::lookup_value(&format!("{}_1", prefix)),
1468 Some(Stored::Dimension(Dimension(111)))
1469 );
1470 assert_eq!(
1471 state::lookup_value(&format!("{}_2", prefix)),
1472 Some(Stored::Dimension(Dimension(222)))
1473 );
1474 }
1475}