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