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.Path;
10
import java.util.*;
11
import java.util.regex.Pattern;
12
import java.util.stream.Stream;
13
14
/**
15
 * Helper functions for working with files.
16
 * <br>
17
 * In all these functions, note that it is disallowed to request a path
18
 * having certain characters - see {@link #badFilePathPatterns}
19
 */
20
public final class FileUtils {
21
22
    /**
23
     * These patterns can be used in path strings to access files higher in
24
     * the directory structure.  We disallow this, as a security precaution.
25
     * <ul>
26
     * <li>1st Alternative {@code //} - This prevents going to the root directory
27
     * <li>2nd Alternative {@code ..} - prevents going up a directory
28
     * <li>3rd Alternative {@code :} - prevents certain special paths, like "C:" or "file://"
29
     * </ul>
30
     */
31
    public static final Pattern badFilePathPatterns = Pattern.compile("//|\\.\\.|:");
32
    private final ILogger logger;
33
    private final IFileReader fileReader;
34
35
    public FileUtils(ILogger logger, Constants constants) {
36
        this(
37
                logger,
38
                new FileReader(
39
                        LRUCache.getLruCache(constants.maxElementsLruCacheStaticFiles),
40
                        constants.useCacheForStaticFiles,
41
                        logger));
42
    }
43
44
    /**
45
     * This version of the constructor is mainly for testing
46
     */
47
    FileUtils(ILogger logger, IFileReader fileReader) {
48
        this.logger = logger;
49
        this.fileReader = fileReader;
50
    }
51
52
    /**
53
     * Write a string to a path on disk.
54
     * <p>
55
     *     Parent directories are made unavailable by searching the path for
56
     *     bad characters.  See {@link #badFilePathPatterns}
57
     * </p>
58
     */
59
    public void writeString(Path path, String content) {
60 1 1. writeString : negated conditional → TIMED_OUT
        if (path.toString().isEmpty()) {
61
            logger.logDebug(() -> "an empty path was provided to writeString");
62
            return;
63
        }
64 1 1. writeString : negated conditional → TIMED_OUT
        if (badFilePathPatterns.matcher(path.toString()).find()) {
65
            logger.logDebug(() -> String.format("Bad path requested at writeString: %s", path));
66
            return;
67
        }
68
        try {
69
            Files.writeString(path, content);
70
        } catch (IOException e) {
71
            throw new UtilsException(e);
72
        }
73
    }
74
75
    /**
76
     * Deletes a directory, deleting everything inside it
77
     * recursively afterwards.  A more dangerous method than
78
     * many others, take care.
79
     * <p>
80
     *     Parent directories are made unavailable by searching the path for
81
     *     bad characters.  See {@link #badFilePathPatterns}
82
     * </p>
83
     */
84
    public void deleteDirectoryRecursivelyIfExists(Path myPath) {
85 1 1. deleteDirectoryRecursivelyIfExists : negated conditional → KILLED
        if (badFilePathPatterns.matcher(myPath.toString()).find()) {
86
            logger.logDebug(() -> String.format("Bad path requested at deleteDirectoryRecursivelyIfExists: %s", myPath));
87
            return;
88
        }
89 1 1. deleteDirectoryRecursivelyIfExists : negated conditional → KILLED
        if (!Files.exists(myPath)) {
90
            logger.logDebug(() -> "system was requested to delete directory: "+myPath+", but it did not exist");
91
        } else {
92 1 1. deleteDirectoryRecursivelyIfExists : removed call to com/renomad/minum/utils/FileUtils::walkPathDeleting → KILLED
            walkPathDeleting(myPath);
93
        }
94
    }
95
96
    void walkPathDeleting(Path myPath) {
97
        try (Stream<Path> walk = Files.walk(myPath)) {
98
99
            final var files = walk.sorted(Comparator.reverseOrder())
100
                    .map(Path::toFile).toList();
101
102
            for(var file: files) {
103
                logger.logDebug(() -> "deleting " + file);
104 1 1. walkPathDeleting : removed call to java/nio/file/Files::delete → KILLED
                Files.delete(file.toPath());
105
            }
106
        } catch (IOException ex) {
107
            throw new UtilsException("Error during deleteDirectoryRecursivelyIfExists: " + ex);
108
        }
109
    }
110
111
    /**
112
     * Creates a directory if it doesn't already exist.
113
     * <p>
114
     *     Parent directories are made unavailable by searching the path for
115
     *     bad characters.  See {@link #badFilePathPatterns}
116
     * </p>
117
     * <p>
118
     * If the directory does exist, the program will simply skip
119
     * building it, and mention it in the logs.
120
     * </p>
121
     */
122
    public void makeDirectory(Path directory) {
123 1 1. makeDirectory : negated conditional → TIMED_OUT
        if (badFilePathPatterns.matcher(directory.toString()).find()) {
124
            logger.logDebug(() -> String.format("Bad path requested at makeDirectory: %s", directory));
125
            return;
126
        }
127
        logger.logDebug(() -> "Creating a directory " + directory);
128
        boolean directoryExists = Files.exists(directory);
129
        logger.logDebug(() -> "Directory: " + directory + ". Already exists: " + directory);
130 1 1. makeDirectory : negated conditional → TIMED_OUT
        if (!directoryExists) {
131
            logger.logDebug(() -> "Creating directory, since it does not already exist: " + directory);
132 1 1. makeDirectory : removed call to com/renomad/minum/utils/FileUtils::innerCreateDirectory → KILLED
            innerCreateDirectory(directory);
133
            logger.logDebug(() -> "Directory: " + directory + " created");
134
        }
135
    }
136
137
    static void innerCreateDirectory(Path directory) {
138
        try {
139
            Files.createDirectories(directory);
140
        } catch (Exception e) {
141
            throw new UtilsException(e);
142
        }
143
    }
144
145
    /**
146
     * Read a binary file, return as a byte array
147
     * <p>
148
     *     If there is an error, this will return an empty byte array.
149
     * </p>
150
     */
151
    public byte[] readBinaryFile(String path) {
152
        try {
153 1 1. readBinaryFile : replaced return value with null for com/renomad/minum/utils/FileUtils::readBinaryFile → KILLED
            return fileReader.readFile(path);
154
        } catch (IOException e) {
155
            logger.logDebug(() -> String.format("Error while reading file %s, returning empty byte array. %s", path, e));
156 1 1. readBinaryFile : replaced return value with null for com/renomad/minum/utils/FileUtils::readBinaryFile → KILLED
            return new byte[0];
157
        }
158
    }
159
160
    /**
161
     * Read a text file from the given path, return as a string.
162
     *
163
     * <p>
164
     *     Access is prevented to data in parent directories or using alternate
165
     *     drives.  If the data is read, it will be added to a cache, if
166
     *     the property {@link Constants#useCacheForStaticFiles} is set to true. The maximum
167
     *     size of the cache is controlled by
168
     * </p>
169
     * <p>
170
     *     If there is an error, this will return an empty string.
171
     * </p>
172
     */
173
    public String readTextFile(String path) {
174
        try {
175 1 1. readTextFile : replaced return value with "" for com/renomad/minum/utils/FileUtils::readTextFile → KILLED
            return new String(fileReader.readFile(path), StandardCharsets.UTF_8);
176
        } catch (IOException e) {
177
            logger.logDebug(() -> String.format("Error while reading file %s, returning empty string. %s", path, e));
178
            return "";
179
        }
180
    }
181
182
}

Mutations

60

1.1
Location : writeString
Killed by : none
negated conditional → TIMED_OUT

64

1.1
Location : writeString
Killed by : none
negated conditional → TIMED_OUT

85

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

89

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

92

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

104

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

123

1.1
Location : makeDirectory
Killed by : none
negated conditional → TIMED_OUT

130

1.1
Location : makeDirectory
Killed by : none
negated conditional → TIMED_OUT

132

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

153

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 : readBinaryFile
Killed by : com.renomad.minum.utils.FileUtilsTests
replaced return value with null for com/renomad/minum/utils/FileUtils::readBinaryFile → KILLED

175

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

Active mutators

Tests examined


Report generated by PIT 1.17.0