# HG changeset patch # Parent a513bc5a1dffe92f1be89ae030638e2ad0f01fc5 #237882: Provide API for detection and handling of symlinks diff --git a/masterfs/manifest.mf b/masterfs/manifest.mf --- a/masterfs/manifest.mf +++ b/masterfs/manifest.mf @@ -1,7 +1,7 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.masterfs/2 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/masterfs/resources/Bundle.properties -OpenIDE-Module-Specification-Version: 2.45 +OpenIDE-Module-Specification-Version: 2.46 OpenIDE-Module-Recommends: org.netbeans.modules.masterfs.providers.Notifier OpenIDE-Module-Provides: org.openide.filesystems.FileUtil.toFileObject AutoUpdate-Show-In-Client: false diff --git a/masterfs/nbproject/project.xml b/masterfs/nbproject/project.xml --- a/masterfs/nbproject/project.xml +++ b/masterfs/nbproject/project.xml @@ -63,7 +63,7 @@ - 8.0 + 8.10 diff --git a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/BaseFileObj.java b/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/BaseFileObj.java --- a/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/BaseFileObj.java +++ b/masterfs/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/BaseFileObj.java @@ -59,6 +59,12 @@ import javax.swing.event.EventListenerList; import java.io.*; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Date; @@ -1061,6 +1067,86 @@ FolderObj getExistingParent() { return getExistingParentFor(getFileName().getFile(), getFactory()); } + + /** + * Get {@link Path} object for this BaseFileObj. + */ + private Path getNativePath() throws IOException { + File file = getFileName().getFile(); + final Path path; + try { + path = file.toPath(); + } catch (RuntimeException e) { + throw new IOException("Cannot get Path for " + this, e); //NOI18N + } + return path; + } + + @Override + public boolean isSymbolicLink() throws IOException { + Path p = getNativePath(); + return Files.isSymbolicLink(p); + } + + @Override + public FileObject readSymbolicLink() throws IOException { + final Path path = getNativePath(); + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction() { + + @Override + public FileObject run() throws Exception { + Path target = Files.readSymbolicLink(path); + Path absoluteTarget = target.isAbsolute() + ? target + : path.getParent().resolve(target); + File file = absoluteTarget.toFile(); + File normFile = FileUtil.normalizeFile(file); + return FileBasedFileSystem.getFileObject(normFile); + } + }); + } catch (PrivilegedActionException ex) { + throw new IOException(ex); + } + } + + @Override + public String readSymbolicLinkPath() throws IOException { + final Path path = getNativePath(); + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction() { + + @Override + public String run() throws Exception { + Path target = Files.readSymbolicLink(path); + return target.toString(); + } + }); + } catch (PrivilegedActionException ex) { + throw new IOException(ex); + } + } + + @Override + public FileObject getRealFileObject() throws IOException { + final Path path = getNativePath(); + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction() { + + @Override + public FileObject run() throws Exception { + Path realPath = path.toRealPath(); + File realFile = realPath.toFile(); + return FileBasedFileSystem.getFileObject(realFile); + } + }); + } catch (PrivilegedActionException ex) { + throw new IOException(ex); + } + } private static class FileEventImpl extends FileEvent implements Enumeration { static { diff --git a/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FileObjSymlinkTest.java b/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FileObjSymlinkTest.java new file mode 100644 --- /dev/null +++ b/masterfs/test/unit/src/org/netbeans/modules/masterfs/filebasedfs/fileobjects/FileObjSymlinkTest.java @@ -0,0 +1,285 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.masterfs.filebasedfs.fileobjects; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.netbeans.junit.NbTestCase; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; + +/** + * Test support for detection and reading of symbolic links. + * + * @author jhavlin + */ +public class FileObjSymlinkTest extends NbTestCase { + + public FileObjSymlinkTest(String name) { + super(name); + } + + @Override + protected void setUp() throws Exception { + deleteSymlinks(getWorkDir()); + clearWorkDir(); + } + + public void testIsSymbolicLink() throws IOException { + if (!checkSymlinksSupported()) { + return; + } + File dir = getWorkDir(); + Path p = dir.toPath(); + Path data = p.resolve("data.dat"); + Path link = p.resolve("link.lk"); + data.toFile().createNewFile(); + Files.createSymbolicLink(link, data); + FileObject dirFO = FileUtil.toFileObject(dir); + dirFO.refresh(); + FileObject dataFO = dirFO.getFileObject("data.dat"); + FileObject linkFO = dirFO.getFileObject("link.lk"); + assertFalse(dataFO.isSymbolicLink()); + assertTrue(linkFO.isSymbolicLink()); + } + + /** + * Test isRecursiveSymbolicLink method. Use this folder tree: + *
+     * - workdir
+     *   - a
+     *     - b
+     *       - c (symlink to a)
+     *       - d (symlink to e)
+     *   - e
+     * 
+ * + * @throws java.io.IOException + */ + public void testIsRecursiveSymbolicLink() throws IOException { + if (!checkSymlinksSupported()) { + return; + } + File dir = getWorkDir(); + File a = new File(dir, "a"); + File b = new File(a, "b"); + File c = new File(b, "c"); + File d = new File(b, "d"); + File e = new File(dir, "e"); + b.mkdirs(); + e.mkdirs(); + Files.createSymbolicLink(c.toPath(), a.toPath()); + Files.createSymbolicLink(d.toPath(), e.toPath()); + FileObject dirFO = FileUtil.toFileObject(dir); + dirFO.refresh(); + FileObject cFO = dirFO.getFileObject("a/b/c"); + FileObject dFO = dirFO.getFileObject("a/b/d"); + assertTrue(cFO.isRecursiveSymbolicLink()); + assertFalse(dFO.isRecursiveSymbolicLink()); + assertFalse(dirFO.isRecursiveSymbolicLink()); + } + + /** + * Test isRecursiveSymbolicLink method. Use this folder tree: + *
+     * - workdir
+     *   - a
+     *     - b
+     *       - c (symlink to d)
+     *   - d (symlink to a)
+     * 
+ * + * @throws java.io.IOException + */ + public void testIsRecursiveSymbolicLinkIndirect() throws IOException { + if (!checkSymlinksSupported()) { + return; + } + File dir = getWorkDir(); + File a = new File(dir, "a"); + File b = new File(a, "b"); + File c = new File(b, "c"); + File d = new File(dir, "d"); + b.mkdirs(); + Files.createSymbolicLink(d.toPath(), a.toPath()); + Files.createSymbolicLink(c.toPath(), d.toPath()); + FileObject dirFO = FileUtil.toFileObject(dir); + FileObject cFO = dirFO.getFileObject("a/b/c"); + assertTrue(cFO.isRecursiveSymbolicLink()); + } + + public void testReadSymbolicLinkAbsolute() throws IOException { + if (!checkSymlinksSupported()) { + return; + } + File dir = getWorkDir(); + File data = new File(dir, "data.dat"); + File link = new File(dir, "link.lnk"); + data.createNewFile(); + Files.createSymbolicLink(link.toPath(), data.toPath()); + FileObject linkFO = FileUtil.toFileObject(link); + FileObject dataFO = linkFO.readSymbolicLink(); + assertNotSame(linkFO, dataFO); + assertNotNull(dataFO); + assertEquals("data.dat", dataFO.getNameExt()); + } + + public void testReadSymbolicLinkRelative() throws IOException { + if (!checkSymlinksSupported()) { + return; + } + File dir = getWorkDir(); + File folder = new File(dir, "folder"); + File link = new File(dir, "link"); + folder.mkdir(); + Path lp = Files.createSymbolicLink(link.toPath(), Paths.get("folder")); + assertNotNull(lp); + FileObject linkFO = FileUtil.toFileObject(link); + assertNotNull(linkFO); + FileObject dataFO = linkFO.readSymbolicLink(); + assertNotSame(linkFO, dataFO); + assertNotNull(dataFO); + assertEquals("folder", dataFO.getNameExt()); + } + + public void testReadSymbolicLinkPath() throws IOException { + if (!checkSymlinksSupported()) { + return; + } + File dir = getWorkDir(); + File data = new File(dir, "data.dat"); + File link = new File(dir, "link.lnk"); + data.createNewFile(); + Files.createSymbolicLink(link.toPath(), data.toPath()); + FileObject linkFO = FileUtil.toFileObject(link); + assertTrue(linkFO.readSymbolicLinkPath().endsWith("data.dat")); + } + + /** + * Test getRealFileObject method. + * + * Use this directory tree: + *
+     * - workdir
+     *   - a
+     *     - data.dat
+     *   - b
+     *     - link.lnk (symlink to data.dat)
+     *   - c
+     *     - link2.lnk (symlink to link.link)
+     *   - d
+     *     - folderLink (symlink to c)
+     * 
+ * @throws java.io.IOException + */ + public void testGetRealFileObject() throws IOException { + if (!checkSymlinksSupported()) { + return; + } + File dir = getWorkDir(); + File a = new File(dir, "a"); + File dataDat = new File(a, "data.dat"); + File b = new File(dir, "b"); + File linkLnk = new File(b, "link.lnk"); + File c = new File(dir, "c"); + File link2Lnk = new File(c, "link2.lnk"); + File d = new File(dir, "d"); + File folderLink = new File(d, "folderLink"); + a.mkdir(); + dataDat.createNewFile(); + b.mkdir(); + Files.createSymbolicLink(linkLnk.toPath(), dataDat.toPath()); + c.mkdir(); + Files.createSymbolicLink(link2Lnk.toPath(), linkLnk.toPath()); + d.mkdir(); + Files.createSymbolicLink(folderLink.toPath(), c.toPath()); + FileObject dirFO = FileUtil.toFileObject(dir); + dirFO.refresh(); + FileObject fo = dirFO.getFileObject("d/folderLink/link2.lnk"); + assertNotNull(fo); + FileObject realFO = fo.getRealFileObject(); + assertNotNull(realFO); + assertTrue(realFO.getPath().endsWith("a/data.dat")); + } + + /** + * Recursively delete all symlinks in a directory. + */ + private void deleteSymlinks(File root) { + for (File f : root.listFiles()) { + if (Files.isSymbolicLink(f.toPath())) { + f.delete(); + } else if (f.isDirectory()) { + deleteSymlinks(f); + } + } + } + + private boolean checkSymlinksSupported() { + File dir; + try { + dir = getWorkDir(); + } catch (IOException ex) { + printSkipping(); + return false; + } + File a = new File(dir, "a"); + try { + File lk = new File(dir, "lk"); + Files.createSymbolicLink(lk.toPath(), a.toPath()); + lk.delete(); + return true; + } catch (RuntimeException e) { + } catch (IOException ex) { + } + printSkipping(); + return false; + } + + private void printSkipping() { + System.out.println( + "Symbolic links are not supported, skipping test " + getName()); + } +} diff --git a/openide.filesystems/apichanges.xml b/openide.filesystems/apichanges.xml --- a/openide.filesystems/apichanges.xml +++ b/openide.filesystems/apichanges.xml @@ -49,6 +49,28 @@ Filesystems API + + + Support detection and reading of symbolic links. + + + + + +

+ Add methods to FileObject for handling of symbolic links: +

+
    +
  • isSymbolicLink()
  • +
  • isRecursiveSymbolicLink()
  • +
  • readSymbolicLink()
  • +
  • readSymbolicLinkPath()
  • +
  • getRealFileObject()
  • +
+
+ + +
Allowed to reveal deleted files, or original files overriden by writable layer diff --git a/openide.filesystems/manifest.mf b/openide.filesystems/manifest.mf --- a/openide.filesystems/manifest.mf +++ b/openide.filesystems/manifest.mf @@ -2,5 +2,5 @@ OpenIDE-Module: org.openide.filesystems OpenIDE-Module-Localizing-Bundle: org/openide/filesystems/Bundle.properties OpenIDE-Module-Layer: org/openide/filesystems/resources/layer.xml -OpenIDE-Module-Specification-Version: 8.9 +OpenIDE-Module-Specification-Version: 8.10 diff --git a/openide.filesystems/src/org/openide/filesystems/FileObject.java b/openide.filesystems/src/org/openide/filesystems/FileObject.java --- a/openide.filesystems/src/org/openide/filesystems/FileObject.java +++ b/openide.filesystems/src/org/openide/filesystems/FileObject.java @@ -65,6 +65,7 @@ import java.util.concurrent.Callable; import java.util.logging.Level; import org.openide.util.Enumerations; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.openide.util.Lookup; import org.openide.util.Lookup.Result; @@ -903,8 +904,12 @@ public FileObject process(FileObject fo, Collection toAdd) { if (rec && fo.isFolder()) { for (FileObject child : fo.getChildren()) { - if (!FileUtil.isRecursiveSymlink(child)) { // #218795 - toAdd.add(child); + try { + if (!child.isRecursiveSymbolicLink()) { // #218795 + toAdd.add(child); + } + } catch (IOException ex) { + ExternalUtil.LOG.log(Level.INFO, null, ex); } } } @@ -1254,6 +1259,105 @@ return false; } + /** + * Check whether this FileObject is a symbolic link. + * + * The default implementation returns false, but on filesystems that support + * symbolic links this method should be overriden. + * + * @return True if this FileObject represents a symbolic link, false + * otherwise. + * @throws java.io.IOException If some I/O problem occurs. + * @since openide.filesystem/8.10 + */ + public boolean isSymbolicLink() throws IOException { + return false; + } + + /** + * Check whether this FileObject is a recursive symbolic link. + * + * Filesystem implementations can override this method for better + * performance. + * + * @return True if this FileObject represents a symbolic link referring to a + * directory that is parent of this FileObject, false otherwise. + * @throws java.io.IOException If some I/O problem occurs. + * @since openide.filesystem/8.10 + */ + public boolean isRecursiveSymbolicLink() throws IOException { + if (isSymbolicLink()) { + FileObject parent = getParent(); + if (parent == null) { + return false; + } + FileObject target = readSymbolicLink(); + if (target != null) { + FileObject realParent = parent.getRealFileObject(); + FileObject realTarget = target.getRealFileObject(); + return realParent != null && realTarget != null + && (realTarget.equals(realParent) + || FileUtil.isParentOf(realTarget, realParent)); + } else { + return false; + } + } else { + return false; + } + } + + /** + * Read symbolic link. + * + * If this FileObject represents a symbolic link, return a FileObject it + * refers to, or null if the referred file or directory does not exist. If + * this FileObject doesn't represent a symbolic link, the return value is + * undefined. + * + * The default implementation returns null, but on filesystems that support + * symbolic links this method should be overriden. + * + * @return The referred FileObject, or null if it is not available. + * @throws java.io.IOException If some I/O problem occurs. + * @since openide.filesystem/8.10 + */ + public FileObject readSymbolicLink() throws IOException { + return null; + } + + /** + * Read symbolic link path. + * + * If this FileObject represents a symbolic link, return the path it refers + * to, which can be absolute or relative. If this FileObject doesn't + * represent a symbolic link, the return value is undefined. + * + * The default implementation returns this FileObject's path, but on + * filesystems that support symbolic links this method should be overriden. + * + * @return The referred FileObject path. + * @throws java.io.IOException If some I/O problem occurs. + * @since openide.filesystem/8.10 + */ + public String readSymbolicLinkPath() throws IOException { + return this.getPath(); + } + + /** + * Return a FileObject with path where all symbolic links are resolved. + * + * The default implementation returns this object, but on filesystems that + * support symbolic links this method should be overriden. + * + * @return The FileObject with path where all symlinks are resolved, or null + * if it doesn't exist. + * @throws java.io.IOException If some I/O problem occurs. + * @since openide.filesystem/8.10 + */ + public FileObject getRealFileObject() throws IOException { + return this; + } + /** Listeners registered from MultiFileObject are considered as priority * listeners. */ diff --git a/openide.filesystems/src/org/openide/filesystems/FileUtil.java b/openide.filesystems/src/org/openide/filesystems/FileUtil.java --- a/openide.filesystems/src/org/openide/filesystems/FileUtil.java +++ b/openide.filesystems/src/org/openide/filesystems/FileUtil.java @@ -58,11 +58,6 @@ import java.net.URI; import java.net.URL; import java.net.URLStreamHandler; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -2332,57 +2327,4 @@ int index = path.lastIndexOf('.'); //NOI18N return (index != -1) && (index > path.lastIndexOf('/') + 1); //NOI18N } - - /** - * Check whether this file is a symbolic link that targets to a folder - * higher in the path. Such link, in case of recursive folder traversing, - * would cause infinite recursion. The method should not be called from - * Event Dispatch Thread. - * - * @param fileObject FileObject to check. - * @return True if the file is a link that could cause infinite recursion, - * false otherwise. - */ - static boolean isRecursiveSymlink(FileObject fileObject) { - File file = (File) fileObject.getAttribute("java.io.File"); // NOI18N - if (file == null) { - return false; - } - final Path path; - try { - path = file.toPath(); - } catch (RuntimeException e) { - LOG.log(Level.INFO, "Cannot get path for {0}", file); //NOI18N - LOG.log(Level.FINE, null, e); - return false; - } - if (Files.isSymbolicLink(path)) { - try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public Boolean run () throws IOException { - Path target = Files.readSymbolicLink(path); - if (!target.isAbsolute()) { - target = path.resolve(target); - } - target = target.toRealPath(); - for (Path ancestor = path.getParent(); - ancestor != null; ancestor = ancestor.getParent()) { - if (target.equals(ancestor)) { - return true; - } - } - return false; - } - }); - } catch (PrivilegedActionException ex) { - LOG.log(Level.INFO, "Cannot read symbolic link {0}",//NOI18N - path); - LOG.log(Level.FINE, null, ex.getException()); - } catch (SecurityException ex) { - LOG.log(Level.INFO, null, ex); - } - } - return false; - } }