blob: d734d4b4faa7b1e2326336f1fc518e56e5b970db [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.util.io;
import com.intellij.Patches;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.win32.FileInfo;
import com.intellij.openapi.util.io.win32.IdeaWin32;
import com.intellij.util.ArrayUtil;
import com.intellij.util.SystemProperties;
import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import static com.intellij.util.BitUtil.isSet;
import static com.intellij.util.BitUtil.notSet;
/**
* @version 11.1
*/
public class FileSystemUtil {
static final String FORCE_USE_NIO2_KEY = "idea.io.use.nio2";
static final String COARSE_TIMESTAMP = "idea.io.coarse.ts";
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.io.FileSystemUtil");
private abstract static class Mediator {
@Nullable
protected abstract FileAttributes getAttributes(@NotNull String path) throws Exception;
@Nullable
protected abstract String resolveSymLink(@NotNull String path) throws Exception;
protected boolean clonePermissions(@NotNull String source, @NotNull String target) throws Exception { return false; }
@NotNull
private String getName() { return getClass().getSimpleName().replace("MediatorImpl", ""); }
}
@NotNull
private static Mediator ourMediator = getMediator();
private static Mediator getMediator() {
Throwable error = null;
final boolean forceUseNio2 = SystemProperties.getBooleanProperty(FORCE_USE_NIO2_KEY, false);
if (!forceUseNio2) {
if (SystemInfo.isWindows && IdeaWin32.isAvailable()) {
try {
return check(new IdeaWin32MediatorImpl());
}
catch (Throwable t) {
error = t;
}
}
else if (SystemInfo.isLinux || SystemInfo.isMac || SystemInfo.isSolaris || SystemInfo.isFreeBSD) {
try {
return check(new JnaUnixMediatorImpl());
}
catch (Throwable t) {
error = t;
}
}
}
if (SystemInfo.isJavaVersionAtLeast("1.7") && !"1.7.0-ea".equals(SystemInfo.JAVA_VERSION)) {
try {
return check(new Nio2MediatorImpl());
}
catch (Throwable t) {
error = t;
}
}
final String message =
"Failed to load filesystem access layer (" + SystemInfo.OS_NAME + ", " + SystemInfo.JAVA_VERSION + ", " + forceUseNio2 + ")";
LOG.error(message, error);
return new FallbackMediatorImpl();
}
private static Mediator check(final Mediator mediator) throws Exception {
final String quickTestPath = SystemInfo.isWindows ? "C:\\" : "/";
mediator.getAttributes(quickTestPath);
return mediator;
}
private FileSystemUtil() { }
@Nullable
public static FileAttributes getAttributes(@NotNull String path) {
try {
return ourMediator.getAttributes(path);
}
catch (Exception e) {
LOG.warn(e);
}
return null;
}
@Nullable
public static FileAttributes getAttributes(@NotNull File file) {
return getAttributes(file.getPath());
}
public static long lastModified(@NotNull File file) {
FileAttributes attributes = getAttributes(file);
return attributes != null ? attributes.lastModified : 0;
}
/**
* Checks if a last element in the path is a symlink.
*/
public static boolean isSymLink(@NotNull String path) {
if (SystemInfo.areSymLinksSupported) {
final FileAttributes attributes = getAttributes(path);
return attributes != null && attributes.isSymLink();
}
return false;
}
/**
* Checks if a last element in the path is a symlink.
*/
public static boolean isSymLink(@NotNull File file) {
return isSymLink(file.getAbsolutePath());
}
@Nullable
public static String resolveSymLink(@NotNull String path) {
try {
final String realPath = ourMediator.resolveSymLink(path);
if (realPath != null && new File(realPath).exists()) {
return realPath;
}
}
catch (Exception e) {
LOG.warn(e);
}
return null;
}
@Nullable
public static String resolveSymLink(@NotNull File file) {
return resolveSymLink(file.getAbsolutePath());
}
/** @deprecated use {@link #clonePermissions(String, String)} (to remove in IDEA 14) */
@SuppressWarnings("UnusedDeclaration")
public static int getPermissions(@NotNull String path) {
return -1;
}
/** @deprecated use {@link #clonePermissions(String, String)} (to remove in IDEA 14) */
@SuppressWarnings("UnusedDeclaration")
public static int getPermissions(@NotNull File file) {
return -1;
}
/** @deprecated use {@link #clonePermissions(String, String)} (to remove in IDEA 14) */
@SuppressWarnings("UnusedDeclaration")
public static void setPermissions(@NotNull String path, int permissions) { }
/** @deprecated use {@link #clonePermissions(String, String)} (to remove in IDEA 14) */
@SuppressWarnings({"UnusedDeclaration", "deprecation"})
public static void setPermissions(@NotNull File file, int permissions) { }
/**
* Gives the second file permissions of the first one if possible; returns true if succeed.
* Will do nothing on Windows.
*/
public static boolean clonePermissions(@NotNull String source, @NotNull String target) {
try {
return ourMediator.clonePermissions(source, target);
}
catch (Exception e) {
LOG.warn(e);
return false;
}
}
private static class Nio2MediatorImpl extends Mediator {
private final Object myDefaultFileSystem;
private final Method myGetPath;
private final Method myIsSymbolicLink;
private final Object myLinkOptions;
private final Object myNoFollowLinkOptions;
private final Method myReadAttributes;
private final Method mySetAttribute;
private final Method myToMillis;
private final String mySchema;
private Nio2MediatorImpl() throws Exception {
//noinspection ConstantConditions
assert Patches.USE_REFLECTION_TO_ACCESS_JDK7;
myDefaultFileSystem = Class.forName("java.nio.file.FileSystems").getMethod("getDefault").invoke(null);
final Class<?> fsClass = Class.forName("java.nio.file.FileSystem");
myGetPath = fsClass.getMethod("getPath", String.class, String[].class);
myGetPath.setAccessible(true);
final Class<?> pathClass = Class.forName("java.nio.file.Path");
final Class<?> filesClass = Class.forName("java.nio.file.Files");
myIsSymbolicLink = filesClass.getMethod("isSymbolicLink", pathClass);
myIsSymbolicLink.setAccessible(true);
final Class<?> linkOptClass = Class.forName("java.nio.file.LinkOption");
myLinkOptions = Array.newInstance(linkOptClass, 0);
myNoFollowLinkOptions = Array.newInstance(linkOptClass, 1);
Array.set(myNoFollowLinkOptions, 0, linkOptClass.getField("NOFOLLOW_LINKS").get(null));
final Class<?> linkOptArrClass = myLinkOptions.getClass();
myReadAttributes = filesClass.getMethod("readAttributes", pathClass, String.class, linkOptArrClass);
myReadAttributes.setAccessible(true);
mySetAttribute = filesClass.getMethod("setAttribute", pathClass, String.class, Object.class, linkOptArrClass);
mySetAttribute.setAccessible(true);
final Class<?> fileTimeClass = Class.forName("java.nio.file.attribute.FileTime");
myToMillis = fileTimeClass.getMethod("toMillis");
myToMillis.setAccessible(true);
mySchema = SystemInfo.isWindows ? "dos:*" : "posix:*";
}
@Override
protected FileAttributes getAttributes(@NotNull String path) throws Exception {
try {
Object pathObj = myGetPath.invoke(myDefaultFileSystem, path, ArrayUtil.EMPTY_STRING_ARRAY);
Map attributes = (Map)myReadAttributes.invoke(null, pathObj, mySchema, myNoFollowLinkOptions);
boolean isSymbolicLink = (Boolean)attributes.get("isSymbolicLink");
if (isSymbolicLink) {
try {
attributes = (Map)myReadAttributes.invoke(null, pathObj, mySchema, myLinkOptions);
}
catch (InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause != null && "java.nio.file.NoSuchFileException".equals(cause.getClass().getName())) {
return FileAttributes.BROKEN_SYMLINK;
}
}
}
boolean isDirectory = (Boolean)attributes.get("isDirectory");
boolean isOther = (Boolean)attributes.get("isOther");
long size = (Long)attributes.get("size");
long lastModified = (Long)myToMillis.invoke(attributes.get("lastModifiedTime"));
if (SystemInfo.isWindows) {
boolean isHidden = new File(path).getParent() == null ? false : (Boolean)attributes.get("hidden");
boolean isWritable = !(Boolean)attributes.get("readonly");
return new FileAttributes(isDirectory, isOther, isSymbolicLink, isHidden, size, lastModified, isWritable);
}
else {
boolean isWritable = new File(path).canWrite();
return new FileAttributes(isDirectory, isOther, isSymbolicLink, false, size, lastModified, isWritable);
}
}
catch (InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause != null && ("java.nio.file.NoSuchFileException".equals(cause.getClass().getName()) ||
"java.nio.file.InvalidPathException".equals(cause.getClass().getName()))) {
LOG.debug(cause);
return null;
}
throw e;
}
}
@Override
protected String resolveSymLink(@NotNull final String path) throws Exception {
if (!new File(path).exists()) return null;
final Object pathObj = myGetPath.invoke(myDefaultFileSystem, path, ArrayUtil.EMPTY_STRING_ARRAY);
final Method toRealPath = pathObj.getClass().getMethod("toRealPath", myLinkOptions.getClass());
toRealPath.setAccessible(true);
return toRealPath.invoke(pathObj, myLinkOptions).toString();
}
@Override
protected boolean clonePermissions(@NotNull String source, @NotNull String target) throws Exception {
if (SystemInfo.isUnix) {
Object pathObj = myGetPath.invoke(myDefaultFileSystem, source, ArrayUtil.EMPTY_STRING_ARRAY);
Map attributes = (Map)myReadAttributes.invoke(null, pathObj, "posix:permissions", myLinkOptions);
if (attributes != null) {
Object permissions = attributes.get("permissions");
if (permissions instanceof Collection) {
mySetAttribute.invoke(null, pathObj, "posix:permissions", permissions, myLinkOptions);
return true;
}
}
}
return false;
}
}
private static class IdeaWin32MediatorImpl extends Mediator {
private IdeaWin32 myInstance = IdeaWin32.getInstance();
@Override
protected FileAttributes getAttributes(@NotNull final String path) throws Exception {
final FileInfo fileInfo = myInstance.getInfo(path);
return fileInfo != null ? fileInfo.toFileAttributes() : null;
}
@Override
protected String resolveSymLink(@NotNull final String path) throws Exception {
return myInstance.resolveSymLink(path);
}
}
// thanks to SVNKit for the idea of platform-specific offsets
private static class JnaUnixMediatorImpl extends Mediator {
@SuppressWarnings({"OctalInteger", "SpellCheckingInspection"})
private interface LibC extends Library {
int S_MASK = 0177777;
int S_IFLNK = 0120000; // symbolic link
int S_IFREG = 0100000; // regular file
int S_IFDIR = 0040000; // directory
int PERM_MASK = 0777;
int WRITE_MASK = 0222;
int W_OK = 2; // write permission flag for access(2)
int getuid();
int getgid();
int lstat(String path, Pointer stat);
int stat(String path, Pointer stat);
int __lxstat64(int ver, String path, Pointer stat);
int __xstat64(int ver, String path, Pointer stat);
int chmod(String path, int mode);
int access(String path, int mode);
}
private static final int[] LINUX_32 = {16, 44, 72, 24, 28};
private static final int[] LINUX_64 = {24, 48, 88, 28, 32};
private static final int[] BSD_32 = { 8, 48, 32, 12, 16};
private static final int[] BSD_64 = { 8, 72, 40, 12, 16};
private static final int[] SUN_OS_32 = {20, 48, 64, 28, 32};
private static final int[] SUN_OS_64 = {16, 40, 64, 24, 28};
private static final int OFF_MODE = 0;
private static final int OFF_SIZE = 1;
private static final int OFF_TIME = 2;
private static final int OFF_UID = 3;
private static final int OFF_GID = 4;
private final LibC myLibC;
private final int[] myOffsets;
private final int myUid;
private final int myGid;
private final boolean myCoarseTs = SystemProperties.getBooleanProperty(COARSE_TIMESTAMP, false);
private JnaUnixMediatorImpl() throws Exception {
myOffsets = SystemInfo.isLinux ? (SystemInfo.is32Bit ? LINUX_32 : LINUX_64) :
SystemInfo.isMac | SystemInfo.isFreeBSD ? (SystemInfo.is32Bit ? BSD_32 : BSD_64) :
SystemInfo.isSolaris ? (SystemInfo.is32Bit ? SUN_OS_32 : SUN_OS_64) :
null;
if (myOffsets == null || myOffsets.length != 5) throw new IllegalStateException("Unsupported OS: " + SystemInfo.OS_NAME);
myLibC = (LibC)Native.loadLibrary("c", LibC.class);
myUid = myLibC.getuid();
myGid = myLibC.getgid();
}
@Override
protected FileAttributes getAttributes(@NotNull String path) throws Exception {
Memory buffer = new Memory(256);
int res = SystemInfo.isLinux ? myLibC.__lxstat64(0, path, buffer) : myLibC.lstat(path, buffer);
if (res != 0) return null;
int mode = (SystemInfo.isLinux ? buffer.getInt(myOffsets[OFF_MODE]) : buffer.getShort(myOffsets[OFF_MODE])) & LibC.S_MASK;
boolean isSymlink = (mode & LibC.S_IFLNK) == LibC.S_IFLNK;
if (isSymlink) {
res = SystemInfo.isLinux ? myLibC.__xstat64(0, path, buffer) : myLibC.stat(path, buffer);
if (res != 0) {
return FileAttributes.BROKEN_SYMLINK;
}
mode = (SystemInfo.isLinux ? buffer.getInt(myOffsets[OFF_MODE]) : buffer.getShort(myOffsets[OFF_MODE])) & LibC.S_MASK;
}
boolean isDirectory = (mode & LibC.S_IFDIR) == LibC.S_IFDIR;
boolean isSpecial = !isDirectory && (mode & LibC.S_IFREG) == 0;
long size = buffer.getLong(myOffsets[OFF_SIZE]);
long mTime1 = SystemInfo.is32Bit ? buffer.getInt(myOffsets[OFF_TIME]) : buffer.getLong(myOffsets[OFF_TIME]);
long mTime2 = myCoarseTs ? 0 : SystemInfo.is32Bit ? buffer.getInt(myOffsets[OFF_TIME] + 4) : buffer.getLong(myOffsets[OFF_TIME] + 8);
long mTime = mTime1 * 1000 + mTime2 / 1000000;
boolean writable = ownFile(buffer) ? (mode & LibC.WRITE_MASK) != 0 : myLibC.access(path, LibC.W_OK) == 0;
return new FileAttributes(isDirectory, isSpecial, isSymlink, false, size, mTime, writable);
}
@Override
protected String resolveSymLink(@NotNull final String path) throws Exception {
try {
return new File(path).getCanonicalPath();
}
catch (IOException e) {
String message = e.getMessage();
if (message != null && message.toLowerCase(Locale.US).contains("too many levels of symbolic links")) {
LOG.debug(e);
return null;
}
throw new IOException("Cannot resolve '" + path + "'", e);
}
}
@Override
protected boolean clonePermissions(@NotNull String source, @NotNull String target) throws Exception {
Memory buffer = new Memory(256);
int res = SystemInfo.isLinux ? myLibC.__xstat64(0, source, buffer) : myLibC.stat(source, buffer);
if (res == 0) {
int permissions = (SystemInfo.isLinux ? buffer.getInt(myOffsets[OFF_MODE]) : buffer.getShort(myOffsets[OFF_MODE])) & LibC.PERM_MASK;
return myLibC.chmod(target, permissions) == 0;
}
return false;
}
private boolean ownFile(Memory buffer) {
return buffer.getInt(myOffsets[OFF_UID]) == myUid && buffer.getInt(myOffsets[OFF_GID]) == myGid;
}
}
private static class FallbackMediatorImpl extends Mediator {
// from java.io.FileSystem
private static final int BA_REGULAR = 0x02;
private static final int BA_DIRECTORY = 0x04;
private static final int BA_HIDDEN = 0x08;
private final Object myFileSystem;
private final Method myGetBooleanAttributes;
private FallbackMediatorImpl() {
Object fileSystem;
Method getBooleanAttributes;
try {
final Class<?> fsClass = Class.forName("java.io.FileSystem");
final Method getFileSystem = fsClass.getMethod("getFileSystem");
getFileSystem.setAccessible(true);
fileSystem = getFileSystem.invoke(null);
getBooleanAttributes = fsClass.getDeclaredMethod("getBooleanAttributes", File.class);
getBooleanAttributes.setAccessible(true);
}
catch (Throwable t) {
fileSystem = null;
getBooleanAttributes = null;
}
myFileSystem = fileSystem;
myGetBooleanAttributes = getBooleanAttributes;
}
@Override
protected FileAttributes getAttributes(@NotNull final String path) throws Exception {
final File file = new File(path);
if (myFileSystem != null) {
final int flags = (Integer)myGetBooleanAttributes.invoke(myFileSystem, file);
if (flags != 0) {
final boolean isDirectory = isSet(flags, BA_DIRECTORY);
final boolean isSpecial = notSet(flags, BA_REGULAR | BA_DIRECTORY);
final boolean isHidden = isSet(flags, BA_HIDDEN);
return new FileAttributes(isDirectory, isSpecial, false, isHidden, file.length(), file.lastModified(), file.canWrite());
}
}
else {
if (file.exists()) {
final boolean isDirectory = file.isDirectory();
final boolean isSpecial = !isDirectory && !file.isFile();
final boolean isHidden = file.isHidden();
return new FileAttributes(isDirectory, isSpecial, false, isHidden, file.length(), file.lastModified(), file.canWrite());
}
}
return null;
}
@Override
protected String resolveSymLink(@NotNull final String path) throws Exception {
return new File(path).getCanonicalPath();
}
@Override
protected boolean clonePermissions(@NotNull String source, @NotNull String target) throws Exception {
if (SystemInfo.isUnix) {
File srcFile = new File(source);
File dstFile = new File(target);
return dstFile.setWritable(srcFile.canWrite(), true) && dstFile.setExecutable(srcFile.canExecute(), true);
}
return false;
}
}
@TestOnly
static void resetMediator() {
ourMediator = getMediator();
}
@TestOnly
static String getMediatorName() {
return ourMediator.getName();
}
}