mac_encoding/lib.rs
1//! This crate changes text between Apple's classic Mac OS encodings and
2//! Unicode.
3//!
4//! Apple wrote the mappings and publishes them as mapping files. The
5//! generator in `tools/generate-tables` reads those files and writes the
6//! tables in `src/tables.rs`. The decode and encode operations obey the WHATWG
7//! [Encoding Standard](https://encoding.spec.whatwg.org/), sections 9.1 and
8//! 9.2. Thus this crate and a web browser give the same result for each
9//! encoding in the standard.
10//!
11//! ```
12//! use mac_encoding::Encoding;
13//!
14//! // A resource type code goes through Mac OS Roman and comes back.
15//! assert_eq!(Encoding::Roman.decode(b"CODE"), "CODE");
16//! assert_eq!(Encoding::Roman.encode("CODE").unwrap(), b"CODE");
17//!
18//! // The standard calls this encoding `macintosh`.
19//! assert_eq!(Encoding::from_label("X-Mac-Roman"), Some(Encoding::Roman));
20//! ```
21//!
22//! # The encodings in the standard
23//!
24//! The standard gives a name to only two of these encodings. Mac OS Roman is
25//! `macintosh` and Mac OS Cyrillic is `x-mac-cyrillic`. The other 19
26//! encodings have no name in the standard. Use [`Encoding::from_id`] for
27//! them and [`Encoding::from_label`] for the other two.
28//!
29//! In the standard, `x-mac-cyrillic` also has the label `x-mac-ukrainian`.
30//! This agrees with Apple. The mapping file `UKRAINE.TXT` tells us that Mac
31//! OS 9 put the Ukrainian characters into Mac OS Cyrillic. Thus this crate
32//! has no Ukrainian encoding, because Apple supplies no mapping file for it.
33//!
34//! # Two conditions that the standard does not include
35//!
36//! Section 9.1 permits a decoder to answer an ASCII byte with the same
37//! value. This is not correct for three of these encodings. Mac OS Symbol has
38//! GREEK CAPITAL LETTER ALPHA at byte `0x41`. Mac OS Dingbats and Mac OS
39//! Keyboard also give other characters to many bytes in that range. Thus the
40//! decoder always uses the table. For the other encodings, the table gives
41//! the same result as the rule in the standard.
42//!
43//! Section 9 also has this rule: one byte gives one code point or none.
44//! Apple's tables do not obey that rule. Mac OS Thai gives a character
45//! and its position as two code points. Mac OS Devanagari, Gujarati, and
46//! Gurmukhi give a two-byte code to some ligatures. The decoder and the
47//! encoder both use the longest match first. Thus these mappings are correct
48//! in the two directions.
49//!
50//! # Encodings that cannot encode a byte again correctly
51//!
52//! Mac OS Arabic, Farsi, and Hebrew give a direction to each mapping. Thus
53//! more than one byte can have the same code point. For example, byte `0x2B`
54//! and byte `0xAB` are both PLUS SIGN. Only the direction is different. The
55//! decoder removes the direction and the encoder gives the lowest byte. Thus
56//! a byte from the right-to-left group comes back as its left-to-right
57//! equivalent.
58//!
59//! Mac OS Keyboard has one such condition for a different reason. Byte `0x09`
60//! and byte `0x61` both decode to U+2423 OPEN BOX. Apple's comment for that
61//! mapping says "duplicates mapping for 0x61, hence no round-trip".
62//!
63//! [`Encoding::encode_is_lossy`] tells you about all four encodings.
64//! [`Encoding::is_directional`] tells you about the three with directions. To
65//! encode text and then to decode it is always correct. Only the opposite
66//! sequence can give a different byte.
67//!
68//! # Control characters in Mac OS Keyboard
69//!
70//! Apple's mapping files do not include the bytes `0x00` to `0x1F` and the
71//! byte `0x7F`. For almost all encodings, each of these bytes gives the
72//! control character with the same value. Mac OS Keyboard is different. It
73//! gives key symbols to 22 of these bytes. For example, byte `0x02` is U+21E5
74//! LEFTWARDS ARROW TO BAR.
75//!
76//! That font has no byte for the control character itself. Thus
77//! [`Encoding::encode`] gives an error for those 22 control characters, and
78//! [`Encoding::encode_html`] writes a character reference.
79//!
80//! # This crate does not need `std`
81//!
82//! The crate is `no_std` and uses `alloc` for [`String`] and [`Vec`]. It has
83//! no features and no dependencies.
84
85#![no_std]
86#![forbid(unsafe_code)]
87
88extern crate alloc;
89
90#[cfg(test)]
91extern crate std;
92
93mod codec;
94mod error;
95pub mod macroman;
96mod tables;
97
98pub use error::{DecodeError, EncodeError};
99pub use tables::{Encoding, ALL};
100
101use alloc::string::String;
102use alloc::vec::Vec;
103use core::fmt;
104
105/// The table for one encoding.
106///
107/// The generator writes these tables. They are not part of the public API.
108pub(crate) struct Table {
109 id: &'static str,
110 apple_name: &'static str,
111 whatwg_name: Option<&'static str>,
112 labels: &'static [&'static str],
113 directional: bool,
114 lossy: bool,
115 /// True if all 256 bytes have a mapping.
116 complete: bool,
117 /// The text for each byte. An empty text shows a byte with no mapping.
118 dec1: &'static [&'static str; 256],
119 /// The two-byte codes, as `(lead, trail, text)`.
120 dec2: &'static [(u8, u8, &'static str)],
121 /// The mappings with one code point, in code point sequence.
122 enc1: &'static [(char, &'static [u8])],
123 /// The mappings with more than one code point, longest first.
124 enc_seq: &'static [(&'static str, &'static [u8])],
125}
126
127impl Encoding {
128 /// The permanent identifier, for example `roman` or `central-european`.
129 ///
130 /// Each encoding has an identifier. Only two have a name in the standard.
131 /// Refer to [`Self::whatwg_name`].
132 pub fn id(self) -> &'static str {
133 self.table().id
134 }
135
136 /// Apple's name, for example `Mac OS Roman`.
137 pub fn apple_name(self) -> &'static str {
138 self.table().apple_name
139 }
140
141 /// The name in the standard, if the standard has one.
142 ///
143 /// The result is `Some("macintosh")` for [`Encoding::Roman`] and
144 /// `Some("x-mac-cyrillic")` for [`Encoding::Cyrillic`]. For the other 19
145 /// encodings the result is `None`.
146 pub fn whatwg_name(self) -> Option<&'static str> {
147 self.table().whatwg_name
148 }
149
150 /// The labels in the standard for this encoding.
151 ///
152 /// The list is empty if the standard does not have this encoding.
153 pub fn labels(self) -> &'static [&'static str] {
154 self.table().labels
155 }
156
157 /// Tells you if the table gives a direction to its mappings.
158 ///
159 /// The result is true for Mac OS Arabic, Farsi, and Hebrew. For these
160 /// three encodings, the direction is also the cause of the condition in
161 /// [`Self::encode_is_lossy`]. But the two conditions are not the same.
162 /// Mac OS Keyboard has the second condition and not the first.
163 pub fn is_directional(self) -> bool {
164 self.table().directional
165 }
166
167 /// Tells you if two codes give the same text.
168 ///
169 /// If the result is true, one byte can decode to text that encodes to a
170 /// different byte. The opposite sequence is always correct. To encode
171 /// text and then to decode it always gives the first text again.
172 ///
173 /// The result is true for Mac OS Arabic, Farsi, Hebrew, and Keyboard. In
174 /// the first three encodings, the left-to-right form and the
175 /// right-to-left form of a character have the same code point. Mac OS
176 /// Keyboard has one such condition at U+2423 OPEN BOX. Apple's comment
177 /// for that mapping says "duplicates mapping for 0x61, hence no
178 /// round-trip".
179 pub fn encode_is_lossy(self) -> bool {
180 self.table().lossy
181 }
182
183 /// Tells you if all 256 bytes have a mapping.
184 ///
185 /// If the result is true, [`Self::decode_strict`] cannot fail. Mac OS
186 /// Roman is such an encoding. This is the reason that a resource type
187 /// code of four bytes always decodes.
188 pub fn defines_every_byte(self) -> bool {
189 self.table().complete
190 }
191
192 /// Finds the encoding with this [`Self::id`]. The text must agree fully.
193 pub fn from_id(id: &str) -> Option<Self> {
194 ALL.iter().copied().find(|e| e.id() == id)
195 }
196
197 /// Finds the encoding with this label.
198 ///
199 /// This function obeys "get an encoding" in section 4.2 of the standard.
200 /// It removes the ASCII space characters at the start and at the end.
201 /// Then it compares the text. A capital letter and a small letter are
202 /// equivalent.
203 ///
204 /// Only two encodings have labels. For the other 19 encodings the result
205 /// is `None`. Use [`Self::from_id`] for them.
206 ///
207 /// ```
208 /// # use mac_encoding::Encoding;
209 /// assert_eq!(Encoding::from_label(" MACINTOSH\n"), Some(Encoding::Roman));
210 /// assert_eq!(Encoding::from_label("x-mac-ukrainian"), Some(Encoding::Cyrillic));
211 /// assert_eq!(Encoding::from_label("Mac OS Thai"), None);
212 /// ```
213 pub fn from_label(label: &str) -> Option<Self> {
214 // Infra's ASCII whitespace is tab, newline, form feed, carriage
215 // return, and space. `str::trim` follows Unicode `White_Space`, a
216 // different set that also strips vertical tab and no-break space, so
217 // it is not used here.
218 let label = label.trim_matches(|c| matches!(c, '\t' | '\n' | '\x0C' | '\r' | ' '));
219 ALL.iter()
220 .copied()
221 .find(|e| e.labels().iter().any(|l| l.eq_ignore_ascii_case(label)))
222 }
223
224 /// Decodes `bytes`. A byte with no mapping becomes U+FFFD.
225 ///
226 /// This is the "replacement" error mode in the standard. If
227 /// [`Self::defines_every_byte`] is true, no byte becomes U+FFFD.
228 pub fn decode(self, bytes: &[u8]) -> String {
229 codec::decode(self, bytes)
230 }
231
232 /// Decodes `bytes`. The first byte with no mapping gives an error.
233 ///
234 /// This is the "fatal" error mode in the standard.
235 pub fn decode_strict(self, bytes: &[u8]) -> Result<String, DecodeError> {
236 codec::decode_strict(self, bytes)
237 }
238
239 /// Encodes `text`. The first code point with no byte gives an error.
240 ///
241 /// This is the "fatal" error mode in the standard. Use this function for
242 /// resource data. A type code of four characters must give four bytes. If
243 /// the encoder replaces a character, you find the wrong resource.
244 pub fn encode(self, text: &str) -> Result<Vec<u8>, EncodeError> {
245 codec::encode(self, text)
246 }
247
248 /// Encodes `text`. A code point with no byte becomes `&#NNN;`.
249 ///
250 /// This is the "html" error mode in the standard. HTML forms need an
251 /// encoder that cannot fail. The standard gives a warning about this
252 /// mode. You cannot see a difference between this result and text that
253 /// contains the same characters. Use [`Self::encode`] for all data that
254 /// is not form data.
255 pub fn encode_html(self, text: &str) -> Vec<u8> {
256 codec::encode_html(self, text)
257 }
258}
259
260impl fmt::Display for Encoding {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 f.write_str(self.apple_name())
263 }
264}