Coverage Report

Created: 2025-12-14 06:48

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/ots/src/name.cc
Line
Count
Source
1
// Copyright (c) 2011-2017 The OTS Authors. All rights reserved.
2
// Use of this source code is governed by a BSD-style license that can be
3
// found in the LICENSE file.
4
5
#include "name.h"
6
7
#include <algorithm>
8
#include <cstring>
9
#include <cctype>
10
11
// name - Naming Table
12
// http://www.microsoft.com/typography/otspec/name.htm
13
14
namespace {
15
16
// We disallow characters outside the URI spec "unreserved characters"
17
// set; any chars outside this set will be replaced by underscore.
18
77.9k
bool AllowedInPsName(char c) {
19
77.9k
  return isalnum(c) || std::strchr("-._~", c);
20
77.9k
}
21
22
2.13k
bool SanitizePsNameAscii(std::string& name) {
23
2.13k
  if (name.size() > 63)
24
94
    return false;
25
26
39.9k
  for (unsigned i = 0; i < name.size(); ++i) {
27
37.9k
    if (!AllowedInPsName(name[i])) {
28
11.3k
      name[i] = '_';
29
11.3k
    }
30
37.9k
  }
31
2.04k
  return true;
32
2.13k
}
33
34
4.97k
bool SanitizePsNameUtf16Be(std::string& name) {
35
4.97k
  if ((name.size() & 1) != 0)
36
843
    return false;
37
4.13k
  if (name.size() > 2 * 63)
38
450
    return false;
39
40
43.7k
  for (unsigned i = 0; i < name.size(); i += 2) {
41
40.8k
    if (name[i] != 0) {
42
      // non-Latin1 char in psname? reject it altogether
43
799
      return false;
44
799
    }
45
40.0k
    if (!AllowedInPsName(name[i+1])) {
46
594
      name[i] = '_';
47
594
    }
48
40.0k
  }
49
2.88k
  return true;
50
3.68k
}
51
52
void AssignToUtf16BeFromAscii(std::string* target,
53
88.3k
                              const std::string& source) {
54
88.3k
  target->resize(source.size() * 2);
55
1.20M
  for (unsigned i = 0, j = 0; i < source.size(); i++) {
56
1.12M
    (*target)[j++] = '\0';
57
1.12M
    (*target)[j++] = source[i];
58
1.12M
  }
59
88.3k
}
60
61
}  // namespace
62
63
64
namespace ots {
65
66
21.7k
bool OpenTypeNAME::Parse(const uint8_t* data, size_t length) {
67
21.7k
  Buffer table(data, length);
68
69
21.7k
  uint16_t format = 0;
70
21.7k
  if (!table.ReadU16(&format) || format > 1) {
71
67
    return Error("Failed to read table format or bad format %d", format);
72
67
  }
73
74
21.6k
  uint16_t count = 0;
75
21.6k
  if (!table.ReadU16(&count)) {
76
19
    return Error("Failed to read name count");
77
19
  }
78
79
21.6k
  uint16_t string_offset = 0;
80
21.6k
  if (!table.ReadU16(&string_offset) || string_offset > length) {
81
21
    return Error("Failed to read or bad stringOffset");
82
21
  }
83
21.6k
  const char* string_base = reinterpret_cast<const char*>(data) +
84
21.6k
      string_offset;
85
86
21.6k
  bool sort_required = false;
87
88
  // Read all the names, discarding any with invalid IDs,
89
  // and any where the offset/length would be outside the table.
90
  // A stricter alternative would be to reject the font if there
91
  // are invalid name records, but it's not clear that is necessary.
92
3.33M
  for (unsigned i = 0; i < count; ++i) {
93
3.31M
    NameRecord rec;
94
3.31M
    uint16_t name_length, name_offset = 0;
95
3.31M
    if (!table.ReadU16(&rec.platform_id) ||
96
3.31M
        !table.ReadU16(&rec.encoding_id) ||
97
3.31M
        !table.ReadU16(&rec.language_id) ||
98
3.31M
        !table.ReadU16(&rec.name_id) ||
99
3.31M
        !table.ReadU16(&name_length) ||
100
3.31M
        !table.ReadU16(&name_offset)) {
101
183
      return Error("Failed to read name entry %d", i);
102
183
    }
103
    // check platform & encoding, discard names with unknown values
104
3.31M
    switch (rec.platform_id) {
105
888k
      case 0:  // Unicode
106
888k
        if (rec.encoding_id > 6) {
107
263k
          continue;
108
263k
        }
109
624k
        break;
110
624k
      case 1:  // Macintosh
111
175k
        if (rec.encoding_id > 32) {
112
36.0k
          continue;
113
36.0k
        }
114
139k
        break;
115
139k
      case 2:  // ISO
116
29.0k
        if (rec.encoding_id > 2) {
117
14.6k
          continue;
118
14.6k
        }
119
14.3k
        break;
120
93.3k
      case 3:  // Windows: IDs 7 to 9 are "reserved"
121
93.3k
        if (rec.encoding_id > 6 && rec.encoding_id != 10) {
122
13.7k
          continue;
123
13.7k
        }
124
79.6k
        break;
125
79.6k
      case 4:  // Custom (OTF Windows NT compatibility)
126
35.9k
        if (rec.encoding_id > 255) {
127
9.26k
          continue;
128
9.26k
        }
129
26.6k
        break;
130
2.09M
      default:  // unknown platform
131
2.09M
        continue;
132
3.31M
    }
133
134
885k
    const unsigned name_end = static_cast<unsigned>(string_offset) +
135
885k
        name_offset + name_length;
136
885k
    if (name_end > length) {
137
109k
      continue;
138
109k
    }
139
775k
    rec.text.resize(name_length);
140
775k
    rec.text.assign(string_base + name_offset, name_length);
141
142
775k
    if (rec.name_id == 6) {
143
      // PostScript name: "sanitize" it by replacing any chars outside the
144
      // URI spec "unreserved" set by underscore, or reject the name entirely
145
      // (and use a fallback) if it looks really broken.
146
7.95k
      if (rec.platform_id == 1) {
147
2.13k
        if (!SanitizePsNameAscii(rec.text)) {
148
94
          continue;
149
94
        }
150
5.81k
      } else if (rec.platform_id == 0 || rec.platform_id == 3) {
151
4.97k
        if (!SanitizePsNameUtf16Be(rec.text)) {
152
2.09k
          continue;
153
2.09k
        }
154
4.97k
      }
155
7.95k
    }
156
157
773k
    if (!this->names.empty() && !(this->names.back() < rec)) {
158
488k
      Warning("name records are not sorted.");
159
488k
      sort_required = true;
160
488k
    }
161
162
773k
    this->names.push_back(rec);
163
773k
    this->name_ids.insert(rec.name_id);
164
773k
  }
165
166
21.4k
  if (format == 1) {
167
    // extended name table format with language tags
168
294
    uint16_t lang_tag_count;
169
294
    if (!table.ReadU16(&lang_tag_count)) {
170
1
      return Error("Failed to read langTagCount");
171
1
    }
172
45.6k
    for (unsigned i = 0; i < lang_tag_count; ++i) {
173
45.4k
      uint16_t tag_length = 0;
174
45.4k
      uint16_t tag_offset = 0;
175
45.4k
      if (!table.ReadU16(&tag_length) || !table.ReadU16(&tag_offset)) {
176
29
        return Error("Failed to read length or offset for langTagRecord %d", i);
177
29
      }
178
45.4k
      const unsigned tag_end = static_cast<unsigned>(string_offset) +
179
45.4k
          tag_offset + tag_length;
180
45.4k
      if (tag_end > length) {
181
43
        return Error("bad end of tag %d > %ld for langTagRecord %d", tag_end, length, i);
182
43
      }
183
      // Lang tag is BCP 47 tag per the spec, the recommonded BCP 47 max tag
184
      // length is 35:
185
      // https://tools.ietf.org/html/bcp47#section-4.4.1
186
      // We are being too generous and allowing for 100 (multiplied by 2 since
187
      // this is UTF-16 string).
188
45.3k
      if (tag_length > 100 * 2) {
189
47
        return Error("Too long language tag for LangTagRecord %d: %d", i, tag_length);
190
47
      }
191
45.3k
      std::string tag(string_base + tag_offset, tag_length);
192
45.3k
      this->lang_tags.push_back(tag);
193
45.3k
    }
194
293
  }
195
196
21.3k
  if (table.offset() > string_offset) {
197
    // the string storage apparently overlapped the name/tag records;
198
    // consider this font to be badly broken
199
54
    return Error("Bad table offset %ld > %d", table.offset(), string_offset);
200
54
  }
201
202
  // check existence of required name strings (synthesize if necessary)
203
  //  [0 - copyright - skip]
204
  //   1 - family
205
  //   2 - subfamily
206
  //  [3 - unique ID - skip]
207
  //   4 - full name
208
  //   5 - version
209
  //   6 - postscript name
210
21.2k
  static const uint16_t kStdNameCount = 7;
211
21.2k
  static const char* kStdNames[kStdNameCount] = {
212
21.2k
    NULL,
213
21.2k
    "OTS derived font",
214
21.2k
    "Unspecified",
215
21.2k
    NULL,
216
21.2k
    "OTS derived font",
217
21.2k
    "1.000",
218
21.2k
    "OTS-derived-font"
219
21.2k
  };
220
221
  // scan the names to check whether the required "standard" ones are present;
222
  // if not, we'll add our fixed versions here
223
21.2k
  bool mac_name[kStdNameCount] = { 0 };
224
21.2k
  bool win_name[kStdNameCount] = { 0 };
225
642k
  for (const auto& name : this->names) {
226
642k
    const uint16_t id = name.name_id;
227
642k
    if (id >= kStdNameCount || kStdNames[id] == NULL) {
228
548k
      continue;
229
548k
    }
230
94.6k
    if (name.platform_id == 1) {
231
23.5k
      mac_name[id] = true;
232
23.5k
      continue;
233
23.5k
    }
234
71.0k
    if (name.platform_id == 3) {
235
22.5k
      win_name[id] = true;
236
22.5k
      continue;
237
22.5k
    }
238
71.0k
  }
239
240
170k
  for (uint16_t i = 0; i < kStdNameCount; ++i) {
241
149k
    if (kStdNames[i] == NULL) {
242
42.5k
      continue;
243
42.5k
    }
244
106k
    if (!mac_name[i] && !win_name[i]) {
245
88.3k
      NameRecord mac_rec(1 /* platform_id */, 0 /* encoding_id */,
246
88.3k
                         0 /* language_id */ , i /* name_id */);
247
88.3k
      mac_rec.text.assign(kStdNames[i]);
248
249
88.3k
      NameRecord win_rec(3 /* platform_id */, 1 /* encoding_id */,
250
88.3k
                         1033 /* language_id */ , i /* name_id */);
251
88.3k
      AssignToUtf16BeFromAscii(&win_rec.text, std::string(kStdNames[i]));
252
253
88.3k
      this->names.push_back(mac_rec);
254
88.3k
      this->names.push_back(win_rec);
255
88.3k
      sort_required = true;
256
88.3k
    }
257
106k
  }
258
259
21.2k
  if (sort_required) {
260
19.8k
    std::sort(this->names.begin(), this->names.end());
261
19.8k
  }
262
263
21.2k
  return true;
264
21.3k
}
265
266
11.3k
bool OpenTypeNAME::Serialize(OTSStream* out) {
267
11.3k
  uint16_t name_count = static_cast<uint16_t>(this->names.size());
268
11.3k
  uint16_t lang_tag_count = static_cast<uint16_t>(this->lang_tags.size());
269
11.3k
  uint16_t format = 0;
270
11.3k
  size_t string_offset = 6 + name_count * 12;
271
272
11.3k
  if (this->lang_tags.size() > 0) {
273
    // lang tags require a format-1 name table
274
35
    format = 1;
275
35
    string_offset += 2 + lang_tag_count * 4;
276
35
  }
277
11.3k
  if (string_offset > 0xffff) {
278
0
    return Error("Bad stringOffset: %ld", string_offset);
279
0
  }
280
11.3k
  if (!out->WriteU16(format) ||
281
11.3k
      !out->WriteU16(name_count) ||
282
11.3k
      !out->WriteU16(static_cast<uint16_t>(string_offset))) {
283
0
    return Error("Failed to write name header");
284
0
  }
285
286
11.3k
  std::string string_data;
287
457k
  for (const auto& rec : this->names) {
288
457k
    if (string_data.size() + rec.text.size() >
289
457k
            std::numeric_limits<uint16_t>::max() ||
290
456k
        !out->WriteU16(rec.platform_id) ||
291
456k
        !out->WriteU16(rec.encoding_id) ||
292
456k
        !out->WriteU16(rec.language_id) ||
293
456k
        !out->WriteU16(rec.name_id) ||
294
456k
        !out->WriteU16(static_cast<uint16_t>(rec.text.size())) ||
295
456k
        !out->WriteU16(static_cast<uint16_t>(string_data.size())) ) {
296
227
      return Error("Failed to write nameRecord");
297
227
    }
298
456k
    string_data.append(rec.text);
299
456k
  }
300
301
11.1k
  if (format == 1) {
302
35
    if (!out->WriteU16(lang_tag_count)) {
303
0
      return Error("Failed to write langTagCount");
304
0
    }
305
157
    for (const auto& tag : this->lang_tags) {
306
157
      if (string_data.size() + tag.size() >
307
157
              std::numeric_limits<uint16_t>::max() ||
308
155
          !out->WriteU16(static_cast<uint16_t>(tag.size())) ||
309
155
          !out->WriteU16(static_cast<uint16_t>(string_data.size()))) {
310
2
        return Error("Failed to write langTagRecord");
311
2
      }
312
155
      string_data.append(tag);
313
155
    }
314
35
  }
315
316
11.1k
  if (!out->Write(string_data.data(), string_data.size())) {
317
0
    return Error("Failed to write string data");
318
0
  }
319
320
11.1k
  return true;
321
11.1k
}
322
323
20.4k
bool OpenTypeNAME::IsValidNameId(uint16_t nameID, bool addIfMissing) {
324
20.4k
  if (addIfMissing && !this->name_ids.count(nameID)) {
325
3.56k
    bool added_unicode = false;
326
3.56k
    bool added_macintosh = false;
327
3.56k
    bool added_windows = false;
328
3.56k
    const size_t names_size = this->names.size();  // original size
329
773k
    for (size_t i = 0; i < names_size; ++i) switch (names[i].platform_id) {
330
410k
     case 0:
331
410k
      if (!added_unicode) {
332
        // If there is an existing NameRecord with platform_id == 0 (Unicode),
333
        // then add a NameRecord for the the specified nameID with arguments
334
        // 0 (Unicode), 0 (v1.0), 0 (unspecified language).
335
3.36k
        this->names.emplace_back(0, 0, 0, nameID);
336
3.36k
        this->names.back().text = "NoName";
337
3.36k
        added_unicode = true;
338
3.36k
      }
339
410k
      break;
340
192k
     case 1:
341
192k
      if (!added_macintosh) {
342
        // If there is an existing NameRecord with platform_id == 1 (Macintosh),
343
        // then add a NameRecord for the specified nameID with arguments
344
        // 1 (Macintosh), 0 (Roman), 0 (English).
345
3.46k
        this->names.emplace_back(1, 0, 0, nameID);
346
3.46k
        this->names.back().text = "NoName";
347
3.46k
        added_macintosh = true;
348
3.46k
      }
349
192k
      break;
350
135k
     case 3:
351
135k
      if (!added_windows) {
352
        // If there is an existing NameRecord with platform_id == 3 (Windows),
353
        // then add a NameRecord for the specified nameID with arguments
354
        // 3 (Windows), 1 (UCS), 1033 (US English).
355
3.56k
        this->names.emplace_back(3, 1, 1033, nameID);
356
3.56k
        this->names.back().text = "NoName";
357
3.56k
        added_windows = true;
358
3.56k
      }
359
135k
      break;
360
769k
    }
361
3.56k
    if (added_unicode || added_macintosh || added_windows) {
362
3.56k
      std::sort(this->names.begin(), this->names.end());
363
3.56k
      this->name_ids.insert(nameID);
364
3.56k
    }
365
3.56k
  }
366
20.4k
  return this->name_ids.count(nameID);
367
20.4k
}
368
369
// List of font names considered "tricky" (dependent on applying original TrueType instructions) by FreeType, see
370
// https://gitlab.freedesktop.org/freetype/freetype/-/blob/2d9fce53d4ce89f36075168282fcdd7289e082f9/src/truetype/ttobjs.c#L170-241
371
static const char* tricky_font_names[] = {
372
  "cpop",
373
  "DFGirl-W6-WIN-BF",
374
  "DFGothic-EB",
375
  "DFGyoSho-Lt",
376
  "DFHei",
377
  "DFHSGothic-W5",
378
  "DFHSMincho-W3",
379
  "DFHSMincho-W7",
380
  "DFKaiSho-SB",
381
  "DFKaiShu",
382
  "DFKai-SB",
383
  "DFMing",
384
  "DLC",
385
  "HuaTianKaiTi?",
386
  "HuaTianSongTi?",
387
  "Ming(for ISO10646)",
388
  "MingLiU",
389
  "MingMedium",
390
  "PMingLiU",
391
  "MingLi43"
392
};
393
394
17.8k
bool OpenTypeNAME::IsTrickyFont() const {
395
701k
  for (const auto& name : this->names) {
396
701k
    const uint16_t id = name.name_id;
397
701k
    if (id != 1) {
398
631k
      continue;
399
631k
    }
400
1.39M
    for (const auto* p : tricky_font_names) {
401
1.39M
      if (name.text.find(p) != std::string::npos) {
402
188
        return true;
403
188
      }
404
1.39M
    }
405
70.0k
  }
406
17.6k
  return false;
407
17.8k
}
408
409
}  // namespace