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