FileUtils.java

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

Mutations

48

1.1
Location : writeString
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
negated conditional → KILLED

70

1.1
Location : deleteDirectoryRecursivelyIfExists
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

73

1.1
Location : deleteDirectoryRecursivelyIfExists
Killed by : com.renomad.minum.utils.FileUtilsTests
removed call to com/renomad/minum/utils/FileUtils::walkPathDeleting → KILLED

85

1.1
Location : walkPathDeleting
Killed by : com.renomad.minum.utils.FileUtilsTests
removed call to java/nio/file/Files::delete → KILLED

108

1.1
Location : makeDirectory
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
negated conditional → KILLED

110

1.1
Location : makeDirectory
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/utils/FileUtils::innerCreateDirectory → KILLED

136

1.1
Location : readBinaryFile
Killed by : com.renomad.minum.utils.FileUtilsTests
replaced return value with null for com/renomad/minum/utils/FileUtils::readBinaryFile → KILLED

139

1.1
Location : readBinaryFile
Killed by : com.renomad.minum.utils.FileUtilsTests
replaced return value with null for com/renomad/minum/utils/FileUtils::readBinaryFile → KILLED

156

1.1
Location : readTextFile
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with "" for com/renomad/minum/utils/FileUtils::readTextFile → KILLED

178

1.1
Location : checkFileIsWithinDirectory
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

193

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

197

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

2.2
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

203

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileUtilsTests
changed conditional boundary → KILLED

2.2
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

3.3
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

4.4
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.FunctionalTests
changed conditional boundary → KILLED

5.5
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

6.6
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

7.7
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

8.8
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.htmlparsing.HtmlParserTests
changed conditional boundary → KILLED

9.9
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

10.10
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.WebTests
changed conditional boundary → KILLED

11.11
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.WebFrameworkTests.test_Edge_ApplicationOctetStream(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

12.12
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

13.13
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

14.14
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.FunctionalTests
changed conditional boundary → KILLED

15.15
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

16.16
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_HTML(com.renomad.minum.web.WebFrameworkTests)
changed conditional boundary → KILLED

17.17
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileUtilsTests
negated conditional → KILLED

204

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

207

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

208

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

215

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

216

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

231

1.1
Location : safeResolve
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
removed call to com/renomad/minum/utils/FileUtils::checkForBadFilePatterns → KILLED

232

1.1
Location : safeResolve
Killed by : none
removed call to com/renomad/minum/utils/FileUtils::checkFileIsWithinDirectory → SURVIVED
Covering tests

233

1.1
Location : safeResolve
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_BadPathRequested(com.renomad.minum.web.ResponseTests)
replaced return value with null for com/renomad/minum/utils/FileUtils::safeResolve → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0