Skip to main content

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}