1use std::collections::HashMap;
23
24use serde::{Deserialize, Deserializer, Serialize};
25
26use crate::signatures::ExpectedDomain;
27
28use crate::error::{CatalogError, Result};
29
30pub const CATALOG_DATA: &str = include_str!("data/catalog.json");
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
35pub struct Locale(String);
36
37impl Locale {
38 pub fn new(value: &str) -> Locale {
40 Locale(value.trim().to_ascii_lowercase())
41 }
42
43 pub fn as_str(&self) -> &str {
45 &self.0
46 }
47}
48
49impl<'de> Deserialize<'de> for Locale {
50 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
51 where
52 D: Deserializer<'de>,
53 {
54 let value = String::deserialize(deserializer)?;
55 Ok(Self::new(&value))
56 }
57}
58
59impl std::fmt::Display for Locale {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.write_str(&self.0)
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum Kind {
68 Structural,
70 Action,
72 Value,
74 Event,
76 Operator,
78 Enum,
80 Setting,
82}
83
84impl Kind {
85 pub fn as_str(self) -> &'static str {
86 match self {
87 Kind::Structural => "structural",
88 Kind::Action => "action",
89 Kind::Value => "value",
90 Kind::Event => "event",
91 Kind::Operator => "operator",
92 Kind::Enum => "enum",
93 Kind::Setting => "setting",
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct CatalogEntry {
101 pub id: String,
102 pub kind: Kind,
103 pub params: Vec<String>,
105 pub param_domains: Vec<Option<String>>,
113 pub param_defaults: Vec<Option<String>>,
117 pub param_types: Vec<Option<String>>,
120 pub return_type: Option<String>,
123 pub variadic: bool,
125 aliases: HashMap<Locale, Vec<String>>,
126}
127
128impl CatalogEntry {
129 pub fn spelling(&self, locale: &Locale) -> Option<&str> {
131 self.aliases
132 .get(locale)
133 .and_then(|spellings| spellings.first())
134 .map(String::as_str)
135 }
136
137 pub fn spellings(&self, locale: &Locale) -> &[String] {
140 self.aliases.get(locale).map(Vec::as_slice).unwrap_or(&[])
141 }
142
143 pub fn param_count(&self) -> usize {
145 self.params.len()
146 }
147
148 pub fn required_param_count(&self) -> usize {
152 (0..self.params.len())
153 .rev()
154 .find(|index| {
155 self.param_defaults
156 .get(*index)
157 .and_then(Option::as_ref)
158 .is_none()
159 })
160 .map_or(0, |index| index + 1)
161 }
162
163 pub fn param_domain(&self, index: usize) -> Option<&str> {
165 self.param_domains
166 .get(index)
167 .or_else(|| self.variadic.then(|| self.param_domains.last()).flatten())
168 .and_then(Option::as_deref)
169 }
170
171 pub fn param_type(&self, index: usize) -> Option<&str> {
174 self.param_types
175 .get(index)
176 .or_else(|| self.variadic.then(|| self.param_types.last()).flatten())
177 .and_then(Option::as_deref)
178 }
179
180 pub fn return_type(&self) -> Option<&str> {
182 self.return_type.as_deref()
183 }
184}
185
186#[derive(Debug, Clone)]
188pub struct EnumMember {
189 pub member: String,
190 aliases: HashMap<Locale, Vec<String>>,
191}
192
193impl EnumMember {
194 pub fn spelling(&self, locale: &Locale) -> Option<&str> {
196 self.aliases
197 .get(locale)
198 .and_then(|spellings| spellings.first())
199 .map(String::as_str)
200 }
201
202 pub fn spellings(&self, locale: &Locale) -> &[String] {
205 self.aliases.get(locale).map(Vec::as_slice).unwrap_or(&[])
206 }
207}
208
209#[derive(Debug, Clone)]
211pub struct EnumDomain {
212 pub domain: String,
213 aliases: HashMap<Locale, Vec<String>>,
214 pub members: Vec<EnumMember>,
215}
216
217impl EnumDomain {
218 pub fn spelling(&self, locale: &Locale) -> Option<&str> {
219 self.aliases
220 .get(locale)
221 .and_then(|spellings| spellings.first())
222 .map(String::as_str)
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
228pub struct TargetMeta {
229 pub game: String,
230 pub format: String,
231 pub surface: String,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
236#[serde(rename_all = "camelCase")]
237pub struct Provenance {
238 pub generator: String,
239 pub generator_version: String,
240 pub source: String,
241 pub license: String,
242 pub reviewed: bool,
243 #[serde(default, skip_serializing_if = "Vec::is_empty")]
246 pub source_notes: Vec<String>,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
252pub struct LocaleCoverage {
253 pub locale: Locale,
254 pub mapped: usize,
256 pub total: usize,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
266#[serde(rename_all = "kebab-case")]
267pub struct CatalogIdentity {
268 pub implementation_version: String,
270 pub catalog_version: String,
272 pub catalog_digest: Option<String>,
275 pub locale_coverage: Vec<LocaleCoverage>,
277 pub target: TargetMeta,
279 pub provenance: Provenance,
281}
282
283#[derive(Debug, Clone)]
285pub struct Catalog {
286 pub schema_version: u32,
287 pub locales: Vec<Locale>,
290 pub target: TargetMeta,
291 pub provenance: Provenance,
292 catalog_version: String,
294 catalog_digest: Option<String>,
297 entries: Vec<CatalogEntry>,
298 enums: Vec<EnumDomain>,
299 by_id: HashMap<(Kind, String), usize>,
300 alias_to_entry: HashMap<(Kind, Locale, String), usize>,
301 enum_by_domain: HashMap<String, usize>,
302 enum_alias_to_domain: HashMap<(Locale, String), String>,
303 enum_alias_to_member: HashMap<(String, Locale, String), (usize, usize)>,
304}
305
306#[derive(Deserialize)]
307#[serde(rename_all = "camelCase")]
308struct CatalogFile {
309 schema_version: u32,
310 locales: Vec<String>,
311 target: TargetMeta,
312 provenance: Provenance,
313 #[serde(default)]
315 version: Option<String>,
316 #[serde(default)]
318 digest: Option<String>,
319 #[serde(default)]
320 structural: Vec<EntryFile>,
321 #[serde(default)]
322 actions: Vec<EntryFile>,
323 #[serde(default)]
324 values: Vec<EntryFile>,
325 #[serde(default)]
326 events: Vec<EntryFile>,
327 #[serde(default)]
328 operators: Vec<EntryFile>,
329 #[serde(default)]
330 settings: Vec<EntryFile>,
331 #[serde(default)]
332 enums: Vec<EnumFile>,
333}
334
335#[derive(Deserialize)]
336#[serde(rename_all = "camelCase")]
337struct EntryFile {
338 id: String,
339 aliases: HashMap<String, AliasFile>,
340 #[serde(default)]
341 params: Vec<String>,
342 #[serde(default)]
345 param_domains: Vec<Option<String>>,
346 #[serde(default)]
353 param_defaults: Vec<Option<String>>,
354 #[serde(default)]
355 param_types: Vec<Option<String>>,
356 #[serde(default)]
357 return_type: Option<String>,
358 #[serde(default)]
359 variadic: bool,
360}
361
362#[derive(Deserialize)]
363struct EnumFile {
364 domain: String,
365 #[serde(default)]
366 aliases: HashMap<String, AliasFile>,
367 members: Vec<MemberFile>,
368}
369
370#[derive(Deserialize)]
371struct MemberFile {
372 id: String,
373 aliases: HashMap<String, AliasFile>,
374}
375
376#[derive(Debug, Deserialize)]
381#[serde(untagged)]
382enum AliasFile {
383 One(String),
384 Many(Vec<String>),
385}
386
387impl AliasFile {
388 fn into_spellings(self, id: &str, locale: &str) -> Result<Vec<String>> {
389 let spellings = match self {
390 AliasFile::One(spelling) => vec![spelling],
391 AliasFile::Many(spellings) => spellings,
392 };
393 if spellings.is_empty() || spellings.iter().any(String::is_empty) {
394 return Err(CatalogError::validation(format!(
395 "catalog entry '{}' declares an empty alias for locale '{}'",
396 id, locale
397 )));
398 }
399 Ok(spellings)
400 }
401}
402
403impl Catalog {
404 pub fn load(json: &str) -> Result<Catalog> {
407 let catalog = Self::load_unverified(json)?;
408 if let Some(declared) = &catalog.catalog_digest {
409 let computed = content_digest(json)?;
410 if declared != &computed {
411 return Err(CatalogError::validation(format!(
412 "catalog digest mismatch: declared '{declared}', content '{computed}' — \
413 run the catalog pipeline (workshop-catalog-gen build)"
414 )));
415 }
416 }
417 Ok(catalog)
418 }
419
420 pub fn load_unverified(json: &str) -> Result<Catalog> {
423 let file: CatalogFile = serde_json::from_str(json)
424 .map_err(|error| CatalogError::malformed(format!("catalog data: {error}")))?;
425 if file.schema_version != 1 {
426 return Err(CatalogError::malformed(format!(
427 "unsupported catalog schemaVersion {}",
428 file.schema_version
429 )));
430 }
431 let locales: Vec<Locale> = file.locales.iter().map(|s| Locale::new(s)).collect();
432 if locales.is_empty() {
433 return Err(CatalogError::malformed(
434 "catalog declares no locales".to_string(),
435 ));
436 }
437
438 let mut catalog = Catalog {
439 schema_version: file.schema_version,
440 locales,
441 target: file.target,
442 provenance: file.provenance,
443 catalog_version: file.version.unwrap_or_else(|| "dev".to_string()),
444 catalog_digest: file.digest,
445 entries: Vec::new(),
446 enums: Vec::new(),
447 by_id: HashMap::new(),
448 alias_to_entry: HashMap::new(),
449 enum_by_domain: HashMap::new(),
450 enum_alias_to_domain: HashMap::new(),
451 enum_alias_to_member: HashMap::new(),
452 };
453
454 for (kind, items) in [
455 (Kind::Structural, file.structural),
456 (Kind::Action, file.actions),
457 (Kind::Value, file.values),
458 (Kind::Event, file.events),
459 (Kind::Operator, file.operators),
460 (Kind::Setting, file.settings),
461 ] {
462 for item in items {
463 catalog.insert_entry(kind, item)?;
464 }
465 }
466 for domain in file.enums {
467 catalog.insert_enum(domain)?;
468 }
469 catalog.validate_param_domains()?;
470 Ok(catalog)
471 }
472
473 pub fn builtin() -> Result<Catalog> {
475 Self::load(CATALOG_DATA)
476 }
477
478 pub fn locales(&self) -> &[Locale] {
480 &self.locales
481 }
482
483 pub fn primary_locale(&self) -> &Locale {
486 &self.locales[0]
487 }
488
489 pub fn supports(&self, locale: &Locale) -> bool {
491 self.locales.contains(locale)
492 }
493
494 pub fn catalog_version(&self) -> &str {
496 &self.catalog_version
497 }
498
499 pub fn catalog_digest(&self) -> Option<&str> {
502 self.catalog_digest.as_deref()
503 }
504
505 pub fn implementation_version() -> &'static str {
507 env!("CARGO_PKG_VERSION")
508 }
509
510 pub fn identity(&self) -> CatalogIdentity {
513 CatalogIdentity {
514 implementation_version: Self::implementation_version().to_string(),
515 catalog_version: self.catalog_version.clone(),
516 catalog_digest: self.catalog_digest.clone(),
517 locale_coverage: self
518 .locales
519 .iter()
520 .map(|locale| self.locale_coverage(locale))
521 .collect(),
522 target: self.target.clone(),
523 provenance: self.provenance.clone(),
524 }
525 }
526
527 pub fn locale_coverage(&self, locale: &Locale) -> LocaleCoverage {
531 let member_total: usize = self.enums.iter().map(|domain| domain.members.len()).sum();
532 let total = self.entries.len() + member_total;
533 let mapped = self
534 .entries
535 .iter()
536 .filter(|entry| entry.aliases.contains_key(locale))
537 .count()
538 + self
539 .enums
540 .iter()
541 .flat_map(|domain| &domain.members)
542 .filter(|member| member.aliases.contains_key(locale))
543 .count();
544 LocaleCoverage {
545 locale: locale.clone(),
546 mapped,
547 total,
548 }
549 }
550
551 pub fn locale_coverage_all(&self) -> Vec<LocaleCoverage> {
553 self.locales
554 .iter()
555 .map(|locale| self.locale_coverage(locale))
556 .collect()
557 }
558
559 pub fn entry(&self, kind: Kind, id: &str) -> Option<&CatalogEntry> {
561 self.by_id
562 .get(&(kind, id.to_string()))
563 .map(|i| &self.entries[*i])
564 }
565
566 pub fn resolve(&self, kind: Kind, locale: &Locale, spelling: &str) -> Option<&CatalogEntry> {
568 self.alias_to_entry
569 .get(&(kind, locale.clone(), spelling.to_string()))
570 .map(|i| &self.entries[*i])
571 }
572
573 pub fn spelling(&self, kind: Kind, locale: &Locale, id: &str) -> Option<&str> {
575 self.entry(kind, id)?.spelling(locale)
576 }
577
578 pub fn entries_of(&self, kind: Kind) -> impl Iterator<Item = &CatalogEntry> {
580 self.entries.iter().filter(move |entry| entry.kind == kind)
581 }
582
583 pub fn entry_count(&self) -> usize {
585 self.entries.len()
586 }
587
588 pub fn enum_domains_count(&self) -> usize {
590 self.enums.len()
591 }
592
593 pub fn enum_domain(&self, domain: &str) -> Option<&EnumDomain> {
595 self.enum_by_domain.get(domain).map(|i| &self.enums[*i])
596 }
597
598 pub fn resolve_enum_domain(&self, locale: &Locale, spelling: &str) -> Option<&str> {
600 self.enum_by_domain
601 .get_key_value(spelling)
602 .map(|(domain, _)| domain.as_str())
603 .or_else(|| {
604 self.enum_alias_to_domain
605 .get(&(locale.clone(), spelling.to_string()))
606 .map(String::as_str)
607 })
608 }
609
610 pub fn enum_domains(&self) -> impl Iterator<Item = &EnumDomain> {
612 self.enums.iter()
613 }
614
615 pub fn resolve_enum_member(
617 &self,
618 domain: &str,
619 locale: &Locale,
620 spelling: &str,
621 ) -> Option<(String, String)> {
622 let (domain_index, member_index) = self.enum_alias_to_member.get(&(
623 domain.to_string(),
624 locale.clone(),
625 spelling.to_string(),
626 ))?;
627 Some((
628 domain.to_string(),
629 self.enums[*domain_index].members[*member_index]
630 .member
631 .clone(),
632 ))
633 }
634
635 pub fn enum_spelling(&self, domain: &str, locale: &Locale, member: &str) -> Option<&str> {
637 let domain_index = self.enum_by_domain.get(domain)?;
638 let domain = &self.enums[*domain_index];
639 domain
640 .members
641 .iter()
642 .find(|candidate| candidate.member == member)?
643 .spelling(locale)
644 }
645
646 pub fn bare_member_matches(&self, locale: &Locale, spelling: &str) -> Vec<(String, String)> {
651 let mut matches = Vec::new();
652 for domain in &self.enums {
653 for member in &domain.members {
654 if member
655 .spellings(locale)
656 .iter()
657 .any(|alias| alias == spelling)
658 {
659 matches.push((domain.domain.clone(), member.member.clone()));
660 }
661 }
662 }
663 matches
664 }
665
666 fn insert_entry(&mut self, kind: Kind, item: EntryFile) -> Result<()> {
667 let index = self.entries.len();
668 let mut aliases = HashMap::new();
669 for (locale_str, alias_file) in item.aliases {
670 let locale = Locale::new(&locale_str);
671 if !self.locales.contains(&locale) {
672 return Err(CatalogError::validation(format!(
673 "entry '{}' declares alias for undeclared locale '{}'",
674 item.id, locale
675 )));
676 }
677 let spellings = alias_file.into_spellings(&item.id, locale.as_str())?;
678 for spelling in &spellings {
679 let key = (kind, locale.clone(), spelling.clone());
680 if self.alias_to_entry.contains_key(&key) {
681 return Err(CatalogError::validation(format!(
682 "duplicate {} alias '{spelling}' for locale '{}'",
683 kind.as_str(),
684 locale
685 )));
686 }
687 self.alias_to_entry.insert(key, index);
688 }
689 aliases.insert(locale, spellings);
690 }
691 let id_key = (kind, item.id.clone());
692 if self.by_id.contains_key(&id_key) {
693 return Err(CatalogError::validation(format!(
694 "duplicate {} id '{}'",
695 kind.as_str(),
696 item.id
697 )));
698 }
699 let primary = self.locales[0].clone();
704 if !aliases.contains_key(&primary) {
705 return Err(CatalogError::validation(format!(
706 "{} '{}' is missing a '{}' alias",
707 kind.as_str(),
708 item.id,
709 primary
710 )));
711 }
712 self.by_id.insert(id_key, index);
713 self.entries.push(CatalogEntry {
714 id: item.id,
715 kind,
716 params: item.params,
717 param_domains: item.param_domains,
718 param_defaults: item.param_defaults,
719 param_types: item.param_types,
720 return_type: item.return_type,
721 variadic: item.variadic,
722 aliases,
723 });
724 Ok(())
725 }
726
727 fn validate_param_domains(&self) -> Result<()> {
729 for entry in &self.entries {
730 if entry.param_domains.len() > entry.params.len() {
731 return Err(CatalogError::validation(format!(
732 "{} '{}' declares more param domains than params",
733 entry.kind.as_str(),
734 entry.id
735 )));
736 }
737 if entry.param_defaults.len() > entry.params.len() {
738 return Err(CatalogError::validation(format!(
739 "{} '{}' declares more param defaults than params",
740 entry.kind.as_str(),
741 entry.id
742 )));
743 }
744 if entry.param_types.len() > entry.params.len() {
745 return Err(CatalogError::validation(format!(
746 "{} '{}' declares more param types than params",
747 entry.kind.as_str(),
748 entry.id
749 )));
750 }
751 if entry.kind != Kind::Value && entry.return_type.is_some() {
752 return Err(CatalogError::validation(format!(
753 "{} '{}' declares a return type but is not a value",
754 entry.kind.as_str(),
755 entry.id
756 )));
757 }
758 for domain in entry.param_domains.iter().flatten() {
759 if !self.enum_by_domain.contains_key(domain) {
760 return Err(CatalogError::validation(format!(
761 "{} '{}' declares undeclared enum domain '{domain}'",
762 entry.kind.as_str(),
763 entry.id
764 )));
765 }
766 }
767 }
768 Ok(())
769 }
770
771 fn insert_enum(&mut self, domain: EnumFile) -> Result<()> {
772 let domain_index = self.enums.len();
773 if self.enum_by_domain.contains_key(&domain.domain) {
774 return Err(CatalogError::validation(format!(
775 "duplicate enum domain '{}'",
776 domain.domain
777 )));
778 }
779 let primary = self.locales[0].clone();
780 let mut domain_aliases = HashMap::new();
781 for (locale_str, alias_file) in domain.aliases {
782 let locale = Locale::new(&locale_str);
783 if !self.locales.contains(&locale) {
784 return Err(CatalogError::validation(format!(
785 "enum domain '{}' declares alias for undeclared locale '{}'",
786 domain.domain, locale
787 )));
788 }
789 let spellings = alias_file.into_spellings(&domain.domain, locale.as_str())?;
790 for spelling in &spellings {
791 let key = (locale.clone(), spelling.clone());
792 if let Some(existing) = self.enum_alias_to_domain.get(&key) {
793 return Err(CatalogError::validation(format!(
794 "duplicate enum domain alias '{spelling}' for '{}' and '{}' in locale '{}'",
795 existing, domain.domain, locale
796 )));
797 }
798 self.enum_alias_to_domain.insert(key, domain.domain.clone());
799 }
800 domain_aliases.insert(locale, spellings);
801 }
802 domain_aliases
803 .entry(primary.clone())
804 .or_insert_with(|| vec![domain.domain.clone()]);
805 self.enum_alias_to_domain
806 .entry((primary.clone(), domain.domain.clone()))
807 .or_insert_with(|| domain.domain.clone());
808 let mut members = Vec::new();
809 for (member_index, member) in domain.members.into_iter().enumerate() {
810 let mut aliases = HashMap::new();
811 for (locale_str, alias_file) in member.aliases {
812 let locale = Locale::new(&locale_str);
813 if !self.locales.contains(&locale) {
814 return Err(CatalogError::validation(format!(
815 "enum {}::{} declares alias for undeclared locale '{}'",
816 domain.domain, member.id, locale
817 )));
818 }
819 let spellings = alias_file.into_spellings(&member.id, locale.as_str())?;
820 for spelling in &spellings {
821 let key = (domain.domain.clone(), locale.clone(), spelling.clone());
822 if self.enum_alias_to_member.contains_key(&key) {
823 return Err(CatalogError::validation(format!(
824 "duplicate enum alias '{spelling}' in '{}' for locale '{}'",
825 domain.domain, locale
826 )));
827 }
828 self.enum_alias_to_member
829 .insert(key, (domain_index, member_index));
830 }
831 aliases.insert(locale, spellings);
832 }
833 if !aliases.contains_key(&primary) {
834 return Err(CatalogError::validation(format!(
835 "enum {}::{} is missing a '{}' alias",
836 domain.domain, member.id, primary
837 )));
838 }
839 members.push(EnumMember {
840 member: member.id,
841 aliases,
842 });
843 }
844 self.enum_by_domain
845 .insert(domain.domain.clone(), domain_index);
846 self.enums.push(EnumDomain {
847 domain: domain.domain,
848 aliases: domain_aliases,
849 members,
850 });
851 Ok(())
852 }
853}
854
855impl ExpectedDomain for Catalog {
862 fn expected_domain(&self, catalog_id: &str, arg_index: usize) -> Option<&str> {
863 for kind in [Kind::Action, Kind::Value] {
864 if let Some(entry) = self.entry(kind, catalog_id) {
865 if let Some(domain) = entry
866 .param_domains
867 .get(arg_index)
868 .and_then(Option::as_deref)
869 {
870 return Some(domain);
871 }
872 }
873 }
874 None
875 }
876}
877
878pub fn canonicalize(json: &str) -> Result<String> {
884 Catalog::load_unverified(json)?;
886 let value: serde_json::Value = serde_json::from_str(json)
887 .map_err(|error| CatalogError::malformed(format!("catalog data: {error}")))?;
888 serde_json::to_string_pretty(&value)
889 .map(|mut out| {
890 out.push('\n');
891 out
892 })
893 .map_err(|error| CatalogError::malformed(format!("cannot serialize catalog: {error}")))
894}
895
896pub fn build_canonical(json: &str) -> Result<String> {
900 let mut value: serde_json::Value = serde_json::from_str(json)
901 .map_err(|error| CatalogError::malformed(format!("catalog data: {error}")))?;
902 let digest = content_digest(json)?;
903 if let Some(object) = value.as_object_mut() {
904 object.insert("digest".to_string(), serde_json::Value::String(digest));
905 }
906 let output = serde_json::to_string_pretty(&value)
907 .map(|mut out| {
908 out.push('\n');
909 out
910 })
911 .map_err(|error| CatalogError::malformed(format!("cannot serialize catalog: {error}")))?;
912 Catalog::load(&output)?;
915 Ok(output)
916}
917
918pub fn content_digest(json: &str) -> Result<String> {
923 let mut value: serde_json::Value = serde_json::from_str(json)
924 .map_err(|error| CatalogError::malformed(format!("catalog data: {error}")))?;
925 if let Some(object) = value.as_object_mut() {
926 object.remove("digest");
927 }
928 let canonical = serde_json::to_string_pretty(&value)
929 .map_err(|error| CatalogError::malformed(format!("cannot serialize catalog: {error}")))?;
930 use sha2::{Digest, Sha256};
931 let mut hasher = Sha256::new();
932 hasher.update(canonical.as_bytes());
933 Ok(format!("{:x}", hasher.finalize()))
934}