1 /*
   2  * Copyright (c) 2008, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.font;
  27 
  28 import java.awt.Font;
  29 import java.io.File;
  30 import java.io.FileInputStream;
  31 import java.io.FileOutputStream;
  32 import java.io.IOException;
  33 import java.net.InetAddress;
  34 import java.net.UnknownHostException;
  35 import java.nio.charset.Charset;
  36 import java.nio.charset.StandardCharsets;
  37 import java.nio.file.Files;
  38 import java.util.HashMap;
  39 import java.util.HashSet;
  40 import java.util.Locale;
  41 import java.util.Properties;
  42 import java.util.Scanner;
  43 import sun.awt.FcFontManager;
  44 import sun.awt.FontConfiguration;
  45 import sun.awt.FontDescriptor;
  46 import sun.awt.SunToolkit;
  47 import sun.font.CompositeFontDescriptor;
  48 import sun.font.FontManager;
  49 import sun.font.FontConfigManager.FontConfigInfo;
  50 import sun.font.FontConfigManager.FcCompFont;
  51 import sun.font.FontConfigManager.FontConfigFont;
  52 import sun.java2d.SunGraphicsEnvironment;
  53 import sun.util.logging.PlatformLogger;
  54 
  55 public class FcFontConfiguration extends FontConfiguration {
  56 
  57     /** Version of the cache file format understood by this code.
  58      * Its part of the file name so that we can rev this at
  59      * any time, even in a minor JDK update.
  60      * It is stored as the value of the "version" property.
  61      * This is distinct from the version of "libfontconfig" that generated
  62      * the cached results, and which is the "fcversion" property in the file.
  63      * {@code FontConfiguration.getVersion()} also returns a version string,
  64      * and has meant the version of the fontconfiguration.properties file
  65      * that was read. Since this class doesn't use such files, then what
  66      * that really means is whether the methods on this class return
  67      * values that are compatible with the classes that do directly read
  68      * from such files. It is a compatible subset of version "1".
  69      */
  70     private static final String fileVersion = "1";
  71     private String fcInfoFileName = null;
  72 
  73     private FcCompFont[] fcCompFonts = null;
  74 
  75     public FcFontConfiguration(SunFontManager fm) {
  76         super(fm);
  77         init();
  78     }
  79 
  80     /* This isn't called but is needed to satisfy super-class contract. */
  81     public FcFontConfiguration(SunFontManager fm,
  82                                boolean preferLocaleFonts,
  83                                boolean preferPropFonts) {
  84         super(fm, preferLocaleFonts, preferPropFonts);
  85         init();
  86     }
  87 
  88     @Override
  89     public synchronized boolean init() {
  90         if (fcCompFonts != null) {
  91             return true;
  92         }
  93 
  94         setFontConfiguration();
  95         readFcInfo();
  96         FcFontManager fm = (FcFontManager) fontManager;
  97         FontConfigManager fcm = fm.getFontConfigManager();
  98         if (fcCompFonts == null) {
  99             fcCompFonts = fcm.loadFontConfig();
 100             if (fcCompFonts != null) {
 101                 try {
 102                     writeFcInfo();
 103                 } catch (Exception e) {
 104                     if (FontUtilities.debugFonts()) {
 105                         warning("Exception writing fcInfo " + e);
 106                     }
 107                 }
 108             } else if (FontUtilities.debugFonts()) {
 109                 warning("Failed to get info from libfontconfig");
 110             }
 111         } else {
 112             fcm.populateFontConfig(fcCompFonts);
 113         }
 114 
 115         if (fcCompFonts == null) {
 116             return false; // couldn't load fontconfig.
 117         }
 118 
 119         // NB already in a privileged block from SGE
 120         String javaHome = System.getProperty("java.home");
 121         if (javaHome == null) {
 122             throw new Error("java.home property not set");
 123         }
 124         String javaLib = javaHome + File.separator + "lib";
 125         getInstalledFallbackFonts(javaLib);
 126 
 127         return true;
 128     }
 129 
 130     @Override
 131     public String getFallbackFamilyName(String fontName,
 132                                         String defaultFallback) {
 133         // maintain compatibility with old font.properties files, which either
 134         // had aliases for TimesRoman & Co. or defined mappings for them.
 135         String compatibilityName = getCompatibilityFamilyName(fontName);
 136         if (compatibilityName != null) {
 137             return compatibilityName;
 138         }
 139         return defaultFallback;
 140     }
 141 
 142     @Override
 143     protected String
 144         getFaceNameFromComponentFontName(String componentFontName) {
 145         return null;
 146     }
 147 
 148     @Override
 149     protected String
 150         getFileNameFromComponentFontName(String componentFontName) {
 151         return null;
 152     }
 153 
 154     @Override
 155     public String getFileNameFromPlatformName(String platformName) {
 156         /* Platform name is the file name, but rather than returning
 157          * the arg, return null*/
 158         return null;
 159     }
 160 
 161     @Override
 162     protected Charset getDefaultFontCharset(String fontName) {
 163         return Charset.forName("ISO8859_1");
 164     }
 165 
 166     @Override
 167     protected String getEncoding(String awtFontName,
 168                                  String characterSubsetName) {
 169         return "default";
 170     }
 171 
 172     @Override
 173     protected void initReorderMap() {
 174         reorderMap = new HashMap<>();
 175     }
 176 
 177     @Override
 178     protected FontDescriptor[] buildFontDescriptors(int fontIndex, int styleIndex) {
 179         CompositeFontDescriptor[] cfi = get2DCompositeFontInfo();
 180         int idx = fontIndex * NUM_STYLES + styleIndex;
 181         String[] componentFaceNames = cfi[idx].getComponentFaceNames();
 182         FontDescriptor[] ret = new FontDescriptor[componentFaceNames.length];
 183         for (int i = 0; i < componentFaceNames.length; i++) {
 184             ret[i] = new FontDescriptor(componentFaceNames[i], StandardCharsets.ISO_8859_1.newEncoder(), new int[0]);
 185         }
 186 
 187         return ret;
 188     }
 189 
 190     @Override
 191     public int getNumberCoreFonts() {
 192         return 1;
 193     }
 194 
 195     @Override
 196     public String[] getPlatformFontNames() {
 197         HashSet<String> nameSet = new HashSet<String>();
 198         FcFontManager fm = (FcFontManager) fontManager;
 199         FontConfigManager fcm = fm.getFontConfigManager();
 200         FcCompFont[] fcCompFonts = fcm.loadFontConfig();
 201         for (int i=0; i<fcCompFonts.length; i++) {
 202             for (int j=0; j<fcCompFonts[i].allFonts.length; j++) {
 203                 nameSet.add(fcCompFonts[i].allFonts[j].fontFile);
 204             }
 205         }
 206         return nameSet.toArray(new String[0]);
 207     }
 208 
 209     @Override
 210     public String getExtraFontPath() {
 211         return null;
 212     }
 213 
 214     @Override
 215     public boolean needToSearchForFile(String fileName) {
 216         return false;
 217     }
 218 
 219     private FontConfigFont[] getFcFontList(FcCompFont[] fcFonts,
 220                                            String fontname, int style) {
 221 
 222         if (fontname.equals("dialog")) {
 223             fontname = "sansserif";
 224         } else if (fontname.equals("dialoginput")) {
 225             fontname = "monospaced";
 226         }
 227         for (int i=0; i<fcFonts.length; i++) {
 228             if (fontname.equals(fcFonts[i].jdkName) &&
 229                 style == fcFonts[i].style) {
 230                 return fcFonts[i].allFonts;
 231             }
 232         }
 233         return fcFonts[0].allFonts;
 234     }
 235 
 236     @Override
 237     public CompositeFontDescriptor[] get2DCompositeFontInfo() {
 238 
 239         FcFontManager fm = (FcFontManager) fontManager;
 240         FontConfigManager fcm = fm.getFontConfigManager();
 241         FcCompFont[] fcCompFonts = fcm.loadFontConfig();
 242 
 243         CompositeFontDescriptor[] result =
 244                 new CompositeFontDescriptor[NUM_FONTS * NUM_STYLES];
 245 
 246         for (int fontIndex = 0; fontIndex < NUM_FONTS; fontIndex++) {
 247             String fontName = publicFontNames[fontIndex];
 248 
 249             for (int styleIndex = 0; styleIndex < NUM_STYLES; styleIndex++) {
 250 
 251                 String faceName = fontName + "." + styleNames[styleIndex];
 252                 FontConfigFont[] fcFonts =
 253                     getFcFontList(fcCompFonts,
 254                                   fontNames[fontIndex], styleIndex);
 255 
 256                 int numFonts = fcFonts.length;
 257                 // fall back fonts listed in the lib/fonts/fallback directory
 258                 if (installedFallbackFontFiles != null) {
 259                     numFonts += installedFallbackFontFiles.length;
 260                 }
 261 
 262                 String[] fileNames = new String[numFonts];
 263                 String[] faceNames = new String[numFonts];
 264 
 265                 int index;
 266                 for (index = 0; index < fcFonts.length; index++) {
 267                     fileNames[index] = fcFonts[index].fontFile;
 268                     faceNames[index] = fcFonts[index].fullName;
 269                 }
 270 
 271                 if (installedFallbackFontFiles != null) {
 272                     System.arraycopy(installedFallbackFontFiles, 0,
 273                                      fileNames, fcFonts.length,
 274                                      installedFallbackFontFiles.length);
 275                 }
 276 
 277                 result[fontIndex * NUM_STYLES + styleIndex]
 278                         = new CompositeFontDescriptor(
 279                             faceName,
 280                             1,
 281                             faceNames,
 282                             fileNames,
 283                             null, null);
 284             }
 285         }
 286         return result;
 287     }
 288 
 289     /**
 290      * Gets the OS version string from a Linux release-specific file.
 291      */
 292     private String getVersionString(File f){
 293         try {
 294             Scanner sc  = new Scanner(f);
 295             return sc.findInLine("(\\d)+((\\.)(\\d)+)*");
 296         }
 297         catch (Exception e){
 298         }
 299         return null;
 300     }
 301 
 302     /**
 303      * Sets the OS name and version from environment information.
 304      */
 305     @Override
 306     protected void setOsNameAndVersion() {
 307 
 308         super.setOsNameAndVersion();
 309 
 310         if (!osName.equals("Linux")) {
 311             return;
 312         }
 313         try {
 314             File f;
 315             if ((f = new File("/etc/lsb-release")).canRead()) {
 316                     /* Ubuntu and (perhaps others) use only lsb-release.
 317                      * Syntax and encoding is compatible with java properties.
 318                      * For Ubuntu the ID is "Ubuntu".
 319                      */
 320                     Properties props = new Properties();
 321                     props.load(new FileInputStream(f));
 322                     osName = props.getProperty("DISTRIB_ID");
 323                     osVersion =  props.getProperty("DISTRIB_RELEASE");
 324             } else if ((f = new File("/etc/redhat-release")).canRead()) {
 325                 osName = "RedHat";
 326                 osVersion = getVersionString(f);
 327             } else if ((f = new File("/etc/SuSE-release")).canRead()) {
 328                 osName = "SuSE";
 329                 osVersion = getVersionString(f);
 330             } else if ((f = new File("/etc/turbolinux-release")).canRead()) {
 331                 osName = "Turbo";
 332                 osVersion = getVersionString(f);
 333             } else if ((f = new File("/etc/fedora-release")).canRead()) {
 334                 osName = "Fedora";
 335                 osVersion = getVersionString(f);
 336             }
 337         } catch (Exception e) {
 338             if (FontUtilities.debugFonts()) {
 339                 warning("Exception identifying Linux distro.");
 340             }
 341         }
 342     }
 343 
 344     private File getFcInfoFile() {
 345         if (fcInfoFileName == null) {
 346             // NB need security permissions to get true IP address, and
 347             // we should have those as the whole initialisation is in a
 348             // doPrivileged block. But in this case no exception is thrown,
 349             // and it returns the loop back address, and so we end up with
 350             // "localhost"
 351             String hostname;
 352             try {
 353                 hostname = InetAddress.getLocalHost().getHostName();
 354             } catch (UnknownHostException e) {
 355                 hostname = "localhost";
 356             }
 357             String userDir = System.getProperty("user.home");
 358             String version = System.getProperty("java.version");
 359             String fs = File.separator;
 360             String dir = userDir+fs+".java"+fs+"fonts"+fs+version;
 361             Locale locale = SunToolkit.getStartupLocale();
 362             String lang = locale.getLanguage();
 363             String country = locale.getCountry();
 364             String name = "fcinfo-"+fileVersion+"-"+hostname+"-"+
 365                 osName+"-"+osVersion+"-"+lang+"-"+country+".properties";
 366             fcInfoFileName = dir+fs+name;
 367         }
 368         return new File(fcInfoFileName);
 369     }
 370 
 371     private void writeFcInfo() {
 372         Properties props = new Properties();
 373         props.setProperty("version", fileVersion);
 374         FcFontManager fm = (FcFontManager) fontManager;
 375         FontConfigManager fcm = fm.getFontConfigManager();
 376         FontConfigInfo fcInfo = fcm.getFontConfigInfo();
 377         props.setProperty("fcversion", Integer.toString(fcInfo.fcVersion));
 378         if (fcInfo.cacheDirs != null) {
 379             for (int i=0;i<fcInfo.cacheDirs.length;i++) {
 380                 if (fcInfo.cacheDirs[i] != null) {
 381                    props.setProperty("cachedir."+i,  fcInfo.cacheDirs[i]);
 382                 }
 383             }
 384         }
 385         for (int i=0; i<fcCompFonts.length; i++) {
 386             FcCompFont fci = fcCompFonts[i];
 387             String styleKey = fci.jdkName+"."+fci.style;
 388             props.setProperty(styleKey+".length",
 389                               Integer.toString(fci.allFonts.length));
 390             for (int j=0; j<fci.allFonts.length; j++) {
 391                 props.setProperty(styleKey+"."+j+".file",
 392                                   fci.allFonts[j].fontFile);
 393                 if (fci.allFonts[j].fullName != null) {
 394                     props.setProperty(styleKey+"."+j+".fullName",
 395                                       fci.allFonts[j].fullName);
 396                 }
 397             }
 398         }
 399         try {
 400             /* This writes into a temp file then renames when done.
 401              * Since the rename is an atomic action within the same
 402              * directory no client will ever see a partially written file.
 403              */
 404             File fcInfoFile = getFcInfoFile();
 405             File dir = fcInfoFile.getParentFile();
 406             dir.mkdirs();
 407             File tempFile = Files.createTempFile(dir.toPath(), "fcinfo", null).toFile();
 408             FileOutputStream fos = new FileOutputStream(tempFile);
 409             props.store(fos,
 410                       "JDK Font Configuration Generated File: *Do Not Edit*");
 411             fos.close();
 412             boolean renamed = tempFile.renameTo(fcInfoFile);
 413             if (!renamed && FontUtilities.debugFonts()) {
 414                 System.out.println("rename failed");
 415                 warning("Failed renaming file to "+ getFcInfoFile());
 416             }
 417         } catch (Exception e) {
 418             if (FontUtilities.debugFonts()) {
 419                 warning("IOException writing to "+ getFcInfoFile());
 420             }
 421         }
 422     }
 423 
 424     /* We want to be able to use this cache instead of invoking
 425      * fontconfig except when we can detect the system cache has changed.
 426      * But there doesn't seem to be a way to find the location of
 427      * the system cache.
 428      */
 429     private void readFcInfo() {
 430         File fcFile = getFcInfoFile();
 431         if (!fcFile.exists()) {
 432             if (FontUtilities.debugFonts()) {
 433                 warning("fontconfig info file " + fcFile.toString() + " does not exist");
 434             }
 435             return;
 436         }
 437         Properties props = new Properties();
 438         FcFontManager fm = (FcFontManager) fontManager;
 439         FontConfigManager fcm = fm.getFontConfigManager();
 440         try {
 441             FileInputStream fis = new FileInputStream(fcFile);
 442             props.load(fis);
 443             fis.close();
 444         } catch (IOException e) {
 445             if (FontUtilities.debugFonts()) {
 446                 warning("IOException reading from "+fcFile.toString());
 447             }
 448             return;
 449         }
 450         String version = (String)props.get("version");
 451         if (version == null || !version.equals(fileVersion)) {
 452             if (FontUtilities.debugFonts()) {
 453                 warning("fontconfig info file caused a version mismatch");
 454             }
 455             return;
 456         }
 457 
 458         // If there's a new, different fontconfig installed on the
 459         // system, we invalidate our fontconfig file.
 460         String fcVersionStr = (String)props.get("fcversion");
 461         if (fcVersionStr != null) {
 462             int fcVersion;
 463             try {
 464                 fcVersion = Integer.parseInt(fcVersionStr);
 465                 if (fcVersion != 0 &&
 466                     fcVersion != FontConfigManager.getFontConfigVersion()) {
 467                     if (FontUtilities.debugFonts()) {
 468                         warning("new, different fontconfig detected");
 469                     }
 470                     return;
 471                 }
 472             } catch (Exception e) {
 473                 if (FontUtilities.debugFonts()) {
 474                     warning("Exception parsing version " + fcVersionStr);
 475                 }
 476                 return;
 477             }
 478         }
 479 
 480         // If we can locate the fontconfig cache dirs, then compare the
 481         // time stamp of those with our properties file. If we are out
 482         // of date then re-generate.
 483         long lastModified = fcFile.lastModified();
 484         int cacheDirIndex = 0;
 485         while (cacheDirIndex<4) { // should never be more than 2 anyway.
 486             String dir = (String)props.get("cachedir."+cacheDirIndex);
 487             if (dir == null) {
 488                 break;
 489             }
 490             File dirFile = new File(dir);
 491             if (dirFile.exists() && dirFile.lastModified() > lastModified) {
 492                 if (FontUtilities.debugFonts()) {
 493                     warning("Out of date cache directories detected");
 494                 }
 495                 return;
 496             }
 497             cacheDirIndex++;
 498         }
 499 
 500         String[] names = { "sansserif", "serif", "monospaced" };
 501         String[] fcnames = { "sans", "serif", "monospace" };
 502         int namesLen = names.length;
 503         int numStyles = 4;
 504         FcCompFont[] fci = new FcCompFont[namesLen*numStyles];
 505 
 506         try {
 507             for (int i=0; i<namesLen; i++) {
 508                 for (int s=0; s<numStyles; s++) {
 509                     int index = i*numStyles+s;
 510                     fci[index] = new FcCompFont();
 511                     String key = names[i]+"."+s;
 512                     fci[index].jdkName = names[i];
 513                     fci[index].fcFamily = fcnames[i];
 514                     fci[index].style = s;
 515                     String lenStr = (String)props.get(key+".length");
 516                     int nfonts = Integer.parseInt(lenStr);
 517                     if (nfonts <= 0) {
 518                         if (FontUtilities.debugFonts()) {
 519                             warning("Bad non-positive .length entry in fontconfig file " + fcFile.toString());
 520                         }
 521                         return; // bad file
 522                     }
 523                     fci[index].allFonts = new FontConfigFont[nfonts];
 524                     for (int f=0; f<nfonts; f++) {
 525                         fci[index].allFonts[f] = new FontConfigFont();
 526                         String fkey = key+"."+f+".fullName";
 527                         String fullName = (String)props.get(fkey);
 528                         fci[index].allFonts[f].fullName = fullName;
 529                         fkey = key+"."+f+".file";
 530                         String file = (String)props.get(fkey);
 531                         if (file == null) {
 532                             if (FontUtilities.debugFonts()) {
 533                                 warning("Missing file value for key " + fkey + " in fontconfig file " + fcFile.toString());
 534                             }
 535                             return; // bad file
 536                         }
 537                         fci[index].allFonts[f].fontFile = file;
 538                     }
 539                     fci[index].firstFont =  fci[index].allFonts[0];
 540 
 541                 }
 542             }
 543             fcCompFonts = fci;
 544         } catch (Throwable t) {
 545             if (FontUtilities.debugFonts()) {
 546                 warning(t.toString());
 547             }
 548         }
 549 
 550         if (FontUtilities.debugFonts()) {
 551             PlatformLogger logger = FontUtilities.getLogger();
 552             logger.info("successfully parsed the fontconfig file at " + fcFile.toString());
 553         }
 554     }
 555 
 556     private static void warning(String msg) {
 557         PlatformLogger logger = PlatformLogger.getLogger("sun.awt.FontConfiguration");
 558         logger.warning(msg);
 559     }
 560 }