Skip to main content

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}