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