/* * Copyright (C) 2008, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is * available at http://www.eclipse.org/org/documents/edl-v10.php * * All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * - Neither the name of the Eclipse Foundation, Inc. nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.eclipse.jgit.util; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.security.AccessController; import java.security.PrivilegedAction; import java.text.MessageFormat; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jgit.errors.SymlinksNotSupportedException; import org.eclipse.jgit.internal.JGitText; /** Abstraction to support various file system operations not in Java. */ public abstract class FS { /** * This class creates FS instances. It will be overridden by a Java7 variant * if such can be detected in {@link #detect(Boolean)}. * * @since 3.0 */ public static class FSFactory { /** * Constructor */ protected FSFactory() { // empty } /** * Detect the file system * * @param cygwinUsed * @return FS instance */ public FS detect(Boolean cygwinUsed) { if (SystemReader.getInstance().isWindows()) { if (cygwinUsed == null) cygwinUsed = Boolean.valueOf(FS_Win32_Cygwin.isCygwin()); if (cygwinUsed.booleanValue()) return new FS_Win32_Cygwin(); else return new FS_Win32(); } else if (FS_POSIX_Java6.hasExecute()) return new FS_POSIX_Java6(); else return new FS_POSIX_Java5(); } } /** The auto-detected implementation selected for this operating system and JRE. */ public static final FS DETECTED = detect(); private static FSFactory factory; /** * Auto-detect the appropriate file system abstraction. * * @return detected file system abstraction */ public static FS detect() { return detect(null); } /** * Auto-detect the appropriate file system abstraction, taking into account * the presence of a Cygwin installation on the system. Using jgit in * combination with Cygwin requires a more elaborate (and possibly slower) * resolution of file system paths. * * @param cygwinUsed * * * Note: this parameter is only relevant on Windows. * * @return detected file system abstraction */ public static FS detect(Boolean cygwinUsed) { if (factory == null) { try { Class activatorClass = Class .forName("org.eclipse.jgit.util.Java7FSFactory"); //$NON-NLS-1$ // found Java7 factory = (FSFactory) activatorClass.newInstance(); } catch (ClassNotFoundException e) { // Java7 module not found // Silently ignore failure to find Java7 FS factory factory = new FS.FSFactory(); } catch (UnsupportedClassVersionError e) { factory = new FS.FSFactory(); } catch (InstantiationException e) { factory = new FS.FSFactory(); } catch (IllegalAccessException e) { factory = new FS.FSFactory(); } } return factory.detect(cygwinUsed); } private volatile Holder userHome; private volatile Holder gitPrefix; /** * Constructs a file system abstraction. */ protected FS() { // Do nothing by default. } /** * Initialize this FS using another's current settings. * * @param src * the source FS to copy from. */ protected FS(FS src) { userHome = src.userHome; gitPrefix = src.gitPrefix; } /** @return a new instance of the same type of FS. */ public abstract FS newInstance(); /** * Does this operating system and JRE support the execute flag on files? * * @return true if this implementation can provide reasonably accurate * executable bit information; false otherwise. */ public abstract boolean supportsExecute(); /** * Does this operating system and JRE supports symbolic links. The * capability to handle symbolic links is detected at runtime. * * @return true if symbolic links may be used * @since 3.0 */ public boolean supportsSymlinks() { return false; } /** * Is this file system case sensitive * * @return true if this implementation is case sensitive */ public abstract boolean isCaseSensitive(); /** * Determine if the file is executable (or not). *

* Not all platforms and JREs support executable flags on files. If the * feature is unsupported this method will always return false. *

* If the platform supports symbolic links and f is a symbolic link * this method returns false, rather than the state of the executable flags * on the target file. * * @param f * abstract path to test. * @return true if the file is believed to be executable by the user. */ public abstract boolean canExecute(File f); /** * Set a file to be executable by the user. *

* Not all platforms and JREs support executable flags on files. If the * feature is unsupported this method will always return false and no * changes will be made to the file specified. * * @param f * path to modify the executable status of. * @param canExec * true to enable execution; false to disable it. * @return true if the change succeeded; false otherwise. */ public abstract boolean setExecute(File f, boolean canExec); /** * Get the last modified time of a file system object. If the OS/JRE support * symbolic links, the modification time of the link is returned, rather * than that of the link target. * * @param f * @return last modified time of f * @throws IOException * @since 3.0 */ public long lastModified(File f) throws IOException { return f.lastModified(); } /** * Set the last modified time of a file system object. If the OS/JRE support * symbolic links, the link is modified, not the target, * * @param f * @param time * @throws IOException * @since 3.0 */ public void setLastModified(File f, long time) throws IOException { f.setLastModified(time); } /** * Get the length of a file or link, If the OS/JRE supports symbolic links * it's the length of the link, else the length of the target. * * @param path * @return length of a file * @throws IOException * @since 3.0 */ public long length(File path) throws IOException { return path.length(); } /** * Delete a file. Throws an exception if delete fails. * * @param f * @throws IOException * this may be a Java7 subclass with detailed information * @since 3.3 */ public void delete(File f) throws IOException { if (!f.delete()) throw new IOException(MessageFormat.format( JGitText.get().deleteFileFailed, f.getAbsolutePath())); } /** * Resolve this file to its actual path name that the JRE can use. *

* This method can be relatively expensive. Computing a translation may * require forking an external process per path name translated. Callers * should try to minimize the number of translations necessary by caching * the results. *

* Not all platforms and JREs require path name translation. Currently only * Cygwin on Win32 require translation for Cygwin based paths. * * @param dir * directory relative to which the path name is. * @param name * path name to translate. * @return the translated path. new File(dir,name) if this * platform does not require path name translation. */ public File resolve(final File dir, final String name) { final File abspn = new File(name); if (abspn.isAbsolute()) return abspn; return new File(dir, name); } /** * Determine the user's home directory (location where preferences are). *

* This method can be expensive on the first invocation if path name * translation is required. Subsequent invocations return a cached result. *

* Not all platforms and JREs require path name translation. Currently only * Cygwin on Win32 requires translation of the Cygwin HOME directory. * * @return the user's home directory; null if the user does not have one. */ public File userHome() { Holder p = userHome; if (p == null) { p = new Holder(userHomeImpl()); userHome = p; } return p.value; } /** * Set the user's home directory location. * * @param path * the location of the user's preferences; null if there is no * home directory for the current user. * @return {@code this}. */ public FS setUserHome(File path) { userHome = new Holder(path); return this; } /** * Does this file system have problems with atomic renames? * * @return true if the caller should retry a failed rename of a lock file. */ public abstract boolean retryFailedLockFileCommit(); /** * Determine the user's home directory (location where preferences are). * * @return the user's home directory; null if the user does not have one. */ protected File userHomeImpl() { final String home = AccessController .doPrivileged(new PrivilegedAction() { public String run() { return System.getProperty("user.home"); //$NON-NLS-1$ } }); if (home == null || home.length() == 0) return null; return new File(home).getAbsoluteFile(); } /** * Searches the given path to see if it contains one of the given files. * Returns the first it finds. Returns null if not found or if path is null. * * @param path * List of paths to search separated by File.pathSeparator * @param lookFor * Files to search for in the given path * @return the first match found, or null * @since 3.0 **/ protected static File searchPath(final String path, final String... lookFor) { if (path == null) return null; for (final String p : path.split(File.pathSeparator)) { for (String command : lookFor) { final File e = new File(p, command); if (e.isFile()) return e.getAbsoluteFile(); } } return null; } /** * Execute a command and return a single line of output as a String * * @param dir * Working directory for the command * @param command * as component array * @param encoding * @return the one-line output of the command */ protected static String readPipe(File dir, String[] command, String encoding) { final boolean debug = Boolean.parseBoolean(SystemReader.getInstance() .getProperty("jgit.fs.debug")); //$NON-NLS-1$ try { if (debug) System.err.println("readpipe " + Arrays.asList(command) + "," //$NON-NLS-1$ //$NON-NLS-2$ + dir); final Process p = Runtime.getRuntime().exec(command, null, dir); final BufferedReader lineRead = new BufferedReader( new InputStreamReader(p.getInputStream(), encoding)); p.getOutputStream().close(); final AtomicBoolean gooblerFail = new AtomicBoolean(false); Thread gobbler = new Thread() { public void run() { InputStream is = p.getErrorStream(); try { int ch; if (debug) while ((ch = is.read()) != -1) System.err.print((char) ch); else while (is.read() != -1) { // ignore } } catch (IOException e) { // Just print on stderr for debugging if (debug) e.printStackTrace(System.err); gooblerFail.set(true); } try { is.close(); } catch (IOException e) { // Just print on stderr for debugging if (debug) e.printStackTrace(System.err); gooblerFail.set(true); } } }; gobbler.start(); String r = null; try { r = lineRead.readLine(); if (debug) { System.err.println("readpipe may return '" + r + "'"); //$NON-NLS-1$ //$NON-NLS-2$ System.err.println("(ignoring remaing output:"); //$NON-NLS-1$ } String l; while ((l = lineRead.readLine()) != null) { if (debug) System.err.println(l); } } finally { p.getErrorStream().close(); lineRead.close(); } for (;;) { try { int rc = p.waitFor(); gobbler.join(); if (rc == 0 && r != null && r.length() > 0 && !gooblerFail.get()) return r; if (debug) System.err.println("readpipe rc=" + rc); //$NON-NLS-1$ break; } catch (InterruptedException ie) { // Stop bothering me, I have a zombie to reap. } } } catch (IOException e) { if (debug) System.err.println(e); // Ignore error (but report) } if (debug) System.err.println("readpipe returns null"); //$NON-NLS-1$ return null; } /** @return the $prefix directory C Git would use. */ public File gitPrefix() { Holder p = gitPrefix; if (p == null) { String overrideGitPrefix = SystemReader.getInstance().getProperty( "jgit.gitprefix"); //$NON-NLS-1$ if (overrideGitPrefix != null) p = new Holder(new File(overrideGitPrefix)); else p = new Holder(discoverGitPrefix()); gitPrefix = p; } return p.value; } /** @return the $prefix directory C Git would use. */ protected abstract File discoverGitPrefix(); /** * Set the $prefix directory C Git uses. * * @param path * the directory. Null if C Git is not installed. * @return {@code this} */ public FS setGitPrefix(File path) { gitPrefix = new Holder(path); return this; } /** * Check if a file is a symbolic link and read it * * @param path * @return target of link or null * @throws IOException * @since 3.0 */ public String readSymLink(File path) throws IOException { throw new SymlinksNotSupportedException( JGitText.get().errorSymlinksNotSupported); } /** * @param path * @return true if the path is a symbolic link (and we support these) * @throws IOException * @since 3.0 */ public boolean isSymLink(File path) throws IOException { return false; } /** * Tests if the path exists, in case of a symbolic link, true even if the * target does not exist * * @param path * @return true if path exists * @since 3.0 */ public boolean exists(File path) { return path.exists(); } /** * Check if path is a directory. If the OS/JRE supports symbolic links and * path is a symbolic link to a directory, this method returns false. * * @param path * @return true if file is a directory, * @since 3.0 */ public boolean isDirectory(File path) { return path.isDirectory(); } /** * Examine if path represents a regular file. If the OS/JRE supports * symbolic links the test returns false if path represents a symbolic link. * * @param path * @return true if path represents a regular file * @since 3.0 */ public boolean isFile(File path) { return path.isFile(); } /** * @param path * @return true if path is hidden, either starts with . on unix or has the * hidden attribute in windows * @throws IOException * @since 3.0 */ public boolean isHidden(File path) throws IOException { return path.isHidden(); } /** * Set the hidden attribute for file whose name starts with a period. * * @param path * @param hidden * @throws IOException * @since 3.0 */ public void setHidden(File path, boolean hidden) throws IOException { if (!path.getName().startsWith(".")) //$NON-NLS-1$ throw new IllegalArgumentException( "Hiding only allowed for names that start with a period"); } /** * Create a symbolic link * * @param path * @param target * @throws IOException * @since 3.0 */ public void createSymLink(File path, String target) throws IOException { throw new SymlinksNotSupportedException( JGitText.get().errorSymlinksNotSupported); } /** * Initialize a ProcesssBuilder to run a command using the system shell. * * @param cmd * command to execute. This string should originate from the * end-user, and thus is platform specific. * @param args * arguments to pass to command. These should be protected from * shell evaluation. * @return a partially completed process builder. Caller should finish * populating directory, environment, and then start the process. */ public abstract ProcessBuilder runInShell(String cmd, String[] args); private static class Holder { final V value; Holder(V value) { this.value = value; } } /** * File attributes we typically care for. * * @since 3.3 */ public static class Attributes { /** * @return true if this are the attributes of a directory */ public boolean isDirectory() { return isDirectory; } /** * @return true if this are the attributes of an executable file */ public boolean isExecutable() { return isExecutable; } /** * @return true if this are the attributes of a symbolic link */ public boolean isSymbolicLink() { return isSymbolicLink; } /** * @return true if this are the attributes of a regular file */ public boolean isRegularFile() { return isRegularFile; } /** * @return the time when the file was created */ public long getCreationTime() { return creationTime; } /** * @return the time (milliseconds since 1970-01-01) when this object was * last modified */ public long getLastModifiedTime() { return lastModifiedTime; } private boolean isDirectory; private boolean isSymbolicLink; private boolean isRegularFile; private long creationTime; private long lastModifiedTime; private boolean isExecutable; private File file; private boolean exists; /** * file length */ protected long length = -1; FS fs; Attributes(FS fs, File file, boolean exists, boolean isDirectory, boolean isExecutable, boolean isSymbolicLink, boolean isRegularFile, long creationTime, long lastModifiedTime, long length) { this.fs = fs; this.file = file; this.exists = exists; this.isDirectory = isDirectory; this.isExecutable = isExecutable; this.isSymbolicLink = isSymbolicLink; this.isRegularFile = isRegularFile; this.creationTime = creationTime; this.lastModifiedTime = lastModifiedTime; this.length = length; } /** * Constructor when there are issues with reading * * @param fs * @param path */ public Attributes(File path, FS fs) { this.file = path; this.fs = fs; } /** * @return length of this file object */ public long getLength() { if (length == -1) return length = file.length(); return length; } /** * @return the filename */ public String getName() { return file.getName(); } /** * @return the file the attributes apply to */ public File getFile() { return file; } boolean exists() { return exists; } } /** * @param path * @return the file attributes we care for * @since 3.3 */ public Attributes getAttributes(File path) { boolean isDirectory = isDirectory(path); boolean isFile = !isDirectory && path.isFile(); assert path.exists() == isDirectory || isFile; boolean exists = isDirectory || isFile; boolean canExecute = exists && !isDirectory && canExecute(path); boolean isSymlink = false; long lastModified = exists ? path.lastModified() : 0L; long createTime = 0L; return new Attributes(this, path, exists, isDirectory, canExecute, isSymlink, isFile, createTime, lastModified, -1); } /** * Normalize the unicode path to composed form. * * @param file * @return NFC-format File * @since 3.3 */ public File normalize(File file) { return file; } /** * Normalize the unicode path to composed form. * * @param name * @return NFC-format string * @since 3.3 */ public String normalize(String name) { return name; } }