1 | package com.renomad.minum.utils; | |
2 | ||
3 | import com.renomad.minum.state.Constants; | |
4 | import com.renomad.minum.logging.ILogger; | |
5 | ||
6 | import java.io.IOException; | |
7 | import java.nio.charset.StandardCharsets; | |
8 | import java.nio.file.Files; | |
9 | import java.nio.file.LinkOption; | |
10 | import java.nio.file.Path; | |
11 | import java.util.*; | |
12 | import java.util.regex.Pattern; | |
13 | import java.util.stream.Stream; | |
14 | ||
15 | /** | |
16 | * Helper functions for working with files. | |
17 | */ | |
18 | public final class FileUtils { | |
19 | ||
20 | /** | |
21 | * These patterns can be used in path strings to access files higher in | |
22 | * the directory structure. We disallow this, as a security precaution. | |
23 | * <ul> | |
24 | * <li>1st Alternative {@code //} - This prevents going to the root directory | |
25 | * </li> | |
26 | * <li>2nd Alternative {@code ..} - prevents going up a directory | |
27 | * </li> | |
28 | * <li>3rd Alternative {@code :} - prevents certain special paths, like "C:" or "file://" | |
29 | * </li> | |
30 | * <li>4th Alternative {@code ^/} - prevents starting with a slash, meaning the root, but | |
31 | * allows intermediate slashes. | |
32 | * </li> | |
33 | * <li>5th Alternative {@code :^\} - prevents starting with a backslash, meaning the root, but | |
34 | * allows intermediate backslashes. | |
35 | * </li> | |
36 | * </ul> | |
37 | */ | |
38 | public static final Pattern badFilePathPatterns = Pattern.compile("//|\\.\\.|:|^/|^\\\\"); | |
39 | ||
40 | private final ILogger logger; | |
41 | private final IFileReader fileReader; | |
42 | ||
43 | public FileUtils(ILogger logger, Constants constants) { | |
44 | this( | |
45 | logger, | |
46 | new FileReader( | |
47 | LRUCache.getLruCache(constants.maxElementsLruCacheStaticFiles), | |
48 | constants.useCacheForStaticFiles, | |
49 | logger)); | |
50 | } | |
51 | ||
52 | /** | |
53 | * This version of the constructor is mainly for testing | |
54 | */ | |
55 | FileUtils(ILogger logger, IFileReader fileReader) { | |
56 | this.logger = logger; | |
57 | this.fileReader = fileReader; | |
58 | } | |
59 | ||
60 | /** | |
61 | * Write a string to a path on disk. | |
62 | * <br> | |
63 | * <p> | |
64 | * <em>Note: This does *not* protect against untrusted data on its own. Call {@link #safeResolve(String, String)} first against | |
65 | * the path to ensure it uses valid characters and prevent it escaping the expected directory.</em> | |
66 | * </p> | |
67 | */ | |
68 | public void writeString(Path path, String content) { | |
69 |
1
1. writeString : negated conditional → KILLED |
if (path.toString().isEmpty()) { |
70 | logger.logDebug(() -> "an empty path was provided to writeString"); | |
71 | return; | |
72 | } | |
73 | try { | |
74 | Files.writeString(path, content); | |
75 | } catch (IOException e) { | |
76 | throw new UtilsException(e); | |
77 | } | |
78 | } | |
79 | ||
80 | /** | |
81 | * Deletes a directory, deleting everything inside it | |
82 | * recursively afterwards. A more dangerous method than | |
83 | * many others, take care. | |
84 | * <br> | |
85 | * <p> | |
86 | * <em>Note: This does *not* protect against untrusted data on its own. Call {@link #safeResolve(String, String)} first against | |
87 | * the path to ensure it uses valid characters and prevent it escaping the expected directory.</em> | |
88 | * </p> | |
89 | */ | |
90 | public void deleteDirectoryRecursivelyIfExists(Path myPath) { | |
91 |
1
1. deleteDirectoryRecursivelyIfExists : negated conditional → KILLED |
if (!Files.exists(myPath)) { |
92 | logger.logDebug(() -> "system was requested to delete directory: "+myPath+", but it did not exist"); | |
93 | } else { | |
94 |
1
1. deleteDirectoryRecursivelyIfExists : removed call to com/renomad/minum/utils/FileUtils::walkPathDeleting → KILLED |
walkPathDeleting(myPath); |
95 | } | |
96 | } | |
97 | ||
98 | void walkPathDeleting(Path myPath) { | |
99 | try (Stream<Path> walk = Files.walk(myPath)) { | |
100 | ||
101 | final var files = walk.sorted(Comparator.reverseOrder()) | |
102 | .map(Path::toFile).toList(); | |
103 | ||
104 | for(var file: files) { | |
105 | logger.logDebug(() -> "deleting " + file); | |
106 |
1
1. walkPathDeleting : removed call to java/nio/file/Files::delete → KILLED |
Files.delete(file.toPath()); |
107 | } | |
108 | } catch (IOException ex) { | |
109 | throw new UtilsException("Error during deleteDirectoryRecursivelyIfExists: " + ex); | |
110 | } | |
111 | } | |
112 | ||
113 | /** | |
114 | * Creates a directory if it doesn't already exist. | |
115 | * <br> | |
116 | * <p> | |
117 | * <em>Note: This does *not* protect against untrusted data on its own. Call {@link #safeResolve(String, String)} first against | |
118 | * the path to ensure it uses valid characters and prevent it escaping the expected directory.</em> | |
119 | * </p> | |
120 | * <p> | |
121 | * If the directory does exist, the program will simply skip | |
122 | * building it, and mention it in the logs. | |
123 | * </p> | |
124 | */ | |
125 | public void makeDirectory(Path directory) { | |
126 | logger.logDebug(() -> "Creating a directory " + directory); | |
127 | boolean directoryExists = Files.exists(directory); | |
128 | logger.logDebug(() -> "Directory: " + directory + ". Already exists: " + directory); | |
129 |
1
1. makeDirectory : negated conditional → KILLED |
if (!directoryExists) { |
130 | logger.logDebug(() -> "Creating directory, since it does not already exist: " + directory); | |
131 |
1
1. makeDirectory : removed call to com/renomad/minum/utils/FileUtils::innerCreateDirectory → KILLED |
innerCreateDirectory(directory); |
132 | logger.logDebug(() -> "Directory: " + directory + " created"); | |
133 | } | |
134 | } | |
135 | ||
136 | static void innerCreateDirectory(Path directory) { | |
137 | try { | |
138 | Files.createDirectories(directory); | |
139 | } catch (Exception e) { | |
140 | throw new UtilsException(e); | |
141 | } | |
142 | } | |
143 | ||
144 | /** | |
145 | * Read a binary file, return as a byte array | |
146 | * <br> | |
147 | * <p> | |
148 | * <em>Note: This does *not* protect against untrusted data on its own. Call {@link #safeResolve(String, String)} first against | |
149 | * the path to ensure it uses valid characters and prevent it escaping the expected directory.</em> | |
150 | * </p> | |
151 | * <p> | |
152 | * If there is an error, this will return an empty byte array. | |
153 | * </p> | |
154 | */ | |
155 | public byte[] readBinaryFile(String path) { | |
156 | try { | |
157 |
1
1. readBinaryFile : replaced return value with null for com/renomad/minum/utils/FileUtils::readBinaryFile → KILLED |
return fileReader.readFile(path); |
158 | } catch (IOException e) { | |
159 | logger.logDebug(() -> String.format("Error while reading file %s, returning empty byte array. %s", path, e)); | |
160 |
1
1. readBinaryFile : replaced return value with null for com/renomad/minum/utils/FileUtils::readBinaryFile → KILLED |
return new byte[0]; |
161 | } | |
162 | } | |
163 | ||
164 | /** | |
165 | * Read a text file from the given path, return as a string. | |
166 | * <br> | |
167 | * <p> | |
168 | * <em>Note: This does *not* protect against untrusted data on its own. Call {@link #safeResolve(String, String)} first against | |
169 | * the path to ensure it uses valid characters and prevent it escaping the expected directory.</em> | |
170 | * </p> | |
171 | * <p> | |
172 | * If there is an error, this will return an empty string. | |
173 | * </p> | |
174 | */ | |
175 | public String readTextFile(String path) { | |
176 | try { | |
177 |
1
1. readTextFile : replaced return value with "" for com/renomad/minum/utils/FileUtils::readTextFile → KILLED |
return new String(fileReader.readFile(path), StandardCharsets.UTF_8); |
178 | } catch (IOException e) { | |
179 | logger.logDebug(() -> String.format("Error while reading file %s, returning empty string. %s", path, e)); | |
180 | return ""; | |
181 | } | |
182 | } | |
183 | ||
184 | /** | |
185 | * This method is to provide assurance that the file specified by the path | |
186 | * parameter is within the directory specified by directoryPath. Use this | |
187 | * for any code that reads from files where the user provides untrusted input. | |
188 | * @throws InvariantException if the file is not within the directory | |
189 | */ | |
190 | public static void checkFileIsWithinDirectory(String path, String directoryPath) { | |
191 | Path directoryRealPath; | |
192 | Path fullRealPath; | |
193 | try { | |
194 | directoryRealPath = Path.of(directoryPath).toRealPath(LinkOption.NOFOLLOW_LINKS); | |
195 | fullRealPath = directoryRealPath.resolve(path).toRealPath(LinkOption.NOFOLLOW_LINKS); | |
196 | } catch (IOException ex) { | |
197 | throw new InvariantException(ex.toString()); | |
198 | } | |
199 |
1
1. checkFileIsWithinDirectory : negated conditional → KILLED |
if (! fullRealPath.startsWith(directoryRealPath)) { |
200 | throw new InvariantException(String.format("path (%s) was not within directory (%s)", path, directoryPath)); | |
201 | } | |
202 | } | |
203 | ||
204 | /** | |
205 | * Checks that the path string avoids bad patterns and meets our | |
206 | * whitelist for acceptable characters. | |
207 | * @throws InvariantException if there are any issues with the path string, such | |
208 | * as being an empty string, containing known bad patterns from {@link #badFilePathPatterns}, | |
209 | * or including characters other than the set of characters we will allow for filenames. | |
210 | * It is a simple set of ascii characters - alphanumerics, underscore, dash, period, | |
211 | * forward and backward slash. | |
212 | */ | |
213 | public static void checkForBadFilePatterns(String path) { | |
214 |
1
1. checkForBadFilePatterns : negated conditional → KILLED |
if (path.isBlank()) { |
215 | throw new InvariantException("filename was empty"); | |
216 | } | |
217 |
1
1. checkForBadFilePatterns : negated conditional → KILLED |
if (badFilePathPatterns.matcher(path).find()) { |
218 | throw new InvariantException("filename ("+path+") contained invalid characters"); | |
219 | } | |
220 | for (char c : path.toCharArray()) { | |
221 |
17
1. checkForBadFilePatterns : changed conditional boundary → SURVIVED 2. checkForBadFilePatterns : changed conditional boundary → KILLED 3. checkForBadFilePatterns : negated conditional → KILLED 4. checkForBadFilePatterns : changed conditional boundary → KILLED 5. checkForBadFilePatterns : negated conditional → KILLED 6. checkForBadFilePatterns : changed conditional boundary → KILLED 7. checkForBadFilePatterns : negated conditional → KILLED 8. checkForBadFilePatterns : negated conditional → KILLED 9. checkForBadFilePatterns : changed conditional boundary → KILLED 10. checkForBadFilePatterns : negated conditional → KILLED 11. checkForBadFilePatterns : negated conditional → KILLED 12. checkForBadFilePatterns : negated conditional → KILLED 13. checkForBadFilePatterns : changed conditional boundary → KILLED 14. checkForBadFilePatterns : negated conditional → KILLED 15. checkForBadFilePatterns : negated conditional → KILLED 16. checkForBadFilePatterns : negated conditional → KILLED 17. checkForBadFilePatterns : negated conditional → KILLED |
boolean isWhitelistedChar = c >= 'A' && c <= 'Z' || c >= 'a' && c<= 'z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.' || c == '\\' || c == '/'; |
222 |
1
1. checkForBadFilePatterns : negated conditional → KILLED |
if (! isWhitelistedChar) { |
223 | throw new InvariantException("filename ("+path+") contained invalid characters ("+c+"). Allowable characters are alpha-numeric ascii both cases, underscore, forward and backward-slash, period, and dash"); | |
224 | } | |
225 | } | |
226 | } | |
227 | ||
228 | /** | |
229 | * This helper method will ensure that the requested path is | |
230 | * within the parent directory and using safe characters | |
231 | */ | |
232 | public static Path safeResolve(String parentDirectory, String path) { | |
233 |
1
1. safeResolve : removed call to com/renomad/minum/utils/FileUtils::checkForBadFilePatterns → KILLED |
checkForBadFilePatterns(path); |
234 |
1
1. safeResolve : removed call to com/renomad/minum/utils/FileUtils::checkFileIsWithinDirectory → SURVIVED |
checkFileIsWithinDirectory(path, parentDirectory); |
235 |
1
1. safeResolve : replaced return value with null for com/renomad/minum/utils/FileUtils::safeResolve → KILLED |
return Path.of(parentDirectory).resolve(path); |
236 | } | |
237 | ||
238 | } | |
Mutations | ||
69 |
1.1 |
|
91 |
1.1 |
|
94 |
1.1 |
|
106 |
1.1 |
|
129 |
1.1 |
|
131 |
1.1 |
|
157 |
1.1 |
|
160 |
1.1 |
|
177 |
1.1 |
|
199 |
1.1 |
|
214 |
1.1 |
|
217 |
1.1 |
|
221 |
1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 10.10 11.11 12.12 13.13 14.14 15.15 16.16 17.17 |
|
222 |
1.1 |
|
233 |
1.1 |
|
234 |
1.1 |
|
235 |
1.1 |