1use std::{borrow::Cow, fmt, ops::Deref};
4
5use crate::{
6 json,
7 warning::{self, IntoCaveat},
8 Caveat, Verdict,
9};
10
11#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
13pub enum WarningKind {
14 ContainsEscapeCodes,
16
17 ContainsNonPrintableASCII,
19
20 InvalidType,
22
23 InvalidLengthMax { length: usize },
25
26 InvalidLengthExact { length: usize },
28
29 PreferUppercase,
34}
35
36impl fmt::Display for WarningKind {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::ContainsEscapeCodes => f.write_str("The string contains escape codes."),
40 Self::ContainsNonPrintableASCII => {
41 f.write_str("The string contains non-printable bytes.")
42 }
43 Self::InvalidType => f.write_str("The value should be a string."),
44 Self::InvalidLengthMax { length } => {
45 write!(
46 f,
47 "The string is longer than the max length `{length}` defined in the spec.",
48 )
49 }
50 Self::InvalidLengthExact { length } => {
51 write!(f, "The string should be length `{length}`.")
52 }
53 Self::PreferUppercase => {
54 write!(f, "Upper case is preffered")
55 }
56 }
57 }
58}
59
60impl warning::Kind for WarningKind {
61 fn id(&self) -> Cow<'static, str> {
62 match self {
63 Self::ContainsEscapeCodes => "contains_escape_codes".into(),
64 Self::ContainsNonPrintableASCII => "contains_non_printable_ascii".into(),
65 Self::InvalidType => "invalid_type".into(),
66 Self::InvalidLengthMax { .. } => "invalid_length_max".into(),
67 Self::InvalidLengthExact { .. } => "invalid_length_exact".into(),
68 Self::PreferUppercase => "prefer_upper_case".into(),
69 }
70 }
71}
72
73#[derive(Copy, Clone, Debug)]
81pub(crate) struct CiMaxLen<'buf, const MAX_LEN: usize>(&'buf str);
82
83impl<const MAX_LEN: usize> Deref for CiMaxLen<'_, MAX_LEN> {
84 type Target = str;
85
86 fn deref(&self) -> &Self::Target {
87 self.0
88 }
89}
90
91impl<const MAX_LEN: usize> fmt::Display for CiMaxLen<'_, MAX_LEN> {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 write!(f, "{}", self.0)
94 }
95}
96
97impl<const MAX_LEN: usize> IntoCaveat for CiMaxLen<'_, MAX_LEN> {
98 fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
99 Caveat::new(self, warnings)
100 }
101}
102
103impl<'buf, 'elem: 'buf, const MAX_LEN: usize> json::FromJson<'elem, 'buf>
104 for CiMaxLen<'buf, MAX_LEN>
105{
106 type WarningKind = WarningKind;
107
108 fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::WarningKind> {
109 let (s, mut warnings) = Base::from_json(elem)?.into_parts();
110
111 if s.len() > MAX_LEN {
112 warnings.with_elem(WarningKind::InvalidLengthMax { length: MAX_LEN }, elem);
113 }
114
115 Ok(Self(s.0).into_caveat(warnings))
116 }
117}
118
119#[derive(Copy, Clone, Debug)]
127pub(crate) struct CiExactLen<'buf, const LEN: usize>(&'buf str);
128
129impl<const LEN: usize> Deref for CiExactLen<'_, LEN> {
130 type Target = str;
131
132 fn deref(&self) -> &Self::Target {
133 self.0
134 }
135}
136
137impl<const LEN: usize> fmt::Display for CiExactLen<'_, LEN> {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(f, "{}", self.0)
140 }
141}
142
143impl<const LEN: usize> IntoCaveat for CiExactLen<'_, LEN> {
144 fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
145 Caveat::new(self, warnings)
146 }
147}
148
149impl<'buf, 'elem: 'buf, const LEN: usize> json::FromJson<'elem, 'buf> for CiExactLen<'buf, LEN> {
150 type WarningKind = WarningKind;
151
152 fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::WarningKind> {
153 let (s, mut warnings) = Base::from_json(elem)?.into_parts();
154
155 if s.len() != LEN {
156 warnings.with_elem(WarningKind::InvalidLengthExact { length: LEN }, elem);
157 }
158
159 Ok(Self(s.0).into_caveat(warnings))
160 }
161}
162
163#[derive(Copy, Clone, Debug)]
168struct Base<'buf>(&'buf str);
169
170impl Deref for Base<'_> {
171 type Target = str;
172
173 fn deref(&self) -> &Self::Target {
174 self.0
175 }
176}
177
178impl fmt::Display for Base<'_> {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 write!(f, "{}", self.0)
181 }
182}
183
184impl IntoCaveat for Base<'_> {
185 fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
186 Caveat::new(self, warnings)
187 }
188}
189
190impl<'buf, 'elem: 'buf> json::FromJson<'elem, 'buf> for Base<'buf> {
191 type WarningKind = WarningKind;
192
193 fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::WarningKind> {
194 let mut warnings = warning::Set::new();
195 let Some(id) = elem.as_raw_str() else {
196 warnings.with_elem(WarningKind::InvalidType, elem);
197 return Err(warnings);
198 };
199
200 let s = id.has_escapes(elem).ignore_warnings();
203 let s = match s {
204 json::decode::PendingStr::NoEscapes(s) => {
205 if check_printable(s) {
206 warnings.with_elem(WarningKind::ContainsNonPrintableASCII, elem);
207 }
208 s
209 }
210 json::decode::PendingStr::HasEscapes(escape_str) => {
211 warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
212 let decoded = escape_str.decode_escapes(elem).ignore_warnings();
214
215 if check_printable(&decoded) {
216 warnings.with_elem(WarningKind::ContainsNonPrintableASCII, elem);
217 }
218
219 escape_str.into_raw()
220 }
221 };
222
223 Ok(Self(s).into_caveat(warnings))
224 }
225}
226
227fn check_printable(s: &str) -> bool {
228 s.chars()
229 .any(|c| c.is_ascii_whitespace() || c.is_ascii_control())
230}
231
232#[cfg(test)]
233mod test {
234 use assert_matches::assert_matches;
235
236 use crate::{
237 json::{self, FromJson},
238 warning,
239 };
240
241 use super::{CiExactLen, CiMaxLen, WarningKind};
242
243 const LEN: usize = 3;
244
245 #[test]
246 fn should_parse_max_len() {
247 let input = "hel";
248 let (output, warnings) = test_max_len(input);
249 assert_matches!(warnings.into_kind_vec().as_slice(), []);
250 assert_eq!(output, input);
251 }
252
253 #[test]
254 fn should_fail_on_max_len() {
255 let input = "hello";
256 let (output, warnings) = test_max_len(input);
257 let length = assert_matches!(
258 warnings.into_kind_vec().as_slice(),
259 [WarningKind::InvalidLengthMax { length }] => *length
260 );
261 assert_eq!(length, LEN);
262 assert_eq!(output, input);
263 }
264
265 #[test]
266 fn should_parse_exact_len() {
267 let input = "hel";
268 let (output, warnings) = test_expect_len(input);
269 assert_matches!(warnings.into_kind_vec().as_slice(), []);
270 assert_eq!(output, input);
271 }
272
273 #[test]
274 fn should_fail_on_exact_len() {
275 let input = "hello";
276 let (output, warnings) = test_expect_len(input);
277 let length = assert_matches!(
278 warnings.into_kind_vec().as_slice(),
279 [WarningKind::InvalidLengthExact { length }] => *length
280 );
281 assert_eq!(length, LEN);
282 assert_eq!(output, input);
283 }
284
285 #[track_caller]
286 fn test_max_len(s: &str) -> (String, warning::Set<WarningKind>) {
287 let quoted_input = format!(r#""{s}""#);
288 let elem = json::parse("ed_input).unwrap();
289 let output = CiMaxLen::<'_, LEN>::from_json(&elem).unwrap();
290 let (output, warnings) = output.into_parts();
291 (output.to_string(), warnings)
292 }
293
294 #[track_caller]
295 fn test_expect_len(s: &str) -> (String, warning::Set<WarningKind>) {
296 let quoted_input = format!(r#""{s}""#);
297 let elem = json::parse("ed_input).unwrap();
298 let output = CiExactLen::<'_, LEN>::from_json(&elem).unwrap();
299 let (output, warnings) = output.into_parts();
300 (output.to_string(), warnings)
301 }
302}