1 // Copyright (C) 2009 Red Hat, Inc.
2 //
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Lesser General Public
5 // License as published by the Free Software Foundation; either
6 // version 2.1 of the License, or (at your option) any later version.
7 //
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 // Lesser General Public License for more details.
12 //
13 // You should have received a copy of the GNU Lesser General Public
14 // License along with this library; if not, write to the Free Software
15 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
16 
17 package net.sourceforge.jnlp.util;
18 
19 import java.awt.Component;
20 import static net.sourceforge.jnlp.runtime.Translator.R;
21 
22 import java.io.BufferedReader;
23 import java.io.BufferedWriter;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileNotFoundException;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.io.OutputStreamWriter;
32 import java.io.RandomAccessFile;
33 import java.io.Reader;
34 import java.io.Writer;
35 import java.nio.channels.FileChannel;
36 import java.nio.channels.FileLock;
37 import java.security.DigestInputStream;
38 import java.security.MessageDigest;
39 import java.security.NoSuchAlgorithmException;
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 import javax.swing.JFrame;
44 import javax.swing.JOptionPane;
45 import javax.swing.SwingUtilities;
46 
47 import net.sourceforge.jnlp.config.DirectoryValidator;
48 import net.sourceforge.jnlp.config.DirectoryValidator.DirectoryCheckResults;
49 import net.sourceforge.jnlp.runtime.JNLPRuntime;
50 import net.sourceforge.jnlp.util.logging.OutputController;
51 
52 /**
53  * This class contains a few file-related utility functions.
54  *
55  * @author Omair Majid
56  */
57 
58 public final class FileUtils {
59 
60     /**
61      * Indicates whether a file was successfully opened. If not, provides specific reasons
62      * along with a general failure case
63      */
64     public enum OpenFileResult {
65         /** The file was successfully opened */
66         SUCCESS,
67         /** The file could not be opened, for non-specified reasons */
68         FAILURE,
69         /** The file could not be opened because it did not exist and could not be created */
70         CANT_CREATE,
71         /** The file can be opened but in read-only */
72         CANT_WRITE,
73         /** The specified path pointed to a non-file filesystem object, ie a directory */
74         NOT_FILE;
75     }
76 
77     /**
78      * list of characters not allowed in filenames
79      */
80     public static final char INVALID_CHARS[] = {'\\', '/', ':', '*', '?', '"', '<', '>', '|', '[', ']', '\'', ';', '=', ','};
81 
82     private static final char SANITIZED_CHAR = '_';
83 
84     /**
85      * Clean up a string by removing characters that can't appear in a local
86      * file name.
87      *
88      * @param path
89      *        the path to sanitize
90      * @return a sanitized version of the input which is suitable for using as a
91      *         file path
92      */
sanitizePath(String path)93     public static String sanitizePath(String path) {
94         return sanitizePath(path, SANITIZED_CHAR);
95     }
96 
sanitizePath(String path, char substitute)97     public static String sanitizePath(String path, char substitute) {
98 
99         for (int i = 0; i < INVALID_CHARS.length; i++) {
100             if (INVALID_CHARS[i] != File.separatorChar) {
101                 if (-1 != path.indexOf(INVALID_CHARS[i])) {
102                     path = path.replace(INVALID_CHARS[i], substitute);
103                 }
104             }
105         }
106 
107         return path;
108     }
109 
110     /**
111      * Given an input, return a sanitized form of the input suitable for use as
112      * a file/directory name
113      *
114      * @param filename the filename to sanitize.
115      * @return a sanitized version of the input
116      */
sanitizeFileName(String filename)117     public static String sanitizeFileName(String filename) {
118         return sanitizeFileName(filename, SANITIZED_CHAR);
119     }
120 
sanitizeFileName(String filename, char substitute)121     public static String sanitizeFileName(String filename, char substitute) {
122 
123         for (int i = 0; i < INVALID_CHARS.length; i++) {
124             if (-1 != filename.indexOf(INVALID_CHARS[i])) {
125                 filename = filename.replace(INVALID_CHARS[i], substitute);
126             }
127         }
128 
129         return filename;
130     }
131 
132     /**
133      * Creates a new directory with minimum permissions. The directory is not
134      * readable or writable by anyone other than the owner. The parent
135      * directories are not created; they must exist before this is called.
136      *
137      * @param directory directory to be created
138      * @throws IOException if IO fails
139      */
createRestrictedDirectory(File directory)140     public static void createRestrictedDirectory(File directory) throws IOException {
141         createRestrictedFile(directory, true, true);
142     }
143 
144     /**
145      * Creates a new file with minimum permissions. The file is not readable or
146      * writable by anyone other than the owner. If writeableByOnwer is false,
147      * even the owner can not write to it.
148      *
149      * @param file path to file
150      * @param writableByOwner true if can be writable by owner
151      * @throws IOException if IO fails
152      */
createRestrictedFile(File file, boolean writableByOwner)153     public static void createRestrictedFile(File file, boolean writableByOwner) throws IOException {
154         createRestrictedFile(file, false, writableByOwner);
155     }
156 
157     /**
158      * Tries to create the ancestor directories of file f. Throws
159      * an IOException if it can't be created (but not if it was
160      * already there).
161      * @param f file to provide parent directory
162      * @param eMsg - the message to use for the exception. null
163      * if the file name is to be used.
164      * @throws IOException if the directory can't be created and doesn't exist.
165      */
createParentDir(File f, String eMsg)166     public static void createParentDir(File f, String eMsg) throws IOException {
167         File parent = f.getParentFile();
168         if (!parent.isDirectory() && !parent.mkdirs()) {
169             throw new IOException(R("RCantCreateDir",
170                     eMsg == null ? parent : eMsg));
171         }
172     }
173 
174     /**
175      * Tries to create the ancestor directories of file f. Throws
176      * an IOException if it can't be created (but not if it was
177      * already there).
178      * @param f file which parent will be created
179      * @throws IOException if the directory can't be created and doesn't exist.
180      */
createParentDir(File f)181     public static void createParentDir(File f) throws IOException {
182         createParentDir(f, null);
183     }
184 
185     /**
186      * Tries to delete file f. If the file exists but couldn't be deleted,
187      * print an error message to stderr with the file name, or eMsg if eMsg
188      * is not null.
189      * @param f the file to be deleted
190      * @param eMsg the message to print on failure (or null to print the
191      * the file name).
192      */
deleteWithErrMesg(File f, String eMsg)193     public static void deleteWithErrMesg(File f, String eMsg) {
194         if (f.exists()) {
195             if (!f.delete()) {
196                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("RCantDeleteFile", eMsg == null ? f : eMsg));
197             }
198         }
199     }
200 
201     /**
202      * Tries to delete file f. If the file exists but couldn't be deleted,
203      * print an error message to stderr with the file name.
204      * @param f the file to be deleted
205      */
deleteWithErrMesg(File f)206     public static void deleteWithErrMesg(File f) {
207         deleteWithErrMesg(f, null);
208     }
209 
210     /**
211      * Creates a new file or directory with minimum permissions. The file is not
212      * readable or writable by anyone other than the owner. If writeableByOnwer
213      * is false, even the owner can not write to it. If isDir is true, then the
214      * directory can be executed by the owner
215      *
216      * @throws IOException
217      */
createRestrictedFile(File file, boolean isDir, boolean writableByOwner)218     private static void createRestrictedFile(File file, boolean isDir, boolean writableByOwner) throws IOException {
219 
220         File tempFile = new File(file.getCanonicalPath() + ".temp");
221 
222         if (isDir) {
223             if (!tempFile.mkdir()) {
224                 throw new IOException(R("RCantCreateDir", tempFile));
225             }
226         } else {
227             if (!tempFile.createNewFile()) {
228                 throw new IOException(R("RCantCreateFile", tempFile));
229             }
230         }
231 
232         if (JNLPRuntime.isWindows()) {
233             // remove all permissions
234             if (!tempFile.setExecutable(false, false)) {
235                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("RRemoveXPermFailed", tempFile));
236             }
237             if (!tempFile.setReadable(false, false)) {
238                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("RRemoveRPermFailed", tempFile));
239             }
240             if (!tempFile.setWritable(false, false)) {
241                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("RRemoveWPermFailed", tempFile));
242             }
243 
244             // allow owner to read
245             if (!tempFile.setReadable(true, true)) {
246                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("RGetRPermFailed", tempFile));
247             }
248 
249             // allow owner to write
250             if (writableByOwner && !tempFile.setWritable(true, true)) {
251                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("RGetWPermFailed", tempFile));
252             }
253 
254             // allow owner to enter directories
255             if (isDir && !tempFile.setExecutable(true, true)) {
256                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("RGetXPermFailed", tempFile));
257             }
258             // rename this file. Unless the file is moved/renamed, any program that
259             // opened the file right after it was created might still be able to
260             // read the data.
261             if (!tempFile.renameTo(file)) {
262                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("RCantRename", tempFile, file));
263             }
264         } else {
265         // remove all permissions
266         if (!tempFile.setExecutable(false, false)) {
267             throw new IOException(R("RRemoveXPermFailed", tempFile));
268         }
269         if (!tempFile.setReadable(false, false)) {
270             throw new IOException(R("RRemoveRPermFailed", tempFile));
271         }
272         if (!tempFile.setWritable(false, false)) {
273             throw new IOException(R("RRemoveWPermFailed", tempFile));
274         }
275 
276         // allow owner to read
277         if (!tempFile.setReadable(true, true)) {
278             throw new IOException(R("RGetRPermFailed", tempFile));
279         }
280 
281         // allow owner to write
282         if (writableByOwner && !tempFile.setWritable(true, true)) {
283             throw new IOException(R("RGetWPermFailed", tempFile));
284         }
285 
286         // allow owner to enter directories
287         if (isDir && !tempFile.setExecutable(true, true)) {
288             throw new IOException(R("RGetXPermFailed", tempFile));
289         }
290 
291         // rename this file. Unless the file is moved/renamed, any program that
292         // opened the file right after it was created might still be able to
293         // read the data.
294         if (!tempFile.renameTo(file)) {
295             throw new IOException(R("RCantRename", tempFile, file));
296         }
297         }
298 
299     }
300 
301     /**
302      * Ensure that the parent directory of the file exists and that we are
303      * able to create and access files within this directory
304      * @param file the {@link File} representing a Java Policy file to test
305      * @return a {@link DirectoryCheckResults} object representing the results of the test
306      */
testDirectoryPermissions(File file)307     public static DirectoryCheckResults testDirectoryPermissions(File file) {
308         try {
309             file = file.getCanonicalFile();
310         } catch (final IOException e) {
311             OutputController.getLogger().log(e);
312             return null;
313         }
314         if (file == null || file.getParentFile() == null || !file.getParentFile().exists()) {
315             return null;
316         }
317         final List<File> policyDirectory = new ArrayList<>();
318         policyDirectory.add(file.getParentFile());
319         final DirectoryValidator validator = new DirectoryValidator(policyDirectory);
320         final DirectoryCheckResults result = validator.ensureDirs();
321 
322         return result;
323     }
324 
325     /**
326      * Verify that a given file object points to a real, accessible plain file.
327      * @param file the {@link File} to verify
328      * @return an {@link OpenFileResult} representing the accessibility level of the file
329      */
testFilePermissions(File file)330     public static OpenFileResult testFilePermissions(File file) {
331         if (file == null || !file.exists()) {
332             return OpenFileResult.FAILURE;
333         }
334         try {
335             file = file.getCanonicalFile();
336         } catch (final IOException e) {
337             return OpenFileResult.FAILURE;
338         }
339         final DirectoryCheckResults dcr = FileUtils.testDirectoryPermissions(file);
340         if (dcr != null && dcr.getFailures() == 0) {
341             if (file.isDirectory())
342                 return OpenFileResult.NOT_FILE;
343             try {
344                 if (!file.exists() && !file.createNewFile()) {
345                     return OpenFileResult.CANT_CREATE;
346                 }
347             } catch (IOException e) {
348                 return OpenFileResult.CANT_CREATE;
349             }
350             final boolean read = file.canRead(), write = file.canWrite();
351             if (read && write)
352                 return OpenFileResult.SUCCESS;
353             else if (read)
354                 return OpenFileResult.CANT_WRITE;
355             else
356                 return OpenFileResult.FAILURE;
357         }
358         return OpenFileResult.FAILURE;
359     }
360 
361     /**
362      * Show a dialog informing the user that the file is currently read-only
363      * @param frame a {@link JFrame} to act as parent to this dialog
364      */
showReadOnlyDialog(final Component frame)365     public static void showReadOnlyDialog(final Component frame) {
366         SwingUtilities.invokeLater(new Runnable() {
367             @Override
368             public void run() {
369                 JOptionPane.showMessageDialog(frame, R("RFileReadOnly"), R("Warning"), JOptionPane.WARNING_MESSAGE);
370             }
371         });
372     }
373 
374     /**
375      * Show a generic error dialog indicating the  file could not be opened
376      * @param frame a {@link JFrame} to act as parent to this dialog
377      * @param filePath a {@link String} representing the path to the file we failed to open
378      */
showCouldNotOpenFilepathDialog(final Component frame, final String filePath)379     public static void showCouldNotOpenFilepathDialog(final Component frame, final String filePath) {
380         showCouldNotOpenDialog(frame, R("RCantOpenFile", filePath));
381     }
382 
383     /**
384      * Show an error dialog indicating the file could not be opened, with a particular reason
385      * @param frame a {@link JFrame} to act as parent to this dialog
386      * @param filePath a {@link String} representing the path to the file we failed to open
387      * @param reason a {@link OpenFileResult} specifying more precisely why we failed to open the file
388      */
showCouldNotOpenFileDialog(final Component frame, final String filePath, final OpenFileResult reason)389     public static void showCouldNotOpenFileDialog(final Component frame, final String filePath, final OpenFileResult reason) {
390         final String message;
391         switch (reason) {
392             case CANT_CREATE:
393                 message = R("RCantCreateFile", filePath);
394                 break;
395             case CANT_WRITE:
396                 message = R("RCantWriteFile", filePath);
397                 break;
398             case NOT_FILE:
399                 message = R("RExpectedFile", filePath);
400                 break;
401             default:
402                 message = R("RCantOpenFile", filePath);
403                 break;
404         }
405         showCouldNotOpenDialog(frame, message);
406     }
407 
408     /**
409      * Show a dialog informing the user that the file could not be opened
410      * @param frame a {@link JFrame} to act as parent to this dialog
411      * @param message a {@link String} giving the specific reason the file could not be opened
412      */
showCouldNotOpenDialog(final Component frame, final String message)413     public static void showCouldNotOpenDialog(final Component frame, final String message) {
414         SwingUtilities.invokeLater(new Runnable() {
415             @Override
416             public void run() {
417                 JOptionPane.showMessageDialog(frame, message, R("Error"), JOptionPane.ERROR_MESSAGE);
418             }
419         });
420     }
421 
422     /**
423      * Returns a String that is suitable for using in GUI elements for
424      * displaying (long) paths to users.
425      *
426      * @param path a path that should be shortened
427      * @return a shortened path suitable for displaying to the user
428      */
displayablePath(String path)429     public static String displayablePath(String path) {
430         final int DEFAULT_LENGTH = 40;
431         return displayablePath(path, DEFAULT_LENGTH);
432     }
433 
434     /**
435      * Return a String that is suitable for using in GUI elements for displaying
436      * paths to users. If the path is longer than visibleChars, it is truncated
437      * in a display-friendly way
438      *
439      * @param path a path that should be shorted
440      * @param visibleChars the maximum number of characters that path should fit
441      *        into. Also the length of the returned string
442      * @return a shortened path that contains limited number of chars
443      */
displayablePath(String path, int visibleChars)444     public static String displayablePath(String path, int visibleChars) {
445         /*
446          * use a very simple method: prefix + "..." + suffix
447          *
448          * where prefix is the beginning part of path (as much as we can squeeze in)
449          * and suffix is the end path of path
450          */
451 
452         if (path == null || path.length() <= visibleChars) {
453             return path;
454         }
455 
456         final String OMITTED = "...";
457         final int OMITTED_LENGTH = OMITTED.length();
458         final int MIN_PREFIX_LENGTH = 4;
459         final int MIN_SUFFIX_LENGTH = 4;
460         /*
461          * we want to show things other than OMITTED. if we have too few for
462          * suffix and prefix, then just return as much as we can of the filename
463          */
464         if (visibleChars < (OMITTED_LENGTH + MIN_PREFIX_LENGTH + MIN_SUFFIX_LENGTH)) {
465             return path.substring(path.length() - visibleChars);
466         }
467 
468         int affixLength = (visibleChars - OMITTED_LENGTH) / 2;
469         String prefix = path.substring(0, affixLength);
470         String suffix = path.substring(path.length() - affixLength);
471 
472         return prefix + OMITTED + suffix;
473     }
474 
475     /**
476      * Recursively delete everything under a directory. Works on either files or
477      * directories
478      *
479      * @param file the file object representing what to delete. Can be either a
480      *        file or a directory.
481      * @param base the directory under which the file and its subdirectories must be located
482      * @throws IOException on an io exception or if trying to delete something
483      *         outside the base
484      */
recursiveDelete(File file, File base)485     public static void recursiveDelete(File file, File base) throws IOException {
486         OutputController.getLogger().log(OutputController.Level.ERROR_DEBUG, "Deleting: " + file);
487 
488         if (!(file.getCanonicalPath().startsWith(base.getCanonicalPath()))) {
489             throw new IOException("Trying to delete a file outside Netx's basedir: "
490                     + file.getCanonicalPath());
491         }
492 
493         if (file.isDirectory()) {
494             File[] children = file.listFiles();
495             for (File children1 : children) {
496                 recursiveDelete(children1, base);
497             }
498         }
499         if (!file.delete()) {
500             throw new IOException("Unable to delete file: " + file);
501         }
502 
503     }
504 
505     /**
506      * This will return a lock to the file specified.
507      *
508      * @param path File path to file we want to lock.
509      * @param shared Specify if the lock will be a shared lock.
510      * @param allowBlock Specify if we should block when we can not get the
511      *            lock. Getting a shared lock will always block.
512      * @return FileLock if we were successful in getting a lock, otherwise null.
513      * @throws FileNotFoundException If the file does not exist.
514      */
getFileLock(String path, boolean shared, boolean allowBlock)515     public static FileLock getFileLock(String path, boolean shared, boolean allowBlock) throws FileNotFoundException {
516         RandomAccessFile rafFile = new RandomAccessFile(path, "rw");
517         FileChannel fc = rafFile.getChannel();
518         FileLock lock = null;
519         try {
520             if (!shared) {
521                 if (allowBlock) {
522                     lock = fc.lock(0, Long.MAX_VALUE, false);
523                 } else {
524                     lock = fc.tryLock(0, Long.MAX_VALUE, false);
525                 }
526             } else { // We want shared lock. This will block regardless if allowBlock is true or not.
527                 // Test to see if we can get a shared lock.
528                 lock = fc.lock(0, 1, true); // Block if a non exclusive lock is being held.
529                 if (!lock.isShared()) { // This lock is an exclusive lock. Use alternate solution.
530                     FileLock tempLock = null;
531                     for (long pos = 1; tempLock == null && pos < Long.MAX_VALUE - 1; pos++) {
532                         tempLock = fc.tryLock(pos, 1, false);
533                     }
534                     lock.release();
535                     lock = tempLock; // Get the unique exclusive lock.
536                 }
537             }
538         } catch (IOException e) {
539             OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e);
540         }
541         return lock;
542     }
543 
544 /**
545      * Method to save String as file in UTF-8 encoding.
546      *
547      * @param content which will be saved as it is saved in this String
548      * @param f file to be saved. No warnings provided
549      * @throws IOException if save fails
550      */
saveFile(String content, File f)551     public static void saveFile(String content, File f) throws IOException {
552         saveFile(content, f, "utf-8");
553     }
554 
555     /**
556      * Method to save String as file in specified encoding/.
557      *
558      * @param content which will be saved as it is saved in this String
559      * @param f file to be saved. No warnings provided
560      * @param encoding of output byte representation
561      * @throws IOException if save fails
562      */
saveFile(String content, File f, String encoding)563     public static void saveFile(String content, File f, String encoding) throws IOException {
564         try (Writer output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), encoding))) {
565             output.write(content);
566             output.flush();
567         }
568     }
569 
570     /**
571      * utility method which can read from any stream as one long String
572      *
573      * @param is stream
574      * @param encoding the encoding to use to convert the bytes from the stream
575      * @return stream as string
576      * @throws IOException if connection can't be established or resource does not exist
577      */
getContentOfStream(InputStream is, String encoding)578     public static String getContentOfStream(InputStream is, String encoding) throws IOException {
579          try {
580             return getContentOfReader(new InputStreamReader(is, encoding));
581         } finally {
582             is.close();
583         }
584     }
getContentOfReader(Reader r)585      public static String getContentOfReader(Reader r) throws IOException {
586         try {
587             BufferedReader br = new BufferedReader(r);
588             StringBuilder sb = new StringBuilder();
589             while (true) {
590                 String s = br.readLine();
591                 if (s == null) {
592                     break;
593                 }
594                 sb.append(s).append("\n");
595 
596             }
597             return sb.toString();
598         } finally {
599             r.close();
600         }
601 
602     }
603 
604     /**
605      * utility method which can read from any stream as one long String
606      *
607      * @param is stream
608      * @return stream as string
609      * @throws IOException if connection can't be established or resource does not exist
610      */
getContentOfStream(InputStream is)611     public static String getContentOfStream(InputStream is) throws IOException {
612         return getContentOfStream(is, "UTF-8");
613 
614     }
615 
loadFileAsString(File f)616     public static String loadFileAsString(File f) throws IOException {
617         return getContentOfStream(new FileInputStream(f));
618     }
619 
loadFileAsString(File f, String encoding)620     public static String loadFileAsString(File f, String encoding) throws IOException {
621         return getContentOfStream(new FileInputStream(f), encoding);
622     }
623 
getFileMD5Sum(final File file, final String algorithm)624     public static byte[] getFileMD5Sum(final File file, final String algorithm) throws NoSuchAlgorithmException,
625             FileNotFoundException, IOException {
626         final MessageDigest md5;
627         InputStream is = null;
628         DigestInputStream dis = null;
629         try {
630             md5 = MessageDigest.getInstance(algorithm);
631             is = new FileInputStream(file);
632             dis = new DigestInputStream(is, md5);
633 
634             md5.update(getContentOfStream(dis).getBytes());
635         } finally {
636             if (is != null) {
637                 is.close();
638             }
639             if (dis != null) {
640                 dis.close();
641             }
642         }
643 
644         return md5.digest();
645     }
646 }
647