fontcull_write_fonts/tables/
name.rs

1//! The name table
2
3include!("../../generated/generated_name.rs");
4use fontcull_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<fontcull_read_fonts::tables::name::NameString<'_>> for String {
156    fn from_obj_ref(obj: &fontcull_read_fonts::tables::name::NameString<'_>, _: FontData) -> Self {
157        obj.chars().collect()
158    }
159}
160
161impl FromTableRef<fontcull_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 =
225            fontcull_read_fonts::tables::name::Name::read(FontData::new(&_dumped)).unwrap();
226        assert_eq!(loaded.name_record()[0].encoding_id, 4);
227        assert_eq!(loaded.name_record()[1].name_id, NameId::new(1029));
228        assert_eq!(loaded.name_record()[2].name_id, NameId::new(1030));
229    }
230
231    /// ensure we are counting characters and not bytes
232    #[test]
233    fn mac_str_length() {
234        let name = NameRecord::new(1, 0, 0, NameId::new(9), String::from("cé").into());
235        let mut table = Name::default();
236        table.name_record.push(name);
237        let bytes = crate::dump_table(&table).unwrap();
238        let load = fontcull_read_fonts::tables::name::Name::read(FontData::new(&bytes)).unwrap();
239
240        let data = load.name_record()[0].string(load.string_data()).unwrap();
241        assert_eq!(data.chars().collect::<String>(), "cé");
242    }
243
244    #[test]
245    fn roundtrip() {
246        init();
247
248        #[rustfmt::skip]
249        static COLINS_BESPOKE_DATA: &[u8] = &[
250            0x0, 0x0, // version
251            0x0, 0x03, // count
252            0x0, 42, // storage offset
253            //record 1:
254            0x00, 0x03, // platformID
255            0x00, 0x01, // encodingID
256            0x04, 0x09, // languageID
257            0x00, 0x01, // nameID
258            0x00, 0x0a, // length
259            0x00, 0x00, // offset
260            //record 2:
261            0x00, 0x03, // platformID
262            0x00, 0x01, // encodingID
263            0x04, 0x09, // languageID
264            0x00, 0x02, // nameID
265            0x00, 0x10, // length
266            0x00, 0x0a, // offset
267            //record 2:
268            0x00, 0x03, // platformID
269            0x00, 0x01, // encodingID
270            0x04, 0x09, // languageID
271            0x00, 0x03, // nameID
272            0x00, 0x18, // length
273            0x00, 0x1a, // offset
274            // storage area:
275            // string 1 'colin'
276            0x0, 0x63, 0x0, 0x6F, 0x0, 0x6C, 0x0, 0x69,
277            0x0, 0x6E,
278            // string 2, 'nicelife'
279            0x0, 0x6E, 0x0, 0x69, 0x0, 0x63, 0x0, 0x65,
280            0x0, 0x6C, 0x0, 0x69, 0x0, 0x66, 0x0, 0x65,
281            // string3 'i hate fonts'
282            0x0, 0x69, 0x0, 0x20, 0x0, 0x68, 0x0, 0x61,
283            0x0, 0x74, 0x0, 0x65, 0x0, 0x20, 0x0, 0x66,
284            0x0, 0x6F, 0x0, 0x6E, 0x0, 0x74, 0x0, 0x73,
285        ];
286
287        let raw_table =
288            fontcull_read_fonts::tables::name::Name::read(FontData::new(COLINS_BESPOKE_DATA))
289                .unwrap();
290        let owned: Name = raw_table.to_owned_table();
291        let dumped = crate::dump_table(&owned).unwrap();
292        let reloaded =
293            fontcull_read_fonts::tables::name::Name::read(FontData::new(&dumped)).unwrap();
294
295        for rec in raw_table.name_record() {
296            let raw_str = rec.string(raw_table.string_data()).unwrap();
297            debug!("{raw_str}");
298        }
299
300        assert_eq!(raw_table.version(), reloaded.version());
301        assert_eq!(raw_table.count(), reloaded.count());
302        assert_eq!(raw_table.storage_offset(), reloaded.storage_offset());
303
304        let mut fail = false;
305        for (old, new) in raw_table
306            .name_record()
307            .iter()
308            .zip(reloaded.name_record().iter())
309        {
310            assert_eq!(old.platform_id(), new.platform_id());
311            assert_eq!(old.encoding_id(), new.encoding_id());
312            assert_eq!(old.language_id(), new.language_id());
313            assert_eq!(old.name_id(), new.name_id());
314            assert_eq!(old.length(), new.length());
315            debug!("{:?} {:?}", old.string_offset(), new.string_offset());
316            let old_str = old.string(raw_table.string_data()).unwrap();
317            let new_str = new.string(reloaded.string_data()).unwrap();
318            if old_str != new_str {
319                debug!("'{old_str}' != '{new_str}'");
320                fail = true;
321            }
322        }
323        if fail {
324            panic!("some comparisons failed");
325        }
326    }
327}