1use std::{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 Warning {
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 Warning {
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 crate::Warning for Warning {
61 fn id(&self) -> warning::Id {
62 match self {
63 Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
64 Self::ContainsNonPrintableASCII => {
65 warning::Id::from_static("contains_non_printable_ascii")
66 }
67 Self::InvalidType => warning::Id::from_static("invalid_type"),
68 Self::InvalidLengthMax { .. } => warning::Id::from_static("invalid_length_max"),
69 Self::InvalidLengthExact { .. } => warning::Id::from_static("invalid_length_exact"),
70 Self::PreferUppercase => warning::Id::from_static("prefer_upper_case"),
71 }
72 }
73}
74
75#[derive(Copy, Clone, Debug)]
83pub(crate) struct CiMaxLen<'buf, const MAX_LEN: usize>(&'buf str);
84
85impl<const MAX_LEN: usize> Deref for CiMaxLen<'_, MAX_LEN> {
86 type Target = str;
87
88 fn deref(&self) -> &Self::Target {
89 self.0
90 }
91}
92
93impl<const MAX_LEN: usize> fmt::Display for CiMaxLen<'_, MAX_LEN> {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(f, "{}", self.0)
96 }
97}
98
99impl<const MAX_LEN: usize> IntoCaveat for CiMaxLen<'_, MAX_LEN> {
100 fn into_caveat<W: crate::Warning>(self, warnings: warning::Set<W>) -> Caveat<Self, W> {
101 Caveat::new(self, warnings)
102 }
103}
104
105impl<'buf, 'elem: 'buf, const MAX_LEN: usize> json::FromJson<'elem, 'buf>
106 for CiMaxLen<'buf, MAX_LEN>
107{
108 type Warning = Warning;
109
110 fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::Warning> {
111 let (s, mut warnings) = Base::from_json(elem)?.into_parts();
112
113 if s.len() > MAX_LEN {
114 warnings.with_elem(Warning::InvalidLengthMax { length: MAX_LEN }, elem);
115 }
116
117 Ok(Self(s.0).into_caveat(warnings))
118 }
119}
120
121#[derive(Copy, Clone, Debug)]
129pub(crate) struct CiExactLen<'buf, const LEN: usize>(&'buf str);
130
131impl<const LEN: usize> Deref for CiExactLen<'_, LEN> {
132 type Target = str;
133
134 fn deref(&self) -> &Self::Target {
135 self.0
136 }
137}
138
139impl<const LEN: usize> fmt::Display for CiExactLen<'_, LEN> {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 write!(f, "{}", self.0)
142 }
143}
144
145impl<const LEN: usize> IntoCaveat for CiExactLen<'_, LEN> {
146 fn into_caveat<W: crate::Warning>(self, warnings: warning::Set<W>) -> Caveat<Self, W> {
147 Caveat::new(self, warnings)
148 }
149}
150
151impl<'buf, 'elem: 'buf, const LEN: usize> json::FromJson<'elem, 'buf> for CiExactLen<'buf, LEN> {
152 type Warning = Warning;
153
154 fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::Warning> {
155 let (s, mut warnings) = Base::from_json(elem)?.into_parts();
156
157 if s.len() != LEN {
158 warnings.with_elem(Warning::InvalidLengthExact { length: LEN }, elem);
159 }
160
161 Ok(Self(s.0).into_caveat(warnings))
162 }
163}
164
165#[derive(Copy, Clone, Debug)]
170struct Base<'buf>(&'buf str);
171
172impl Deref for Base<'_> {
173 type Target = str;
174
175 fn deref(&self) -> &Self::Target {
176 self.0
177 }
178}
179
180impl fmt::Display for Base<'_> {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(f, "{}", self.0)
183 }
184}
185
186impl IntoCaveat for Base<'_> {
187 fn into_caveat<W: crate::Warning>(self, warnings: warning::Set<W>) -> Caveat<Self, W> {
188 Caveat::new(self, warnings)
189 }
190}
191
192impl<'buf, 'elem: 'buf> json::FromJson<'elem, 'buf> for Base<'buf> {
193 type Warning = Warning;
194
195 fn from_json(elem: &'elem json::Element<'buf>) -> Verdict<Self, Self::Warning> {
196 let mut warnings = warning::Set::new();
197 let Some(id) = elem.as_raw_str() else {
198 return warnings.bail(Warning::InvalidType, elem);
199 };
200
201 let s = id.has_escapes(elem).ignore_warnings();
204 let s = match s {
205 json::decode::PendingStr::NoEscapes(s) => {
206 if check_printable(s) {
207 warnings.with_elem(Warning::ContainsNonPrintableASCII, elem);
208 }
209 s
210 }
211 json::decode::PendingStr::HasEscapes(escape_str) => {
212 warnings.with_elem(Warning::ContainsEscapeCodes, elem);
213 let decoded = escape_str.decode_escapes(elem).ignore_warnings();
215
216 if check_printable(&decoded) {
217 warnings.with_elem(Warning::ContainsNonPrintableASCII, elem);
218 }
219
220 escape_str.into_raw()
221 }
222 };
223
224 Ok(Self(s).into_caveat(warnings))
225 }
226}
227
228fn check_printable(s: &str) -> bool {
229 s.chars()
230 .any(|c| c.is_ascii_whitespace() || c.is_ascii_control())
231}
232
233#[cfg(test)]
234mod test {
235 #![allow(
236 clippy::indexing_slicing,
237 reason = "unwraps are allowed anywhere in tests"
238 )]
239
240 use assert_matches::assert_matches;
241
242 use crate::{
243 json::{self, FromJson},
244 warning,
245 };
246
247 use super::{CiExactLen, CiMaxLen, Warning};
248
249 const LEN: usize = 3;
250
251 #[test]
252 fn should_parse_max_len() {
253 let input = "hel";
254 let (output, warnings) = test_max_len(input);
255 assert!(warnings.is_empty(), "{warnings:#?}");
256 assert_eq!(output, input);
257 }
258
259 #[test]
260 fn should_fail_on_max_len() {
261 let input = "hello";
262 let (output, warnings) = test_max_len(input);
263 let warnings = warnings.into_path_as_str_map();
264 let warnings = &warnings["$"];
265 let length = assert_matches!(
266 warnings.as_slice(),
267 [Warning::InvalidLengthMax { length }] => *length
268 );
269 assert_eq!(length, LEN);
270 assert_eq!(output, input);
271 }
272
273 #[test]
274 fn should_parse_exact_len() {
275 let input = "hel";
276 let (output, warnings) = test_expect_len(input);
277 assert!(warnings.is_empty(), "{warnings:#?}");
278 assert_eq!(output, input);
279 }
280
281 #[test]
282 fn should_fail_on_exact_len() {
283 let input = "hello";
284 let (output, warnings) = test_expect_len(input);
285 let warnings = warnings.into_path_as_str_map();
286 let warnings = &warnings["$"];
287 let length = assert_matches!(
288 warnings.as_slice(),
289 [Warning::InvalidLengthExact { length }] => *length
290 );
291 assert_eq!(length, LEN);
292 assert_eq!(output, input);
293 }
294
295 #[track_caller]
296 fn test_max_len(s: &str) -> (String, warning::Set<Warning>) {
297 let quoted_input = format!(r#""{s}""#);
298 let elem = json::parse("ed_input).unwrap();
299 let output = CiMaxLen::<'_, LEN>::from_json(&elem).unwrap();
300 let (output, warnings) = output.into_parts();
301 (output.to_string(), warnings)
302 }
303
304 #[track_caller]
305 fn test_expect_len(s: &str) -> (String, warning::Set<Warning>) {
306 let quoted_input = format!(r#""{s}""#);
307 let elem = json::parse("ed_input).unwrap();
308 let output = CiExactLen::<'_, LEN>::from_json(&elem).unwrap();
309 let (output, warnings) = output.into_parts();
310 (output.to_string(), warnings)
311 }
312}