mac_encoding/macroman.rs
1//! Mac OS Roman, for classic Mac OS resource text.
2//!
3//! This module is a short front for [`Encoding::Roman`]. Persons who work
4//! with resources use this encoding much more than the other 20 encodings.
5//! The module operates on resource type codes and on string resources. For
6//! all other work, use [`Encoding`].
7//!
8//! # The source of the mapping
9//!
10//! The mapping is Apple's `ROMAN.TXT`. It agrees fully with
11//! `index-macintosh.txt` in the standard, which gives the `macintosh`
12//! encoding. The tests in `tests/whatwg_conformance.rs` compare the two.
13//!
14//! # Two bytes that need an explanation
15//!
16//! Byte `0xDB` is `€` U+20AC. Before Mac OS 8.5, it was `¤` U+00A4. Apple
17//! Technote TN1140 gives this change. Apple's table and the standard both
18//! have the new value. Thus this crate has it also. Text from before Mac OS
19//! 8.5 with a currency sign decodes to a euro sign. The bytes alone do not
20//! show you which of the two characters the text had.
21//!
22//! Byte `0xF0` is the Apple logo. Unicode has no character for it. The
23//! mapping uses U+F8FF, which is a private use character. Thus it has a
24//! meaning only for programs that use Apple's rule. The mapping file
25//! `CORPCHAR.TXT` is the register of these characters.
26//!
27//! # A different mapping with the same name
28//!
29//! RFC 1345 gives a different mapping. The IANA `macintosh` registration
30//! (MIBenum 2027) refers to RFC 1345. Both show The Unicode Standard 1.0 of
31//! 1991, which is earlier than the two changes above. That mapping has `¤` at
32//! byte `0xDB`. It also has no character for bytes `0xF0`, `0xF6`, and
33//! `0xF7`. Text that you decode with those tables is different.
34
35use alloc::string::String;
36use alloc::vec::Vec;
37
38use crate::{EncodeError, Encoding};
39
40/// Decodes Mac OS Roman bytes.
41///
42/// The encoding has a mapping for all 256 bytes. Thus this function never
43/// writes a replacement character. [`Encoding::defines_every_byte`] gives the
44/// same data for all encodings. The test `every_byte_is_defined` checks it.
45pub fn decode(bytes: &[u8]) -> String {
46 Encoding::Roman.decode(bytes)
47}
48
49/// Encodes text to Mac OS Roman. The first character with no byte gives an
50/// error.
51///
52/// This function does not replace a character that has no byte. A resource
53/// type code of four characters must give four bytes. If the encoder replaces
54/// a character, you find the wrong resource.
55pub fn encode(text: &str) -> Result<Vec<u8>, EncodeError> {
56 Encoding::Roman.encode(text)
57}
58
59#[cfg(test)]
60mod tests {
61 use alloc::vec;
62
63 use super::*;
64
65 #[test]
66 fn ascii_round_trips() {
67 assert_eq!(decode(b"CODE"), "CODE");
68 assert_eq!(encode("CODE").unwrap(), b"CODE");
69 }
70
71 #[test]
72 fn pi_is_0xb9_not_a_utf8_construction() {
73 // Do not make a host file name for `π` from the Mac OS Roman byte
74 // 0xB9. That gives a different path, or a path that does not exist.
75 // The mapping is always 0xB9 to U+03C0. A file name must be correct
76 // UTF-8.
77 assert_eq!(decode(&[0xB9]), "π");
78 assert_eq!(encode("π").unwrap(), vec![0xB9]);
79 }
80
81 #[test]
82 fn bullet_is_0xa5_and_asterisk_is_0x2a() {
83 // These two bytes cause all of the LearnOOP 5/6 difference.
84 assert_eq!(decode(&[0xA5]), "•");
85 assert_eq!(decode(&[0x2A]), "*");
86 }
87
88 #[test]
89 fn every_high_byte_decodes_and_re_encodes() {
90 for byte in 0x80u8..=0xFF {
91 let decoded = decode(&[byte]);
92 assert_eq!(
93 encode(&decoded),
94 Ok(vec![byte]),
95 "byte {byte:#04x} did not round-trip"
96 );
97 }
98 }
99
100 #[test]
101 fn every_byte_is_defined() {
102 assert!(Encoding::Roman.defines_every_byte());
103 assert!(Encoding::Roman
104 .decode_strict(&(0..=255).collect::<Vec<u8>>())
105 .is_ok());
106 }
107
108 #[test]
109 fn apple_logo_is_private_use() {
110 // U+F8FF has a meaning only in Apple's corporate zone. But it must
111 // go through the two operations correctly. If not, the resource names
112 // that contain it are not correct.
113 assert_eq!(decode(&[0xF0]), "\u{F8FF}");
114 assert_eq!(encode("\u{F8FF}").unwrap(), vec![0xF0]);
115 }
116
117 #[test]
118 fn byte_0xdb_is_the_euro_not_the_currency_sign() {
119 // Mac OS 8.5 replaced ¤ with €. Apple's table and the standard both
120 // have this change. RFC 1345 is earlier than the change.
121 assert_eq!(decode(&[0xDB]), "€");
122 assert_eq!(
123 encode("¤"),
124 Err(EncodeError {
125 encoding: Encoding::Roman,
126 code_point: '¤',
127 index: 0,
128 })
129 );
130 }
131
132 #[test]
133 fn encode_error_locates_the_character() {
134 // The position uses bytes. Thus `π` moves the position by two.
135 let err = encode("πx→").unwrap_err();
136 assert_eq!(err.code_point, '→');
137 assert_eq!(err.index, 3);
138 }
139}