View Javadoc
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  
14  package org.eclipse.jgit.util;
15  
16  import java.io.File;
17  import java.io.IOException;
18  import java.net.InetAddress;
19  import java.net.UnknownHostException;
20  import java.nio.charset.Charset;
21  import java.nio.charset.IllegalCharsetNameException;
22  import java.nio.charset.UnsupportedCharsetException;
23  import java.nio.file.InvalidPathException;
24  import java.nio.file.Path;
25  import java.nio.file.Paths;
26  import java.security.AccessController;
27  import java.security.PrivilegedAction;
28  import java.text.DateFormat;
29  import java.text.SimpleDateFormat;
30  import java.util.Locale;
31  import java.util.TimeZone;
32  import java.util.concurrent.atomic.AtomicReference;
33  
34  import org.eclipse.jgit.errors.ConfigInvalidException;
35  import org.eclipse.jgit.errors.CorruptObjectException;
36  import org.eclipse.jgit.internal.JGitText;
37  import org.eclipse.jgit.lib.Config;
38  import org.eclipse.jgit.lib.Constants;
39  import org.eclipse.jgit.lib.ObjectChecker;
40  import org.eclipse.jgit.lib.StoredConfig;
41  import org.eclipse.jgit.storage.file.FileBasedConfig;
42  import org.eclipse.jgit.util.time.MonotonicClock;
43  import org.eclipse.jgit.util.time.MonotonicSystemClock;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  /**
48   * Interface to read values from the system.
49   * <p>
50   * When writing unit tests, extending this interface with a custom class
51   * permits to simulate an access to a system variable or property and
52   * permits to control the user's global configuration.
53   * </p>
54   */
55  public abstract class SystemReader {
56  
57  	private static final Logger LOG = LoggerFactory
58  			.getLogger(SystemReader.class);
59  
60  	private static final SystemReader DEFAULT;
61  
62  	private static volatile Boolean isMacOS;
63  
64  	private static volatile Boolean isWindows;
65  
66  	static {
67  		SystemReader r = new Default();
68  		r.init();
69  		DEFAULT = r;
70  	}
71  
72  	private static class Default extends SystemReader {
73  		private volatile String hostname;
74  
75  		@Override
76  		public String getenv(String variable) {
77  			return System.getenv(variable);
78  		}
79  
80  		@Override
81  		public String getProperty(String key) {
82  			return System.getProperty(key);
83  		}
84  
85  		@Override
86  		public FileBasedConfig openSystemConfig(Config parent, FS fs) {
87  			if (StringUtils
88  					.isEmptyOrNull(getenv(Constants.GIT_CONFIG_NOSYSTEM_KEY))) {
89  				File configFile = fs.getGitSystemConfig();
90  				if (configFile != null) {
91  					return new FileBasedConfig(parent, configFile, fs);
92  				}
93  			}
94  			return new FileBasedConfig(parent, null, fs) {
95  				@Override
96  				public void load() {
97  					// empty, do not load
98  				}
99  
100 				@Override
101 				public boolean isOutdated() {
102 					// regular class would bomb here
103 					return false;
104 				}
105 			};
106 		}
107 
108 		@Override
109 		public FileBasedConfig openUserConfig(Config parent, FS fs) {
110 			return new FileBasedConfig(parent, new File(fs.userHome(), ".gitconfig"), //$NON-NLS-1$
111 					fs);
112 		}
113 
114 		private Path getXDGConfigHome(FS fs) {
115 			String configHomePath = getenv(Constants.XDG_CONFIG_HOME);
116 			if (StringUtils.isEmptyOrNull(configHomePath)) {
117 				configHomePath = new File(fs.userHome(), ".config") //$NON-NLS-1$
118 						.getAbsolutePath();
119 			}
120 			try {
121 				return Paths.get(configHomePath);
122 			} catch (InvalidPathException e) {
123 				LOG.error(JGitText.get().logXDGConfigHomeInvalid,
124 						configHomePath, e);
125 			}
126 			return null;
127 		}
128 
129 		@Override
130 		public FileBasedConfig openJGitConfig(Config parent, FS fs) {
131 			Path xdgPath = getXDGConfigHome(fs);
132 			if (xdgPath != null) {
133 				Path configPath = xdgPath.resolve("jgit") //$NON-NLS-1$
134 						.resolve(Constants.CONFIG);
135 				return new FileBasedConfig(parent, configPath.toFile(), fs);
136 			}
137 			return new FileBasedConfig(parent,
138 					new File(fs.userHome(), ".jgitconfig"), fs); //$NON-NLS-1$
139 		}
140 
141 		@Override
142 		public String getHostname() {
143 			if (hostname == null) {
144 				try {
145 					InetAddress localMachine = InetAddress.getLocalHost();
146 					hostname = localMachine.getCanonicalHostName();
147 				} catch (UnknownHostException e) {
148 					// we do nothing
149 					hostname = "localhost"; //$NON-NLS-1$
150 				}
151 				assert hostname != null;
152 			}
153 			return hostname;
154 		}
155 
156 		@Override
157 		public long getCurrentTime() {
158 			return System.currentTimeMillis();
159 		}
160 
161 		@Override
162 		public int getTimezone(long when) {
163 			return getTimeZone().getOffset(when) / (60 * 1000);
164 		}
165 	}
166 
167 	private static volatile SystemReader INSTANCE = DEFAULT;
168 
169 	/**
170 	 * Get the current SystemReader instance
171 	 *
172 	 * @return the current SystemReader instance.
173 	 */
174 	public static SystemReader getInstance() {
175 		return INSTANCE;
176 	}
177 
178 	/**
179 	 * Set a new SystemReader instance to use when accessing properties.
180 	 *
181 	 * @param newReader
182 	 *            the new instance to use when accessing properties, or null for
183 	 *            the default instance.
184 	 */
185 	public static void setInstance(SystemReader newReader) {
186 		isMacOS = null;
187 		isWindows = null;
188 		if (newReader == null)
189 			INSTANCE = DEFAULT;
190 		else {
191 			newReader.init();
192 			INSTANCE = newReader;
193 		}
194 	}
195 
196 	private ObjectChecker platformChecker;
197 
198 	private AtomicReference<FileBasedConfig> systemConfig = new AtomicReference<>();
199 
200 	private AtomicReference<FileBasedConfig> userConfig = new AtomicReference<>();
201 
202 	private AtomicReference<FileBasedConfig> jgitConfig = new AtomicReference<>();
203 
204 	private volatile Charset defaultCharset;
205 
206 	private void init() {
207 		// Creating ObjectChecker must be deferred. Unit tests change
208 		// behavior of is{Windows,MacOS} in constructor of subclass.
209 		if (platformChecker == null)
210 			setPlatformChecker();
211 	}
212 
213 	/**
214 	 * Should be used in tests when the platform is explicitly changed.
215 	 *
216 	 * @since 3.6
217 	 */
218 	protected final void setPlatformChecker() {
219 		platformChecker = new ObjectChecker()
220 			.setSafeForWindows(isWindows())
221 			.setSafeForMacOS(isMacOS());
222 	}
223 
224 	/**
225 	 * Gets the hostname of the local host. If no hostname can be found, the
226 	 * hostname is set to the default value "localhost".
227 	 *
228 	 * @return the canonical hostname
229 	 */
230 	public abstract String getHostname();
231 
232 	/**
233 	 * Get value of the system variable
234 	 *
235 	 * @param variable
236 	 *            system variable to read
237 	 * @return value of the system variable
238 	 */
239 	public abstract String getenv(String variable);
240 
241 	/**
242 	 * Get value of the system property
243 	 *
244 	 * @param key
245 	 *            of the system property to read
246 	 * @return value of the system property
247 	 */
248 	public abstract String getProperty(String key);
249 
250 	/**
251 	 * Open the git configuration found in the user home. Use
252 	 * {@link #getUserConfig()} to get the current git configuration in the user
253 	 * home since it manages automatic reloading when the gitconfig file was
254 	 * modified and avoids unnecessary reloads.
255 	 *
256 	 * @param parent
257 	 *            a config with values not found directly in the returned config
258 	 * @param fs
259 	 *            the file system abstraction which will be necessary to perform
260 	 *            certain file system operations.
261 	 * @return the git configuration found in the user home
262 	 */
263 	public abstract FileBasedConfig openUserConfig(Config parent, FS fs);
264 
265 	/**
266 	 * Open the gitconfig configuration found in the system-wide "etc"
267 	 * directory. Use {@link #getSystemConfig()} to get the current system-wide
268 	 * git configuration since it manages automatic reloading when the gitconfig
269 	 * file was modified and avoids unnecessary reloads.
270 	 *
271 	 * @param parent
272 	 *            a config with values not found directly in the returned
273 	 *            config. Null is a reasonable value here.
274 	 * @param fs
275 	 *            the file system abstraction which will be necessary to perform
276 	 *            certain file system operations.
277 	 * @return the gitconfig configuration found in the system-wide "etc"
278 	 *         directory
279 	 */
280 	public abstract FileBasedConfig openSystemConfig(Config parent, FS fs);
281 
282 	/**
283 	 * Open the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. Use
284 	 * {@link #getJGitConfig()} to get the current jgit configuration in the
285 	 * user home since it manages automatic reloading when the jgit config file
286 	 * was modified and avoids unnecessary reloads.
287 	 *
288 	 * @param parent
289 	 *            a config with values not found directly in the returned config
290 	 * @param fs
291 	 *            the file system abstraction which will be necessary to perform
292 	 *            certain file system operations.
293 	 * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
294 	 * @since 5.5.2
295 	 */
296 	public abstract FileBasedConfig openJGitConfig(Config parent, FS fs);
297 
298 	/**
299 	 * Get the git configuration found in the user home. The configuration will
300 	 * be reloaded automatically if the configuration file was modified. Also
301 	 * reloads the system config if the system config file was modified. If the
302 	 * configuration file wasn't modified returns the cached configuration.
303 	 *
304 	 * @return the git configuration found in the user home
305 	 * @throws ConfigInvalidException
306 	 *             if configuration is invalid
307 	 * @throws IOException
308 	 *             if something went wrong when reading files
309 	 * @since 5.1.9
310 	 */
311 	public StoredConfig getUserConfig()
312 			throws ConfigInvalidException, IOException {
313 		FileBasedConfig c = userConfig.get();
314 		if (c == null) {
315 			userConfig.compareAndSet(null,
316 					openUserConfig(getSystemConfig(), FS.DETECTED));
317 			c = userConfig.get();
318 		}
319 		// on the very first call this will check a second time if the system
320 		// config is outdated
321 		updateAll(c);
322 		return c;
323 	}
324 
325 	/**
326 	 * Get the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. The
327 	 * configuration will be reloaded automatically if the configuration file
328 	 * was modified. If the configuration file wasn't modified returns the
329 	 * cached configuration.
330 	 *
331 	 * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
332 	 * @throws ConfigInvalidException
333 	 *             if configuration is invalid
334 	 * @throws IOException
335 	 *             if something went wrong when reading files
336 	 * @since 5.5.2
337 	 */
338 	public StoredConfig getJGitConfig()
339 			throws ConfigInvalidException, IOException {
340 		FileBasedConfig c = jgitConfig.get();
341 		if (c == null) {
342 			jgitConfig.compareAndSet(null,
343 					openJGitConfig(null, FS.DETECTED));
344 			c = jgitConfig.get();
345 		}
346 		updateAll(c);
347 		return c;
348 	}
349 
350 	/**
351 	 * Get the gitconfig configuration found in the system-wide "etc" directory.
352 	 * The configuration will be reloaded automatically if the configuration
353 	 * file was modified otherwise returns the cached system level config.
354 	 *
355 	 * @return the gitconfig configuration found in the system-wide "etc"
356 	 *         directory
357 	 * @throws ConfigInvalidException
358 	 *             if configuration is invalid
359 	 * @throws IOException
360 	 *             if something went wrong when reading files
361 	 * @since 5.1.9
362 	 */
363 	public StoredConfig getSystemConfig()
364 			throws ConfigInvalidException, IOException {
365 		FileBasedConfig c = systemConfig.get();
366 		if (c == null) {
367 			systemConfig.compareAndSet(null,
368 					openSystemConfig(getJGitConfig(), FS.DETECTED));
369 			c = systemConfig.get();
370 		}
371 		updateAll(c);
372 		return c;
373 	}
374 
375 	/**
376 	 * Update config and its parents if they seem modified
377 	 *
378 	 * @param config
379 	 *            configuration to reload if outdated
380 	 * @throws ConfigInvalidException
381 	 *             if configuration is invalid
382 	 * @throws IOException
383 	 *             if something went wrong when reading files
384 	 */
385 	private void updateAll(Config config)
386 			throws ConfigInvalidException, IOException {
387 		if (config == null) {
388 			return;
389 		}
390 		updateAll(config.getBaseConfig());
391 		if (config instanceof FileBasedConfig) {
392 			FileBasedConfig cfg = (FileBasedConfig) config;
393 			if (cfg.isOutdated()) {
394 				LOG.debug("loading config {}", cfg); //$NON-NLS-1$
395 				cfg.load();
396 			}
397 		}
398 	}
399 
400 	/**
401 	 * Get the current system time
402 	 *
403 	 * @return the current system time
404 	 */
405 	public abstract long getCurrentTime();
406 
407 	/**
408 	 * Get clock instance preferred by this system.
409 	 *
410 	 * @return clock instance preferred by this system.
411 	 * @since 4.6
412 	 */
413 	public MonotonicClock getClock() {
414 		return new MonotonicSystemClock();
415 	}
416 
417 	/**
418 	 * Get the local time zone
419 	 *
420 	 * @param when
421 	 *            a system timestamp
422 	 * @return the local time zone
423 	 */
424 	public abstract int getTimezone(long when);
425 
426 	/**
427 	 * Get system time zone, possibly mocked for testing
428 	 *
429 	 * @return system time zone, possibly mocked for testing
430 	 * @since 1.2
431 	 */
432 	public TimeZone getTimeZone() {
433 		return TimeZone.getDefault();
434 	}
435 
436 	/**
437 	 * Get the locale to use
438 	 *
439 	 * @return the locale to use
440 	 * @since 1.2
441 	 */
442 	public Locale getLocale() {
443 		return Locale.getDefault();
444 	}
445 
446 	/**
447 	 * Retrieves the default {@link Charset} depending on the system locale.
448 	 *
449 	 * @return the {@link Charset}
450 	 * @since 6.0
451 	 * @see <a href="https://openjdk.java.net/jeps/400">JEP 400</a>
452 	 */
453 	public Charset getDefaultCharset() {
454 		Charset result = defaultCharset;
455 		if (result == null) {
456 			// JEP 400: Java 18 populates this system property.
457 			String encoding = getProperty("native.encoding"); //$NON-NLS-1$
458 			try {
459 				if (!StringUtils.isEmptyOrNull(encoding)) {
460 					result = Charset.forName(encoding);
461 				}
462 			} catch (IllegalCharsetNameException
463 					| UnsupportedCharsetException e) {
464 				LOG.error(JGitText.get().logInvalidDefaultCharset, encoding);
465 			}
466 			if (result == null) {
467 				// This is always UTF-8 on Java >= 18.
468 				result = Charset.defaultCharset();
469 			}
470 			defaultCharset = result;
471 		}
472 		return result;
473 	}
474 
475 	/**
476 	 * Returns a simple date format instance as specified by the given pattern.
477 	 *
478 	 * @param pattern
479 	 *            the pattern as defined in
480 	 *            {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}
481 	 * @return the simple date format
482 	 * @since 2.0
483 	 */
484 	public SimpleDateFormat getSimpleDateFormat(String pattern) {
485 		return new SimpleDateFormat(pattern);
486 	}
487 
488 	/**
489 	 * Returns a simple date format instance as specified by the given pattern.
490 	 *
491 	 * @param pattern
492 	 *            the pattern as defined in
493 	 *            {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}
494 	 * @param locale
495 	 *            locale to be used for the {@code SimpleDateFormat}
496 	 * @return the simple date format
497 	 * @since 3.2
498 	 */
499 	public SimpleDateFormat getSimpleDateFormat(String pattern, Locale locale) {
500 		return new SimpleDateFormat(pattern, locale);
501 	}
502 
503 	/**
504 	 * Returns a date/time format instance for the given styles.
505 	 *
506 	 * @param dateStyle
507 	 *            the date style as specified in
508 	 *            {@link java.text.DateFormat#getDateTimeInstance(int, int)}
509 	 * @param timeStyle
510 	 *            the time style as specified in
511 	 *            {@link java.text.DateFormat#getDateTimeInstance(int, int)}
512 	 * @return the date format
513 	 * @since 2.0
514 	 */
515 	public DateFormat getDateTimeInstance(int dateStyle, int timeStyle) {
516 		return DateFormat.getDateTimeInstance(dateStyle, timeStyle);
517 	}
518 
519 	/**
520 	 * Whether we are running on Windows.
521 	 *
522 	 * @return true if we are running on Windows.
523 	 */
524 	public boolean isWindows() {
525 		if (isWindows == null) {
526 			String osDotName = getOsName();
527 			isWindows = Boolean.valueOf(osDotName.startsWith("Windows")); //$NON-NLS-1$
528 		}
529 		return isWindows.booleanValue();
530 	}
531 
532 	/**
533 	 * Whether we are running on Mac OS X
534 	 *
535 	 * @return true if we are running on Mac OS X
536 	 */
537 	public boolean isMacOS() {
538 		if (isMacOS == null) {
539 			String osDotName = getOsName();
540 			isMacOS = Boolean.valueOf(
541 					"Mac OS X".equals(osDotName) || "Darwin".equals(osDotName)); //$NON-NLS-1$ //$NON-NLS-2$
542 		}
543 		return isMacOS.booleanValue();
544 	}
545 
546 	private String getOsName() {
547 		return AccessController.doPrivileged(
548 				(PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$
549 		);
550 	}
551 
552 	/**
553 	 * Check tree path entry for validity.
554 	 * <p>
555 	 * Scans a multi-directory path string such as {@code "src/main.c"}.
556 	 *
557 	 * @param path path string to scan.
558 	 * @throws org.eclipse.jgit.errors.CorruptObjectException path is invalid.
559 	 * @since 3.6
560 	 */
561 	public void checkPath(String path) throws CorruptObjectException {
562 		platformChecker.checkPath(path);
563 	}
564 
565 	/**
566 	 * Check tree path entry for validity.
567 	 * <p>
568 	 * Scans a multi-directory path string such as {@code "src/main.c"}.
569 	 *
570 	 * @param path
571 	 *            path string to scan.
572 	 * @throws org.eclipse.jgit.errors.CorruptObjectException
573 	 *             path is invalid.
574 	 * @since 4.2
575 	 */
576 	public void checkPath(byte[] path) throws CorruptObjectException {
577 		platformChecker.checkPath(path, 0, path.length);
578 	}
579 }