write_fonts/tables/
name.rs

1//! The name table
2
3include!("../../generated/generated_name.rs");
4use read_fonts::tables::name::{Encoding, MacRomanMapping};
5
6impl Name {
7    ///// Sort the name records in the table.
8    /////
9    ///// The `name_record` array must be sorted; if it hasn't been sorted before
10    ///// construction this can be used to sort it afterwards.
11    //pub fn sort(&mut self) {
12    //self.name_record.sort();
13    //}
14
15    fn compute_storage_offset(&self) -> u16 {
16        let v0 = 6 // version, count, storage_offset
17            + self.name_record.len() * 12;
18        if let Some(lang_tag_records) = self.lang_tag_record.as_ref() {
19            v0 + 4 * lang_tag_records.len()
20        } else {
21            v0
22        }
23        .try_into()
24        .unwrap()
25    }
26
27    fn compute_version(&self) -> u16 {
28        self.lang_tag_record.is_some().into()
29    }
30
31    fn check_sorted_and_unique_name_records(&self, ctx: &mut ValidationCtx) {
32        //TODO: replace with `is_sorted` whenever oss_fuzz is using rustc >= 1.82
33        if self
34            .name_record
35            .windows(2)
36            .any(|window| window[0] > window[1])
37        {
38            ctx.report("name_record array must be sorted");
39        }
40        for (left, right) in self.name_record.iter().zip(self.name_record.iter().skip(1)) {
41            let left = (
42                left.platform_id,
43                left.encoding_id,
44                left.language_id,
45                left.name_id,
46            );
47            let right = (
48                right.platform_id,
49                right.encoding_id,
50                right.language_id,
51                right.name_id,
52            );
53            if left == right {
54                ctx.report(format!("duplicate entry in name_record: '{}'", left.3))
55            }
56        }
57    }
58}
59
60impl NameRecord {
61    fn string(&self) -> &str {
62        self.string.as_str()
63    }
64
65    fn compile_name_string(&self) -> NameStringAndLenWriter<'_> {
66        NameStringAndLenWriter(NameStringWriter {
67            encoding: Encoding::new(self.platform_id, self.encoding_id),
68            string: self.string(),
69        })
70    }
71
72    fn validate_string_data(&self, ctx: &mut ValidationCtx) {
73        let encoding = Encoding::new(self.platform_id, self.encoding_id);
74        match encoding {
75            Encoding::Unknown => ctx.report(format!(
76                "Unhandled platform/encoding id pair: ({}, {})",
77                self.platform_id, self.encoding_id
78            )),
79            Encoding::Utf16Be => (), // lgtm
80            Encoding::MacRoman => {
81                for c in self.string().chars() {
82                    if MacRomanMapping.encode(c).is_none() {
83                        ctx.report(format!(
84                            "char {c} {} not representable in MacRoman encoding",
85                            c.escape_unicode()
86                        ))
87                    }
88                }
89            }
90        }
91    }
92}
93
94impl LangTagRecord {
95    fn lang_tag(&self) -> &str {
96        self.lang_tag.as_str()
97    }
98
99    fn compile_name_string(&self) -> NameStringAndLenWriter<'_> {
100        NameStringAndLenWriter(NameStringWriter {
101            encoding: Encoding::Utf16Be,
102            string: self.lang_tag(),
103        })
104    }
105}
106
107/// A helper that compiles both the length filed and the offset string data
108struct NameStringAndLenWriter<'a>(NameStringWriter<'a>);
109
110struct NameStringWriter<'a> {
111    encoding: Encoding,
112    string: &'a str,
113}
114
115impl NameStringWriter<'_> {
116    fn compute_length(&self) -> u16 {
117        match self.encoding {
118            Encoding::Utf16Be => self.string.chars().map(|c| c.len_utf16() as u16 * 2).sum(),
119            // this will be correct assuming we pass validation
120            Encoding::MacRoman => self.string.chars().count().try_into().unwrap(),
121            Encoding::Unknown => 0,
122        }
123    }
124}
125
126impl FontWrite for NameStringAndLenWriter<'_> {
127    fn write_into(&self, writer: &mut TableWriter) {
128        self.0.compute_length().write_into(writer);
129        writer.write_offset(&self.0, 2)
130    }
131}
132
133impl FontWrite for NameStringWriter<'_> {
134    fn write_into(&self, writer: &mut TableWriter) {
135        for c in self.string.chars() {
136            match self.encoding {
137                Encoding::Utf16Be => {
138                    let mut buf = [0, 0];
139                    let enc = c.encode_utf16(&mut buf);
140                    enc.iter()
141                        .for_each(|unit| writer.write_slice(&unit.to_be_bytes()))
142                }
143                Encoding::MacRoman => {
144                    MacRomanMapping
145                        .encode(c)
146                        .expect("invalid char for MacRoman")
147                        .write_into(writer);
148                }
149                Encoding::Unknown => panic!("unknown encoding"),
150            }
151        }
152    }
153}
154
155impl FromObjRef<read_fonts::tables::name::NameString<'_>> for String {
156    fn from_obj_ref(obj: &read_fonts::tables::name::NameString<'_>, _: FontData) -> Self {
157        obj.chars().collect()
158    }
159}
160
161impl FromTableRef<read_fonts::tables::name::NameString<'_>> for String {}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use env_logger;
167    use log::debug;
168
169    fn init() {
170        let _ = env_logger::builder().is_test(true).try_init();
171    }
172
173    fn make_name_record(
174        platform_id: u16,
175        encoding_id: u16,
176        language_id: u16,
177        name_id: u16,
178        name: &str,
179    ) -> NameRecord {
180        NameRecord {
181            platform_id,
182            encoding_id,
183            language_id,
184            name_id: NameId::new(name_id),
185            string: name.to_string().into(),
186        }
187    }
188
189    #[test]
190    fn encoding() {
191        let stringthing = NameStringWriter {
192            encoding: Encoding::Utf16Be,
193            string: "hello",
194        };
195        assert_eq!(stringthing.compute_length(), 10);
196    }
197
198    #[test]
199    fn compute_version() {
200        let mut table = Name::default();
201        assert_eq!(table.compute_version(), 0);
202        table.lang_tag_record = Some(Vec::new());
203        assert_eq!(table.compute_version(), 1);
204    }
205
206    #[test]
207    fn sorting() {
208        let mut table = Name::default();
209        table
210            .name_record
211            .push(make_name_record(3, 1, 0, 1030, "Ordinær"));
212        table.name_record.push(make_name_record(0, 4, 0, 4, "oh"));
213        table
214            .name_record
215            .push(make_name_record(3, 1, 0, 1029, "Regular"));
216
217        // we aren't sorted so we should fail validation
218        assert!(crate::dump_table(&table).is_err());
219
220        // after sorting we should be fine
221        table.name_record.sort();
222
223        let _dumped = crate::dump_table(&table).unwrap();
224        let loaded = read_fonts::tables::name::Name::read(FontData::new(&_dumped)).unwrap();
225        assert_eq!(loaded.name_record()[0].encoding_id, 4);
226        assert_eq!(loaded.name_record()[1].name_id, NameId::new(1029));
227        assert_eq!(loaded.name_record()[2].name_id, NameId::new(1030));
228    }
229
230    /// ensure we are counting characters and not bytes
231    #[test]
232    fn mac_str_length() {
233        let name = NameRecord::new(1, 0, 0, NameId::new(9), String::from("cé").into());
234        let mut table = Name::default();
235        table.name_record.push(name);
236        let bytes = crate::dump_table(&table).unwrap();
237        let load = read_fonts::tables::name::Name::read(FontData::new(&bytes)).unwrap();
238
239        let data = load.name_record()[0].string(load.string_data()).unwrap();
240        assert_eq!(data.chars().collect::<String>(), "cé");
241    }
242
243    #[test]
244    fn roundtrip() {
245        init();
246
247        #[rustfmt::skip]
248        static COLINS_BESPOKE_DATA: &[u8] = &[
249            0x0, 0x0, // version
250            0x0, 0x03, // count
251            0x0, 42, // storage offset
252            //record 1:
253            0x00, 0x03, // platformID
254            0x00, 0x01, // encodingID
255            0x04, 0x09, // languageID
256            0x00, 0x01, // nameID
257            0x00, 0x0a, // length
258            0x00, 0x00, // offset
259            //record 2:
260            0x00, 0x03, // platformID
261            0x00, 0x01, // encodingID
262            0x04, 0x09, // languageID
263            0x00, 0x02, // nameID
264            0x00, 0x10, // length
265            0x00, 0x0a, // offset
266            //record 2:
267            0x00, 0x03, // platformID
268            0x00, 0x01, // encodingID
269            0x04, 0x09, // languageID
270            0x00, 0x03, // nameID
271            0x00, 0x18, // length
272            0x00, 0x1a, // offset
273            // storage area:
274            // string 1 'colin'
275            0x0, 0x63, 0x0, 0x6F, 0x0, 0x6C, 0x0, 0x69,
276            0x0, 0x6E,
277            // string 2, 'nicelife'
278            0x0, 0x6E, 0x0, 0x69, 0x0, 0x63, 0x0, 0x65,
279            0x0, 0x6C, 0x0, 0x69, 0x0, 0x66, 0x0, 0x65,
280            // string3 'i hate fonts'
281            0x0, 0x69, 0x0, 0x20, 0x0, 0x68, 0x0, 0x61,
282            0x0, 0x74, 0x0, 0x65, 0x0, 0x20, 0x0, 0x66,
283            0x0, 0x6F, 0x0, 0x6E, 0x0, 0x74, 0x0, 0x73,
284        ];
285
286        let raw_table =
287            read_fonts::tables::name::Name::read(FontData::new(COLINS_BESPOKE_DATA)).unwrap();
288        let owned: Name = raw_table.to_owned_table();
289        let dumped = crate::dump_table(&owned).unwrap();
290        let reloaded = read_fonts::tables::name::Name::read(FontData::new(&dumped)).unwrap();
291
292        for rec in raw_table.name_record() {
293            let raw_str = rec.string(raw_table.string_data()).unwrap();
294            debug!("{raw_str}");
295        }
296
297        assert_eq!(raw_table.version(), reloaded.version());
298        assert_eq!(raw_table.count(), reloaded.count());
299        assert_eq!(raw_table.storage_offset(), reloaded.storage_offset());
300
301        let mut fail = false;
302        for (old, new) in raw_table
303            .name_record()
304            .iter()
305            .zip(reloaded.name_record().iter())
306        {
307            assert_eq!(old.platform_id(), new.platform_id());
308            assert_eq!(old.encoding_id(), new.encoding_id());
309            assert_eq!(old.language_id(), new.language_id());
310            assert_eq!(old.name_id(), new.name_id());
311            assert_eq!(old.length(), new.length());
312            debug!("{:?} {:?}", old.string_offset(), new.string_offset());
313            let old_str = old.string(raw_table.string_data()).unwrap();
314            let new_str = new.string(reloaded.string_data()).unwrap();
315            if old_str != new_str {
316                debug!("'{old_str}' != '{new_str}'");
317                fail = true;
318            }
319        }
320        if fail {
321            panic!("some comparisons failed");
322        }
323    }
324}