uqa_analysis/
normalization.rs1use serde::{Deserialize, Serialize};
10use uqa_core::memory::{Budgeted, MemoryBudget};
11
12use crate::{AnalysisError, AnalysisResult};
13
14mod resolved;
15pub(crate) mod text;
16pub(crate) use resolved::PreparedNormalization;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
30pub enum NormalizationConfig {
31 #[serde(deserialize_with = "deserialize_empty")]
32 Unavailable,
33 #[serde(rename = "cjk_width", deserialize_with = "deserialize_empty")]
34 CJKWidth,
35 #[cfg(any(feature = "nori", feature = "kuromoji"))]
36 UnicodeSimpleLowercase { profile: UnicodeProfile },
37 #[cfg(any(feature = "nori", feature = "kuromoji"))]
38 #[serde(rename = "cjk_width_simple_lowercase")]
39 CJKWidthSimpleLowercase { profile: UnicodeProfile },
40}
41
42fn deserialize_empty<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<(), D::Error> {
43 #[derive(Deserialize)]
44 #[serde(deny_unknown_fields)]
45 struct Empty {}
46 Empty::deserialize(deserializer).map(|_| ())
47}
48
49#[cfg(any(feature = "nori", feature = "kuromoji"))]
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(tag = "provider", rename_all = "snake_case", deny_unknown_fields)]
53pub enum UnicodeProfile {
54 #[cfg(feature = "nori")]
55 Nori { dictionary: String },
56 #[cfg(feature = "kuromoji")]
57 Kuromoji { dictionary: String },
58}
59
60impl NormalizationConfig {
61 pub(crate) fn width(&self) -> bool {
62 match self {
63 Self::CJKWidth => true,
64 #[cfg(any(feature = "nori", feature = "kuromoji"))]
65 Self::CJKWidthSimpleLowercase { .. } => true,
66 _ => false,
67 }
68 }
69
70 pub(crate) fn stage_count(&self) -> usize {
71 match self {
72 Self::Unavailable => 0,
73 Self::CJKWidth => 1,
74 #[cfg(any(feature = "nori", feature = "kuromoji"))]
75 Self::UnicodeSimpleLowercase { .. } => 1,
76 #[cfg(any(feature = "nori", feature = "kuromoji"))]
77 Self::CJKWidthSimpleLowercase { .. } => 2,
78 }
79 }
80
81 #[cfg(any(feature = "nori", feature = "kuromoji"))]
82 pub(crate) fn profile(&self) -> Option<&UnicodeProfile> {
83 match self {
84 Self::UnicodeSimpleLowercase { profile }
85 | Self::CJKWidthSimpleLowercase { profile } => Some(profile),
86 _ => None,
87 }
88 }
89
90 #[cfg(any(feature = "nori", feature = "kuromoji"))]
91 pub(crate) fn profile_mut(&mut self) -> Option<&mut UnicodeProfile> {
92 match self {
93 Self::UnicodeSimpleLowercase { profile }
94 | Self::CJKWidthSimpleLowercase { profile } => Some(profile),
95 _ => None,
96 }
97 }
98
99 #[cfg(any(feature = "nori", feature = "kuromoji"))]
100 pub(crate) fn validate(&self) -> AnalysisResult<()> {
101 match self.profile() {
102 #[cfg(feature = "nori")]
103 Some(UnicodeProfile::Nori { dictionary }) => {
104 crate::nori::NoriResources::default()
105 .load(&crate::nori::pipeline::request(dictionary)?)?;
106 }
107 #[cfg(feature = "kuromoji")]
108 Some(UnicodeProfile::Kuromoji { dictionary }) => {
109 crate::kuromoji::KuromojiResources::default()
110 .load(&crate::kuromoji::pipeline::request(dictionary)?)?;
111 }
112 None => {}
113 }
114 Ok(())
115 }
116}
117
118struct WidthPolicy;
119impl text::Policy for WidthPolicy {
120 fn input(&self, length: usize) -> AnalysisResult<()> {
121 crate::descriptor::limits::check_limit(
122 "normalization input UTF-16 units",
123 length,
124 16 * 1024 * 1024,
125 )
126 }
127 fn output(&self, length: usize) -> AnalysisResult<()> {
128 crate::descriptor::limits::check_limit(
129 "normalization output UTF-16 units",
130 length,
131 64 * 1024 * 1024,
132 )
133 }
134 fn invalid_scalar(&self) -> AnalysisError {
135 AnalysisError::Descriptor("normalization produced an invalid scalar result")
136 }
137}
138
139fn width_budgeted(
140 input: &str,
141 budget: &MemoryBudget,
142 poll: &mut dyn FnMut() -> AnalysisResult<()>,
143) -> AnalysisResult<Budgeted<String>> {
144 text::run(input, true, &WidthPolicy, budget, poll)
145}