Skip to content

Commit fd90461

Browse files
author
Joao Goncalves
committed
FOP-3193: Avoid recursion when folders contain symlinks
1 parent dd0d8ce commit fd90461

File tree

4 files changed

+178
-10
lines changed

4 files changed

+178
-10
lines changed

fop-core/src/main/java/org/apache/fop/fonts/DefaultFontConfigurator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.apache.fop.fonts;
2121

22+
import java.io.File;
2223
import java.io.IOException;
2324
import java.net.URI;
2425
import java.net.URISyntaxException;
@@ -118,7 +119,7 @@ private void addDirectories(DefaultFontConfig fontInfoConfig, FontAdder fontAdde
118119
FontFileFinder fontFileFinder = new FontFileFinder(directory.isRecursive() ? -1 : 1, listener);
119120
List<URL> fontURLList;
120121
try {
121-
fontURLList = fontFileFinder.find(directory.getDirectory());
122+
fontURLList = fontFileFinder.find(new File(directory.getDirectory()));
122123
fontAdder.add(fontURLList, fontInfoList);
123124
} catch (IOException e) {
124125
LogUtil.handleException(log, e, strict);

fop-core/src/main/java/org/apache/fop/fonts/FontDetectorFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void detect(FontManager fontManager, FontAdder fontAdder, boolean strict,
9393
URI fontBaseURI = fontManager.getResourceResolver().getBaseURI();
9494
File fontBase = FileUtils.toFile(fontBaseURI.toURL());
9595
if (fontBase != null) {
96-
List<URL> fontURLList = fontFileFinder.find(fontBase.getAbsolutePath());
96+
List<URL> fontURLList = fontFileFinder.find(fontBase);
9797
fontAdder.add(fontURLList, fontInfoList);
9898

9999
//Can only use the font base URL if it's a file URL

fop-core/src/main/java/org/apache/fop/fonts/autodetect/FontFileFinder.java

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@
2020
package org.apache.fop.fonts.autodetect;
2121

2222
import java.io.File;
23+
import java.io.FileFilter;
2324
import java.io.IOException;
2425
import java.net.MalformedURLException;
2526
import java.net.URL;
27+
import java.nio.file.Files;
2628
import java.util.Collection;
29+
import java.util.HashMap;
2730
import java.util.List;
31+
import java.util.Map;
32+
import java.util.Objects;
2833

2934
import org.apache.commons.io.DirectoryWalker;
3035
import org.apache.commons.io.IOCase;
@@ -150,28 +155,89 @@ public List<URL> find() throws IOException {
150155
}
151156
}
152157
List<File> fontDirs = fontDirFinder.find();
153-
List<URL> results = new java.util.ArrayList<URL>();
158+
List<URL> results = new java.util.ArrayList<>();
154159
for (File dir : fontDirs) {
155-
super.walk(dir, results);
160+
walkDirectory(dir, results);
156161
}
157162
return results;
158163
}
159164

160165
/**
161166
* Searches a given directory for font files
162167
*
163-
* @param dir directory to search
168+
* @param directory directory to search
164169
* @return list of font files
165170
* @throws IOException thrown if an I/O exception of some sort has occurred
166171
*/
167-
public List<URL> find(String dir) throws IOException {
168-
List<URL> results = new java.util.ArrayList<URL>();
169-
File directory = new File(dir);
172+
public List<URL> find(File directory) throws IOException {
173+
List<URL> results = new java.util.ArrayList<>();
170174
if (!directory.isDirectory()) {
171-
eventListener.fontDirectoryNotFound(this, dir);
175+
eventListener.fontDirectoryNotFound(this, directory.getAbsolutePath());
172176
} else {
173-
super.walk(directory, results);
177+
walkDirectory(directory, results);
174178
}
175179
return results;
176180
}
181+
182+
private void walkDirectory(File startDirectory, Collection<URL> results) throws IOException {
183+
Objects.requireNonNull(startDirectory, "startDirectory");
184+
185+
try {
186+
walk(startDirectory, 0, results, new HashMap<>());
187+
} catch (CancelException cancel) {
188+
handleCancelled(startDirectory, results, cancel);
189+
}
190+
191+
}
192+
193+
private void walk(File directory, int depth, Collection<URL> results,
194+
Map<String, String> visitedSymlinks) throws IOException {
195+
IOFileFilter fileFilter = FileFilterUtils.makeFileOnly(getFileFilter());
196+
IOFileFilter directoryFilter = FileFilterUtils.makeDirectoryOnly(getDirectoryFilter());
197+
FileFilter filter = directoryFilter.or(fileFilter);
198+
199+
checkIfCancelled(directory, depth, results);
200+
201+
int childDepth = depth + 1;
202+
203+
File[] childFiles = directory.listFiles(filter);
204+
if (childFiles != null) {
205+
for (File childFile : childFiles) {
206+
if (childFile.isDirectory()) {
207+
if (Files.isSymbolicLink(childFile.toPath()) && hasSymlinkBeenWalked(childFile, visitedSymlinks)) {
208+
continue;
209+
}
210+
walk(childFile, childDepth, results, visitedSymlinks);
211+
} else {
212+
checkIfCancelled(childFile, childDepth, results);
213+
handleFile(childFile, childDepth, results);
214+
checkIfCancelled(childFile, childDepth, results);
215+
}
216+
}
217+
}
218+
219+
handleDirectoryEnd(directory, depth, results);
220+
checkIfCancelled(directory, depth, results);
221+
}
222+
223+
private boolean hasSymlinkBeenWalked(File symlink, Map<String, String> visitedSymlinks) {
224+
String symlinkPath;
225+
try {
226+
symlinkPath = symlink.toPath().toRealPath().toString();
227+
} catch (IOException e) {
228+
log.warn("Failed to get symlink path: " + e.getMessage());
229+
symlinkPath = symlink.getAbsolutePath();
230+
}
231+
for (Map.Entry<String, String> entry : visitedSymlinks.entrySet()) {
232+
//startsWith being used in the event there's an exception in the call above
233+
//if there's no exception, startsWith will work as an equals
234+
if (symlinkPath.startsWith(entry.getKey()) && entry.getValue().equals(symlink.getName())) {
235+
return true;
236+
}
237+
}
238+
239+
visitedSymlinks.put(symlinkPath, symlink.getName());
240+
241+
return false;
242+
}
177243
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/* $Id$ */
19+
20+
package org.apache.fop.fonts.autodetect;
21+
22+
import java.io.File;
23+
import java.io.FileFilter;
24+
import java.io.IOException;
25+
import java.nio.file.FileSystem;
26+
import java.nio.file.Path;
27+
import java.nio.file.attribute.BasicFileAttributes;
28+
import java.nio.file.spi.FileSystemProvider;
29+
30+
import org.junit.Test;
31+
32+
import static org.mockito.ArgumentMatchers.any;
33+
import static org.mockito.Mockito.mock;
34+
import static org.mockito.Mockito.times;
35+
import static org.mockito.Mockito.verify;
36+
import static org.mockito.Mockito.when;
37+
38+
public class FontFileFinderTestCase {
39+
40+
@Test
41+
public void testSymlinkRecursion() throws IOException {
42+
FontFileFinder finder = new FontFileFinder(null);
43+
44+
File targetDirectory = createMockDirectory("target");
45+
File linkDirectory = createMockDirectory("link");
46+
47+
setupChildFiles(targetDirectory, linkDirectory);
48+
setupChildFiles(linkDirectory, targetDirectory);
49+
50+
setupSymLink(linkDirectory.toPath());
51+
52+
finder.find(targetDirectory);
53+
54+
//targetDir -> linkDir -> targetDir -> linkDir (skipped as it has been seen)
55+
//it's repeating the folder because we need to access the symlink once only.
56+
//in this scenario, the symlink is recursive, hence the repetition
57+
verify(targetDirectory, times(2)).listFiles(any(FileFilter.class));
58+
verify(linkDirectory, times(1)).listFiles(any(FileFilter.class));
59+
60+
//this is only accessed for symlinks
61+
// it's used to make sure we don't access the same synlink twice
62+
verify(targetDirectory.toPath(), times(0)).toRealPath();
63+
verify(linkDirectory.toPath(), times(2)).toRealPath();
64+
}
65+
66+
private File createMockDirectory(String path) throws IOException {
67+
Path mockRealPath = mock(Path.class);
68+
when(mockRealPath.toString()).thenReturn(path);
69+
70+
Path mockPath = mock(Path.class);
71+
when(mockPath.toRealPath()).thenReturn(mockRealPath);
72+
setupPathFileSystem(mockPath, false);
73+
74+
File mockDir = mock(File.class);
75+
when(mockDir.isDirectory()).thenReturn(true);
76+
when(mockDir.toPath()).thenReturn(mockPath);
77+
when(mockDir.getName()).thenReturn(path);
78+
79+
return mockDir;
80+
}
81+
82+
private void setupChildFiles(File dir, File childFile) {
83+
when(dir.listFiles(any(FileFilter.class))).thenReturn(new File[]{childFile});
84+
}
85+
86+
private void setupSymLink(Path linkPath) throws IOException {
87+
setupPathFileSystem(linkPath, true);
88+
}
89+
90+
private void setupPathFileSystem(Path path, boolean symlink) throws IOException {
91+
BasicFileAttributes mockBasicFileAttributes = mock(BasicFileAttributes.class);
92+
when(mockBasicFileAttributes.isSymbolicLink()).thenReturn(symlink);
93+
94+
FileSystemProvider mockProvider = mock(FileSystemProvider.class);
95+
when(mockProvider.readAttributes(any(), any(Class.class), any())).thenReturn(mockBasicFileAttributes);
96+
97+
FileSystem mockFileSystem = mock(FileSystem.class);
98+
when(mockFileSystem.provider()).thenReturn(mockProvider);
99+
when(path.getFileSystem()).thenReturn(mockFileSystem);
100+
}
101+
}

0 commit comments

Comments
 (0)