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 |
} |