6595866: File does work with symbolic links (win,vista)
authoralanb
Thu, 20 Aug 2009 08:39:18 +0100
changeset 3627 d0ad40d5adab
parent 3626 78722c321f85
child 3628 2768d95a0e7d
6595866: File does work with symbolic links (win,vista) Reviewed-by: sherman
jdk/src/windows/native/java/io/WinNTFileSystem_md.c
jdk/test/java/io/File/SymLinks.java
--- a/jdk/src/windows/native/java/io/WinNTFileSystem_md.c	Thu Aug 20 11:24:42 2009 +0800
+++ b/jdk/src/windows/native/java/io/WinNTFileSystem_md.c	Thu Aug 20 08:39:18 2009 +0100
@@ -51,13 +51,25 @@
     jfieldID path;
 } ids;
 
+/**
+ * GetFinalPathNameByHandle is available on Windows Vista and newer
+ */
+typedef BOOL (WINAPI* GetFinalPathNameByHandleProc) (HANDLE, LPWSTR, DWORD, DWORD);
+static GetFinalPathNameByHandleProc GetFinalPathNameByHandle_func;
+
 JNIEXPORT void JNICALL
 Java_java_io_WinNTFileSystem_initIDs(JNIEnv *env, jclass cls)
 {
+    HANDLE handle;
     jclass fileClass = (*env)->FindClass(env, "java/io/File");
     if (!fileClass) return;
     ids.path =
              (*env)->GetFieldID(env, fileClass, "path", "Ljava/lang/String;");
+    handle = LoadLibrary("kernel32");
+    if (handle != NULL) {
+        GetFinalPathNameByHandle_func = (GetFinalPathNameByHandleProc)
+            GetProcAddress(handle, "GetFinalPathNameByHandleW");
+    }
 }
 
 /* -- Path operations -- */
@@ -65,6 +77,138 @@
 extern int wcanonicalize(const WCHAR *path, WCHAR *out, int len);
 extern int wcanonicalizeWithPrefix(const WCHAR *canonicalPrefix, const WCHAR *pathWithCanonicalPrefix, WCHAR *out, int len);
 
+/**
+ * Retrieves the fully resolved (final) path for the given path or NULL
+ * if the function fails.
+ */
+static WCHAR* getFinalPath(const WCHAR *path)
+{
+    HANDLE h;
+    WCHAR *result;
+    DWORD error;
+
+    /* Need Windows Vista or newer to get the final path */
+    if (GetFinalPathNameByHandle_func == NULL)
+        return NULL;
+
+    h = CreateFileW(path,
+                    FILE_READ_ATTRIBUTES,
+                    FILE_SHARE_DELETE |
+                        FILE_SHARE_READ | FILE_SHARE_WRITE,
+                    NULL,
+                    OPEN_EXISTING,
+                    FILE_FLAG_BACKUP_SEMANTICS,
+                    NULL);
+    if (h == INVALID_HANDLE_VALUE)
+        return NULL;
+
+    /**
+     * Allocate a buffer for the resolved path. For a long path we may need
+     * to allocate a larger buffer.
+     */
+    result = (WCHAR*)malloc(MAX_PATH * sizeof(WCHAR));
+    if (result != NULL) {
+        DWORD len = (*GetFinalPathNameByHandle_func)(h, result, MAX_PATH, 0);
+        if (len >= MAX_PATH) {
+            /* retry with a buffer of the right size */
+            result = (WCHAR*)realloc(result, (len+1) * sizeof(WCHAR));
+            if (result != NULL) {
+                len = (*GetFinalPathNameByHandle_func)(h, result, len, 0);
+            } else {
+                len = 0;
+            }
+        }
+        if (len > 0) {
+            /**
+             * Strip prefix (should be \\?\ or \\?\UNC)
+             */
+            if (result[0] == L'\\' && result[1] == L'\\' &&
+                result[2] == L'?' && result[3] == L'\\')
+            {
+                int isUnc = (result[4] == L'U' &&
+                             result[5] == L'N' &&
+                             result[6] == L'C');
+                int prefixLen = (isUnc) ? 7 : 4;
+                /* actual result length (includes terminator) */
+                int resultLen = len - prefixLen + (isUnc ? 1 : 0) + 1;
+
+                /* copy result without prefix into new buffer */
+                WCHAR *tmp = (WCHAR*)malloc(resultLen * sizeof(WCHAR));
+                if (tmp == NULL) {
+                    len = 0;
+                } else {
+                    WCHAR *p = result;
+                    p += prefixLen;
+                    if (isUnc) {
+                        WCHAR *p2 = tmp;
+                        p2[0] = L'\\';
+                        p2++;
+                        wcscpy(p2, p);
+                    } else {
+                        wcscpy(tmp, p);
+                    }
+                    free(result);
+                    result = tmp;
+                }
+            }
+        }
+
+        /* unable to get final path */
+        if (len == 0 && result != NULL) {
+            free(result);
+            result = NULL;
+        }
+    }
+
+    error = GetLastError();
+    if (CloseHandle(h))
+        SetLastError(error);
+    return result;
+}
+
+/**
+ * Retrieves file information for the specified file. If the file is
+ * symbolic link then the information on fully resolved target is
+ * returned.
+ */
+static BOOL getFileInformation(const WCHAR *path,
+                               BY_HANDLE_FILE_INFORMATION *finfo)
+{
+    BOOL result;
+    DWORD error;
+    HANDLE h = CreateFileW(path,
+                           FILE_READ_ATTRIBUTES,
+                           FILE_SHARE_DELETE |
+                               FILE_SHARE_READ | FILE_SHARE_WRITE,
+                           NULL,
+                           OPEN_EXISTING,
+                           FILE_FLAG_BACKUP_SEMANTICS,
+                           NULL);
+    if (h == INVALID_HANDLE_VALUE)
+        return FALSE;
+    result = GetFileInformationByHandle(h, finfo);
+    error = GetLastError();
+    if (CloseHandle(h))
+        SetLastError(error);
+    return result;
+}
+
+/**
+ * If the given attributes are the attributes of a reparse point, then
+ * read and return the attributes of the final target.
+ */
+DWORD getFinalAttributesIfReparsePoint(WCHAR *path, DWORD a)
+{
+    if ((a != INVALID_FILE_ATTRIBUTES) &&
+        ((a & FILE_ATTRIBUTE_REPARSE_POINT) != 0))
+    {
+        BY_HANDLE_FILE_INFORMATION finfo;
+        BOOL res = getFileInformation(path, &finfo);
+        a = (res) ? finfo.dwFileAttributes : INVALID_FILE_ATTRIBUTES;
+    }
+    return a;
+}
+
 JNIEXPORT jstring JNICALL
 Java_java_io_WinNTFileSystem_canonicalize0(JNIEnv *env, jobject this,
                                            jstring pathname)
@@ -202,12 +346,15 @@
         return rv;
     if (!isReservedDeviceNameW(pathbuf)) {
         if (GetFileAttributesExW(pathbuf, GetFileExInfoStandard, &wfad)) {
-            rv = (java_io_FileSystem_BA_EXISTS
-                  | ((wfad.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
-                     ? java_io_FileSystem_BA_DIRECTORY
-                     : java_io_FileSystem_BA_REGULAR)
-                  | ((wfad.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
-                     ? java_io_FileSystem_BA_HIDDEN : 0));
+            DWORD a = getFinalAttributesIfReparsePoint(pathbuf, wfad.dwFileAttributes);
+            if (a != INVALID_FILE_ATTRIBUTES) {
+                rv = (java_io_FileSystem_BA_EXISTS
+                    | ((a & FILE_ATTRIBUTE_DIRECTORY)
+                        ? java_io_FileSystem_BA_DIRECTORY
+                        : java_io_FileSystem_BA_REGULAR)
+                    | ((a & FILE_ATTRIBUTE_HIDDEN)
+                        ? java_io_FileSystem_BA_HIDDEN : 0));
+            }
         } else { /* pagefile.sys is a special case */
             if (GetLastError() == ERROR_SHARING_VIOLATION) {
                 rv = java_io_FileSystem_BA_EXISTS;
@@ -234,6 +381,7 @@
     if (pathbuf == NULL)
         return JNI_FALSE;
     attr = GetFileAttributesW(pathbuf);
+    attr = getFinalAttributesIfReparsePoint(pathbuf, attr);
     free(pathbuf);
     if (attr == INVALID_FILE_ATTRIBUTES)
         return JNI_FALSE;
@@ -272,6 +420,20 @@
     if (pathbuf == NULL)
         return JNI_FALSE;
     a = GetFileAttributesW(pathbuf);
+
+    /* if reparse point, get final target */
+    if ((a != INVALID_FILE_ATTRIBUTES) &&
+        ((a & FILE_ATTRIBUTE_REPARSE_POINT) != 0))
+    {
+        WCHAR *fp = getFinalPath(pathbuf);
+        if (fp == NULL) {
+            a = INVALID_FILE_ATTRIBUTES;
+        } else {
+            free(pathbuf);
+            pathbuf = fp;
+            a = GetFileAttributesW(pathbuf);
+        }
+    }
     if (a != INVALID_FILE_ATTRIBUTES) {
         if (enable)
             a =  a & ~FILE_ATTRIBUTE_READONLY;
@@ -305,7 +467,7 @@
                     /* Open existing or fail */
                     OPEN_EXISTING,
                     /* Backup semantics for directories */
-                    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
+                    FILE_FLAG_BACKUP_SEMANTICS,
                     /* No template file */
                     NULL);
     if (h != INVALID_HANDLE_VALUE) {
@@ -332,7 +494,16 @@
     if (GetFileAttributesExW(pathbuf,
                              GetFileExInfoStandard,
                              &wfad)) {
-        rv = wfad.nFileSizeHigh * ((jlong)MAXDWORD + 1) + wfad.nFileSizeLow;
+        if ((wfad.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) {
+            rv = wfad.nFileSizeHigh * ((jlong)MAXDWORD + 1) + wfad.nFileSizeLow;
+        } else {
+            /* file is a reparse point so read attributes of final target */
+            BY_HANDLE_FILE_INFORMATION finfo;
+            if (getFileInformation(pathbuf, &finfo)) {
+                rv = finfo.nFileSizeHigh * ((jlong)MAXDWORD + 1) +
+                    finfo.nFileSizeLow;
+            }
+        }
     } else {
         if (GetLastError() == ERROR_SHARING_VIOLATION) {
             /* The error is "share violation", which means the file/dir
@@ -360,31 +531,29 @@
     if (pathbuf == NULL)
         return JNI_FALSE;
     h = CreateFileW(
-        pathbuf,                             /* Wide char path name */
-        GENERIC_READ | GENERIC_WRITE,  /* Read and write permission */
+        pathbuf,                              /* Wide char path name */
+        GENERIC_READ | GENERIC_WRITE,         /* Read and write permission */
         FILE_SHARE_READ | FILE_SHARE_WRITE,   /* File sharing flags */
-        NULL,                                /* Security attributes */
-        CREATE_NEW,                         /* creation disposition */
-        FILE_ATTRIBUTE_NORMAL,              /* flags and attributes */
+        NULL,                                 /* Security attributes */
+        CREATE_NEW,                           /* creation disposition */
+        FILE_ATTRIBUTE_NORMAL |
+            FILE_FLAG_OPEN_REPARSE_POINT,     /* flags and attributes */
         NULL);
 
     if (h == INVALID_HANDLE_VALUE) {
         DWORD error = GetLastError();
         if ((error != ERROR_FILE_EXISTS) && (error != ERROR_ALREADY_EXISTS)) {
-
-            // If a directory by the named path already exists,
-            // return false (behavior of solaris and linux) instead of
-            // throwing an exception
-            DWORD fattr = GetFileAttributesW(pathbuf);
-            if ((fattr == INVALID_FILE_ATTRIBUTES) ||
-                    (fattr & ~FILE_ATTRIBUTE_DIRECTORY)) {
+            // return false rather than throwing an exception when there is
+            // an existing file.
+            DWORD a = GetFileAttributesW(pathbuf);
+            if (a == INVALID_FILE_ATTRIBUTES) {
                 SetLastError(error);
                 JNU_ThrowIOExceptionWithLastError(env, "Could not open file");
             }
          }
          free(pathbuf);
          return JNI_FALSE;
-    }
+        }
     free(pathbuf);
     CloseHandle(h);
     return JNI_TRUE;
@@ -396,9 +565,9 @@
     /* Returns 0 on success */
     DWORD a;
 
-    SetFileAttributesW(path, 0);
+    SetFileAttributesW(path, FILE_ATTRIBUTE_NORMAL);
     a = GetFileAttributesW(path);
-    if (a == ((DWORD)-1)) {
+    if (a == INVALID_FILE_ATTRIBUTES) {
         return 1;
     } else if (a & FILE_ATTRIBUTE_DIRECTORY) {
         return !RemoveDirectoryW(path);
@@ -578,8 +747,13 @@
     HANDLE h;
     if (pathbuf == NULL)
         return JNI_FALSE;
-    h = CreateFileW(pathbuf, GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
-                    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, 0);
+    h = CreateFileW(pathbuf,
+                    FILE_WRITE_ATTRIBUTES,
+                    FILE_SHARE_READ | FILE_SHARE_WRITE,
+                    NULL,
+                    OPEN_EXISTING,
+                    FILE_FLAG_BACKUP_SEMANTICS,
+                    0);
     if (h != INVALID_HANDLE_VALUE) {
         LARGE_INTEGER modTime;
         FILETIME t;
@@ -607,6 +781,21 @@
     if (pathbuf == NULL)
         return JNI_FALSE;
     a = GetFileAttributesW(pathbuf);
+
+    /* if reparse point, get final target */
+    if ((a != INVALID_FILE_ATTRIBUTES) &&
+        ((a & FILE_ATTRIBUTE_REPARSE_POINT) != 0))
+    {
+        WCHAR *fp = getFinalPath(pathbuf);
+        if (fp == NULL) {
+            a = INVALID_FILE_ATTRIBUTES;
+        } else {
+            free(pathbuf);
+            pathbuf = fp;
+            a = GetFileAttributesW(pathbuf);
+        }
+    }
+
     if (a != INVALID_FILE_ATTRIBUTES) {
         if (SetFileAttributesW(pathbuf, a | FILE_ATTRIBUTE_READONLY))
         rv = JNI_TRUE;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/io/File/SymLinks.java	Thu Aug 20 08:39:18 2009 +0100
@@ -0,0 +1,380 @@
+/*
+ * Copyright 2009 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+/* @test
+ * @bug 6595866
+ * @summary Test java.io.File operations with sym links
+ */
+
+import java.io.*;
+import java.nio.file.Path;
+import java.nio.file.attribute.*;
+import static java.nio.file.LinkOption.*;
+
+public class SymLinks {
+    final static PrintStream out = System.out;
+
+    final static File top = new File(System.getProperty("test.dir", "."));
+
+    // files used by the test
+
+    final static File file              = new File(top, "foofile");
+    final static File link2file         = new File(top, "link2file");
+    final static File link2link2file    = new File(top, "link2link2file");
+
+    final static File dir               = new File(top, "foodir");
+    final static File link2dir          = new File(top, "link2dir");
+    final static File link2link2dir     = new File(top, "link2link2dir");
+
+    final static File link2nobody       = new File(top, "link2nobody");
+    final static File link2link2nobody  = new File(top, "link2link2nobody");
+
+    /**
+     * Setup files, directories, and sym links used by test.
+     */
+    static void setup() throws IOException {
+        // link2link2file -> link2file -> foofile
+        FileOutputStream fos = new FileOutputStream(file);
+        try {
+            fos.write(new byte[16*1024]);
+        } finally {
+            fos.close();
+        }
+        mklink(link2file, file);
+        mklink(link2link2file, link2file);
+
+        // link2link2dir -> link2dir -> dir
+        assertTrue(dir.mkdir());
+        mklink(link2dir, dir);
+        mklink(link2link2dir, link2dir);
+
+        // link2link2nobody -> link2nobody -> <does-not-exist>
+        mklink(link2nobody, new File(top, "DoesNotExist"));
+        mklink(link2link2nobody, link2nobody);
+    }
+
+    /**
+     * Remove files, directories, and sym links used by test.
+     */
+    static void cleanup() throws IOException {
+        if (file != null)
+            file.delete();
+        if (link2file != null)
+            link2file.toPath().deleteIfExists();
+        if (link2link2file != null)
+            link2link2file.toPath().deleteIfExists();
+        if (dir != null)
+            dir.delete();
+        if (link2dir != null)
+            link2dir.toPath().deleteIfExists();
+        if (link2link2dir != null)
+            link2link2dir.toPath().deleteIfExists();
+        if (link2nobody != null)
+            link2nobody.toPath().deleteIfExists();
+        if (link2link2nobody != null)
+            link2link2nobody.toPath().deleteIfExists();
+    }
+
+    /**
+     * Creates a sym link source->target
+     */
+    static void mklink(File source, File target) throws IOException {
+        source.toPath().createSymbolicLink(target.toPath());
+    }
+
+    /**
+     * Returns true if the "link" exists and is a sym link.
+     */
+    static boolean isSymLink(File link) {
+         try {
+            BasicFileAttributes attrs =
+                Attributes.readBasicFileAttributes(link.toPath(), NOFOLLOW_LINKS);
+            return attrs.isSymbolicLink();
+         } catch (IOException x) {
+             return false;
+         }
+    }
+
+    /**
+     * Returns the last modified time of a sym link.
+     */
+    static long lastModifiedOfSymLink(File link) throws IOException {
+        BasicFileAttributes attrs =
+            Attributes.readBasicFileAttributes(link.toPath(), NOFOLLOW_LINKS);
+        assertTrue(attrs.isSymbolicLink());
+        return attrs.lastModifiedTime().toMillis();
+    }
+
+    /**
+     * Returns true if sym links are supported on the file system where
+     * "dir" exists.
+     */
+    static boolean supportsSymLinks(File dir) {
+        Path link = dir.toPath().resolve("link");
+        Path target = dir.toPath().resolve("target");
+        try {
+            link.createSymbolicLink(target);
+            link.delete();
+            return true;
+        } catch (UnsupportedOperationException x) {
+            return false;
+        } catch (IOException x) {
+            return false;
+        }
+    }
+
+    static void assertTrue(boolean v) {
+        if (!v) throw new RuntimeException("Test failed");
+    }
+
+    static void assertFalse(boolean v) {
+        assertTrue(!v);
+    }
+
+    static void header(String h) {
+        out.println();
+        out.println();
+        out.println("-- " + h + " --");
+    }
+
+    /**
+     * Tests go here.
+     */
+    static void go() throws IOException {
+
+        // check setup
+        assertTrue(file.isFile());
+        assertTrue(isSymLink(link2file));
+        assertTrue(isSymLink(link2link2file));
+        assertTrue(dir.isDirectory());
+        assertTrue(isSymLink(link2dir));
+        assertTrue(isSymLink(link2link2dir));
+        assertTrue(isSymLink(link2nobody));
+        assertTrue(isSymLink(link2link2nobody));
+
+        header("createNewFile");
+
+        assertFalse(file.createNewFile());
+        assertFalse(link2file.createNewFile());
+        assertFalse(link2link2file.createNewFile());
+        assertFalse(dir.createNewFile());
+        assertFalse(link2dir.createNewFile());
+        assertFalse(link2link2dir.createNewFile());
+        assertFalse(link2nobody.createNewFile());
+        assertFalse(link2link2nobody.createNewFile());
+
+        header("mkdir");
+
+        assertFalse(file.mkdir());
+        assertFalse(link2file.mkdir());
+        assertFalse(link2link2file.mkdir());
+        assertFalse(dir.mkdir());
+        assertFalse(link2dir.mkdir());
+        assertFalse(link2link2dir.mkdir());
+        assertFalse(link2nobody.mkdir());
+        assertFalse(link2link2nobody.mkdir());
+
+        header("delete");
+
+        File link = new File(top, "mylink");
+        try {
+            mklink(link, file);
+            assertTrue(link.delete());
+            assertTrue(!isSymLink(link));
+            assertTrue(file.exists());
+
+            mklink(link, link2file);
+            assertTrue(link.delete());
+            assertTrue(!isSymLink(link));
+            assertTrue(link2file.exists());
+
+            mklink(link, dir);
+            assertTrue(link.delete());
+            assertTrue(!isSymLink(link));
+            assertTrue(dir.exists());
+
+            mklink(link, link2dir);
+            assertTrue(link.delete());
+            assertTrue(!isSymLink(link));
+            assertTrue(link2dir.exists());
+
+            mklink(link, link2nobody);
+            assertTrue(link.delete());
+            assertTrue(!isSymLink(link));
+            assertTrue(isSymLink(link2nobody));
+
+        } finally {
+            link.toPath().deleteIfExists();
+        }
+
+        header("renameTo");
+
+        File newlink = new File(top, "newlink");
+        assertTrue(link2file.renameTo(newlink));
+        try {
+            assertTrue(file.exists());
+            assertTrue(isSymLink(newlink));
+            assertTrue(!isSymLink(link2file));
+        } finally {
+            newlink.renameTo(link2file);  // restore link
+        }
+
+        assertTrue(link2dir.renameTo(newlink));
+        try {
+            assertTrue(dir.exists());
+            assertTrue(isSymLink(newlink));
+            assertTrue(!isSymLink(link2dir));
+        } finally {
+            newlink.renameTo(link2dir);  // restore link
+        }
+
+        header("list");
+
+        final String name = "entry";
+        File entry = new File(dir, name);
+        try {
+            assertTrue(dir.list().length == 0);   // directory should be empty
+            assertTrue(link2dir.list().length == 0);
+            assertTrue(link2link2dir.list().length == 0);
+
+            assertTrue(entry.createNewFile());
+            assertTrue(dir.list().length == 1);
+            assertTrue(dir.list()[0].equals(name));
+
+            // access directory by following links
+            assertTrue(link2dir.list().length == 1);
+            assertTrue(link2dir.list()[0].equals(name));
+            assertTrue(link2link2dir.list().length == 1);
+            assertTrue(link2link2dir.list()[0].equals(name));
+
+            // files that are not directories
+            assertTrue(link2file.list() == null);
+            assertTrue(link2nobody.list() == null);
+
+        } finally {
+            entry.delete();
+        }
+
+        header("isXXX");
+
+        assertTrue(file.isFile());
+        assertTrue(link2file.isFile());
+        assertTrue(link2link2file.isFile());
+
+        assertTrue(dir.isDirectory());
+        assertTrue(link2dir.isDirectory());
+        assertTrue(link2link2dir.isDirectory());
+
+        // on Windows we test with the DOS hidden attribute set
+        if (System.getProperty("os.name").startsWith("Windows")) {
+            DosFileAttributeView view = file.toPath()
+                .getFileAttributeView(DosFileAttributeView.class);
+            view.setHidden(true);
+            try {
+                assertTrue(file.isHidden());
+                assertTrue(link2file.isHidden());
+                assertTrue(link2link2file.isHidden());
+            } finally {
+                view.setHidden(false);
+            }
+            assertFalse(file.isHidden());
+            assertFalse(link2file.isHidden());
+            assertFalse(link2link2file.isHidden());
+        }
+
+        header("length");
+
+        long len = file.length();
+        assertTrue(len > 0L);
+        // these tests should follow links
+        assertTrue(link2file.length() == len);
+        assertTrue(link2link2file.length() == len);
+        assertTrue(link2nobody.length() == 0L);
+
+        header("lastModified / setLastModified");
+
+        // need time to diff between link and file
+        long origLastModified = file.lastModified();
+        assertTrue(origLastModified != 0L);
+        try { Thread.sleep(2000); } catch (InterruptedException x) { }
+        file.setLastModified(System.currentTimeMillis());
+
+        long lastModified = file.lastModified();
+        assertTrue(lastModified != origLastModified);
+        assertTrue(lastModifiedOfSymLink(link2file) != lastModified);
+        assertTrue(lastModifiedOfSymLink(link2link2file) != lastModified);
+        assertTrue(link2file.lastModified() == lastModified);
+        assertTrue(link2link2file.lastModified() == lastModified);
+        assertTrue(link2nobody.lastModified() == 0L);
+
+        origLastModified = dir.lastModified();
+        assertTrue(origLastModified != 0L);
+        dir.setLastModified(0L);
+        assertTrue(dir.lastModified() == 0L);
+        assertTrue(link2dir.lastModified() == 0L);
+        assertTrue(link2link2dir.lastModified() == 0L);
+        dir.setLastModified(origLastModified);
+
+        header("setXXX / canXXX");
+
+        assertTrue(file.canRead());
+        assertTrue(file.canWrite());
+        assertTrue(link2file.canRead());
+        assertTrue(link2file.canWrite());
+        assertTrue(link2link2file.canRead());
+        assertTrue(link2link2file.canWrite());
+
+        if (file.setReadOnly()) {
+            assertFalse(file.canWrite());
+            assertFalse(link2file.canWrite());
+            assertFalse(link2link2file.canWrite());
+
+            assertTrue(file.setWritable(true));             // make writable
+            assertTrue(file.canWrite());
+            assertTrue(link2file.canWrite());
+            assertTrue(link2link2file.canWrite());
+
+            assertTrue(link2file.setReadOnly());            // make read only
+            assertFalse(file.canWrite());
+            assertFalse(link2file.canWrite());
+            assertFalse(link2link2file.canWrite());
+
+            assertTrue(link2link2file.setWritable(true));   // make writable
+            assertTrue(file.canWrite());
+            assertTrue(link2file.canWrite());
+            assertTrue(link2link2file.canWrite());
+        }
+    }
+
+    public static void main(String[] args) throws IOException {
+        if (supportsSymLinks(top)) {
+            try {
+                setup();
+                go();
+            } finally {
+                cleanup();
+            }
+        }
+    }
+
+}