SystemReader.java

  1. /*
  2.  * Copyright (C) 2009, Google Inc.
  3.  * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com>
  4.  * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com>
  5.  * Copyright (C) 2012, Daniel Megert <daniel_megert@ch.ibm.com> and others
  6.  *
  7.  * This program and the accompanying materials are made available under the
  8.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  9.  * https://www.eclipse.org/org/documents/edl-v10.php.
  10.  *
  11.  * SPDX-License-Identifier: BSD-3-Clause
  12.  */

  13. package org.eclipse.jgit.util;

  14. import java.io.File;
  15. import java.io.IOException;
  16. import java.net.InetAddress;
  17. import java.net.UnknownHostException;
  18. import java.nio.charset.Charset;
  19. import java.nio.charset.IllegalCharsetNameException;
  20. import java.nio.charset.UnsupportedCharsetException;
  21. import java.nio.file.InvalidPathException;
  22. import java.nio.file.Path;
  23. import java.nio.file.Paths;
  24. import java.security.AccessController;
  25. import java.security.PrivilegedAction;
  26. import java.text.DateFormat;
  27. import java.text.SimpleDateFormat;
  28. import java.util.Locale;
  29. import java.util.TimeZone;
  30. import java.util.concurrent.atomic.AtomicReference;

  31. import org.eclipse.jgit.errors.ConfigInvalidException;
  32. import org.eclipse.jgit.errors.CorruptObjectException;
  33. import org.eclipse.jgit.internal.JGitText;
  34. import org.eclipse.jgit.lib.Config;
  35. import org.eclipse.jgit.lib.Constants;
  36. import org.eclipse.jgit.lib.ObjectChecker;
  37. import org.eclipse.jgit.lib.StoredConfig;
  38. import org.eclipse.jgit.storage.file.FileBasedConfig;
  39. import org.eclipse.jgit.util.time.MonotonicClock;
  40. import org.eclipse.jgit.util.time.MonotonicSystemClock;
  41. import org.slf4j.Logger;
  42. import org.slf4j.LoggerFactory;

  43. /**
  44.  * Interface to read values from the system.
  45.  * <p>
  46.  * When writing unit tests, extending this interface with a custom class
  47.  * permits to simulate an access to a system variable or property and
  48.  * permits to control the user's global configuration.
  49.  * </p>
  50.  */
  51. public abstract class SystemReader {

  52.     private static final Logger LOG = LoggerFactory
  53.             .getLogger(SystemReader.class);

  54.     private static final SystemReader DEFAULT;

  55.     private static volatile Boolean isMacOS;

  56.     private static volatile Boolean isWindows;

  57.     private static volatile Boolean isLinux;

  58.     static {
  59.         SystemReader r = new Default();
  60.         r.init();
  61.         DEFAULT = r;
  62.     }

  63.     private static class Default extends SystemReader {
  64.         private volatile String hostname;

  65.         @Override
  66.         public String getenv(String variable) {
  67.             return System.getenv(variable);
  68.         }

  69.         @Override
  70.         public String getProperty(String key) {
  71.             return System.getProperty(key);
  72.         }

  73.         @Override
  74.         public FileBasedConfig openSystemConfig(Config parent, FS fs) {
  75.             if (StringUtils
  76.                     .isEmptyOrNull(getenv(Constants.GIT_CONFIG_NOSYSTEM_KEY))) {
  77.                 File configFile = fs.getGitSystemConfig();
  78.                 if (configFile != null) {
  79.                     return new FileBasedConfig(parent, configFile, fs);
  80.                 }
  81.             }
  82.             return new FileBasedConfig(parent, null, fs) {
  83.                 @Override
  84.                 public void load() {
  85.                     // empty, do not load
  86.                 }

  87.                 @Override
  88.                 public boolean isOutdated() {
  89.                     // regular class would bomb here
  90.                     return false;
  91.                 }
  92.             };
  93.         }

  94.         @Override
  95.         public FileBasedConfig openUserConfig(Config parent, FS fs) {
  96.             return new FileBasedConfig(parent, new File(fs.userHome(), ".gitconfig"), //$NON-NLS-1$
  97.                     fs);
  98.         }

  99.         private Path getXDGConfigHome(FS fs) {
  100.             String configHomePath = getenv(Constants.XDG_CONFIG_HOME);
  101.             if (StringUtils.isEmptyOrNull(configHomePath)) {
  102.                 configHomePath = new File(fs.userHome(), ".config") //$NON-NLS-1$
  103.                         .getAbsolutePath();
  104.             }
  105.             try {
  106.                 return Paths.get(configHomePath);
  107.             } catch (InvalidPathException e) {
  108.                 LOG.error(JGitText.get().logXDGConfigHomeInvalid,
  109.                         configHomePath, e);
  110.             }
  111.             return null;
  112.         }

  113.         @Override
  114.         public FileBasedConfig openJGitConfig(Config parent, FS fs) {
  115.             Path xdgPath = getXDGConfigHome(fs);
  116.             if (xdgPath != null) {
  117.                 Path configPath = xdgPath.resolve("jgit") //$NON-NLS-1$
  118.                         .resolve(Constants.CONFIG);
  119.                 return new FileBasedConfig(parent, configPath.toFile(), fs);
  120.             }
  121.             return new FileBasedConfig(parent,
  122.                     new File(fs.userHome(), ".jgitconfig"), fs); //$NON-NLS-1$
  123.         }

  124.         @Override
  125.         public String getHostname() {
  126.             if (hostname == null) {
  127.                 try {
  128.                     InetAddress localMachine = InetAddress.getLocalHost();
  129.                     hostname = localMachine.getCanonicalHostName();
  130.                 } catch (UnknownHostException e) {
  131.                     // we do nothing
  132.                     hostname = "localhost"; //$NON-NLS-1$
  133.                 }
  134.                 assert hostname != null;
  135.             }
  136.             return hostname;
  137.         }

  138.         @Override
  139.         public long getCurrentTime() {
  140.             return System.currentTimeMillis();
  141.         }

  142.         @Override
  143.         public int getTimezone(long when) {
  144.             return getTimeZone().getOffset(when) / (60 * 1000);
  145.         }
  146.     }

  147.     private static volatile SystemReader INSTANCE = DEFAULT;

  148.     /**
  149.      * Get the current SystemReader instance
  150.      *
  151.      * @return the current SystemReader instance.
  152.      */
  153.     public static SystemReader getInstance() {
  154.         return INSTANCE;
  155.     }

  156.     /**
  157.      * Set a new SystemReader instance to use when accessing properties.
  158.      *
  159.      * @param newReader
  160.      *            the new instance to use when accessing properties, or null for
  161.      *            the default instance.
  162.      */
  163.     public static void setInstance(SystemReader newReader) {
  164.         isMacOS = null;
  165.         isWindows = null;
  166.         isLinux = null;
  167.         if (newReader == null)
  168.             INSTANCE = DEFAULT;
  169.         else {
  170.             newReader.init();
  171.             INSTANCE = newReader;
  172.         }
  173.     }

  174.     private ObjectChecker platformChecker;

  175.     private AtomicReference<FileBasedConfig> systemConfig = new AtomicReference<>();

  176.     private AtomicReference<FileBasedConfig> userConfig = new AtomicReference<>();

  177.     private AtomicReference<FileBasedConfig> jgitConfig = new AtomicReference<>();

  178.     private volatile Charset defaultCharset;

  179.     private void init() {
  180.         // Creating ObjectChecker must be deferred. Unit tests change
  181.         // behavior of is{Windows,MacOS} in constructor of subclass.
  182.         if (platformChecker == null)
  183.             setPlatformChecker();
  184.     }

  185.     /**
  186.      * Should be used in tests when the platform is explicitly changed.
  187.      *
  188.      * @since 3.6
  189.      */
  190.     protected final void setPlatformChecker() {
  191.         platformChecker = new ObjectChecker()
  192.             .setSafeForWindows(isWindows())
  193.             .setSafeForMacOS(isMacOS());
  194.     }

  195.     /**
  196.      * Gets the hostname of the local host. If no hostname can be found, the
  197.      * hostname is set to the default value "localhost".
  198.      *
  199.      * @return the canonical hostname
  200.      */
  201.     public abstract String getHostname();

  202.     /**
  203.      * Get value of the system variable
  204.      *
  205.      * @param variable
  206.      *            system variable to read
  207.      * @return value of the system variable
  208.      */
  209.     public abstract String getenv(String variable);

  210.     /**
  211.      * Get value of the system property
  212.      *
  213.      * @param key
  214.      *            of the system property to read
  215.      * @return value of the system property
  216.      */
  217.     public abstract String getProperty(String key);

  218.     /**
  219.      * Open the git configuration found in the user home. Use
  220.      * {@link #getUserConfig()} to get the current git configuration in the user
  221.      * home since it manages automatic reloading when the gitconfig file was
  222.      * modified and avoids unnecessary reloads.
  223.      *
  224.      * @param parent
  225.      *            a config with values not found directly in the returned config
  226.      * @param fs
  227.      *            the file system abstraction which will be necessary to perform
  228.      *            certain file system operations.
  229.      * @return the git configuration found in the user home
  230.      */
  231.     public abstract FileBasedConfig openUserConfig(Config parent, FS fs);

  232.     /**
  233.      * Open the gitconfig configuration found in the system-wide "etc"
  234.      * directory. Use {@link #getSystemConfig()} to get the current system-wide
  235.      * git configuration since it manages automatic reloading when the gitconfig
  236.      * file was modified and avoids unnecessary reloads.
  237.      *
  238.      * @param parent
  239.      *            a config with values not found directly in the returned
  240.      *            config. Null is a reasonable value here.
  241.      * @param fs
  242.      *            the file system abstraction which will be necessary to perform
  243.      *            certain file system operations.
  244.      * @return the gitconfig configuration found in the system-wide "etc"
  245.      *         directory
  246.      */
  247.     public abstract FileBasedConfig openSystemConfig(Config parent, FS fs);

  248.     /**
  249.      * Open the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. Use
  250.      * {@link #getJGitConfig()} to get the current jgit configuration in the
  251.      * user home since it manages automatic reloading when the jgit config file
  252.      * was modified and avoids unnecessary reloads.
  253.      *
  254.      * @param parent
  255.      *            a config with values not found directly in the returned config
  256.      * @param fs
  257.      *            the file system abstraction which will be necessary to perform
  258.      *            certain file system operations.
  259.      * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
  260.      * @since 5.5.2
  261.      */
  262.     public abstract FileBasedConfig openJGitConfig(Config parent, FS fs);

  263.     /**
  264.      * Get the git configuration found in the user home. The configuration will
  265.      * be reloaded automatically if the configuration file was modified. Also
  266.      * reloads the system config if the system config file was modified. If the
  267.      * configuration file wasn't modified returns the cached configuration.
  268.      *
  269.      * @return the git configuration found in the user home
  270.      * @throws ConfigInvalidException
  271.      *             if configuration is invalid
  272.      * @throws IOException
  273.      *             if something went wrong when reading files
  274.      * @since 5.1.9
  275.      */
  276.     public StoredConfig getUserConfig()
  277.             throws ConfigInvalidException, IOException {
  278.         FileBasedConfig c = userConfig.get();
  279.         if (c == null) {
  280.             userConfig.compareAndSet(null,
  281.                     openUserConfig(getSystemConfig(), FS.DETECTED));
  282.             c = userConfig.get();
  283.         }
  284.         // on the very first call this will check a second time if the system
  285.         // config is outdated
  286.         updateAll(c);
  287.         return c;
  288.     }

  289.     /**
  290.      * Get the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. The
  291.      * configuration will be reloaded automatically if the configuration file
  292.      * was modified. If the configuration file wasn't modified returns the
  293.      * cached configuration.
  294.      *
  295.      * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
  296.      * @throws ConfigInvalidException
  297.      *             if configuration is invalid
  298.      * @throws IOException
  299.      *             if something went wrong when reading files
  300.      * @since 5.5.2
  301.      */
  302.     public StoredConfig getJGitConfig()
  303.             throws ConfigInvalidException, IOException {
  304.         FileBasedConfig c = jgitConfig.get();
  305.         if (c == null) {
  306.             jgitConfig.compareAndSet(null,
  307.                     openJGitConfig(null, FS.DETECTED));
  308.             c = jgitConfig.get();
  309.         }
  310.         updateAll(c);
  311.         return c;
  312.     }

  313.     /**
  314.      * Get the gitconfig configuration found in the system-wide "etc" directory.
  315.      * The configuration will be reloaded automatically if the configuration
  316.      * file was modified otherwise returns the cached system level config.
  317.      *
  318.      * @return the gitconfig configuration found in the system-wide "etc"
  319.      *         directory
  320.      * @throws ConfigInvalidException
  321.      *             if configuration is invalid
  322.      * @throws IOException
  323.      *             if something went wrong when reading files
  324.      * @since 5.1.9
  325.      */
  326.     public StoredConfig getSystemConfig()
  327.             throws ConfigInvalidException, IOException {
  328.         FileBasedConfig c = systemConfig.get();
  329.         if (c == null) {
  330.             systemConfig.compareAndSet(null,
  331.                     openSystemConfig(getJGitConfig(), FS.DETECTED));
  332.             c = systemConfig.get();
  333.         }
  334.         updateAll(c);
  335.         return c;
  336.     }

  337.     /**
  338.      * Update config and its parents if they seem modified
  339.      *
  340.      * @param config
  341.      *            configuration to reload if outdated
  342.      * @throws ConfigInvalidException
  343.      *             if configuration is invalid
  344.      * @throws IOException
  345.      *             if something went wrong when reading files
  346.      */
  347.     private void updateAll(Config config)
  348.             throws ConfigInvalidException, IOException {
  349.         if (config == null) {
  350.             return;
  351.         }
  352.         updateAll(config.getBaseConfig());
  353.         if (config instanceof FileBasedConfig) {
  354.             FileBasedConfig cfg = (FileBasedConfig) config;
  355.             if (cfg.isOutdated()) {
  356.                 LOG.debug("loading config {}", cfg); //$NON-NLS-1$
  357.                 cfg.load();
  358.             }
  359.         }
  360.     }

  361.     /**
  362.      * Get the current system time
  363.      *
  364.      * @return the current system time
  365.      */
  366.     public abstract long getCurrentTime();

  367.     /**
  368.      * Get clock instance preferred by this system.
  369.      *
  370.      * @return clock instance preferred by this system.
  371.      * @since 4.6
  372.      */
  373.     public MonotonicClock getClock() {
  374.         return new MonotonicSystemClock();
  375.     }

  376.     /**
  377.      * Get the local time zone
  378.      *
  379.      * @param when
  380.      *            a system timestamp
  381.      * @return the local time zone
  382.      */
  383.     public abstract int getTimezone(long when);

  384.     /**
  385.      * Get system time zone, possibly mocked for testing
  386.      *
  387.      * @return system time zone, possibly mocked for testing
  388.      * @since 1.2
  389.      */
  390.     public TimeZone getTimeZone() {
  391.         return TimeZone.getDefault();
  392.     }

  393.     /**
  394.      * Get the locale to use
  395.      *
  396.      * @return the locale to use
  397.      * @since 1.2
  398.      */
  399.     public Locale getLocale() {
  400.         return Locale.getDefault();
  401.     }

  402.     /**
  403.      * Retrieves the default {@link Charset} depending on the system locale.
  404.      *
  405.      * @return the {@link Charset}
  406.      * @since 6.0
  407.      * @see <a href="https://openjdk.java.net/jeps/400">JEP 400</a>
  408.      */
  409.     public Charset getDefaultCharset() {
  410.         Charset result = defaultCharset;
  411.         if (result == null) {
  412.             // JEP 400: Java 18 populates this system property.
  413.             String encoding = getProperty("native.encoding"); //$NON-NLS-1$
  414.             try {
  415.                 if (!StringUtils.isEmptyOrNull(encoding)) {
  416.                     result = Charset.forName(encoding);
  417.                 }
  418.             } catch (IllegalCharsetNameException
  419.                     | UnsupportedCharsetException e) {
  420.                 LOG.error(JGitText.get().logInvalidDefaultCharset, encoding);
  421.             }
  422.             if (result == null) {
  423.                 // This is always UTF-8 on Java >= 18.
  424.                 result = Charset.defaultCharset();
  425.             }
  426.             defaultCharset = result;
  427.         }
  428.         return result;
  429.     }

  430.     /**
  431.      * Returns a simple date format instance as specified by the given pattern.
  432.      *
  433.      * @param pattern
  434.      *            the pattern as defined in
  435.      *            {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}
  436.      * @return the simple date format
  437.      * @since 2.0
  438.      */
  439.     public SimpleDateFormat getSimpleDateFormat(String pattern) {
  440.         return new SimpleDateFormat(pattern);
  441.     }

  442.     /**
  443.      * Returns a simple date format instance as specified by the given pattern.
  444.      *
  445.      * @param pattern
  446.      *            the pattern as defined in
  447.      *            {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}
  448.      * @param locale
  449.      *            locale to be used for the {@code SimpleDateFormat}
  450.      * @return the simple date format
  451.      * @since 3.2
  452.      */
  453.     public SimpleDateFormat getSimpleDateFormat(String pattern, Locale locale) {
  454.         return new SimpleDateFormat(pattern, locale);
  455.     }

  456.     /**
  457.      * Returns a date/time format instance for the given styles.
  458.      *
  459.      * @param dateStyle
  460.      *            the date style as specified in
  461.      *            {@link java.text.DateFormat#getDateTimeInstance(int, int)}
  462.      * @param timeStyle
  463.      *            the time style as specified in
  464.      *            {@link java.text.DateFormat#getDateTimeInstance(int, int)}
  465.      * @return the date format
  466.      * @since 2.0
  467.      */
  468.     public DateFormat getDateTimeInstance(int dateStyle, int timeStyle) {
  469.         return DateFormat.getDateTimeInstance(dateStyle, timeStyle);
  470.     }

  471.     /**
  472.      * Whether we are running on Windows.
  473.      *
  474.      * @return true if we are running on Windows.
  475.      */
  476.     public boolean isWindows() {
  477.         if (isWindows == null) {
  478.             String osDotName = getOsName();
  479.             isWindows = Boolean.valueOf(osDotName.startsWith("Windows")); //$NON-NLS-1$
  480.         }
  481.         return isWindows.booleanValue();
  482.     }

  483.     /**
  484.      * Whether we are running on Mac OS X
  485.      *
  486.      * @return true if we are running on Mac OS X
  487.      */
  488.     public boolean isMacOS() {
  489.         if (isMacOS == null) {
  490.             String osDotName = getOsName();
  491.             isMacOS = Boolean.valueOf(
  492.                     "Mac OS X".equals(osDotName) || "Darwin".equals(osDotName)); //$NON-NLS-1$ //$NON-NLS-2$
  493.         }
  494.         return isMacOS.booleanValue();
  495.     }

  496.     /**
  497.      * Whether we are running on Linux.
  498.      *
  499.      * @return true if we are running on Linux.
  500.      * @since 6.3
  501.      */
  502.     public boolean isLinux() {
  503.         if (isLinux == null) {
  504.             String osname = getOsName();
  505.             isLinux = Boolean.valueOf(osname.toLowerCase().startsWith("linux")); //$NON-NLS-1$
  506.         }
  507.         return isLinux.booleanValue();
  508.     }

  509.     private String getOsName() {
  510.         return AccessController.doPrivileged(
  511.                 (PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$
  512.         );
  513.     }

  514.     /**
  515.      * Check tree path entry for validity.
  516.      * <p>
  517.      * Scans a multi-directory path string such as {@code "src/main.c"}.
  518.      *
  519.      * @param path path string to scan.
  520.      * @throws org.eclipse.jgit.errors.CorruptObjectException path is invalid.
  521.      * @since 3.6
  522.      */
  523.     public void checkPath(String path) throws CorruptObjectException {
  524.         platformChecker.checkPath(path);
  525.     }

  526.     /**
  527.      * Check tree path entry for validity.
  528.      * <p>
  529.      * Scans a multi-directory path string such as {@code "src/main.c"}.
  530.      *
  531.      * @param path
  532.      *            path string to scan.
  533.      * @throws org.eclipse.jgit.errors.CorruptObjectException
  534.      *             path is invalid.
  535.      * @since 4.2
  536.      */
  537.     public void checkPath(byte[] path) throws CorruptObjectException {
  538.         platformChecker.checkPath(path, 0, path.length);
  539.     }
  540. }