Tue, 06 Jan 2015 21:39:09 +0100
Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
michael@0 | 1 | /* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
michael@0 | 2 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | #include "gfxGraphiteShaper.h" |
michael@0 | 7 | #include "nsString.h" |
michael@0 | 8 | #include "gfxContext.h" |
michael@0 | 9 | |
michael@0 | 10 | #include "graphite2/Font.h" |
michael@0 | 11 | #include "graphite2/Segment.h" |
michael@0 | 12 | |
michael@0 | 13 | #include "harfbuzz/hb.h" |
michael@0 | 14 | |
michael@0 | 15 | #define FloatToFixed(f) (65536 * (f)) |
michael@0 | 16 | #define FixedToFloat(f) ((f) * (1.0 / 65536.0)) |
michael@0 | 17 | // Right shifts of negative (signed) integers are undefined, as are overflows |
michael@0 | 18 | // when converting unsigned to negative signed integers. |
michael@0 | 19 | // (If speed were an issue we could make some 2's complement assumptions.) |
michael@0 | 20 | #define FixedToIntRound(f) ((f) > 0 ? ((32768 + (f)) >> 16) \ |
michael@0 | 21 | : -((32767 - (f)) >> 16)) |
michael@0 | 22 | |
michael@0 | 23 | using namespace mozilla; // for AutoSwap_* types |
michael@0 | 24 | |
michael@0 | 25 | /* |
michael@0 | 26 | * Creation and destruction; on deletion, release any font tables we're holding |
michael@0 | 27 | */ |
michael@0 | 28 | |
michael@0 | 29 | gfxGraphiteShaper::gfxGraphiteShaper(gfxFont *aFont) |
michael@0 | 30 | : gfxFontShaper(aFont), |
michael@0 | 31 | mGrFace(mFont->GetFontEntry()->GetGrFace()), |
michael@0 | 32 | mGrFont(nullptr) |
michael@0 | 33 | { |
michael@0 | 34 | mCallbackData.mFont = aFont; |
michael@0 | 35 | mCallbackData.mShaper = this; |
michael@0 | 36 | } |
michael@0 | 37 | |
michael@0 | 38 | gfxGraphiteShaper::~gfxGraphiteShaper() |
michael@0 | 39 | { |
michael@0 | 40 | if (mGrFont) { |
michael@0 | 41 | gr_font_destroy(mGrFont); |
michael@0 | 42 | } |
michael@0 | 43 | mFont->GetFontEntry()->ReleaseGrFace(mGrFace); |
michael@0 | 44 | } |
michael@0 | 45 | |
michael@0 | 46 | /*static*/ float |
michael@0 | 47 | gfxGraphiteShaper::GrGetAdvance(const void* appFontHandle, uint16_t glyphid) |
michael@0 | 48 | { |
michael@0 | 49 | const CallbackData *cb = |
michael@0 | 50 | static_cast<const CallbackData*>(appFontHandle); |
michael@0 | 51 | return FixedToFloat(cb->mFont->GetGlyphWidth(cb->mContext, glyphid)); |
michael@0 | 52 | } |
michael@0 | 53 | |
michael@0 | 54 | static inline uint32_t |
michael@0 | 55 | MakeGraphiteLangTag(uint32_t aTag) |
michael@0 | 56 | { |
michael@0 | 57 | uint32_t grLangTag = aTag; |
michael@0 | 58 | // replace trailing space-padding with NULs for graphite |
michael@0 | 59 | uint32_t mask = 0x000000FF; |
michael@0 | 60 | while ((grLangTag & mask) == ' ') { |
michael@0 | 61 | grLangTag &= ~mask; |
michael@0 | 62 | mask <<= 8; |
michael@0 | 63 | } |
michael@0 | 64 | return grLangTag; |
michael@0 | 65 | } |
michael@0 | 66 | |
michael@0 | 67 | struct GrFontFeatures { |
michael@0 | 68 | gr_face *mFace; |
michael@0 | 69 | gr_feature_val *mFeatures; |
michael@0 | 70 | }; |
michael@0 | 71 | |
michael@0 | 72 | static PLDHashOperator |
michael@0 | 73 | AddFeature(const uint32_t& aTag, uint32_t& aValue, void *aUserArg) |
michael@0 | 74 | { |
michael@0 | 75 | GrFontFeatures *f = static_cast<GrFontFeatures*>(aUserArg); |
michael@0 | 76 | |
michael@0 | 77 | const gr_feature_ref* fref = gr_face_find_fref(f->mFace, aTag); |
michael@0 | 78 | if (fref) { |
michael@0 | 79 | gr_fref_set_feature_value(fref, aValue, f->mFeatures); |
michael@0 | 80 | } |
michael@0 | 81 | return PL_DHASH_NEXT; |
michael@0 | 82 | } |
michael@0 | 83 | |
michael@0 | 84 | bool |
michael@0 | 85 | gfxGraphiteShaper::ShapeText(gfxContext *aContext, |
michael@0 | 86 | const char16_t *aText, |
michael@0 | 87 | uint32_t aOffset, |
michael@0 | 88 | uint32_t aLength, |
michael@0 | 89 | int32_t aScript, |
michael@0 | 90 | gfxShapedText *aShapedText) |
michael@0 | 91 | { |
michael@0 | 92 | // some font back-ends require this in order to get proper hinted metrics |
michael@0 | 93 | if (!mFont->SetupCairoFont(aContext)) { |
michael@0 | 94 | return false; |
michael@0 | 95 | } |
michael@0 | 96 | |
michael@0 | 97 | mCallbackData.mContext = aContext; |
michael@0 | 98 | |
michael@0 | 99 | if (!mGrFont) { |
michael@0 | 100 | if (!mGrFace) { |
michael@0 | 101 | return false; |
michael@0 | 102 | } |
michael@0 | 103 | |
michael@0 | 104 | if (mFont->ProvidesGlyphWidths()) { |
michael@0 | 105 | gr_font_ops ops = { |
michael@0 | 106 | sizeof(gr_font_ops), |
michael@0 | 107 | &GrGetAdvance, |
michael@0 | 108 | nullptr // vertical text not yet implemented |
michael@0 | 109 | }; |
michael@0 | 110 | mGrFont = gr_make_font_with_ops(mFont->GetAdjustedSize(), |
michael@0 | 111 | &mCallbackData, &ops, mGrFace); |
michael@0 | 112 | } else { |
michael@0 | 113 | mGrFont = gr_make_font(mFont->GetAdjustedSize(), mGrFace); |
michael@0 | 114 | } |
michael@0 | 115 | |
michael@0 | 116 | if (!mGrFont) { |
michael@0 | 117 | return false; |
michael@0 | 118 | } |
michael@0 | 119 | } |
michael@0 | 120 | |
michael@0 | 121 | gfxFontEntry *entry = mFont->GetFontEntry(); |
michael@0 | 122 | const gfxFontStyle *style = mFont->GetStyle(); |
michael@0 | 123 | uint32_t grLang = 0; |
michael@0 | 124 | if (style->languageOverride) { |
michael@0 | 125 | grLang = MakeGraphiteLangTag(style->languageOverride); |
michael@0 | 126 | } else if (entry->mLanguageOverride) { |
michael@0 | 127 | grLang = MakeGraphiteLangTag(entry->mLanguageOverride); |
michael@0 | 128 | } else { |
michael@0 | 129 | nsAutoCString langString; |
michael@0 | 130 | style->language->ToUTF8String(langString); |
michael@0 | 131 | grLang = GetGraphiteTagForLang(langString); |
michael@0 | 132 | } |
michael@0 | 133 | gr_feature_val *grFeatures = gr_face_featureval_for_lang(mGrFace, grLang); |
michael@0 | 134 | |
michael@0 | 135 | nsDataHashtable<nsUint32HashKey,uint32_t> mergedFeatures; |
michael@0 | 136 | |
michael@0 | 137 | // if style contains font-specific features |
michael@0 | 138 | if (MergeFontFeatures(style, |
michael@0 | 139 | mFont->GetFontEntry()->mFeatureSettings, |
michael@0 | 140 | aShapedText->DisableLigatures(), |
michael@0 | 141 | mFont->GetFontEntry()->FamilyName(), |
michael@0 | 142 | mergedFeatures)) |
michael@0 | 143 | { |
michael@0 | 144 | // enumerate result and insert into Graphite feature list |
michael@0 | 145 | GrFontFeatures f = {mGrFace, grFeatures}; |
michael@0 | 146 | mergedFeatures.Enumerate(AddFeature, &f); |
michael@0 | 147 | } |
michael@0 | 148 | |
michael@0 | 149 | size_t numChars = gr_count_unicode_characters(gr_utf16, |
michael@0 | 150 | aText, aText + aLength, |
michael@0 | 151 | nullptr); |
michael@0 | 152 | gr_segment *seg = gr_make_seg(mGrFont, mGrFace, 0, grFeatures, |
michael@0 | 153 | gr_utf16, aText, numChars, |
michael@0 | 154 | aShapedText->IsRightToLeft()); |
michael@0 | 155 | |
michael@0 | 156 | gr_featureval_destroy(grFeatures); |
michael@0 | 157 | |
michael@0 | 158 | if (!seg) { |
michael@0 | 159 | return false; |
michael@0 | 160 | } |
michael@0 | 161 | |
michael@0 | 162 | nsresult rv = SetGlyphsFromSegment(aContext, aShapedText, aOffset, aLength, |
michael@0 | 163 | aText, seg); |
michael@0 | 164 | |
michael@0 | 165 | gr_seg_destroy(seg); |
michael@0 | 166 | |
michael@0 | 167 | return NS_SUCCEEDED(rv); |
michael@0 | 168 | } |
michael@0 | 169 | |
michael@0 | 170 | #define SMALL_GLYPH_RUN 256 // avoid heap allocation of per-glyph data arrays |
michael@0 | 171 | // for short (typical) runs up to this length |
michael@0 | 172 | |
michael@0 | 173 | struct Cluster { |
michael@0 | 174 | uint32_t baseChar; // in UTF16 code units, not Unicode character indices |
michael@0 | 175 | uint32_t baseGlyph; |
michael@0 | 176 | uint32_t nChars; // UTF16 code units |
michael@0 | 177 | uint32_t nGlyphs; |
michael@0 | 178 | Cluster() : baseChar(0), baseGlyph(0), nChars(0), nGlyphs(0) { } |
michael@0 | 179 | }; |
michael@0 | 180 | |
michael@0 | 181 | nsresult |
michael@0 | 182 | gfxGraphiteShaper::SetGlyphsFromSegment(gfxContext *aContext, |
michael@0 | 183 | gfxShapedText *aShapedText, |
michael@0 | 184 | uint32_t aOffset, |
michael@0 | 185 | uint32_t aLength, |
michael@0 | 186 | const char16_t *aText, |
michael@0 | 187 | gr_segment *aSegment) |
michael@0 | 188 | { |
michael@0 | 189 | int32_t dev2appUnits = aShapedText->GetAppUnitsPerDevUnit(); |
michael@0 | 190 | bool rtl = aShapedText->IsRightToLeft(); |
michael@0 | 191 | |
michael@0 | 192 | uint32_t glyphCount = gr_seg_n_slots(aSegment); |
michael@0 | 193 | |
michael@0 | 194 | // identify clusters; graphite may have reordered/expanded/ligated glyphs. |
michael@0 | 195 | AutoFallibleTArray<Cluster,SMALL_GLYPH_RUN> clusters; |
michael@0 | 196 | AutoFallibleTArray<uint16_t,SMALL_GLYPH_RUN> gids; |
michael@0 | 197 | AutoFallibleTArray<float,SMALL_GLYPH_RUN> xLocs; |
michael@0 | 198 | AutoFallibleTArray<float,SMALL_GLYPH_RUN> yLocs; |
michael@0 | 199 | |
michael@0 | 200 | if (!clusters.SetLength(aLength) || |
michael@0 | 201 | !gids.SetLength(glyphCount) || |
michael@0 | 202 | !xLocs.SetLength(glyphCount) || |
michael@0 | 203 | !yLocs.SetLength(glyphCount)) |
michael@0 | 204 | { |
michael@0 | 205 | return NS_ERROR_OUT_OF_MEMORY; |
michael@0 | 206 | } |
michael@0 | 207 | |
michael@0 | 208 | // walk through the glyph slots and check which original character |
michael@0 | 209 | // each is associated with |
michael@0 | 210 | uint32_t gIndex = 0; // glyph slot index |
michael@0 | 211 | uint32_t cIndex = 0; // current cluster index |
michael@0 | 212 | for (const gr_slot *slot = gr_seg_first_slot(aSegment); |
michael@0 | 213 | slot != nullptr; |
michael@0 | 214 | slot = gr_slot_next_in_segment(slot), gIndex++) |
michael@0 | 215 | { |
michael@0 | 216 | uint32_t before = |
michael@0 | 217 | gr_cinfo_base(gr_seg_cinfo(aSegment, gr_slot_before(slot))); |
michael@0 | 218 | uint32_t after = |
michael@0 | 219 | gr_cinfo_base(gr_seg_cinfo(aSegment, gr_slot_after(slot))); |
michael@0 | 220 | gids[gIndex] = gr_slot_gid(slot); |
michael@0 | 221 | xLocs[gIndex] = gr_slot_origin_X(slot); |
michael@0 | 222 | yLocs[gIndex] = gr_slot_origin_Y(slot); |
michael@0 | 223 | |
michael@0 | 224 | // if this glyph has a "before" character index that precedes the |
michael@0 | 225 | // current cluster's char index, we need to merge preceding |
michael@0 | 226 | // clusters until it gets included |
michael@0 | 227 | while (before < clusters[cIndex].baseChar && cIndex > 0) { |
michael@0 | 228 | clusters[cIndex-1].nChars += clusters[cIndex].nChars; |
michael@0 | 229 | clusters[cIndex-1].nGlyphs += clusters[cIndex].nGlyphs; |
michael@0 | 230 | --cIndex; |
michael@0 | 231 | } |
michael@0 | 232 | |
michael@0 | 233 | // if there's a gap between the current cluster's base character and |
michael@0 | 234 | // this glyph's, extend the cluster to include the intervening chars |
michael@0 | 235 | if (gr_slot_can_insert_before(slot) && clusters[cIndex].nChars && |
michael@0 | 236 | before >= clusters[cIndex].baseChar + clusters[cIndex].nChars) |
michael@0 | 237 | { |
michael@0 | 238 | NS_ASSERTION(cIndex < aLength - 1, "cIndex at end of word"); |
michael@0 | 239 | Cluster& c = clusters[cIndex + 1]; |
michael@0 | 240 | c.baseChar = clusters[cIndex].baseChar + clusters[cIndex].nChars; |
michael@0 | 241 | c.nChars = before - c.baseChar; |
michael@0 | 242 | c.baseGlyph = gIndex; |
michael@0 | 243 | c.nGlyphs = 0; |
michael@0 | 244 | ++cIndex; |
michael@0 | 245 | } |
michael@0 | 246 | |
michael@0 | 247 | // increment cluster's glyph count to include current slot |
michael@0 | 248 | NS_ASSERTION(cIndex < aLength, "cIndex beyond word length"); |
michael@0 | 249 | ++clusters[cIndex].nGlyphs; |
michael@0 | 250 | |
michael@0 | 251 | // extend cluster if necessary to reach the glyph's "after" index |
michael@0 | 252 | if (clusters[cIndex].baseChar + clusters[cIndex].nChars < after + 1) { |
michael@0 | 253 | clusters[cIndex].nChars = after + 1 - clusters[cIndex].baseChar; |
michael@0 | 254 | } |
michael@0 | 255 | } |
michael@0 | 256 | |
michael@0 | 257 | bool roundX; |
michael@0 | 258 | bool roundY; |
michael@0 | 259 | aContext->GetRoundOffsetsToPixels(&roundX, &roundY); |
michael@0 | 260 | |
michael@0 | 261 | gfxShapedText::CompressedGlyph *charGlyphs = |
michael@0 | 262 | aShapedText->GetCharacterGlyphs() + aOffset; |
michael@0 | 263 | |
michael@0 | 264 | // now put glyphs into the textrun, one cluster at a time |
michael@0 | 265 | for (uint32_t i = 0; i <= cIndex; ++i) { |
michael@0 | 266 | const Cluster& c = clusters[i]; |
michael@0 | 267 | |
michael@0 | 268 | float adv; // total advance of the cluster |
michael@0 | 269 | if (rtl) { |
michael@0 | 270 | if (i == 0) { |
michael@0 | 271 | adv = gr_seg_advance_X(aSegment) - xLocs[c.baseGlyph]; |
michael@0 | 272 | } else { |
michael@0 | 273 | adv = xLocs[clusters[i-1].baseGlyph] - xLocs[c.baseGlyph]; |
michael@0 | 274 | } |
michael@0 | 275 | } else { |
michael@0 | 276 | if (i == cIndex) { |
michael@0 | 277 | adv = gr_seg_advance_X(aSegment) - xLocs[c.baseGlyph]; |
michael@0 | 278 | } else { |
michael@0 | 279 | adv = xLocs[clusters[i+1].baseGlyph] - xLocs[c.baseGlyph]; |
michael@0 | 280 | } |
michael@0 | 281 | } |
michael@0 | 282 | |
michael@0 | 283 | // Check for default-ignorable char that didn't get filtered, combined, |
michael@0 | 284 | // etc by the shaping process, and skip it. |
michael@0 | 285 | uint32_t offs = c.baseChar; |
michael@0 | 286 | NS_ASSERTION(offs < aLength, "unexpected offset"); |
michael@0 | 287 | if (c.nGlyphs == 1 && c.nChars == 1 && |
michael@0 | 288 | aShapedText->FilterIfIgnorable(aOffset + offs, aText[offs])) { |
michael@0 | 289 | continue; |
michael@0 | 290 | } |
michael@0 | 291 | |
michael@0 | 292 | uint32_t appAdvance = roundX ? NSToIntRound(adv) * dev2appUnits : |
michael@0 | 293 | NSToIntRound(adv * dev2appUnits); |
michael@0 | 294 | if (c.nGlyphs == 1 && |
michael@0 | 295 | gfxShapedText::CompressedGlyph::IsSimpleGlyphID(gids[c.baseGlyph]) && |
michael@0 | 296 | gfxShapedText::CompressedGlyph::IsSimpleAdvance(appAdvance) && |
michael@0 | 297 | charGlyphs[offs].IsClusterStart() && |
michael@0 | 298 | yLocs[c.baseGlyph] == 0) |
michael@0 | 299 | { |
michael@0 | 300 | charGlyphs[offs].SetSimpleGlyph(appAdvance, gids[c.baseGlyph]); |
michael@0 | 301 | } else { |
michael@0 | 302 | // not a one-to-one mapping with simple metrics: use DetailedGlyph |
michael@0 | 303 | nsAutoTArray<gfxShapedText::DetailedGlyph,8> details; |
michael@0 | 304 | float clusterLoc; |
michael@0 | 305 | for (uint32_t j = c.baseGlyph; j < c.baseGlyph + c.nGlyphs; ++j) { |
michael@0 | 306 | gfxShapedText::DetailedGlyph* d = details.AppendElement(); |
michael@0 | 307 | d->mGlyphID = gids[j]; |
michael@0 | 308 | d->mYOffset = roundY ? NSToIntRound(-yLocs[j]) * dev2appUnits : |
michael@0 | 309 | -yLocs[j] * dev2appUnits; |
michael@0 | 310 | if (j == c.baseGlyph) { |
michael@0 | 311 | d->mXOffset = 0; |
michael@0 | 312 | d->mAdvance = appAdvance; |
michael@0 | 313 | clusterLoc = xLocs[j]; |
michael@0 | 314 | } else { |
michael@0 | 315 | float dx = rtl ? (xLocs[j] - clusterLoc) : |
michael@0 | 316 | (xLocs[j] - clusterLoc - adv); |
michael@0 | 317 | d->mXOffset = roundX ? NSToIntRound(dx) * dev2appUnits : |
michael@0 | 318 | dx * dev2appUnits; |
michael@0 | 319 | d->mAdvance = 0; |
michael@0 | 320 | } |
michael@0 | 321 | } |
michael@0 | 322 | gfxShapedText::CompressedGlyph g; |
michael@0 | 323 | g.SetComplex(charGlyphs[offs].IsClusterStart(), |
michael@0 | 324 | true, details.Length()); |
michael@0 | 325 | aShapedText->SetGlyphs(aOffset + offs, g, details.Elements()); |
michael@0 | 326 | } |
michael@0 | 327 | |
michael@0 | 328 | for (uint32_t j = c.baseChar + 1; j < c.baseChar + c.nChars; ++j) { |
michael@0 | 329 | NS_ASSERTION(j < aLength, "unexpected offset"); |
michael@0 | 330 | gfxShapedText::CompressedGlyph &g = charGlyphs[j]; |
michael@0 | 331 | NS_ASSERTION(!g.IsSimpleGlyph(), "overwriting a simple glyph"); |
michael@0 | 332 | g.SetComplex(g.IsClusterStart(), false, 0); |
michael@0 | 333 | } |
michael@0 | 334 | } |
michael@0 | 335 | |
michael@0 | 336 | return NS_OK; |
michael@0 | 337 | } |
michael@0 | 338 | |
michael@0 | 339 | #undef SMALL_GLYPH_RUN |
michael@0 | 340 | |
michael@0 | 341 | // for language tag validation - include list of tags from the IANA registry |
michael@0 | 342 | #include "gfxLanguageTagList.cpp" |
michael@0 | 343 | |
michael@0 | 344 | nsTHashtable<nsUint32HashKey> *gfxGraphiteShaper::sLanguageTags; |
michael@0 | 345 | |
michael@0 | 346 | /*static*/ uint32_t |
michael@0 | 347 | gfxGraphiteShaper::GetGraphiteTagForLang(const nsCString& aLang) |
michael@0 | 348 | { |
michael@0 | 349 | int len = aLang.Length(); |
michael@0 | 350 | if (len < 2) { |
michael@0 | 351 | return 0; |
michael@0 | 352 | } |
michael@0 | 353 | |
michael@0 | 354 | // convert primary language subtag to a left-packed, NUL-padded integer |
michael@0 | 355 | // for the Graphite API |
michael@0 | 356 | uint32_t grLang = 0; |
michael@0 | 357 | for (int i = 0; i < 4; ++i) { |
michael@0 | 358 | grLang <<= 8; |
michael@0 | 359 | if (i < len) { |
michael@0 | 360 | uint8_t ch = aLang[i]; |
michael@0 | 361 | if (ch == '-') { |
michael@0 | 362 | // found end of primary language subtag, truncate here |
michael@0 | 363 | len = i; |
michael@0 | 364 | continue; |
michael@0 | 365 | } |
michael@0 | 366 | if (ch < 'a' || ch > 'z') { |
michael@0 | 367 | // invalid character in tag, so ignore it completely |
michael@0 | 368 | return 0; |
michael@0 | 369 | } |
michael@0 | 370 | grLang += ch; |
michael@0 | 371 | } |
michael@0 | 372 | } |
michael@0 | 373 | |
michael@0 | 374 | // valid tags must have length = 2 or 3 |
michael@0 | 375 | if (len < 2 || len > 3) { |
michael@0 | 376 | return 0; |
michael@0 | 377 | } |
michael@0 | 378 | |
michael@0 | 379 | if (!sLanguageTags) { |
michael@0 | 380 | // store the registered IANA tags in a hash for convenient validation |
michael@0 | 381 | sLanguageTags = new nsTHashtable<nsUint32HashKey>(ArrayLength(sLanguageTagList)); |
michael@0 | 382 | for (const uint32_t *tag = sLanguageTagList; *tag != 0; ++tag) { |
michael@0 | 383 | sLanguageTags->PutEntry(*tag); |
michael@0 | 384 | } |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | // only accept tags known in the IANA registry |
michael@0 | 388 | if (sLanguageTags->GetEntry(grLang)) { |
michael@0 | 389 | return grLang; |
michael@0 | 390 | } |
michael@0 | 391 | |
michael@0 | 392 | return 0; |
michael@0 | 393 | } |
michael@0 | 394 | |
michael@0 | 395 | /*static*/ void |
michael@0 | 396 | gfxGraphiteShaper::Shutdown() |
michael@0 | 397 | { |
michael@0 | 398 | #ifdef NS_FREE_PERMANENT_DATA |
michael@0 | 399 | if (sLanguageTags) { |
michael@0 | 400 | sLanguageTags->Clear(); |
michael@0 | 401 | delete sLanguageTags; |
michael@0 | 402 | sLanguageTags = nullptr; |
michael@0 | 403 | } |
michael@0 | 404 | #endif |
michael@0 | 405 | } |