1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2#[cfg(feature = "clap")]
9use clap::ValueEnum;
10use serde::{Deserialize, Serialize};
11use std::str::FromStr;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "clap", derive(ValueEnum))]
27pub enum Codepage {
28 ASCII,
30 #[cfg_attr(feature = "clap", value(name = "cp037"))]
32 CP037,
33 #[cfg_attr(feature = "clap", value(name = "cp273"))]
35 CP273,
36 #[cfg_attr(feature = "clap", value(name = "cp500"))]
38 CP500,
39 #[cfg_attr(feature = "clap", value(name = "cp1047"))]
41 CP1047,
42 #[cfg_attr(feature = "clap", value(name = "cp1140"))]
44 CP1140,
45}
46
47#[derive(Debug, Clone, Copy)]
48struct CodepageMetadata {
49 display_name: &'static str,
50 description: &'static str,
51 code_page_number: Option<u16>,
52}
53
54impl CodepageMetadata {
55 const ASCII: Self = Self {
56 display_name: "ascii",
57 description: "ASCII encoding",
58 code_page_number: None,
59 };
60
61 const fn ebcdic(
62 display_name: &'static str,
63 description: &'static str,
64 code_page_number: u16,
65 ) -> Self {
66 Self {
67 display_name,
68 description,
69 code_page_number: Some(code_page_number),
70 }
71 }
72}
73
74impl Codepage {
75 const METADATA: [(Self, CodepageMetadata); 6] = [
76 (Self::ASCII, CodepageMetadata::ASCII),
77 (
78 Self::CP037,
79 CodepageMetadata::ebcdic("cp037", "EBCDIC Code Page 037 (US/Canada)", 37),
80 ),
81 (
82 Self::CP273,
83 CodepageMetadata::ebcdic("cp273", "EBCDIC Code Page 273 (Germany/Austria)", 273),
84 ),
85 (
86 Self::CP500,
87 CodepageMetadata::ebcdic("cp500", "EBCDIC Code Page 500 (International)", 500),
88 ),
89 (
90 Self::CP1047,
91 CodepageMetadata::ebcdic("cp1047", "EBCDIC Code Page 1047 (Open Systems)", 1047),
92 ),
93 (
94 Self::CP1140,
95 CodepageMetadata::ebcdic(
96 "cp1140",
97 "EBCDIC Code Page 1140 (US/Canada with Euro)",
98 1140,
99 ),
100 ),
101 ];
102
103 const fn metadata(self) -> CodepageMetadata {
104 match self {
105 Self::ASCII => Self::METADATA[0].1,
106 Self::CP037 => Self::METADATA[1].1,
107 Self::CP273 => Self::METADATA[2].1,
108 Self::CP500 => Self::METADATA[3].1,
109 Self::CP1047 => Self::METADATA[4].1,
110 Self::CP1140 => Self::METADATA[5].1,
111 }
112 }
113
114 #[must_use]
116 #[inline]
117 pub const fn is_ascii(self) -> bool {
118 self.metadata().code_page_number.is_none()
119 }
120
121 #[must_use]
123 #[inline]
124 pub const fn is_ebcdic(self) -> bool {
125 !self.is_ascii()
126 }
127
128 #[must_use]
130 #[inline]
131 pub const fn code_page_number(self) -> Option<u16> {
132 self.metadata().code_page_number
133 }
134
135 #[must_use]
137 #[inline]
138 pub const fn description(self) -> &'static str {
139 self.metadata().description
140 }
141
142 #[must_use]
144 #[inline]
145 pub const fn as_str(self) -> &'static str {
146 self.metadata().display_name
147 }
148}
149
150impl std::fmt::Display for Codepage {
151 #[inline]
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 f.write_str(self.as_str())
154 }
155}
156
157impl FromStr for Codepage {
158 type Err = std::convert::Infallible;
159
160 #[inline]
161 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
162 Ok(Self::METADATA
163 .iter()
164 .find_map(|(codepage, metadata)| {
165 metadata
166 .display_name
167 .eq_ignore_ascii_case(s)
168 .then_some(*codepage)
169 })
170 .unwrap_or(Self::CP037))
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[cfg_attr(feature = "clap", derive(ValueEnum))]
187pub enum UnmappablePolicy {
188 #[cfg_attr(feature = "clap", value(name = "error"))]
190 Error,
191 #[cfg_attr(feature = "clap", value(name = "replace"))]
193 Replace,
194 #[cfg_attr(feature = "clap", value(name = "skip"))]
196 Skip,
197}
198
199impl UnmappablePolicy {
200 const NAMES: [(Self, &'static str); 3] = [
201 (Self::Error, "error"),
202 (Self::Replace, "replace"),
203 (Self::Skip, "skip"),
204 ];
205
206 #[must_use]
208 #[inline]
209 pub const fn as_str(self) -> &'static str {
210 match self {
211 Self::Error => Self::NAMES[0].1,
212 Self::Replace => Self::NAMES[1].1,
213 Self::Skip => Self::NAMES[2].1,
214 }
215 }
216}
217
218impl std::fmt::Display for UnmappablePolicy {
219 #[inline]
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 f.write_str(self.as_str())
222 }
223}
224
225impl FromStr for UnmappablePolicy {
226 type Err = std::convert::Infallible;
227
228 #[inline]
229 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
230 Ok(Self::NAMES
231 .iter()
232 .find_map(|(policy, name)| name.eq_ignore_ascii_case(s).then_some(*policy))
233 .unwrap_or(Self::Error))
235 }
236}
237
238static EBCDIC_ZONED_SIGNS: [(bool, bool); 16] = [
240 (false, false), (false, false), (false, false), (false, false), (false, false), (false, false), (false, false), (false, false), (false, false), (false, false), (false, false), (false, false), (true, false), (true, true), (false, false), (true, false), ];
257
258static ASCII_ZONED_SIGNS: [(bool, bool); 16] = [(false, false); 16];
261
262#[must_use]
264#[inline]
265pub fn get_zoned_sign_table(codepage: Codepage) -> &'static [(bool, bool); 16] {
266 match codepage {
267 Codepage::ASCII => &ASCII_ZONED_SIGNS,
268 _ => &EBCDIC_ZONED_SIGNS,
269 }
270}
271
272#[must_use]
276#[inline]
277pub const fn space_byte(codepage: Codepage) -> u8 {
278 match codepage {
279 Codepage::ASCII => 0x20,
280 Codepage::CP037
281 | Codepage::CP273
282 | Codepage::CP500
283 | Codepage::CP1047
284 | Codepage::CP1140 => 0x40,
285 }
286}
287
288#[cfg(test)]
289#[allow(clippy::expect_used, clippy::unwrap_used)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_space_byte_ascii() {
295 assert_eq!(space_byte(Codepage::ASCII), 0x20);
296 }
297
298 #[test]
299 fn test_space_byte_ebcdic() {
300 assert_eq!(space_byte(Codepage::CP037), 0x40);
301 assert_eq!(space_byte(Codepage::CP273), 0x40);
302 assert_eq!(space_byte(Codepage::CP500), 0x40);
303 assert_eq!(space_byte(Codepage::CP1047), 0x40);
304 assert_eq!(space_byte(Codepage::CP1140), 0x40);
305 }
306
307 #[test]
308 fn test_codepage_is_ascii() {
309 assert!(Codepage::ASCII.is_ascii());
310 assert!(!Codepage::CP037.is_ascii());
311 }
312
313 #[test]
314 fn test_codepage_is_ebcdic() {
315 assert!(!Codepage::ASCII.is_ebcdic());
316 assert!(Codepage::CP037.is_ebcdic());
317 }
318
319 #[test]
320 fn test_codepage_code_page_number() {
321 assert_eq!(Codepage::ASCII.code_page_number(), None);
322 assert_eq!(Codepage::CP037.code_page_number(), Some(37));
323 assert_eq!(Codepage::CP1140.code_page_number(), Some(1140));
324 }
325
326 #[test]
327 fn test_codepage_from_str_defaults_to_cp037() {
328 assert_eq!(
329 <Codepage as std::str::FromStr>::from_str("unknown").unwrap(),
330 Codepage::CP037
331 );
332 }
333
334 #[test]
335 fn test_unmappable_policy_from_str_defaults_to_error() {
336 assert_eq!(
337 <UnmappablePolicy as std::str::FromStr>::from_str("unknown").unwrap(),
338 UnmappablePolicy::Error
339 );
340 }
341
342 #[test]
343 fn test_get_zoned_sign_table_ascii_is_unsigned() {
344 let table = get_zoned_sign_table(Codepage::ASCII);
345 assert!(table.iter().all(|entry| *entry == (false, false)));
346 }
347
348 #[test]
349 fn test_get_zoned_sign_table_ebcdic_has_signed_entries() {
350 let table = get_zoned_sign_table(Codepage::CP037);
351 assert_eq!(table[0xC], (true, false));
352 assert_eq!(table[0xD], (true, true));
353 assert_eq!(table[0xF], (true, false));
354 }
355
356 #[test]
359 fn test_codepage_description_all_variants() {
360 assert_eq!(Codepage::ASCII.description(), "ASCII encoding");
361 assert_eq!(
362 Codepage::CP037.description(),
363 "EBCDIC Code Page 037 (US/Canada)"
364 );
365 assert_eq!(
366 Codepage::CP273.description(),
367 "EBCDIC Code Page 273 (Germany/Austria)"
368 );
369 assert_eq!(
370 Codepage::CP500.description(),
371 "EBCDIC Code Page 500 (International)"
372 );
373 assert_eq!(
374 Codepage::CP1047.description(),
375 "EBCDIC Code Page 1047 (Open Systems)"
376 );
377 assert_eq!(
378 Codepage::CP1140.description(),
379 "EBCDIC Code Page 1140 (US/Canada with Euro)"
380 );
381 }
382
383 #[test]
386 fn test_codepage_display_all_variants() {
387 assert_eq!(format!("{}", Codepage::ASCII), "ascii");
388 assert_eq!(format!("{}", Codepage::CP037), "cp037");
389 assert_eq!(format!("{}", Codepage::CP273), "cp273");
390 assert_eq!(format!("{}", Codepage::CP500), "cp500");
391 assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
392 assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
393 }
394
395 #[test]
398 fn test_codepage_from_str_all_valid_variants() {
399 assert_eq!(
400 <Codepage as std::str::FromStr>::from_str("ascii").unwrap(),
401 Codepage::ASCII
402 );
403 assert_eq!(
404 <Codepage as std::str::FromStr>::from_str("cp273").unwrap(),
405 Codepage::CP273
406 );
407 assert_eq!(
408 <Codepage as std::str::FromStr>::from_str("cp500").unwrap(),
409 Codepage::CP500
410 );
411 assert_eq!(
412 <Codepage as std::str::FromStr>::from_str("cp1047").unwrap(),
413 Codepage::CP1047
414 );
415 assert_eq!(
416 <Codepage as std::str::FromStr>::from_str("cp1140").unwrap(),
417 Codepage::CP1140
418 );
419 }
420
421 #[test]
422 fn test_codepage_from_str_case_insensitive() {
423 assert_eq!(
424 <Codepage as std::str::FromStr>::from_str("ASCII").unwrap(),
425 Codepage::ASCII
426 );
427 assert_eq!(
428 <Codepage as std::str::FromStr>::from_str("CP273").unwrap(),
429 Codepage::CP273
430 );
431 assert_eq!(
432 <Codepage as std::str::FromStr>::from_str("Cp500").unwrap(),
433 Codepage::CP500
434 );
435 }
436
437 #[test]
438 fn test_codepage_from_str_empty_string_defaults_to_cp037() {
439 assert_eq!(
440 <Codepage as std::str::FromStr>::from_str("").unwrap(),
441 Codepage::CP037
442 );
443 }
444
445 #[test]
448 fn test_codepage_is_ebcdic_all_variants() {
449 assert!(!Codepage::ASCII.is_ebcdic());
450 assert!(Codepage::CP037.is_ebcdic());
451 assert!(Codepage::CP273.is_ebcdic());
452 assert!(Codepage::CP500.is_ebcdic());
453 assert!(Codepage::CP1047.is_ebcdic());
454 assert!(Codepage::CP1140.is_ebcdic());
455 }
456
457 #[test]
460 fn test_codepage_code_page_number_all_variants() {
461 assert_eq!(Codepage::ASCII.code_page_number(), None);
462 assert_eq!(Codepage::CP037.code_page_number(), Some(37));
463 assert_eq!(Codepage::CP273.code_page_number(), Some(273));
464 assert_eq!(Codepage::CP500.code_page_number(), Some(500));
465 assert_eq!(Codepage::CP1047.code_page_number(), Some(1047));
466 assert_eq!(Codepage::CP1140.code_page_number(), Some(1140));
467 }
468
469 #[test]
472 fn test_unmappable_policy_display_all_variants() {
473 assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
474 assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
475 assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
476 }
477
478 #[test]
481 fn test_unmappable_policy_from_str_all_valid() {
482 assert_eq!(
483 <UnmappablePolicy as std::str::FromStr>::from_str("replace").unwrap(),
484 UnmappablePolicy::Replace
485 );
486 assert_eq!(
487 <UnmappablePolicy as std::str::FromStr>::from_str("skip").unwrap(),
488 UnmappablePolicy::Skip
489 );
490 assert_eq!(
491 <UnmappablePolicy as std::str::FromStr>::from_str("error").unwrap(),
492 UnmappablePolicy::Error
493 );
494 }
495
496 #[test]
497 fn test_unmappable_policy_from_str_case_insensitive() {
498 assert_eq!(
499 <UnmappablePolicy as std::str::FromStr>::from_str("REPLACE").unwrap(),
500 UnmappablePolicy::Replace
501 );
502 assert_eq!(
503 <UnmappablePolicy as std::str::FromStr>::from_str("SKIP").unwrap(),
504 UnmappablePolicy::Skip
505 );
506 }
507
508 #[test]
511 fn test_get_zoned_sign_table_ebcdic_unsigned_nibbles() {
512 let table = get_zoned_sign_table(Codepage::CP037);
513 for (i, &entry) in table.iter().enumerate().take(0xB + 1) {
514 assert_eq!(entry, (false, false), "Expected unsigned at nibble 0x{i:X}");
515 }
516 assert_eq!(table[0xE], (false, false));
517 }
518
519 #[test]
520 fn test_get_zoned_sign_table_all_ebcdic_codepages_same() {
521 let cp037 = get_zoned_sign_table(Codepage::CP037);
522 let cp273 = get_zoned_sign_table(Codepage::CP273);
523 let cp500 = get_zoned_sign_table(Codepage::CP500);
524 let cp1047 = get_zoned_sign_table(Codepage::CP1047);
525 let cp1140 = get_zoned_sign_table(Codepage::CP1140);
526 assert_eq!(cp037, cp273);
527 assert_eq!(cp037, cp500);
528 assert_eq!(cp037, cp1047);
529 assert_eq!(cp037, cp1140);
530 }
531
532 #[test]
535 fn test_codepage_serde_roundtrip() {
536 let cp = Codepage::CP037;
537 let json = serde_json::to_string(&cp).unwrap();
538 let deserialized: Codepage = serde_json::from_str(&json).unwrap();
539 assert_eq!(cp, deserialized);
540 }
541
542 #[test]
543 fn test_unmappable_policy_serde_roundtrip() {
544 let policy = UnmappablePolicy::Replace;
545 let json = serde_json::to_string(&policy).unwrap();
546 let deserialized: UnmappablePolicy = serde_json::from_str(&json).unwrap();
547 assert_eq!(policy, deserialized);
548 }
549
550 #[test]
553 fn test_codepage_clone_preserves_value() {
554 let cp = Codepage::CP500;
555 let cloned = cp;
556 assert_eq!(cp, cloned);
557 }
558
559 #[test]
560 fn test_codepage_eq_different_variants() {
561 assert_ne!(Codepage::ASCII, Codepage::CP037);
562 assert_ne!(Codepage::CP037, Codepage::CP273);
563 assert_ne!(Codepage::CP273, Codepage::CP500);
564 assert_ne!(Codepage::CP500, Codepage::CP1047);
565 assert_ne!(Codepage::CP1047, Codepage::CP1140);
566 }
567
568 #[test]
569 fn test_codepage_debug_format() {
570 let debug = format!("{:?}", Codepage::CP037);
571 assert_eq!(debug, "CP037");
572 let debug = format!("{:?}", Codepage::ASCII);
573 assert_eq!(debug, "ASCII");
574 }
575
576 #[test]
577 fn test_codepage_serde_all_variants_roundtrip() {
578 let variants = [
579 Codepage::ASCII,
580 Codepage::CP037,
581 Codepage::CP273,
582 Codepage::CP500,
583 Codepage::CP1047,
584 Codepage::CP1140,
585 ];
586 for cp in variants {
587 let json = serde_json::to_string(&cp).unwrap();
588 let deserialized: Codepage = serde_json::from_str(&json).unwrap();
589 assert_eq!(cp, deserialized, "Roundtrip failed for {cp}");
590 }
591 }
592
593 #[test]
594 fn test_codepage_from_str_cp037_explicit() {
595 assert_eq!(
597 <Codepage as std::str::FromStr>::from_str("cp037").unwrap(),
598 Codepage::CP037
599 );
600 }
601
602 #[test]
603 fn test_codepage_display_roundtrip_via_from_str() {
604 let variants = [
605 Codepage::ASCII,
606 Codepage::CP273,
607 Codepage::CP500,
608 Codepage::CP1047,
609 Codepage::CP1140,
610 ];
611 for cp in variants {
612 let displayed = cp.to_string();
613 let parsed: Codepage = displayed.parse().unwrap();
614 assert_eq!(cp, parsed, "Display/FromStr roundtrip failed for {cp}");
615 }
616 }
617
618 #[test]
619 fn test_unmappable_policy_clone_preserves_value() {
620 let policy = UnmappablePolicy::Skip;
621 let cloned = policy;
622 assert_eq!(policy, cloned);
623 }
624
625 #[test]
626 fn test_unmappable_policy_debug_format() {
627 assert_eq!(format!("{:?}", UnmappablePolicy::Error), "Error");
628 assert_eq!(format!("{:?}", UnmappablePolicy::Replace), "Replace");
629 assert_eq!(format!("{:?}", UnmappablePolicy::Skip), "Skip");
630 }
631
632 #[test]
633 fn test_unmappable_policy_serde_all_variants_roundtrip() {
634 let variants = [
635 UnmappablePolicy::Error,
636 UnmappablePolicy::Replace,
637 UnmappablePolicy::Skip,
638 ];
639 for policy in variants {
640 let json = serde_json::to_string(&policy).unwrap();
641 let deserialized: UnmappablePolicy = serde_json::from_str(&json).unwrap();
642 assert_eq!(policy, deserialized, "Roundtrip failed for {policy}");
643 }
644 }
645
646 #[test]
647 fn test_unmappable_policy_eq_different_variants() {
648 assert_ne!(UnmappablePolicy::Error, UnmappablePolicy::Replace);
649 assert_ne!(UnmappablePolicy::Replace, UnmappablePolicy::Skip);
650 assert_ne!(UnmappablePolicy::Skip, UnmappablePolicy::Error);
651 }
652
653 #[test]
654 fn test_unmappable_policy_from_str_empty_defaults_to_error() {
655 assert_eq!(
656 <UnmappablePolicy as std::str::FromStr>::from_str("").unwrap(),
657 UnmappablePolicy::Error
658 );
659 }
660
661 #[test]
662 fn test_space_byte_consistency_with_is_ebcdic() {
663 let variants = [
664 Codepage::ASCII,
665 Codepage::CP037,
666 Codepage::CP273,
667 Codepage::CP500,
668 Codepage::CP1047,
669 Codepage::CP1140,
670 ];
671 for cp in variants {
672 if cp.is_ebcdic() {
673 assert_eq!(space_byte(cp), 0x40, "EBCDIC {cp} should have space 0x40");
674 } else {
675 assert_eq!(space_byte(cp), 0x20, "ASCII should have space 0x20");
676 }
677 }
678 }
679
680 #[test]
681 fn test_codepage_is_ascii_and_is_ebcdic_mutually_exclusive() {
682 let variants = [
683 Codepage::ASCII,
684 Codepage::CP037,
685 Codepage::CP273,
686 Codepage::CP500,
687 Codepage::CP1047,
688 Codepage::CP1140,
689 ];
690 for cp in variants {
691 assert_ne!(
692 cp.is_ascii(),
693 cp.is_ebcdic(),
694 "is_ascii and is_ebcdic must be mutually exclusive for {cp}"
695 );
696 }
697 }
698
699 #[test]
700 fn test_get_zoned_sign_table_ebcdic_positive_nibble_f() {
701 let table = get_zoned_sign_table(Codepage::CP037);
702 let (is_signed, is_negative) = table[0xF];
704 assert!(is_signed);
705 assert!(!is_negative);
706 }
707
708 #[test]
709 fn test_get_zoned_sign_table_ebcdic_negative_nibble_d() {
710 let table = get_zoned_sign_table(Codepage::CP037);
711 let (is_signed, is_negative) = table[0xD];
712 assert!(is_signed);
713 assert!(is_negative);
714 }
715}