FileUtils.java

1
package com.renomad.minum.utils;
2
3
import com.renomad.minum.security.ForbiddenUseException;
4
import com.renomad.minum.state.Constants;
5
import com.renomad.minum.logging.ILogger;
6
7
import java.io.BufferedReader;
8
import java.io.BufferedWriter;
9
import java.io.IOException;
10
import java.nio.charset.Charset;
11
import java.nio.charset.StandardCharsets;
12
import java.nio.file.*;
13
import java.util.*;
14
import java.util.stream.Stream;
15
16
/**
17
 * Helper functions for working with files.
18
 */
19
public final class FileUtils implements IFileUtils {
20
21
    private final ILogger logger;
22
    private final IFileReader fileReader;
23
24
    public FileUtils(ILogger logger, Constants constants) {
25
        this(
26
                logger,
27
                new FileReader(
28
                        LRUCache.getLruCache(constants.maxElementsLruCacheStaticFiles),
29
                        constants.useCacheForStaticFiles,
30
                        logger));
31
    }
32
33
    /**
34
     * This version of the constructor is mainly for testing
35
     */
36
    FileUtils(ILogger logger, IFileReader fileReader) {
37
        this.logger = logger;
38
        this.fileReader = fileReader;
39
    }
40
41
    @Override
42
    public void writeString(Path path, String content, OpenOption... options) throws IOException {
43 1 1. writeString : negated conditional → KILLED
        if (path.toString().isEmpty()) {
44
            throw new UtilsException("an empty path was provided to writeString");
45
        }
46
        Files.writeString(path, content, options);
47
    }
48
49
    @Override
50
    public Path write(Path path, Iterable<? extends CharSequence> lines,
51
                      Charset cs, OpenOption... options) throws IOException {
52 1 1. write : replaced return value with null for com/renomad/minum/utils/FileUtils::write → KILLED
        return Files.write(path, lines, cs, options);
53
    }
54
55
    @Override
56
    public String readString(Path path) throws IOException {
57 1 1. readString : negated conditional → KILLED
        if (path.toString().isEmpty()) {
58
            throw new UtilsException("an empty path was provided to readString");
59
        }
60 1 1. readString : replaced return value with "" for com/renomad/minum/utils/FileUtils::readString → KILLED
        return Files.readString(path);
61
    }
62
63
    @Override
64
    public void deleteDirectoryRecursivelyIfExists(Path myPath) throws IOException {
65 1 1. deleteDirectoryRecursivelyIfExists : negated conditional → KILLED
        if (!Files.exists(myPath)) {
66
            logger.logDebug(() -> "system was requested to delete directory: "+myPath+", but it did not exist");
67
        } else {
68 1 1. deleteDirectoryRecursivelyIfExists : removed call to com/renomad/minum/utils/FileUtils::walkPathDeleting → KILLED
            walkPathDeleting(myPath);
69
        }
70
    }
71
72
    void walkPathDeleting(Path myPath) throws IOException {
73
        try (Stream<Path> walk = Files.walk(myPath)) {
74
75
            final var files = walk.sorted(Comparator.reverseOrder())
76
                    .map(Path::toFile).toList();
77
78
            for (var file : files) {
79
                logger.logTrace(() -> "deleting " + file);
80 1 1. walkPathDeleting : removed call to java/nio/file/Files::delete → KILLED
                Files.delete(file.toPath());
81
            }
82
        }
83
    }
84
85
    @Override
86
    public void makeDirectory(Path directory) throws IOException {
87
        logger.logDebug(() -> "Creating a directory " + directory);
88
        boolean directoryExists = Files.exists(directory);
89 1 1. makeDirectory : negated conditional → KILLED
        if (directoryExists) {
90
            logger.logDebug(() -> "Directory: (" + directory + ") Already exists. Returning.");
91
        } else {
92 1 1. makeDirectory : removed call to com/renomad/minum/utils/FileUtils::innerCreateDirectory → KILLED
            innerCreateDirectory(directory);
93
            logger.logDebug(() -> "Directory: " + directory + " created");
94
        }
95
    }
96
97
    void innerCreateDirectory(Path directory) throws IOException {
98 1 1. innerCreateDirectory : negated conditional → KILLED
        if (directory == null) throw new IllegalArgumentException("directory parameter is disallowed to be null when creating a directory");
99
        Files.createDirectories(directory);
100
    }
101
102
    @Override
103
    public byte[] readBinaryFile(String path) throws IOException {
104 1 1. readBinaryFile : replaced return value with null for com/renomad/minum/utils/FileUtils::readBinaryFile → KILLED
        return fileReader.readFile(path);
105
    }
106
107
    @Override
108
    public List<String> readAllLines(Path path) throws IOException {
109 1 1. readAllLines : replaced return value with Collections.emptyList for com/renomad/minum/utils/FileUtils::readAllLines → KILLED
        return Files.readAllLines(path);
110
    }
111
112
    @Override
113
    public String readTextFile(String path) throws IOException {
114 1 1. readTextFile : replaced return value with "" for com/renomad/minum/utils/FileUtils::readTextFile → KILLED
        return new String(fileReader.readFile(path), StandardCharsets.UTF_8);
115
    }
116
117
    @Override
118
    public void checkFileIsWithinDirectory(String path, String directoryPath) throws IOException {
119
        Path directoryRealPath;
120
        Path fullRealPath;
121
        directoryRealPath = Path.of(directoryPath).toRealPath(LinkOption.NOFOLLOW_LINKS);
122
        fullRealPath = directoryRealPath.resolve(path).toRealPath(LinkOption.NOFOLLOW_LINKS);
123 1 1. checkFileIsWithinDirectory : negated conditional → KILLED
        if (! fullRealPath.startsWith(directoryRealPath)) {
124
            throw new ForbiddenUseException(String.format("path (%s) was not within directory (%s)", path, directoryPath));
125
        }
126
    }
127
128
    /**
129
     * Checks that the path string avoids bad patterns and meets our
130
     * whitelist for acceptable characters.
131
     * @throws IllegalArgumentException if the input is blank
132
     * @throws ForbiddenUseException if the path parameter contains known bad patterns
133
     *                            or includes characters other than the set of characters we will allow for filenames.
134
     *                            It is a small set of ascii characters - alphanumerics, underscore, dash, period,
135
     *                            forward and backward slash.
136
     */
137
    public static void checkForBadFilePatterns(String path) {
138 1 1. checkForBadFilePatterns : negated conditional → KILLED
        if (path.isBlank()) {
139
            throw new IllegalArgumentException("path was blank");
140
        }
141
        char firstChar = path.charAt(0);
142 2 1. checkForBadFilePatterns : negated conditional → KILLED
2. checkForBadFilePatterns : negated conditional → KILLED
        if (firstChar == '\\' || firstChar == '/') {
143
            throw new ForbiddenUseException("filename ("+path+") contained invalid characters");
144
        }
145
        boolean isPreviousCharDot = false;
146
        boolean isPreviousCharSlash = false;
147 2 1. checkForBadFilePatterns : negated conditional → KILLED
2. checkForBadFilePatterns : changed conditional boundary → KILLED
        for (int i = 0; i < path.length(); i++) {
148
            char c = path.charAt(i);
149 17 1. checkForBadFilePatterns : changed conditional boundary → TIMED_OUT
2. checkForBadFilePatterns : changed conditional boundary → KILLED
3. checkForBadFilePatterns : negated conditional → KILLED
4. checkForBadFilePatterns : changed conditional boundary → KILLED
5. checkForBadFilePatterns : changed conditional boundary → KILLED
6. checkForBadFilePatterns : negated conditional → KILLED
7. checkForBadFilePatterns : changed conditional boundary → KILLED
8. checkForBadFilePatterns : negated conditional → KILLED
9. checkForBadFilePatterns : negated conditional → KILLED
10. checkForBadFilePatterns : negated conditional → KILLED
11. checkForBadFilePatterns : negated conditional → KILLED
12. checkForBadFilePatterns : negated conditional → KILLED
13. checkForBadFilePatterns : negated conditional → KILLED
14. checkForBadFilePatterns : negated conditional → KILLED
15. checkForBadFilePatterns : changed conditional boundary → 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' ||
150
                    c == '-' || c == '_' || c == '.' || c == '\\' || c == '/';
151 1 1. checkForBadFilePatterns : negated conditional → KILLED
            if (!isWhitelistedChar) {
152
                throw new ForbiddenUseException("filename (" + path + ") contained invalid characters (" + c + ").  Allowable characters are alpha-numeric ascii both cases, underscore, forward and backward-slash, period, and dash");
153
            }
154 1 1. checkForBadFilePatterns : negated conditional → KILLED
            if (c == '.') {
155 1 1. checkForBadFilePatterns : negated conditional → KILLED
                if (isPreviousCharDot) {
156
                    throw new ForbiddenUseException("filename ("+path+") contained invalid characters");
157
                }
158
                isPreviousCharDot = true;
159
            } else {
160
                isPreviousCharDot = false;
161
            }
162 1 1. checkForBadFilePatterns : negated conditional → KILLED
            if (c == '/') {
163 1 1. checkForBadFilePatterns : negated conditional → KILLED
                if (isPreviousCharSlash) {
164
                    throw new ForbiddenUseException("filename ("+path+") contained invalid characters");
165
                }
166
                isPreviousCharSlash = true;
167
            } else {
168
                isPreviousCharSlash = false;
169
            }
170
        }
171
    }
172
173
    @Override
174
    public Path safeResolve(String parentDirectory, String path) throws IOException {
175 1 1. safeResolve : removed call to com/renomad/minum/utils/FileUtils::checkForBadFilePatterns → KILLED
        checkForBadFilePatterns(path);
176 1 1. safeResolve : removed call to com/renomad/minum/utils/FileUtils::checkFileIsWithinDirectory → SURVIVED
        checkFileIsWithinDirectory(path, parentDirectory);
177 1 1. safeResolve : replaced return value with null for com/renomad/minum/utils/FileUtils::safeResolve → KILLED
        return Path.of(parentDirectory).resolve(path);
178
    }
179
180
    @Override
181
    public void delete(Path path) throws IOException {
182 1 1. delete : removed call to java/nio/file/Files::delete → TIMED_OUT
        Files.delete(path);
183
    }
184
185
    @Override
186
    public void move(Path source, Path target, CopyOption... options) throws IOException {
187
        Files.move(source, target, options);
188
    }
189
190
    @Override
191
    public boolean exists(Path path, LinkOption... options) {
192 2 1. exists : replaced boolean return with true for com/renomad/minum/utils/FileUtils::exists → KILLED
2. exists : replaced boolean return with false for com/renomad/minum/utils/FileUtils::exists → KILLED
        return Files.exists(path, options);
193
    }
194
195
    @Override
196
    public BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options) throws IOException {
197 1 1. newBufferedWriter : replaced return value with null for com/renomad/minum/utils/FileUtils::newBufferedWriter → KILLED
        return Files.newBufferedWriter(path, cs, options);
198
    }
199
200
    @Override
201
    public BufferedReader newBufferedReader(Path path, Charset cs) throws IOException {
202 1 1. newBufferedReader : replaced return value with null for com/renomad/minum/utils/FileUtils::newBufferedReader → KILLED
        return Files.newBufferedReader(path, cs);
203
    }
204
205
    @Override
206
    public Stream<Path> walk(Path start, FileVisitOption... options) throws IOException {
207 1 1. walk : replaced return value with Stream.empty for com/renomad/minum/utils/FileUtils::walk → KILLED
        return Files.walk(start, options);
208
    }
209
210
    @Override
211
    public boolean isRegularFile(Path path, LinkOption... options) {
212 2 1. isRegularFile : replaced boolean return with true for com/renomad/minum/utils/FileUtils::isRegularFile → KILLED
2. isRegularFile : replaced boolean return with false for com/renomad/minum/utils/FileUtils::isRegularFile → KILLED
        return Files.isRegularFile(path, options);
213
    }
214
215
    @Override
216
    public Stream<String> lines(Path path, Charset cs) throws IOException {
217 1 1. lines : replaced return value with Stream.empty for com/renomad/minum/utils/FileUtils::lines → TIMED_OUT
        return Files.lines(path, cs);
218
    }
219
220
    @Override
221
    public boolean deleteIfExists(Path path) throws IOException {
222 2 1. deleteIfExists : replaced boolean return with false for com/renomad/minum/utils/FileUtils::deleteIfExists → KILLED
2. deleteIfExists : replaced boolean return with true for com/renomad/minum/utils/FileUtils::deleteIfExists → KILLED
        return Files.deleteIfExists(path);
223
    }
224
225
    @Override
226
    public long size(Path path) throws IOException {
227 1 1. size : replaced long return with 0 for com/renomad/minum/utils/FileUtils::size → KILLED
        return Files.size(path);
228
    }
229
230
    @Override
231
    public Stream<Path> list(Path dir) throws IOException {
232 1 1. list : replaced return value with Stream.empty for com/renomad/minum/utils/FileUtils::list → KILLED
        return Files.list(dir);
233
    }
234
235
}

Mutations

43

1.1
Location : writeString
Killed by : com.renomad.minum.utils.FileUtilsTests
negated conditional → KILLED

52

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

57

1.1
Location : readString
Killed by : com.renomad.minum.database.ChecksumUtilityTests
negated conditional → KILLED

60

1.1
Location : readString
Killed by : com.renomad.minum.security.TheBrigTests
replaced return value with "" for com/renomad/minum/utils/FileUtils::readString → KILLED

65

1.1
Location : deleteDirectoryRecursivelyIfExists
Killed by : com.renomad.minum.database.DatabaseConsolidatorTests
negated conditional → KILLED

68

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

80

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

89

1.1
Location : makeDirectory
Killed by : com.renomad.minum.database.ChecksumUtilityTests
negated conditional → KILLED

92

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

98

1.1
Location : innerCreateDirectory
Killed by : com.renomad.minum.database.ChecksumUtilityTests
negated conditional → KILLED

104

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

109

1.1
Location : readAllLines
Killed by : com.renomad.minum.database.DbFileConverterTests
replaced return value with Collections.emptyList for com/renomad/minum/utils/FileUtils::readAllLines → KILLED

114

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

123

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

138

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

142

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

2.2
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

147

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

2.2
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
changed conditional boundary → KILLED

149

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_IOExceptionThrown_WebFramework(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

2.2
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

3.3
Location : checkForBadFilePatterns
Killed by : none
changed conditional boundary → TIMED_OUT

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

5.5
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

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

7.7
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_IOExceptionThrown_WebFramework(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

8.8
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests
negated conditional → KILLED

9.9
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

10.10
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

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

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

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

14.14
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

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

16.16
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests
negated conditional → KILLED

17.17
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.web.ResponseTests
negated conditional → KILLED

151

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

154

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

155

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

162

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

163

1.1
Location : checkForBadFilePatterns
Killed by : com.renomad.minum.utils.FileReaderTests
negated conditional → KILLED

175

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

176

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

177

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

182

1.1
Location : delete
Killed by : none
removed call to java/nio/file/Files::delete → TIMED_OUT

192

1.1
Location : exists
Killed by : com.renomad.minum.database.ChecksumUtilityTests
replaced boolean return with true for com/renomad/minum/utils/FileUtils::exists → KILLED

2.2
Location : exists
Killed by : com.renomad.minum.database.ChecksumUtilityTests
replaced boolean return with false for com/renomad/minum/utils/FileUtils::exists → KILLED

197

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

202

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

207

1.1
Location : walk
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_IOExceptionThrown_WebFramework(com.renomad.minum.FunctionalTests)
replaced return value with Stream.empty for com/renomad/minum/utils/FileUtils::walk → KILLED

212

1.1
Location : isRegularFile
Killed by : com.renomad.minum.database.DbFileConverterTests
replaced boolean return with true for com/renomad/minum/utils/FileUtils::isRegularFile → KILLED

2.2
Location : isRegularFile
Killed by : com.renomad.minum.database.DbFileConverterTests
replaced boolean return with false for com/renomad/minum/utils/FileUtils::isRegularFile → KILLED

217

1.1
Location : lines
Killed by : none
replaced return value with Stream.empty for com/renomad/minum/utils/FileUtils::lines → TIMED_OUT

222

1.1
Location : deleteIfExists
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_Response_MultiCookies(com.renomad.minum.FunctionalTests)
replaced boolean return with false for com/renomad/minum/utils/FileUtils::deleteIfExists → KILLED

2.2
Location : deleteIfExists
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced boolean return with true for com/renomad/minum/utils/FileUtils::deleteIfExists → KILLED

227

1.1
Location : size
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced long return with 0 for com/renomad/minum/utils/FileUtils::size → KILLED

232

1.1
Location : list
Killed by : com.renomad.minum.database.DbFileConverterTests
replaced return value with Stream.empty for com/renomad/minum/utils/FileUtils::list → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0