Skip to main content

ocpi_tariffs/
string.rs

1//! Case Insensitive String. Only printable ASCII allowed.
2
3#[cfg(test)]
4mod test;
5
6#[cfg(test)]
7mod test_from_schema;
8
9#[cfg(test)]
10mod test_reasonable_str;
11
12use std::{fmt, ops::Deref};
13
14use crate::{
15    json, schema,
16    warning::{self, IntoCaveat as _},
17    FromSchema, Verdict,
18};
19
20/// The warnings that can happen when parsing a case-insensitive string.
21#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
22pub enum Warning {
23    /// There should be no escape codes in a `CiString`.
24    ContainsEscapeCodes,
25
26    /// There should only be printable ASCII bytes in a `CiString`.
27    ContainsNonPrintableASCII,
28
29    /// The JSON value given is not a string.
30    InvalidType { type_found: json::ValueKind },
31
32    /// The length of the string exceeds the specs constraint.
33    InvalidLengthMax { length: usize },
34
35    /// The length of the string is not equal to the specs constraint.
36    InvalidLengthExact { length: usize },
37
38    /// The casing of the string is not common practice.
39    ///
40    /// Note: This is not enforced by the string types in this module, but can be used
41    /// by linting code to signal that the casing of a given string is unorthodox.
42    PreferUppercase,
43}
44
45impl Warning {
46    /// Create a `Warning` for invalid type.
47    fn invalid_type(elem: &json::Element<'_>) -> Self {
48        Self::InvalidType {
49            type_found: elem.value().kind(),
50        }
51    }
52}
53
54impl fmt::Display for Warning {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::ContainsEscapeCodes => f.write_str("The string contains escape codes."),
58            Self::ContainsNonPrintableASCII => {
59                f.write_str("The string contains non-printable bytes.")
60            }
61            Self::InvalidType { type_found } => {
62                write!(f, "The value should be a string but is `{type_found}`")
63            }
64            Self::InvalidLengthMax { length } => {
65                write!(
66                    f,
67                    "The string is longer than the max length `{length}` defined in the spec.",
68                )
69            }
70            Self::InvalidLengthExact { length } => {
71                write!(f, "The string should be length `{length}`.")
72            }
73            Self::PreferUppercase => {
74                write!(f, "Upper case is preferred")
75            }
76        }
77    }
78}
79
80impl crate::Warning for Warning {
81    fn id(&self) -> warning::Id {
82        match self {
83            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
84            Self::ContainsNonPrintableASCII => {
85                warning::Id::from_static("contains_non_printable_ascii")
86            }
87            Self::InvalidType { type_found } => {
88                warning::Id::from_string(format!("invalid_type({type_found})"))
89            }
90            Self::InvalidLengthMax { .. } => warning::Id::from_static("invalid_length_max"),
91            Self::InvalidLengthExact { .. } => warning::Id::from_static("invalid_length_exact"),
92            Self::PreferUppercase => warning::Id::from_static("prefer_upper_case"),
93        }
94    }
95}
96
97/// String that can have `[0..=MAX_LEN]` bytes.
98///
99/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
100/// Case insensitivity is not enforced.
101///
102/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>.
103/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>.
104#[derive(Copy, Clone, Debug)]
105pub(crate) struct CiMaxLen<'buf, const MAX_LEN: usize>(&'buf str);
106
107impl<const MAX_LEN: usize> Deref for CiMaxLen<'_, MAX_LEN> {
108    type Target = str;
109
110    fn deref(&self) -> &Self::Target {
111        self.0
112    }
113}
114
115impl<const MAX_LEN: usize> fmt::Display for CiMaxLen<'_, MAX_LEN> {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        write!(f, "{}", self.0)
118    }
119}
120
121impl<'buf, const MAX_LEN: usize> json::FromJson<'buf> for CiMaxLen<'buf, MAX_LEN> {
122    type Warning = Warning;
123
124    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
125        let (s, mut warnings) = Base::from_json(elem)?.into_parts();
126
127        if s.len() > MAX_LEN {
128            warnings.insert(elem, Warning::InvalidLengthMax { length: MAX_LEN });
129        }
130
131        Ok(Self(s.0).into_caveat(warnings))
132    }
133}
134
135impl<'buf, const MAX_LEN: usize> FromSchema<'buf, schema::Str<'buf>> for CiMaxLen<'buf, MAX_LEN> {
136    type Warning = Warning;
137
138    fn from_schema(source: &schema::Str<'buf>) -> Verdict<Self, Self::Warning> {
139        let (s, mut warnings) = Base::from_schema(source)?.into_parts();
140
141        if s.len() > MAX_LEN {
142            warnings.insert(
143                source.element(),
144                Warning::InvalidLengthMax { length: MAX_LEN },
145            );
146        }
147
148        Ok(Self(s.0).into_caveat(warnings))
149    }
150}
151
152/// String that can have `LEN` bytes exactly.
153///
154/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
155/// Case insensitivity is not enforced.
156///
157/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>.
158/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>.
159#[derive(Copy, Clone, Debug)]
160pub(crate) struct CiExactLen<'buf, const LEN: usize>(&'buf str);
161
162impl<const LEN: usize> Deref for CiExactLen<'_, LEN> {
163    type Target = str;
164
165    fn deref(&self) -> &Self::Target {
166        self.0
167    }
168}
169
170impl<const LEN: usize> fmt::Display for CiExactLen<'_, LEN> {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(f, "{}", self.0)
173    }
174}
175
176impl<'buf, const LEN: usize> json::FromJson<'buf> for CiExactLen<'buf, LEN> {
177    type Warning = Warning;
178
179    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
180        let (s, mut warnings) = Base::from_json(elem)?.into_parts();
181
182        if s.len() != LEN {
183            warnings.insert(elem, Warning::InvalidLengthExact { length: LEN });
184        }
185
186        Ok(Self(s.0).into_caveat(warnings))
187    }
188}
189
190impl<'buf, const LEN: usize> FromSchema<'buf, schema::Str<'buf>> for CiExactLen<'buf, LEN> {
191    type Warning = Warning;
192
193    fn from_schema(source: &schema::Str<'buf>) -> Verdict<Self, Self::Warning> {
194        let (s, mut warnings) = Base::from_schema(source)?.into_parts();
195
196        if s.len() != LEN {
197            warnings.insert(
198                source.element(),
199                Warning::InvalidLengthExact { length: LEN },
200            );
201        }
202
203        Ok(Self(s.0).into_caveat(warnings))
204    }
205}
206
207/// Case Insensitive String. Only printable ASCII allowed. (Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed).
208///
209/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>.
210/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>.
211#[derive(Copy, Clone, Debug)]
212struct Base<'buf>(&'buf str);
213
214impl Deref for Base<'_> {
215    type Target = str;
216
217    fn deref(&self) -> &Self::Target {
218        self.0
219    }
220}
221
222impl fmt::Display for Base<'_> {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        write!(f, "{}", self.0)
225    }
226}
227
228impl<'buf> json::FromJson<'buf> for Base<'buf> {
229    type Warning = Warning;
230
231    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
232        let mut warnings = warning::Set::new();
233        let Some(raw) = elem.to_raw_str() else {
234            return warnings.bail(elem, Warning::invalid_type(elem));
235        };
236
237        // A `CiString` should contain neither escapes nor non-printable ASCII; both are
238        // detected in a single pass without decoding into a fresh allocation.
239        let issues = raw.lexical_issues();
240        if issues.escapes {
241            warnings.insert(elem, Warning::ContainsEscapeCodes);
242        }
243        if issues.non_printable_ascii {
244            warnings.insert(elem, Warning::ContainsNonPrintableASCII);
245        }
246
247        Ok(Self(raw.as_unescaped_str()).into_caveat(warnings))
248    }
249}
250
251impl<'buf> FromSchema<'buf, schema::Str<'buf>> for Base<'buf> {
252    type Warning = Warning;
253
254    fn from_schema(source: &schema::Str<'buf>) -> Verdict<Self, Self::Warning> {
255        let mut warnings = warning::Set::new();
256        let elem = source.element();
257        let raw = source.value();
258
259        // The schema confirmed the value is a string, so there is no kind check. A
260        // `CiString` should contain neither escapes nor non-printable ASCII; both are
261        // detected in a single pass without decoding into a fresh allocation.
262        let issues = raw.lexical_issues();
263        if issues.escapes {
264            warnings.insert(elem, Warning::ContainsEscapeCodes);
265        }
266        if issues.non_printable_ascii {
267            warnings.insert(elem, Warning::ContainsNonPrintableASCII);
268        }
269
270        Ok(Self(raw.as_unescaped_str()).into_caveat(warnings))
271    }
272}
273
274/// The size of the input `str` exceeds the maximum deemed reasonable.
275pub(crate) struct SizeExceedsMax(());
276
277impl std::error::Error for SizeExceedsMax {}
278
279impl fmt::Debug for SizeExceedsMax {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        f.debug_tuple("SizeExceedsMax").finish()
282    }
283}
284
285impl fmt::Display for SizeExceedsMax {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        write!(
288            f,
289            "The size of the input string exceeds the maximum length of {} megabytes",
290            ReasonableLen::FACTOR
291        )
292    }
293}
294
295/// A `str` that is checked for having a reasonable size.
296#[derive(Copy, Clone)]
297pub(crate) struct ReasonableLen<'buf>(&'buf str);
298
299impl<'buf> ReasonableLen<'buf> {
300    /// One million bytes of information.
301    const MEGA: usize = 1_000_000;
302
303    /// Limit the string to this many megabyte.
304    pub(crate) const FACTOR: usize = 5;
305
306    /// The maximum allowed size for a `str` given to a parse function.
307    ///
308    /// If the input `str` exceeds this size, a [`SizeExceedsMax`] is returned.
309    ///
310    /// NOTE: Currently the largest tariff at `NLENE` is ~440 kilobyte and the largest CDR is ~1.1 megabytes.
311    ///
312    /// NOTE: The motivation for a limit is to avoid parsing unseasonably large JSON objects
313    /// whether supplied through incompetence or maliciousness. Large JSON objects can be constructed
314    /// to have many warnings. This could bog down the function processing the JSON object.
315    pub(crate) const MAX_STR_INPUT_LEN: usize = Self::FACTOR * Self::MEGA;
316
317    /// Create new `ReasonableLen` object.
318    pub(crate) fn new(s: &'buf str) -> Result<ReasonableLen<'buf>, SizeExceedsMax> {
319        if s.len() >= Self::MAX_STR_INPUT_LEN {
320            return Err(SizeExceedsMax(()));
321        }
322
323        Ok(Self(s))
324    }
325
326    /// Unpack the contained `str`.
327    pub(crate) fn into_inner(self) -> &'buf str {
328        self.0
329    }
330}