From 329fe66a6507edd4a8e6e2e28c5413aef454d140 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Mon, 22 Aug 2011 17:27:10 +0200 Subject: [PATCH] Symlink support for Windows --- .../com/intellij/openapi/vfs/VirtualFile.java | 19 ++ .../intellij/openapi/vfs/LocalFileSystem.java | 21 ++ .../openapi/vfs/impl/local/FileWatcher.java | 10 +- .../vfs/impl/local/LocalFileSystemBase.java | 18 +- .../vfs/impl/local/LocalFileSystemImpl.java | 7 + .../openapi/vfs/impl/local/SymLinkUtil.java | 141 -------- .../newvfs/impl/VirtualFileSystemEntry.java | 6 + .../vfs/local/SymLinkHandlingTest.java | 31 +- .../com/intellij/openapi/util/SystemInfo.java | 4 +- .../intellij/openapi/util/io/FileUtil.java | 23 +- .../intellij/openapi/util/io/SymLinkUtil.java | 320 ++++++++++++++++++ 11 files changed, 421 insertions(+), 179 deletions(-) delete mode 100644 platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/SymLinkUtil.java create mode 100644 platform/util/src/com/intellij/openapi/util/io/SymLinkUtil.java diff --git a/platform/core-api/src/com/intellij/openapi/vfs/VirtualFile.java b/platform/core-api/src/com/intellij/openapi/vfs/VirtualFile.java index 7388140e6dc2..8f1e8da849e8 100644 --- a/platform/core-api/src/com/intellij/openapi/vfs/VirtualFile.java +++ b/platform/core-api/src/com/intellij/openapi/vfs/VirtualFile.java @@ -200,10 +200,29 @@ public abstract class VirtualFile extends UserDataHolderBase implements Modifica */ public abstract boolean isDirectory(); + /** + * Checks whether this file is a symbolic link. + * + * @since 11.0 + * @return true if this file is a symbolic link, false otherwise + */ public boolean isSymLink() { return false; } + /** + * Attempts to resolve a symbolic link represented by this file and returns link target. + * + * @since 11.0 + * @return this if the file isn't a symbolic link; + * instance of VirtualFile if the link was successfully resolved; + * null otherwise + */ + @Nullable + public VirtualFile getRealFile() { + return this; + } + /** * Checks whether this VirtualFile is valid. File can be invalidated either by deleting it or one of its * parents with {@link #delete} method or by an external change. diff --git a/platform/platform-api/src/com/intellij/openapi/vfs/LocalFileSystem.java b/platform/platform-api/src/com/intellij/openapi/vfs/LocalFileSystem.java index b03a30718020..8c7b28a6a98f 100644 --- a/platform/platform-api/src/com/intellij/openapi/vfs/LocalFileSystem.java +++ b/platform/platform-api/src/com/intellij/openapi/vfs/LocalFileSystem.java @@ -40,10 +40,31 @@ public abstract class LocalFileSystem extends NewVirtualFileSystem { return LocalFileSystemHolder.ourInstance; } + /** + * Checks whether given file is a symbolic link. + * + * @param file a file to check. + * @return true if the file is a symbolic link, false otherwise + * @since 11.0 + */ public boolean isSymLink(@NotNull final VirtualFile file) { return false; } + /** + * Attempts to resolve a symbolic link represented by given file and returns link target. + * + * @since 11.0 + * @param file a file to resolve. + * @return this if the file isn't a symbolic link; + * instance of VirtualFile if the link was successfully resolved; + * null otherwise + */ + @Nullable + public VirtualFile getRealFile(@NotNull final VirtualFile file) { + return file; + } + @Nullable public abstract VirtualFile findFileByIoFile(File file); diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/FileWatcher.java b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/FileWatcher.java index 422a3f34beee..883850311c12 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/FileWatcher.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/FileWatcher.java @@ -26,6 +26,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.SymLinkUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.newvfs.ManagingFS; import com.intellij.openapi.vfs.newvfs.NewVirtualFile; @@ -214,13 +215,8 @@ public class FileWatcher { } private static String getCanonicalPath(final String path) { - try { - return new File(path).getCanonicalPath(); - } - catch (IOException e) { - LOG.warn(e.getMessage() + ": " + path); - return path; - } + final String realPath = SymLinkUtil.resolveSymLink(path); + return realPath != null ? realPath : path; } private boolean isAlive() { diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemBase.java b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemBase.java index bca4f936c4db..96fe8bb9d867 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemBase.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemBase.java @@ -20,6 +20,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.SymLinkUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.*; import com.intellij.openapi.vfs.ex.VirtualFileManagerEx; @@ -170,22 +171,21 @@ public abstract class LocalFileSystemBase extends LocalFileSystem { } } - File ioFile = convertToIOFile(file); - if (file.isSymLink() && isRecursiveSymLink(ioFile)) { + if (isInvalidSymLink(file)) { return ArrayUtil.EMPTY_STRING_ARRAY; } + final File ioFile = convertToIOFile(file); final String[] names = ioFile.list(); return names != null ? names : ArrayUtil.EMPTY_STRING_ARRAY; } - protected static boolean isRecursiveSymLink(File ioFile) { - try { - if (FileUtil.isAncestor(ioFile.getCanonicalFile(), ioFile, true)) return true; - } - catch (IOException ignore) { - } - return false; + protected static boolean isInvalidSymLink(@NotNull final VirtualFile file) { + if (!file.isSymLink()) return false; + final VirtualFile realFile = file.getRealFile(); + return realFile == null || + realFile == file || + FileUtil.isAncestor(convertToIOFile(realFile), convertToIOFile(file), true); } @NotNull diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemImpl.java b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemImpl.java index 6d45f014f470..f6c9b2da036e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemImpl.java @@ -22,6 +22,7 @@ import com.intellij.openapi.components.ApplicationComponent; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.SymLinkUtil; import com.intellij.openapi.vfs.JarFileSystem; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; @@ -471,6 +472,12 @@ public final class LocalFileSystemImpl extends LocalFileSystemBase implements Ap return SymLinkUtil.isSymLink(file.getPath()); } + @Override + public VirtualFile getRealFile(@NotNull final VirtualFile file) { + final String realPath = SymLinkUtil.resolveSymLink(file.getPath()); + return realPath != null ? findFileByPath(realPath) : null; + } + public boolean isWritable(@NotNull final VirtualFile file) { if (myNativeFileSystem == null) return super.isWritable(file); else return myNativeFileSystem.isWritable(file); diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/SymLinkUtil.java b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/SymLinkUtil.java deleted file mode 100644 index ee8e557327e9..000000000000 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/SymLinkUtil.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2000-2011 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.vfs.impl.local; - -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.util.SystemInfo; -import com.intellij.util.ArrayUtil; -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 java.io.File; -import java.lang.reflect.Method; - -// todo[r.sh] use NIO2 API after migration to JDK 7 -public class SymLinkUtil { - private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.impl.local.SymLinkUtil"); - - @Nullable - private static final Mediator ourMediator; - - static { - Mediator mediator = null; - if (SystemInfo.isJavaVersionAtLeast("1.7")) { - try { - mediator = new Jdk7MediatorImpl(); - mediator.isSymLink("/"); // quick test - } - catch (Exception e) { - LOG.error(e); - mediator = null; - } - } - if (mediator == null && (SystemInfo.isLinux || SystemInfo.isMac || SystemInfo.isSolaris)) { - try { - mediator = new JnaMediatorImpl(); - mediator.isSymLink("/"); // quick test - } - catch (Exception e) { - LOG.error(e); - mediator = null; - } - } - ourMediator = mediator; - } - - private SymLinkUtil() { } - - public static boolean isSymLink(@NotNull final File file) { - return isSymLink(file.getAbsolutePath()); - } - - public static boolean isSymLink(@NotNull final String path) { - try { - return ourMediator != null && ourMediator.isSymLink(path); - } - catch (Exception e) { - LOG.error(e); - return false; - } - } - - private interface Mediator { - boolean isSymLink(@NotNull final String path) throws Exception; - } - - private static class Jdk7MediatorImpl implements Mediator { - private final Method myGetDefault; - private final Method myGetPath; - private final Method myIsSymbolicLink; - - private Jdk7MediatorImpl() throws Exception { - myGetDefault = Class.forName("java.nio.file.FileSystems").getMethod("getDefault"); - myGetPath = Class.forName("java.nio.file.FileSystem").getMethod("getPath", String.class, String[].class); - myIsSymbolicLink = Class.forName("java.nio.file.Files").getMethod("isSymbolicLink", Class.forName("java.nio.file.Path")); - } - - @Override - public boolean isSymLink(@NotNull final String path) throws Exception { - final Object fileSystem = myGetDefault.invoke(null); - final Object pathObj = myGetPath.invoke(fileSystem, path, ArrayUtil.EMPTY_STRING_ARRAY); - return (Boolean)myIsSymbolicLink.invoke(null, pathObj); - } - } - - // thanks to SVNKit for the idea - @SuppressWarnings("OctalInteger") - private static class JnaMediatorImpl implements Mediator { - private interface LibC extends Library { - int S_MASK = 0177777; - int S_IFLNK = 0120000; - - int lstat(String path, Pointer stat); - int __lxstat64(int ver, String path, Pointer stat); - } - - private final LibC myLibC; - private final Memory mySharedMem; - private final int myOffset; - - private JnaMediatorImpl() throws Exception { - myLibC = (LibC)Native.loadLibrary("c", LibC.class); - mySharedMem = new Memory(512); - myOffset = SystemInfo.isLinux ? (SystemInfo.is32Bit ? 16 : 24) : - SystemInfo.isMac ? 8 : - SystemInfo.isSolaris ? (SystemInfo.is32Bit ? 20 : 16) : - -1; - if (myOffset < 0) throw new IllegalStateException("Unsupported OS: " + SystemInfo.OS_NAME); - } - - @Override - public synchronized boolean isSymLink(@NotNull final String path) throws Exception { - mySharedMem.clear(); - final int res = SystemInfo.isLinux ? myLibC.__lxstat64(0, path, mySharedMem) : myLibC.lstat(path, mySharedMem); - if (res == 0) { - final int mode = (SystemInfo.isLinux ? mySharedMem.getInt(myOffset) : mySharedMem.getShort(myOffset)) & LibC.S_MASK; - return (mode & LibC.S_IFLNK) == LibC.S_IFLNK; - } - else { - //LOG.warn("lstat(" + path + "): " + res); - return false; - } - } - } -} diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/impl/VirtualFileSystemEntry.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/impl/VirtualFileSystemEntry.java index f728f6f344da..c7c5d5595caf 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/impl/VirtualFileSystemEntry.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/impl/VirtualFileSystemEntry.java @@ -425,4 +425,10 @@ public abstract class VirtualFileSystemEntry extends NewVirtualFile { final NewVirtualFileSystem fs = getFileSystem(); return fs instanceof LocalFileSystem && ((LocalFileSystem)fs).isSymLink(this); } + + @Override + public VirtualFile getRealFile() { + final NewVirtualFileSystem fs = getFileSystem(); + return fs instanceof LocalFileSystem ? ((LocalFileSystem)fs).getRealFile(this) : super.getRealFile(); + } } diff --git a/platform/platform-impl/testSrc/com/intellij/openapi/vfs/local/SymLinkHandlingTest.java b/platform/platform-impl/testSrc/com/intellij/openapi/vfs/local/SymLinkHandlingTest.java index 1985ec96ff23..95c3aae8c9e1 100644 --- a/platform/platform-impl/testSrc/com/intellij/openapi/vfs/local/SymLinkHandlingTest.java +++ b/platform/platform-impl/testSrc/com/intellij/openapi/vfs/local/SymLinkHandlingTest.java @@ -67,12 +67,17 @@ public class SymLinkHandlingTest extends LightPlatformTestCase { final VirtualFile linkVDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(linkDir); assertTrue("link=" + linkDir + ", vLink=" + linkVDir, linkVDir != null && linkVDir.isDirectory() && linkVDir.isSymLink()); - assertTrue(targetDir.getAbsolutePath(), targetDir.setWritable(true, false) && targetDir.canWrite()); - linkVDir.refresh(false, true); - assertTrue(linkVDir.getPath(), linkVDir.isWritable()); - assertTrue(targetDir.getAbsolutePath(), targetDir.setWritable(false, false) && !targetDir.canWrite()); - linkVDir.refresh(false, true); - assertFalse(linkVDir.getPath(), linkVDir.isWritable()); + if (!SystemInfo.isWindows) { + assertTrue(targetDir.getAbsolutePath(), targetDir.setWritable(true, false) && targetDir.canWrite()); + linkVDir.refresh(false, true); + assertTrue(linkVDir.getPath(), linkVDir.isWritable()); + assertTrue(targetDir.getAbsolutePath(), targetDir.setWritable(false, false) && !targetDir.canWrite()); + linkVDir.refresh(false, true); + assertFalse(linkVDir.getPath(), linkVDir.isWritable()); + } + else { + assertEquals(linkVDir.getPath(), targetDir.canWrite(), linkVDir.isWritable()); + } } public void testLinkDeleteIsSafe() throws Exception { @@ -122,14 +127,20 @@ public class SymLinkHandlingTest extends LightPlatformTestCase { final File parentDir = linkFile.getParentFile(); assertTrue("link=" + link + ", parent=" + parentDir, parentDir != null && (parentDir.isDirectory() || parentDir.mkdirs())); - final ProcessBuilder builder = new ProcessBuilder("ln", "-s", target, linkFile.getAbsolutePath()); + final ProcessBuilder builder; + if (SystemInfo.isWindows) { + builder = new File(target).isDirectory() + ? new ProcessBuilder("cmd", "/C", "mklink", "/D", linkFile.getAbsolutePath(), target) + : new ProcessBuilder("cmd", "/C", "mklink", linkFile.getAbsolutePath(), target); + } + else { + builder = new ProcessBuilder("ln", "-s", target, linkFile.getAbsolutePath()); + } final Process process = builder.start(); final int res = process.waitFor(); assertTrue(builder.command() + ": " + res, res == 0); final File targetFile = new File(target); - assertTrue("target=" + target + ", link=" + linkFile, - linkFile.exists() == targetFile.exists() && - linkFile.getCanonicalPath().equals(targetFile.getAbsolutePath()) == targetFile.exists()); + assertEquals("target=" + target + ", link=" + linkFile, targetFile.exists(), linkFile.exists()); return linkFile; } } diff --git a/platform/util/src/com/intellij/openapi/util/SystemInfo.java b/platform/util/src/com/intellij/openapi/util/SystemInfo.java index 79cd594314d5..aad3b4845727 100644 --- a/platform/util/src/com/intellij/openapi/util/SystemInfo.java +++ b/platform/util/src/com/intellij/openapi/util/SystemInfo.java @@ -50,12 +50,12 @@ public class SystemInfo { public static final boolean isMacSystemMenu = isMac && "true".equals(System.getProperty("apple.laf.useScreenMenuBar")); public static final boolean isFileSystemCaseSensitive = !isWindows && !isOS2 && !isMac; - public static final boolean areSymLinksSupported = isUnix; + public static final boolean areSymLinksSupported = isUnix || + isWindows && OS_VERSION.compareTo("6.0") >= 0 && isJavaVersionAtLeast("1.7"); public static final boolean is32Bit = ARCH_DATA_MODEL == null || ARCH_DATA_MODEL.equals("32"); public static final boolean is64Bit = !is32Bit; public static final boolean isAMD64 = "amd64".equals(OS_ARCH); - public static final boolean isMacIntel64 = isMac && "x86_64".equals(OS_ARCH); public static final String nativeFileManagerName = isMac ? "Finder" : isGnome ? "Nautilus" : isKDE ? "Konqueror" : "Explorer"; diff --git a/platform/util/src/com/intellij/openapi/util/io/FileUtil.java b/platform/util/src/com/intellij/openapi/util/io/FileUtil.java index 0bad8df44f64..562fa027e7b2 100644 --- a/platform/util/src/com/intellij/openapi/util/io/FileUtil.java +++ b/platform/util/src/com/intellij/openapi/util/io/FileUtil.java @@ -42,8 +42,10 @@ import java.util.regex.Pattern; @SuppressWarnings({"UtilityClassWithoutPrivateConstructor"}) public class FileUtil { public static final int MEGABYTE = 1024 * 1024; + public static final String ASYNC_DELETE_EXTENSION = ".__del__"; private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.io.FileUtil"); + private static final ThreadLocal BUFFER = new ThreadLocal() { protected byte[] initialValue() { return new byte[1024 * 20]; @@ -51,9 +53,9 @@ public class FileUtil { }; // do not use channels to copy files larger than 5 Mb because of possible MapFailed error - private static final long CHANNELS_COPYING_LIMIT = 5L * 1024L * 1024L; + private static final long CHANNELS_COPYING_LIMIT = 5L * MEGABYTE; private static String ourCanonicalTempPathCache = null; - public static final String ASYNC_DELETE_EXTENSION = ".__del__"; + private static final int MAX_FILE_DELETE_ATTEMPTS = 10; @Nullable public static String getRelativePath(File base, File file) { @@ -541,21 +543,22 @@ public class FileUtil { } public static boolean delete(@NotNull File file) { - File[] files = file.listFiles(); - if (files != null) { - for (File file1 : files) { - if (!delete(file1)) return false; + if (!SymLinkUtil.isSymLink(file)) { + File[] files = file.listFiles(); + if (files != null) { + for (File child : files) { + if (!delete(child)) return false; + } } } - for (int i = 0; i < 10; i++) { + for (int i = 0; i < MAX_FILE_DELETE_ATTEMPTS; i++) { if (file.delete() || !file.exists()) return true; try { + //noinspection BusyWait Thread.sleep(10); } - catch (InterruptedException ignored) { - - } + catch (InterruptedException ignored) { } } return false; } diff --git a/platform/util/src/com/intellij/openapi/util/io/SymLinkUtil.java b/platform/util/src/com/intellij/openapi/util/io/SymLinkUtil.java new file mode 100644 index 000000000000..17829dda6468 --- /dev/null +++ b/platform/util/src/com/intellij/openapi/util/io/SymLinkUtil.java @@ -0,0 +1,320 @@ +/* + * Copyright 2000-2011 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.openapi.diagnostic.Logger; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.util.ArrayUtil; +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 java.io.File; +import java.lang.reflect.Array; +import java.lang.reflect.Method; + +// todo[r.sh] use NIO2 API after migration to JDK 7 +public class SymLinkUtil { + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.io.SymLinkUtil"); + + @Nullable + private static final Mediator ourMediator; + + static { + Mediator mediator = null; + if (SystemInfo.areSymLinksSupported) { + if (SystemInfo.isJavaVersionAtLeast("1.7")) { + try { + mediator = new Jdk7MediatorImpl(); + mediator.isSymLink("/"); // quick test + } + catch (Throwable t) { + LOG.error(t); + mediator = null; + } + } + if (mediator == null) { + if (SystemInfo.isLinux || SystemInfo.isMac || SystemInfo.isSolaris) { + try { + mediator = new JnaUnixMediatorImpl(); + mediator.isSymLink("/"); // quick test + } + catch (Throwable t) { + LOG.error(t); + mediator = null; + } + } + /*else if (SystemInfo.isWindows) { + try { + mediator = new JnaWindowsMediatorImpl(); + mediator.isSymLink("/"); // quick test + } + catch (Throwable t) { + LOG.error(t); + mediator = null; + } + }*/ + } + } + ourMediator = mediator; + } + + private SymLinkUtil() { } + + public static boolean isSymLink(@NotNull final File file) { + return isSymLink(file.getAbsolutePath()); + } + + public static boolean isSymLink(@NotNull final String path) { + try { + return ourMediator != null && ourMediator.isSymLink(path); + } + catch (Exception e) { + LOG.warn(e); + return false; + } + } + + @Nullable + public static String resolveSymLink(@NotNull final File file) { + return resolveSymLink(file.getAbsolutePath()); + } + + @Nullable + public static String resolveSymLink(@NotNull final String path) { + if (ourMediator != null) { + try { + final String realPath = ourMediator.resolveSymLink(path); + if (realPath != null && new File(realPath).exists()) { + return realPath; + } + } + catch (Exception e) { + LOG.warn(e); + } + } + return null; + } + + private interface Mediator { + boolean isSymLink(@NotNull final String path) throws Exception; + + @Nullable + String resolveSymLink(@NotNull final String path) throws Exception; + } + + private static class Jdk7MediatorImpl implements Mediator { + private final Object myDefaultFileSystem; + private final Method myGetPath; + private final Method myIsSymbolicLink; + private final Object myLinkOptions; + + private Jdk7MediatorImpl() throws Exception { + myDefaultFileSystem = Class.forName("java.nio.file.FileSystems").getMethod("getDefault").invoke(null); + myGetPath = Class.forName("java.nio.file.FileSystem").getMethod("getPath", String.class, String[].class); + myGetPath.setAccessible(true); + myIsSymbolicLink = Class.forName("java.nio.file.Files").getMethod("isSymbolicLink", Class.forName("java.nio.file.Path")); + myIsSymbolicLink.setAccessible(true); + myLinkOptions = Array.newInstance(Class.forName("java.nio.file.LinkOption"), 0); + } + + @Override + public boolean isSymLink(@NotNull final String path) throws Exception { + final Object pathObj = myGetPath.invoke(myDefaultFileSystem, path, ArrayUtil.EMPTY_STRING_ARRAY); + return (Boolean)myIsSymbolicLink.invoke(null, pathObj); + } + + @Override + public String resolveSymLink(@NotNull final String path) throws Exception { + 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(); + } + } + + // thanks to SVNKit for the idea + @SuppressWarnings("OctalInteger") + private static class JnaUnixMediatorImpl implements Mediator { + private interface LibC extends Library { + int S_MASK = 0177777; + int S_IFLNK = 0120000; + + int lstat(String path, Pointer stat); + int __lxstat64(int ver, String path, Pointer stat); + } + + private final LibC myLibC; + private final Memory mySharedMem; + private final int myOffset; + + private JnaUnixMediatorImpl() throws Exception { + myLibC = (LibC)Native.loadLibrary("c", LibC.class); + mySharedMem = new Memory(512); + myOffset = SystemInfo.isLinux ? (SystemInfo.is32Bit ? 16 : 24) : + SystemInfo.isMac ? 8 : + SystemInfo.isSolaris ? (SystemInfo.is32Bit ? 20 : 16) : + -1; + if (myOffset < 0) throw new IllegalStateException("Unsupported OS: " + SystemInfo.OS_NAME); + } + + @Override + public synchronized boolean isSymLink(@NotNull final String path) throws Exception { + mySharedMem.clear(); + final int res = SystemInfo.isLinux ? myLibC.__lxstat64(0, path, mySharedMem) : myLibC.lstat(path, mySharedMem); + if (res == 0) { + final int mode = (SystemInfo.isLinux ? mySharedMem.getInt(myOffset) : mySharedMem.getShort(myOffset)) & LibC.S_MASK; + return (mode & LibC.S_IFLNK) == LibC.S_IFLNK; + } + else { + LOG.debug("lstat(" + path + "): " + res); + return false; + } + } + + @Override + public String resolveSymLink(@NotNull final String path) throws Exception { + return new File(path).getCanonicalPath(); + } + } + + /*private static class JnaWindowsMediatorImpl implements Mediator { + private interface Kernel32 extends StdCallLibrary { + int IO_REPARSE_TAG_SYMLINK = 0xA000000C; + int FILE_ACCESS_FLAGS = 0x0080; + int FILE_SHARE_FLAGS = 0x00000001 | 0x00000002 | 0x00000004; + int OPEN_EXISTING = 3; + int FILE_OPEN_FLAGS = 0x02000000 | 0x00200000; + int FSCTL_GET_REPARSE_POINT = 0x000900A8; + int SYMLINK_FLAG_RELATIVE = 0x00000001; + + @SuppressWarnings({"UnusedDeclaration", "MultipleVariablesInDeclaration"}) + class Win32FindData extends Structure implements Structure.ByReference { + public int dwFileAttributes; + public int ftCreationTimeL, ftCreationTimeH; + public int ftLastAccessTimeL, ftLastAccessTimeH; + public int ftLastWriteTimeL, ftLastWriteTimeH; + public int lFileSizeH, lFileSizeL; + public int dwReserved0; + public int dwReserved1; + public char[] cFileName = new char[260]; + public char[] cAlternateFileName = new char[14]; + } + + int MAX_SUPPORTED_TARGET_LENGTH = 4 * 1024; + @SuppressWarnings({"UnusedDeclaration", "MultipleVariablesInDeclaration"}) + class ReparseDataBuffer extends Structure implements Structure.ByReference { + public NativeLong ReparseTag; + public short ReparseDataLength; + public short Reserved; + public short SubstituteNameOffset, SubstituteNameLength; + public short PrintNameOffset, PrintNameLength; + public NativeLong Flags; + public char[] PathBuffer = new char[MAX_SUPPORTED_TARGET_LENGTH]; + } + + Pointer INVALID_HANDLE = Pointer.createConstant(-1); + + Pointer FindFirstFile(String lpFileName, Win32FindData lpFindFileData); + + boolean FindClose(Pointer hFindFile); + + Pointer CreateFile(String lpFileName, + int dwDesiredAccess, + int dwShareMode, + @Nullable Pointer lpSecurityAttributes, + int dwCreationDisposition, + int dwFlagsAndAttributes, + @Nullable Pointer hTemplateFile); + + boolean CloseHandle(Pointer hObject); + + boolean DeviceIoControl(Pointer hDevice, + int dwIoControlCode, + @Nullable Structure.ByReference lpInBuffer, + int nInBufferSize, + @Nullable Structure.ByReference lpOutBuffer, + int nOutBufferSize, + IntByReference lpBytesReturned, + @Nullable Pointer lpOverlapped); + } + + private final Kernel32 myKernel32; + private final Kernel32.Win32FindData myFindData; + private final Kernel32.ReparseDataBuffer myReparseData; + + private JnaWindowsMediatorImpl() throws Exception { + myKernel32 = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class, W32APIOptions.UNICODE_OPTIONS); + myFindData = new Kernel32.Win32FindData(); + myReparseData = new Kernel32.ReparseDataBuffer(); + } + + @SuppressWarnings("NonPrivateFieldAccessedInSynchronizedContext") + @Override + public synchronized boolean isSymLink(@NotNull final String path) throws Exception { + synchronized (myFindData) { + myFindData.dwReserved0 = 0; + final Pointer handle = myKernel32.FindFirstFile(path, myFindData); + if (Kernel32.INVALID_HANDLE.equals(handle)) { + LOG.debug("FindFirstFile(" + path + "): " + handle); + return false; + } + myKernel32.FindClose(handle); + return (myFindData.dwReserved0 & Kernel32.IO_REPARSE_TAG_SYMLINK) == Kernel32.IO_REPARSE_TAG_SYMLINK; + } + } + + @SuppressWarnings("NonPrivateFieldAccessedInSynchronizedContext") + @Override + public String resolveSymLink(@NotNull final String path) throws Exception { + final Pointer handle = myKernel32.CreateFile(path, Kernel32.FILE_ACCESS_FLAGS, Kernel32.FILE_SHARE_FLAGS, null, + Kernel32.OPEN_EXISTING, Kernel32.FILE_OPEN_FLAGS, null); + if (Kernel32.INVALID_HANDLE.equals(handle)) { + LOG.debug("CreateFile(" + path + "): " + handle); + return null; + } + synchronized (myReparseData) { + try { + myReparseData.ReparseTag.setValue(0); + myReparseData.SubstituteNameOffset = myReparseData.SubstituteNameLength = 0; + myReparseData.Flags.setValue(0); + final boolean result = myKernel32.DeviceIoControl(handle, Kernel32.FSCTL_GET_REPARSE_POINT, null, 0, + myReparseData, myReparseData.size(), new IntByReference(), null); + if (!result || myReparseData.ReparseTag.intValue() != Kernel32.IO_REPARSE_TAG_SYMLINK) { + LOG.debug("DeviceIoControl(" + path + "): " + result + "," + myReparseData.ReparseTag); + return null; + } + String target = new String(myReparseData.PathBuffer, myReparseData.SubstituteNameOffset / 2, myReparseData.SubstituteNameLength / 2); + if ((myReparseData.Flags.intValue() & Kernel32.SYMLINK_FLAG_RELATIVE) == Kernel32.SYMLINK_FLAG_RELATIVE) { + return new File(new File(path).getParent(), target).getCanonicalPath(); + } + else { + if (target.startsWith("\\??\\") || target.startsWith("\\\\?\\")) { + target = target.substring(4); + } + return new File(target).getCanonicalPath(); + } + } + finally { + myKernel32.CloseHandle(handle); + } + } + } + }*/ +}