This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

View | Details | Raw Unified | Return to bug 218795
Collapse All | Expand All

(-)a/masterfs/test/unit/src/org/netbeans/modules/masterfs/FileObjectCyclicSymlinksTest.java (+433 lines)
Line 0 Link Here
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2013 Oracle and/or its affiliates. All rights reserved.
5
 *
6
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
7
 * Other names may be trademarks of their respective owners.
8
 *
9
 * The contents of this file are subject to the terms of either the GNU
10
 * General Public License Version 2 only ("GPL") or the Common
11
 * Development and Distribution License("CDDL") (collectively, the
12
 * "License"). You may not use this file except in compliance with the
13
 * License. You can obtain a copy of the License at
14
 * http://www.netbeans.org/cddl-gplv2.html
15
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
16
 * specific language governing permissions and limitations under the
17
 * License.  When distributing the software, include this License Header
18
 * Notice in each file and include the License file at
19
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
20
 * particular file as subject to the "Classpath" exception as provided
21
 * by Oracle in the GPL Version 2 section of the License file that
22
 * accompanied this code. If applicable, add the following below the
23
 * License Header, with the fields enclosed by brackets [] replaced by
24
 * your own identifying information:
25
 * "Portions Copyrighted [year] [name of copyright owner]"
26
 *
27
 * If you wish your version of this file to be governed by only the CDDL
28
 * or only the GPL Version 2, indicate your decision by adding
29
 * "[Contributor] elects to include this software in this distribution
30
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
31
 * single choice of license, a recipient has the option to distribute
32
 * your version of this file under either the CDDL, the GPL Version 2 or
33
 * to extend the choice of license to its licensees as provided above.
34
 * However, if you add GPL Version 2 code and therefore, elected the GPL
35
 * Version 2 license, then the option applies only if the new code is
36
 * made subject to such option by the copyright holder.
37
 *
38
 * Contributor(s):
39
 *
40
 * Portions Copyrighted 2013 Sun Microsystems, Inc.
41
 */
42
package org.netbeans.modules.masterfs;
43
44
import java.io.File;
45
import java.io.IOException;
46
import java.nio.file.Files;
47
import java.nio.file.Path;
48
import java.nio.file.Paths;
49
import java.util.Enumeration;
50
import java.util.logging.Level;
51
import java.util.logging.Logger;
52
import org.netbeans.junit.NbTestCase;
53
import org.openide.filesystems.FileObject;
54
import org.openide.filesystems.FileUtil;
55
import org.openide.util.Utilities;
56
57
/**
58
 *
59
 * @author jhavlin
60
 */
61
public class FileObjectCyclicSymlinksTest extends NbTestCase {
62
63
    private static final int DEPTH = 30;
64
65
    private static final Logger LOG
66
            = Logger.getLogger(FileObjectCyclicSymlinksTest.class.getName());
67
68
    public FileObjectCyclicSymlinksTest(String name) {
69
        super(name);
70
    }
71
72
    @Override
73
    protected void setUp() throws Exception {
74
        deleteChildren(getWorkDir());
75
        clearWorkDir();
76
    }
77
78
    @Override
79
    protected void tearDown() throws Exception {
80
        deleteChildren(getWorkDir());
81
        clearWorkDir();
82
    }
83
84
//    /**
85
//     * Try several orderings.
86
//     */
87
//    public static TestSuite suite() {
88
//        TestSuite s = new NbTestSuite();
89
//
90
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsRecursiveSymlinkInStandardTree"));
91
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsRecursiveSymlinkInArtificialTree"));
92
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsUnderRecursiveSymlinkInStandardTree"));
93
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsUnderRecursiveSymlinkInArtificialTree"));
94
//
95
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsUnderRecursiveSymlinkInStandardTree"));
96
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsUnderRecursiveSymlinkInArtificialTree"));
97
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsRecursiveSymlinkInStandardTree"));
98
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsRecursiveSymlinkInArtificialTree"));
99
//
100
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsRecursiveSymlinkInArtificialTree"));
101
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsRecursiveSymlinkInStandardTree"));
102
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsUnderRecursiveSymlinkInArtificialTree"));
103
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsUnderRecursiveSymlinkInStandardTree"));
104
//
105
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsUnderRecursiveSymlinkInArtificialTree"));
106
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsUnderRecursiveSymlinkInStandardTree"));
107
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsRecursiveSymlinkInArtificialTree"));
108
//        s.addTest(new FileObjectCyclicSymlinksTest("testIsRecursiveSymlinkInStandardTree"));
109
//
110
//        return s;
111
//    }
112
113
    /**
114
     * Test that symbolic links are handled correctly, without infinite
115
     * recursion.
116
     *
117
     * Let's use the following data structure:
118
     * <pre>
119
     * f1
120
     *   f2
121
     *     f3
122
     *       f4
123
     *         f5 (symbolic link with target = f1)
124
     * </pre>
125
     *
126
     * @throws java.io.IOException
127
     */
128
    public void test218795() throws IOException {
129
130
        File rootF = getWorkDir();
131
        createDirTreeWithChangingNames(rootF, 5);
132
133
        FileObject rootFO = FileUtil.toFileObject(rootF);
134
        assertNotNull(rootFO);
135
        assertEquals(rootF, FileUtil.toFile(rootFO));
136
        rootFO.refresh();
137
        assertEquals(4, countRecursiveChildren(rootFO));
138
    }
139
140
    public void testIsUnderRecursiveSymlinkInStandardTree() throws IOException {
141
        long start = System.currentTimeMillis();
142
        File symlink = createDirTreeWithChangingNames(getWorkDir(), DEPTH);
143
        if (symlink != null) {
144
            assertEquals(DEPTH + 1, traverseDirTreeAndCountFolders(getRootDir(),
145
                    new CyclicSymLinkFilter() {
146
147
                        @Override
148
                        public boolean traverse(FileObject fileObject) {
149
                            return !FileUtil.isUnderRecursiveSymlink(fileObject);
150
                        }
151
                    }));
152
            printTime(start);
153
        } else {
154
            printSkipped();
155
        }
156
    }
157
158
    public void testIsUnderRecursiveSymlinkInArtificialTree() throws IOException {
159
        long start = System.currentTimeMillis();
160
        File symlink = createDirTreeWithTheSameNames(getWorkDir(), DEPTH);
161
        if (symlink != null) {
162
            assertEquals(DEPTH, traverseDirTreeAndCountFolders(getRootDir(),
163
                    new CyclicSymLinkFilter() {
164
165
                        @Override
166
                        public boolean traverse(FileObject fileObject) {
167
                            return !FileUtil.isUnderRecursiveSymlink(fileObject);
168
                        }
169
                    }));
170
            printTime(start);
171
            symlink.delete();
172
            LOG.log(Level.FINE, "Note that number of visited directories"
173
                    + "depends on names of the directories! So this is"
174
                    + " different from testIsUnderRecursiveSymlinkInStandardTree");
175
        } else {
176
            printSkipped();
177
        }
178
    }
179
180
    public void testIsRecursiveSymlinkInStandardTree() throws IOException {
181
        long start = System.currentTimeMillis();
182
        File symlink = createDirTreeWithChangingNames(getWorkDir(), DEPTH);
183
        if (symlink != null) {
184
            assertEquals(DEPTH, traverseDirTreeAndCountFolders(getRootDir(),
185
                    new CyclicSymLinkFilter() {
186
187
                        @Override
188
                        public boolean traverse(FileObject fileObject) {
189
                            return !FileUtil.isRecursiveSymlink(fileObject);
190
                        }
191
                    }));
192
            printTime(start);
193
            symlink.delete();
194
        } else {
195
            printSkipped();
196
        }
197
    }
198
199
    public void testIsRecursiveSymlinkInArtificialTree() throws IOException {
200
        long start = System.currentTimeMillis();
201
        File symlink = createDirTreeWithTheSameNames(getWorkDir(), DEPTH);
202
        if (symlink != null) {
203
            assertEquals(DEPTH, traverseDirTreeAndCountFolders(getRootDir(),
204
                    new CyclicSymLinkFilter() {
205
206
                        @Override
207
                        public boolean traverse(FileObject fileObject) {
208
                            return !FileUtil.isRecursiveSymlink(fileObject);
209
                        }
210
                    }));
211
            symlink.delete();
212
            printTime(start);
213
        } else {
214
            printSkipped();
215
        }
216
    }
217
218
    /**
219
     * Count number of children returned by FileObject.getChildren(true). Limit
220
     * the number to a reasonable constant.
221
     */
222
    private int countRecursiveChildren(FileObject root) {
223
        Enumeration<? extends FileObject> children = root.getChildren(true);
224
        int count = 0;
225
        LOG.log(Level.FINER, "Enumerating recursive children under {0}", root);
226
        while (children.hasMoreElements()) {
227
            FileObject child = children.nextElement();
228
            assertNotNull(child);
229
            File f = FileUtil.toFile(child);
230
            LOG.log(Level.FINER, "Found child: {0}", f.getPath());
231
            assertNotNull(f);
232
            count++;
233
            if (count > 100) {
234
                break;
235
            }
236
        }
237
        return count;
238
    }
239
240
    /**
241
     * Print info about consumed time.
242
     */
243
    private void printTime(long start) {
244
        long end = System.currentTimeMillis();
245
        LOG.log(Level.INFO, "Test {0} took {1} ms.",
246
                new Object[]{getName(), end - start});
247
    }
248
249
    /**
250
     * Print info about skipped test.
251
     */
252
    private void printSkipped() {
253
        LOG.log(Level.INFO, "Skipping {0}", getName());
254
    }
255
256
    /**
257
     * Get workDir as a FileObject.
258
     */
259
    private FileObject getRootDir() throws IOException {
260
        return FileUtil.toFileObject(getWorkDir());
261
    }
262
263
    /**
264
     * Traverse a directory tree, use a filter that prevents traversing cyclic
265
     * symbolic links.
266
     *
267
     * @return Number of visited directories.
268
     */
269
    private int traverseDirTreeAndCountFolders(FileObject root,
270
            CyclicSymLinkFilter filter) {
271
        int sum = 1;
272
        for (FileObject fo : root.getChildren()) {
273
            if (fo.isFolder() && filter.traverse(fo)) {
274
                sum += traverseDirTreeAndCountFolders(fo, filter);
275
            }
276
        }
277
        return sum;
278
    }
279
280
    /**
281
     * Create a directory "tree" where folder have increasing names, e.g.
282
     * f1/f2/f3/f4. The last folder is a symlink to the first folder.
283
     *
284
     * @param root Root directory
285
     * @param depth Depth of the tree.
286
     * @return The symbolic link on success, null otherwise.
287
     */
288
    private File createDirTreeWithChangingNames(File root, int depth) {
289
        return createDirTree(root, depth, "f", true);
290
    }
291
292
    /**
293
     * Create a directory "tree" where folders have the same name, e.g.
294
     * fld/fld/fld/fld. The last folder is a symlink to the first folder.
295
     *
296
     * @param root Root directory
297
     * @param depth Depth of the tree.
298
     * @return The symbolic link on success, null otherwise.
299
     */
300
    private File createDirTreeWithTheSameNames(File root, int depth) {
301
        return createDirTree(root, depth, "fld", false);
302
    }
303
304
    /**
305
     * Create directory tree with one cyclic symbolic link.
306
     */
307
    private File createDirTree(File root, int depth, String name,
308
            boolean appendLevelToName) {
309
310
        assert depth > 1;
311
        File parent = root;
312
        File first = null;
313
314
        for (int i = 1; i <= depth; i++) {
315
            File file = new File(parent, name + (appendLevelToName ? i : ""));
316
            if (i == depth) {
317
                Path newLink = Paths.get(Utilities.toURI(file));
318
                Path target = Paths.get(Utilities.toURI(first));
319
                try {
320
                    Files.createSymbolicLink(newLink, target);
321
                    return file;
322
                } catch (IOException ex) {
323
                    return null;
324
                }
325
            } else {
326
                file.mkdir();
327
                parent = file;
328
                if (i == 1) {
329
                    first = file;
330
                }
331
            }
332
        }
333
        return null;
334
    }
335
336
    /**
337
     * Test directory tree with a indirect cyclic link.
338
     * <pre>
339
     *  a
340
     *    b
341
     *      c ( -> a)
342
     *    d
343
     *      e ( -> c)
344
     * </pre>
345
     *
346
     * @throws java.io.IOException
347
     */
348
    public void testIndirectSymbolicLink() throws IOException {
349
        File a = new File(getWorkDir(), "a");
350
        File b = new File(a, "b");
351
        File c = new File(b, "c");
352
        File d = new File(a, "d");
353
        File e = new File(d, "e");
354
        assertTrue(b.mkdirs());
355
        assertTrue(d.mkdirs());
356
        try {
357
            Files.createSymbolicLink(c.toPath(), a.toPath());
358
            Files.createSymbolicLink(e.toPath(), c.toPath());
359
            FileObject fo = FileUtil.toFileObject(getWorkDir());
360
            assertEquals(3, countRecursiveChildren(fo));
361
        } catch (IOException ex) {
362
            printSkipped();
363
        }
364
    }
365
366
    /**
367
     * Test allowed (non-cyclic) symlink.
368
     * <pre>
369
     * a
370
     *   b
371
     *     c ( -> d)
372
     *   d
373
     *     e
374
     *       f
375
     * </pre>
376
     *
377
     * @throws java.io.IOException
378
     */
379
    public void testAllowedSymbolicLink() throws IOException {
380
        File a = new File(getWorkDir(), "a");
381
        File b = new File(a, "b");
382
        File c = new File(b, "c");
383
        File d = new File(a, "d");
384
        File e = new File(d, "e");
385
        File f = new File(e, "f");
386
        assertTrue(b.mkdirs());
387
        assertTrue(f.mkdirs());
388
        try {
389
            Files.createSymbolicLink(c.toPath(), d.toPath());
390
            assertEquals(3, countRecursiveChildren(FileUtil.toFileObject(b)));
391
        } catch (IOException ex) {
392
            printSkipped();
393
        }
394
    }
395
396
    /**
397
     * Delete all child files and directories in a directory.
398
     */
399
    private void deleteChildren(File root) throws IOException {
400
        for (File f : root.listFiles()) {
401
            deleteFile(f);
402
        }
403
    }
404
405
    /**
406
     * Recursively delete directory, do not throw an exception if a file cannot
407
     * be deleted, do not traverse symbolic links.
408
     */
409
    private static void deleteFile(File file) throws IOException {
410
        if (file.isDirectory() && file.equals(file.getCanonicalFile())
411
                && !Files.isSymbolicLink(file.toPath())) {
412
            // file is a directory - delete sub files first
413
            File files[] = file.listFiles();
414
            for (File file1 : files) {
415
                deleteFile(file1);
416
            }
417
        }
418
        // file is a File :-)
419
        file.delete();
420
    }
421
422
    private static interface CyclicSymLinkFilter {
423
424
        /**
425
         * Tell whether the file can be traversed, without risk of invinite
426
         * recursion.
427
         *
428
         * @param fileObject
429
         * @return
430
         */
431
        boolean traverse(FileObject fileObject);
432
    }
433
}
(-)a/openide.filesystems/src/org/openide/filesystems/FileObject.java (-1 / +5 lines)
Lines 902-908 Link Here
902
            @Override
902
            @Override
903
            public FileObject process(FileObject fo, Collection<FileObject> toAdd) {
903
            public FileObject process(FileObject fo, Collection<FileObject> toAdd) {
904
                if (rec && fo.isFolder()) {
904
                if (rec && fo.isFolder()) {
905
                    toAdd.addAll(Arrays.asList(fo.getChildren()));
905
                    for (FileObject child : fo.getChildren()) {
906
                        if (!FileUtil.isRecursiveSymlink(child)) { // #218795
907
                            toAdd.add(child);
908
                        }
909
                    }
906
                }
910
                }
907
911
908
                return fo;
912
                return fo;
(-)a/openide.filesystems/src/org/openide/filesystems/FileUtil.java (-1 / +90 lines)
Lines 44-49 Link Here
44
44
45
package org.openide.filesystems;
45
package org.openide.filesystems;
46
46
47
import java.awt.EventQueue;
47
import java.io.File;
48
import java.io.File;
48
import java.io.FileFilter;
49
import java.io.FileFilter;
49
import java.io.FilenameFilter;
50
import java.io.FilenameFilter;
Lines 53-64 Link Here
53
import java.io.SyncFailedException;
54
import java.io.SyncFailedException;
54
import java.lang.ref.Reference;
55
import java.lang.ref.Reference;
55
import java.lang.ref.SoftReference;
56
import java.lang.ref.SoftReference;
56
import java.lang.ref.WeakReference;
57
import java.lang.reflect.Method;
57
import java.lang.reflect.Method;
58
import java.net.MalformedURLException;
58
import java.net.MalformedURLException;
59
import java.net.URI;
59
import java.net.URI;
60
import java.net.URL;
60
import java.net.URL;
61
import java.net.URLStreamHandler;
61
import java.net.URLStreamHandler;
62
import java.nio.file.Files;
63
import java.nio.file.InvalidPathException;
64
import java.nio.file.Path;
62
import java.util.ArrayList;
65
import java.util.ArrayList;
63
import java.util.Arrays;
66
import java.util.Arrays;
64
import java.util.Collection;
67
import java.util.Collection;
Lines 2331-2334 Link Here
2331
        int index = path.lastIndexOf('.');  //NOI18N
2334
        int index = path.lastIndexOf('.');  //NOI18N
2332
        return (index != -1) && (index > path.lastIndexOf('/') + 1);    //NOI18N
2335
        return (index != -1) && (index > path.lastIndexOf('/') + 1);    //NOI18N
2333
    }
2336
    }
2337
2338
    /**
2339
     * Check whether this file is a symbolic link that targets to a folder
2340
     * higher in the path. Such link, in case of recursive folder traversing,
2341
     * would cause infinite recursion. The method cannot be called from Event
2342
     * Dispatch Thread.
2343
     *
2344
     * @param fileObject FileObject to check.
2345
     * @return True if the file is a link that could cause infinite recursion,
2346
     * false otherwise.
2347
     */
2348
    public static boolean isRecursiveSymlink(FileObject fileObject) {
2349
        assert !EventQueue.isDispatchThread();
2350
        File file = toFile(fileObject);
2351
        if (file == null) {
2352
            return false;
2353
        }
2354
        Path path;
2355
        try {
2356
            path = file.toPath();
2357
        } catch (RuntimeException e) {
2358
            LOG.log(Level.INFO, "Cannot get path for {0}", file);       //NOI18N
2359
            LOG.log(Level.FINE, null, e);
2360
            return false;
2361
        }
2362
        try {
2363
            if (Files.isSymbolicLink(path)) {
2364
                try {
2365
                    Path target = Files.readSymbolicLink(path).toRealPath();
2366
                    for (Path ancestor = path.getParent().toRealPath();
2367
                            ancestor != null; ancestor = ancestor.getParent()) {
2368
                        if (target.equals(ancestor)) {
2369
                            return true;
2370
                        }
2371
                    }
2372
                } catch (IOException ex) {
2373
                    LOG.log(Level.INFO, "Cannot read symbolic link {0}",//NOI18N
2374
                            path);
2375
                    LOG.log(Level.FINE, null, ex);
2376
                }
2377
            }
2378
        } catch (SecurityException e) {
2379
            LOG.log(Level.INFO, null, e);
2380
        }
2381
        return false;
2382
    }
2383
2384
    /**
2385
     * Check whether the file object is under a recursive symbolic link. Such
2386
     * file, in case of recursive folder traversing, would cause infinite
2387
     * recursion. The method cannot be called from Event Dispatch Thread.
2388
     *
2389
     * @param fileObject FileObject to check.
2390
     * @return True if the file is under a recursive symbolic link and it could
2391
     * cause infinite recursion, false otherwise.
2392
     */
2393
    public static boolean isUnderRecursiveSymlink(FileObject fileObject) {
2394
        assert !EventQueue.isDispatchThread();
2395
        File file = toFile(fileObject);
2396
        if (file == null) {
2397
            return false;
2398
        }
2399
        Path path;
2400
        try {
2401
            path = file.toPath();
2402
        } catch (InvalidPathException e) {
2403
            LOG.log(Level.INFO, "Cannot get path for {0}", file);       //NOI18N
2404
            LOG.log(Level.FINE, null, e);
2405
            return false;
2406
        }
2407
        for (Path anc = path.getParent(); anc != null; anc = anc.getParent()) {
2408
            if (path.getFileName().equals(anc.getFileName())) {
2409
                try {
2410
                    if (Files.isSameFile(path, anc)) {
2411
                        return true;
2412
                    }
2413
                } catch (IOException ex) {
2414
                    LOG.log(Level.INFO, "Cannot check whether {0} " //NOI18N
2415
                            + "is the same file as {1}", //NOI18N
2416
                            new Object[]{path, anc});
2417
                    LOG.log(Level.FINE, null, ex);
2418
                }
2419
            }
2420
        }
2421
        return false;
2422
    }
2334
}
2423
}

Return to bug 218795