1 /*
2  * Copyright 2011 The Android Open Source Project
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  * 2021.9.9 SkFontMgr_config_parser on previewer of ohos.
7  *           Copyright (c) 2021 Huawei Device Co., Ltd. All rights reserved.
8  */
9 
10 // Despite the name and location, this is portable code.
11 
12 #include "src/ports/SkFontMgr_config_parser.h"
13 #include "src/ports/skia_ohos/HmSymbolConfig_ohos.h"
14 
15 #include <expat.h>
16 
17 #include "include/core/SkStream.h"
18 #include "include/private/SkFixed.h"
19 #include "src/core/SkTSearch.h"
20 
21 #define SK_FONT_CONFIG_FILE_NAME "fonts.xml"
22 std::string SkFontMgr::containerFontPath = "";
23 
24 namespace {
25 
26 std::string g_lmpSystemFontsFile = "INVALID_FILE_PATH";
27 
28 }
29 
30 /**
31  * This parser file for android contains TWO 'familyset' handlers:
32  * One for JB and earlier and the other for LMP and later.
33  * For previewer, we only use the fonts.xml in LMP, so the LMP 'familyset' handler is only needed.
34  */
35 
36 struct FamilyData;
37 
38 struct TagHandler {
39     /** Called at the start tag.
40      *  Called immediately after the parent tag retuns this handler from a call to 'tag'.
41      *  Allows setting up for handling the tag content and processing attributes.
42      *  If nullptr, will not be called.
43      */
44     void (*start)(FamilyData* data, const char* tag, const char** attributes);
45 
46     /** Called at the end tag.
47      *  Allows post-processing of any accumulated information.
48      *  This will be the last call made in relation to the current tag.
49      *  If nullptr, will not be called.
50      */
51     void (*end)(FamilyData* data, const char* tag);
52 
53     /** Called when a nested tag is encountered.
54      *  This is responsible for determining how to handle the tag.
55      *  If the tag is not recognized, return nullptr to skip the tag.
56      *  If nullptr, all nested tags will be skipped.
57      */
58     const TagHandler* (*tag)(FamilyData* data, const char* tag, const char** attributes);
59 
60     /** The character handler for this tag.
61      *  This is only active for character data contained directly in this tag (not sub-tags).
62      *  The first parameter will be castable to a FamilyData*.
63      *  If nullptr, any character data in this tag will be ignored.
64      */
65     XML_CharacterDataHandler chars;
66 };
67 
68 /** Represents the current parsing state. */
69 struct FamilyData {
FamilyDataFamilyData70     FamilyData(XML_Parser parser, SkTDArray<FontFamily*>& families,
71                const SkString& basePath, bool isFallback, const char* filename,
72                const TagHandler* topLevelHandler)
73         : fParser(parser)
74         , fFamilies(families)
75         , fCurrentFamily(nullptr)
76         , fCurrentFontInfo(nullptr)
77         , fVersion(0)
78         , fBasePath(basePath)
79         , fIsFallback(isFallback)
80         , fFilename(filename)
81         , fDepth(1)
82         , fSkip(0)
83         , fHandler(&topLevelHandler, 1)
84     { }
85 
86     XML_Parser fParser;                         // The expat parser doing the work, owned by caller
87     SkTDArray<FontFamily*>& fFamilies;          // The array to append families, owned by caller
88     std::unique_ptr<FontFamily> fCurrentFamily; // The family being created, owned by this
89     FontFileInfo* fCurrentFontInfo;             // The info being created, owned by fCurrentFamily
90     int fVersion;                               // The version of the file parsed.
91     const SkString& fBasePath;                  // The current base path.
92     const bool fIsFallback;                     // The file being parsed is a fallback file
93     const char* fFilename;                      // The name of the file currently being parsed.
94 
95     int fDepth;                                 // The current element depth of the parse.
96     int fSkip;                                  // The depth to stop skipping, 0 if not skipping.
97     SkTDArray<const TagHandler*> fHandler;      // The stack of current tag handlers.
98 };
99 
memeq(const char *s1, const char *s2, size_t n1, size_t n2)100 static bool memeq(const char *s1, const char *s2, size_t n1, size_t n2)
101 {
102     return n1 == n2 && 0 == memcmp(s1, s2, n1);
103 }
104 #define MEMEQ(c, s, n) memeq(c, s, sizeof(c) - 1, n)
105 
106 #define ATTS_NON_NULL(a, i) (a[i] != nullptr && a[i + 1] != nullptr)
107 
108 #define SK_FONTMGR_CONFIG_PARSER_PREFIX "[SkFontMgr Config Parser] "
109 
110 #define SK_FONTCONFIGPARSER_WARNING(message, ...)                                                 \
111     SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "%s:%d:%d: warning: " message "\n", self->fFilename, \
112              XML_GetCurrentLineNumber(self->fParser), XML_GetCurrentColumnNumber(self->fParser), ##__VA_ARGS__)
113 
is_whitespace(char c)114 static bool is_whitespace(char c)
115 {
116     return c == ' ' || c == '\n' || c == '\r' || c == '\t';
117 }
118 
trim_string(SkString* s)119 static void trim_string(SkString* s)
120 {
121     char* str = s->writable_str();
122     const char* start = str;  // start is inclusive
123     const char* end = start + s->size();  // end is exclusive
124     while (is_whitespace(*start)) { ++start; }
125     if (start != end) {
126         --end;  // make end inclusive
127         while (is_whitespace(*end)) {
128             --end;
129         }
130         ++end;  // make end exclusive
131     }
132     size_t len = end - start;
133     memmove(str, start, len);
134     s->resize(len);
135 }
136 
137 namespace lmpParser {
138 
139 static const TagHandler axisHandler = {
140     /*start*/[](FamilyData* self, const char* tag, const char** attributes) {
141         FontFileInfo& file = *self->fCurrentFontInfo;
142         SkFourByteTag axisTag = SkSetFourByteTag('\0', '\0', '\0', '\0');
143         SkFixed axisStyleValue = 0;
144         bool axisTagIsValid = false;
145         bool axisStyleValueIsValid = false;
146         for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
147             const char* name = attributes[i];
148             const char* value = attributes[i + 1];
149             size_t nameLen = strlen(name);
150             if (MEMEQ("tag", name, nameLen)) {
151                 size_t valueLen = strlen(value);
152                 if (valueLen == 4) {
153                     axisTag = SkSetFourByteTag(value[0], value[1], value[2], value[3]);
154                     axisTagIsValid = true;
155                     for (int j = 0; j < file.fVariationDesignPosition.count() - 1; ++j) {
156                         if (file.fVariationDesignPosition[j].axis == axisTag) {
157                             axisTagIsValid = false;
158                             SK_FONTCONFIGPARSER_WARNING("'%c%c%c%c' axis specified more than once",
159                                                         (axisTag >> 24) & 0xFF,
160                                                         (axisTag >> 16) & 0xFF,
161                                                         (axisTag >>  8) & 0xFF,
162                                                         (axisTag      ) & 0xFF);
163                         }
164                     }
165                 } else {
166                     SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid axis tag", value);
167                 }
168             } else if (MEMEQ("stylevalue", name, nameLen)) {
169                 if (parse_fixed<16>(value, &axisStyleValue)) {
170                     axisStyleValueIsValid = true;
171                 } else {
172                     SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid axis stylevalue", value);
173                 }
174             }
175         }
176         if (axisTagIsValid && axisStyleValueIsValid) {
177             auto& coordinate = file.fVariationDesignPosition.push_back();
178             coordinate.axis = axisTag;
179             coordinate.value = SkFixedToScalar(axisStyleValue);
180         }
181     },
182     /*end*/nullptr,
183     /*tag*/nullptr,
184     /*chars*/nullptr,
185 };
186 
187 static const TagHandler fontHandler = {
188     /*start*/[](FamilyData* self, const char* tag, const char** attributes) {
189         // 'weight' (non-negative integer) [default 0]
190         // 'style' ("normal", "italic") [default "auto"]
191         // 'index' (non-negative integer) [default 0]
192         // The character data should be a filename.
193         FontFileInfo& file = self->fCurrentFamily->fFonts.push_back();
194         self->fCurrentFontInfo = &file;
195         SkString fallbackFor;
196         for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
197             const char* name = attributes[i];
198             const char* value = attributes[i + 1];
199             size_t nameLen = strlen(name);
200             if (MEMEQ("weight", name, nameLen)) {
201                 if (!parse_non_negative_integer(value, &file.fWeight)) {
202                     SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid weight", value);
203                 }
204             } else if (MEMEQ("style", name, nameLen)) {
205                 size_t valueLen = strlen(value);
206                 if (MEMEQ("normal", value, valueLen)) {
207                     file.fStyle = FontFileInfo::Style::kNormal;
208                 } else if (MEMEQ("italic", value, valueLen)) {
209                     file.fStyle = FontFileInfo::Style::kItalic;
210                 }
211             } else if (MEMEQ("index", name, nameLen)) {
212                 if (!parse_non_negative_integer(value, &file.fIndex)) {
213                     SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid index", value);
214                 }
215             } else if (MEMEQ("fallbackFor", name, nameLen)) {
216                 /** fallbackFor specifies a family fallback and should have been on family. */
217                 fallbackFor = value;
218             }
219         }
220         if (!fallbackFor.isEmpty()) {
221             std::unique_ptr<FontFamily>* fallbackFamily =
222                 self->fCurrentFamily->fallbackFamilies.find(fallbackFor);
223             if (!fallbackFamily) {
224                 std::unique_ptr<FontFamily> newFallbackFamily(
225                     new FontFamily(self->fCurrentFamily->fBasePath, true));
226                 fallbackFamily = self->fCurrentFamily->fallbackFamilies.set(
227                     fallbackFor, std::move(newFallbackFamily));
228                 (*fallbackFamily)->fLanguages = self->fCurrentFamily->fLanguages;
229                 (*fallbackFamily)->fVariant = self->fCurrentFamily->fVariant;
230                 (*fallbackFamily)->fOrder = self->fCurrentFamily->fOrder;
231                 (*fallbackFamily)->fFallbackFor = fallbackFor;
232             }
233             self->fCurrentFontInfo = &(*fallbackFamily)->fFonts.emplace_back(file);
234             self->fCurrentFamily->fFonts.pop_back();
235         }
236     },
237     /*end*/[](FamilyData* self, const char* tag) {
238         trim_string(&self->fCurrentFontInfo->fFileName);
239     },
240     /*tag*/[](FamilyData* self, const char* tag, const char** attributes) -> const TagHandler* {
241         size_t len = strlen(tag);
242         if (MEMEQ("axis", tag, len)) {
243             return &axisHandler;
244         }
245         return nullptr;
246     },
247     /*chars*/[](void* data, const char* s, int len) {
248         FamilyData* self = static_cast<FamilyData*>(data);
249         self->fCurrentFontInfo->fFileName.append(s, len);
250     }
251 };
252 
253 static const TagHandler familyHandler = {
254     /*start*/[](FamilyData* self, const char* tag, const char** attributes) {
255         // 'name' (string) [optional]
256         // 'lang' (space separated string) [default ""]
257         // 'variant' ("elegant", "compact") [default "default"]
258         // If there is no name, this is a fallback only font.
259         FontFamily* family = new FontFamily(self->fBasePath, true);
260         self->fCurrentFamily.reset(family);
261         for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
262             const char* name = attributes[i];
263             const char* value = attributes[i + 1];
264             size_t nameLen = strlen(name);
265             size_t valueLen = strlen(value);
266             if (MEMEQ("name", name, nameLen)) {
267                 SkAutoAsciiToLC tolc(value);
268                 family->fNames.push_back().set(tolc.lc());
269                 family->fIsFallbackFont = false;
270             } else if (MEMEQ("lang", name, nameLen)) {
271                 size_t i = 0;
272                 while (true) {
273                     for (; i < valueLen && is_whitespace(value[i]); ++i) { }
274                     if (i == valueLen) {
275                         break;
276                     }
277                     size_t j;
278                     for (j = i + 1; j < valueLen && !is_whitespace(value[j]); ++j) { }
279                     family->fLanguages.emplace_back(value + i, j - i);
280                     i = j;
281                     if (i == valueLen) {
282                         break;
283                     }
284                 }
285             } else if (MEMEQ("variant", name, nameLen)) {
286                 if (MEMEQ("elegant", value, valueLen)) {
287                     family->fVariant = kElegant_FontVariant;
288                 } else if (MEMEQ("compact", value, valueLen)) {
289                     family->fVariant = kCompact_FontVariant;
290                 }
291             }
292         }
293     },
294     /*end*/[](FamilyData* self, const char* tag) {
295         *self->fFamilies.append() = self->fCurrentFamily.release();
296     },
297     /*tag*/[](FamilyData* self, const char* tag, const char** attributes) -> const TagHandler* {
298         size_t len = strlen(tag);
299         if (MEMEQ("font", tag, len)) {
300             return &fontHandler;
301         }
302         return nullptr;
303     },
304     /*chars*/nullptr,
305 };
306 
find_family(FamilyData* self, const SkString& familyName)307 static FontFamily* find_family(FamilyData* self, const SkString& familyName)
308 {
309     for (int i = 0; i < self->fFamilies.count(); i++) {
310         FontFamily* candidate = self->fFamilies[i];
311         for (int j = 0; j < candidate->fNames.count(); j++) {
312             if (candidate->fNames[j] == familyName) {
313                 return candidate;
314             }
315         }
316     }
317     return nullptr;
318 }
319 
320 static const TagHandler aliasHandler = {
321     /*start*/[](FamilyData* self, const char* tag, const char** attributes) {
322         // 'name' (string) introduces a new family name.
323         // 'to' (string) specifies which (previous) family to alias
324         // 'weight' (non-negative integer) [optional]
325         // If it *does not* have a weight, 'name' is an alias for the entire 'to' family.
326         // If it *does* have a weight, 'name' is a new family consisting of
327         // the font(s) with 'weight' from the 'to' family.
328 
329         SkString aliasName;
330         SkString to;
331         int weight = 0;
332         for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
333             const char* name = attributes[i];
334             const char* value = attributes[i + 1];
335             size_t nameLen = strlen(name);
336             if (MEMEQ("name", name, nameLen)) {
337                 SkAutoAsciiToLC tolc(value);
338                 aliasName.set(tolc.lc());
339             } else if (MEMEQ("to", name, nameLen)) {
340                 to.set(value);
341             } else if (MEMEQ("weight", name, nameLen)) {
342                 if (!parse_non_negative_integer(value, &weight)) {
343                     SK_FONTCONFIGPARSER_WARNING("'%s' is an invalid weight", value);
344                 }
345             }
346         }
347 
348         // Assumes that the named family is already declared
349         FontFamily* targetFamily = find_family(self, to);
350         if (!targetFamily) {
351             SK_FONTCONFIGPARSER_WARNING("'%s' alias target not found", to.c_str());
352             return;
353         }
354 
355         if (weight) {
356             FontFamily* family = new FontFamily(targetFamily->fBasePath, self->fIsFallback);
357             family->fNames.push_back().set(aliasName);
358 
359             for (int i = 0; i < targetFamily->fFonts.count(); i++) {
360                 if (targetFamily->fFonts[i].fWeight == weight) {
361                     family->fFonts.push_back(targetFamily->fFonts[i]);
362                 }
363             }
364             *self->fFamilies.append() = family;
365         } else {
366             targetFamily->fNames.push_back().set(aliasName);
367         }
368     },
369     /*end*/nullptr,
370     /*tag*/nullptr,
371     /*chars*/nullptr,
372 };
373 
374 static const TagHandler familySetHandler = {
375     /*start*/[](FamilyData* self, const char* tag, const char** attributes) { },
376     /*end*/nullptr,
377     /*tag*/[](FamilyData* self, const char* tag, const char** attributes) -> const TagHandler* {
378         size_t len = strlen(tag);
379         if (MEMEQ("family", tag, len)) {
380             return &familyHandler;
381         } else if (MEMEQ("alias", tag, len)) {
382             return &aliasHandler;
383         }
384         return nullptr;
385     },
386     /*chars*/nullptr,
387 };
388 
389 } // namespace lmpParser
390 
391 static const TagHandler topLevelHandler = {
392     /*start*/nullptr,
393     /*end*/nullptr,
394     /*tag*/[](FamilyData* self, const char* tag, const char** attributes) -> const TagHandler* {
395         size_t len = strlen(tag);
396         if (MEMEQ("familyset", tag, len)) {
397             // 'version' (non-negative integer) [default 0]
398             for (size_t i = 0; ATTS_NON_NULL(attributes, i); i += 2) {
399                 const char* name = attributes[i];
400                 size_t nameLen = strlen(name);
401                 if (MEMEQ("version", name, nameLen)) {
402                     const char* value = attributes[i + 1];
403                     if (parse_non_negative_integer(value, &self->fVersion)) {
404                         if (self->fVersion >= 21) {
405                             return &lmpParser::familySetHandler;
406                         }
407                     }
408                 }
409             }
410         }
411         return nullptr;
412     },
413     /*chars*/ nullptr,
414 };
415 
start_element_handler(void *data, const char *tag, const char **attributes)416 static void XMLCALL start_element_handler(void *data, const char *tag, const char **attributes)
417 {
418     FamilyData* self = static_cast<FamilyData*>(data);
419 
420     if (!self->fSkip) {
421         const TagHandler* parent = self->fHandler.top();
422         const TagHandler* child = parent->tag ? parent->tag(self, tag, attributes) : nullptr;
423         if (child) {
424             if (child->start) {
425                 child->start(self, tag, attributes);
426             }
427             self->fHandler.push_back(child);
428             XML_SetCharacterDataHandler(self->fParser, child->chars);
429         } else {
430             SK_FONTCONFIGPARSER_WARNING("'%s' tag not recognized, skipping", tag);
431             XML_SetCharacterDataHandler(self->fParser, nullptr);
432             self->fSkip = self->fDepth;
433         }
434     }
435 
436     ++self->fDepth;
437 }
438 
end_element_handler(void* data, const char* tag)439 static void XMLCALL end_element_handler(void* data, const char* tag)
440 {
441     FamilyData* self = static_cast<FamilyData*>(data);
442     --self->fDepth;
443 
444     if (!self->fSkip) {
445         const TagHandler* child = self->fHandler.top();
446         if (child->end) {
447             child->end(self, tag);
448         }
449         self->fHandler.pop();
450         const TagHandler* parent = self->fHandler.top();
451         XML_SetCharacterDataHandler(self->fParser, parent->chars);
452     }
453 
454     if (self->fSkip == self->fDepth) {
455         self->fSkip = 0;
456         const TagHandler* parent = self->fHandler.top();
457         XML_SetCharacterDataHandler(self->fParser, parent->chars);
458     }
459 }
460 
xml_entity_decl_handler(void *data, const XML_Char *entityName, int is_parameter_entity, const XML_Char *value, int value_length, const XML_Char *base, const XML_Char *systemId, const XML_Char *publicId, const XML_Char *notationName)461 static void XMLCALL xml_entity_decl_handler(void *data,
462                                             const XML_Char *entityName,
463                                             int is_parameter_entity,
464                                             const XML_Char *value,
465                                             int value_length,
466                                             const XML_Char *base,
467                                             const XML_Char *systemId,
468                                             const XML_Char *publicId,
469                                             const XML_Char *notationName)
470 {
471     FamilyData* self = static_cast<FamilyData*>(data);
472     SK_FONTCONFIGPARSER_WARNING("'%s' entity declaration found, stopping processing", entityName);
473     XML_StopParser(self->fParser, XML_FALSE);
474 }
475 
476 static const XML_Memory_Handling_Suite sk_XML_alloc = {
477     sk_malloc_throw,
478     sk_realloc_throw,
479     sk_free
480 };
481 
482 /**
483  * This function parses the given filename and stores the results in the given
484  * families array. Returns the version of the file, negative if the file does not exist.
485  */
parse_config_file(const char* filename, SkTDArray<FontFamily*>& families, const SkString& basePath, bool isFallback)486 static int parse_config_file(const char* filename, SkTDArray<FontFamily*>& families,
487                              const SkString& basePath, bool isFallback)
488 {
489     SkFILEStream file(filename);
490 
491     // Some of the files we attempt to parse (in particular, /vendor/etc/fallback_fonts.xml)
492     // are optional - failure here is okay because one of these optional files may not exist.
493     if (!file.isValid()) {
494         SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "'%s' could not be opened\n", filename);
495         return -1;
496     }
497 
498     SkAutoTCallVProc<std::remove_pointer_t<XML_Parser>, XML_ParserFree> parser(
499         XML_ParserCreate_MM(nullptr, &sk_XML_alloc, nullptr));
500     if (!parser) {
501         SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "could not create XML parser\n");
502         return -1;
503     }
504 
505     FamilyData self(parser, families, basePath, isFallback, filename, &topLevelHandler);
506     XML_SetUserData(parser, &self);
507 
508     // Disable entity processing, to inhibit internal entity expansion. See expat CVE-2013-0340
509     XML_SetEntityDeclHandler(parser, xml_entity_decl_handler);
510 
511     // Start parsing oldschool; switch these in flight if we detect a newer version of the file.
512     XML_SetElementHandler(parser, start_element_handler, end_element_handler);
513 
514     // One would assume it would be faster to have a buffer on the stack and call XML_Parse.
515     // But XML_Parse will call XML_GetBuffer anyway and memmove the passed buffer into it.
516     // (Unless XML_CONTEXT_BYTES is undefined, but all users define it.)
517     // In debug, buffer a small odd number of bytes to detect slicing in XML_CharacterDataHandler.
518     static const int bufferSize = 512 SkDEBUGCODE(-507);
519     bool done = false;
520     while (!done) {
521         void* buffer = XML_GetBuffer(parser, bufferSize);
522         if (!buffer) {
523             SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "could not buffer enough to continue\n");
524             return -1;
525         }
526         size_t len = file.read(buffer, bufferSize);
527         done = file.isAtEnd();
528         XML_Status status = XML_ParseBuffer(parser, len, done);
529         if (XML_STATUS_ERROR == status) {
530             XML_Error error = XML_GetErrorCode(parser);
531             int line = XML_GetCurrentLineNumber(parser);
532             int column = XML_GetCurrentColumnNumber(parser);
533             const XML_LChar* errorString = XML_ErrorString(error);
534             SkDebugf(SK_FONTMGR_CONFIG_PARSER_PREFIX "%s:%d:%d error %d: %s.\n",
535                      filename, line, column, error, errorString);
536             return -1;
537         }
538     }
539     return self.fVersion;
540 }
541 
542 /** Returns the version of the system font file actually found, negative if none. */
append_system_font_families(SkTDArray<FontFamily*>& fontFamilies, const SkString& basePath)543 static int append_system_font_families(SkTDArray<FontFamily*>& fontFamilies,
544                                        const SkString& basePath)
545 {
546     int version = parse_config_file(g_lmpSystemFontsFile.c_str(), fontFamilies, basePath, false);
547     return version;
548 }
549 
GetSystemFontFamilies(SkTDArray<FontFamily*>& fontFamilies)550 void SkFontMgr_Config_Parser::GetSystemFontFamilies(SkTDArray<FontFamily*>& fontFamilies)
551 {
552     std::string containerFontBasePath = SkFontMgr::containerFontPath;
553     if (containerFontBasePath.empty()) {
554         printf("error getting font base path '%s'\n", containerFontBasePath.c_str());
555     }
556     SkString basePath(containerFontBasePath.c_str());
557     g_lmpSystemFontsFile = containerFontBasePath.append(SK_FONT_CONFIG_FILE_NAME);
558     HmSymbolConfig_OHOS::GetInstance()->ParseConfigOfHmSymbol("hm_symbol_config_next.json", basePath);
559     append_system_font_families(fontFamilies, basePath);
560 }
561 
getParent() const562 SkLanguage SkLanguage::getParent() const
563 {
564     SkASSERT(!fTag.isEmpty());
565     const char* tag = fTag.c_str();
566 
567     // strip off the rightmost "-.*"
568     const char* parentTagEnd = strrchr(tag, '-');
569     if (parentTagEnd == nullptr) {
570         return SkLanguage();
571     }
572     size_t parentTagLen = parentTagEnd - tag;
573     return SkLanguage(tag, parentTagLen);
574 }