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