Skip to main content

copybook_charset/
codepage.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Codepage identity and character-conversion policy.
3#![allow(clippy::missing_inline_in_public_items)]
4
5use serde::{Deserialize, Serialize};
6use std::{fmt, str::FromStr};
7
8/// Character encoding specification supported by copybook-rs.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum Codepage {
11    /// ASCII encoding.
12    ASCII,
13    /// EBCDIC Code Page 037 (US/Canada).
14    CP037,
15    /// EBCDIC Code Page 273 (Germany/Austria).
16    CP273,
17    /// EBCDIC Code Page 500 (International).
18    CP500,
19    /// EBCDIC Code Page 1047 (Open Systems).
20    CP1047,
21    /// EBCDIC Code Page 1140 (US/Canada with Euro).
22    CP1140,
23}
24
25#[derive(Debug, Clone, Copy)]
26struct CodepageMetadata {
27    display_name: &'static str,
28    description: &'static str,
29    code_page_number: Option<u16>,
30}
31
32impl CodepageMetadata {
33    const ASCII: Self = Self {
34        display_name: "ascii",
35        description: "ASCII encoding",
36        code_page_number: None,
37    };
38
39    const fn ebcdic(
40        display_name: &'static str,
41        description: &'static str,
42        code_page_number: u16,
43    ) -> Self {
44        Self {
45            display_name,
46            description,
47            code_page_number: Some(code_page_number),
48        }
49    }
50}
51
52impl Codepage {
53    const METADATA: [(Self, CodepageMetadata); 6] = [
54        (Self::ASCII, CodepageMetadata::ASCII),
55        (
56            Self::CP037,
57            CodepageMetadata::ebcdic("cp037", "EBCDIC Code Page 037 (US/Canada)", 37),
58        ),
59        (
60            Self::CP273,
61            CodepageMetadata::ebcdic("cp273", "EBCDIC Code Page 273 (Germany/Austria)", 273),
62        ),
63        (
64            Self::CP500,
65            CodepageMetadata::ebcdic("cp500", "EBCDIC Code Page 500 (International)", 500),
66        ),
67        (
68            Self::CP1047,
69            CodepageMetadata::ebcdic("cp1047", "EBCDIC Code Page 1047 (Open Systems)", 1047),
70        ),
71        (
72            Self::CP1140,
73            CodepageMetadata::ebcdic(
74                "cp1140",
75                "EBCDIC Code Page 1140 (US/Canada with Euro)",
76                1140,
77            ),
78        ),
79    ];
80
81    /// Return all supported codepages in their canonical display order.
82    #[must_use]
83    pub const fn variants() -> &'static [Self; 6] {
84        &[
85            Self::ASCII,
86            Self::CP037,
87            Self::CP273,
88            Self::CP500,
89            Self::CP1047,
90            Self::CP1140,
91        ]
92    }
93
94    const fn metadata(self) -> CodepageMetadata {
95        match self {
96            Self::ASCII => Self::METADATA[0].1,
97            Self::CP037 => Self::METADATA[1].1,
98            Self::CP273 => Self::METADATA[2].1,
99            Self::CP500 => Self::METADATA[3].1,
100            Self::CP1047 => Self::METADATA[4].1,
101            Self::CP1140 => Self::METADATA[5].1,
102        }
103    }
104
105    /// Check if this is ASCII.
106    #[must_use]
107    pub const fn is_ascii(self) -> bool {
108        self.metadata().code_page_number.is_none()
109    }
110
111    /// Check if this is an EBCDIC codepage.
112    #[must_use]
113    pub const fn is_ebcdic(self) -> bool {
114        !self.is_ascii()
115    }
116
117    /// Return the numeric codepage identifier, if this is an EBCDIC page.
118    #[must_use]
119    pub const fn code_page_number(self) -> Option<u16> {
120        self.metadata().code_page_number
121    }
122
123    /// Return a human-readable description.
124    #[must_use]
125    pub const fn description(self) -> &'static str {
126        self.metadata().description
127    }
128
129    /// Return the canonical lower-case spelling.
130    #[must_use]
131    pub const fn as_str(self) -> &'static str {
132        self.metadata().display_name
133    }
134
135    /// Parse a codepage and reject unknown values.
136    ///
137    /// # Errors
138    ///
139    /// Returns [`ParseCodepageError`] when `input` is not a supported
140    /// codepage spelling.
141    pub fn parse(input: &str) -> Result<Self, ParseCodepageError> {
142        Self::METADATA
143            .iter()
144            .find_map(|(codepage, metadata)| {
145                metadata
146                    .display_name
147                    .eq_ignore_ascii_case(input)
148                    .then_some(*codepage)
149            })
150            .ok_or_else(|| ParseCodepageError {
151                input: input.to_owned(),
152            })
153    }
154}
155
156impl fmt::Display for Codepage {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.write_str(self.as_str())
159    }
160}
161
162impl FromStr for Codepage {
163    type Err = ParseCodepageError;
164
165    fn from_str(input: &str) -> Result<Self, Self::Err> {
166        Self::parse(input)
167    }
168}
169
170/// Error returned by strict codepage parsing.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct ParseCodepageError {
173    input: String,
174}
175
176impl fmt::Display for ParseCodepageError {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(f, "unsupported codepage `{}`", self.input)
179    }
180}
181
182impl std::error::Error for ParseCodepageError {}
183
184/// Policy for handling characters that have no mapping.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186pub enum UnmappablePolicy {
187    /// Return an error.
188    Error,
189    /// Replace with U+FFFD during decode.
190    Replace,
191    /// Skip the character during decode.
192    Skip,
193}
194
195impl UnmappablePolicy {
196    /// Return the canonical lower-case spelling.
197    #[must_use]
198    pub const fn as_str(self) -> &'static str {
199        match self {
200            Self::Error => "error",
201            Self::Replace => "replace",
202            Self::Skip => "skip",
203        }
204    }
205
206    /// Parse a policy and reject unknown values.
207    ///
208    /// # Errors
209    ///
210    /// Returns [`ParseUnmappablePolicyError`] when `input` is not a supported
211    /// policy spelling.
212    pub fn parse(input: &str) -> Result<Self, ParseUnmappablePolicyError> {
213        match input.to_ascii_lowercase().as_str() {
214            "error" => Ok(Self::Error),
215            "replace" => Ok(Self::Replace),
216            "skip" => Ok(Self::Skip),
217            _ => Err(ParseUnmappablePolicyError {
218                input: input.to_owned(),
219            }),
220        }
221    }
222}
223
224impl fmt::Display for UnmappablePolicy {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        f.write_str(self.as_str())
227    }
228}
229
230impl FromStr for UnmappablePolicy {
231    type Err = ParseUnmappablePolicyError;
232
233    fn from_str(input: &str) -> Result<Self, Self::Err> {
234        Self::parse(input)
235    }
236}
237
238/// Error returned by strict unmappable-policy parsing.
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct ParseUnmappablePolicyError {
241    input: String,
242}
243
244impl fmt::Display for ParseUnmappablePolicyError {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        write!(
247            f,
248            "unsupported unmappable-character policy `{}`",
249            self.input
250        )
251    }
252}
253
254impl std::error::Error for ParseUnmappablePolicyError {}
255
256/// Return the padding byte for a codepage.
257#[must_use]
258pub const fn space_byte(codepage: Codepage) -> u8 {
259    match codepage {
260        Codepage::ASCII => 0x20,
261        Codepage::CP037
262        | Codepage::CP273
263        | Codepage::CP500
264        | Codepage::CP1047
265        | Codepage::CP1140 => 0x40,
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn parsing_codepage_metadata_and_validation() {
275        assert_eq!(
276            Codepage::CP037.description(),
277            "EBCDIC Code Page 037 (US/Canada)"
278        );
279        assert_eq!(Codepage::parse("CP1140"), Ok(Codepage::CP1140));
280        assert!(Codepage::parse("unknown").is_err());
281        assert_eq!(
282            UnmappablePolicy::parse("REPLACE"),
283            Ok(UnmappablePolicy::Replace)
284        );
285        assert!(UnmappablePolicy::parse("unknown").is_err());
286        assert_eq!("cp037".parse::<Codepage>(), Ok(Codepage::CP037));
287        assert_eq!(
288            "replace".parse::<UnmappablePolicy>(),
289            Ok(UnmappablePolicy::Replace)
290        );
291        assert_eq!(
292            "unknown".parse::<Codepage>(),
293            Err(ParseCodepageError {
294                input: "unknown".to_owned(),
295            })
296        );
297        assert_eq!(
298            "unknown".parse::<UnmappablePolicy>(),
299            Err(ParseUnmappablePolicyError {
300                input: "unknown".to_owned(),
301            })
302        );
303    }
304
305    #[test]
306    fn parsing_codepage_padding_bytes() {
307        assert_eq!(space_byte(Codepage::ASCII), 0x20);
308        assert_eq!(space_byte(Codepage::CP037), 0x40);
309    }
310}