All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.commons.io.file.FilenameHelper Maven / Gradle / Ivy

There is a newer version: 11.1.10
Show newest version
/*
 * Copyright (C) 2014-2024 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.commons.io.file;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.CGlobal;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.collection.ArrayHelper;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.exception.InitializationException;
import com.helger.commons.string.StringHelper;
import com.helger.commons.system.EOperatingSystem;
import com.helger.commons.system.SystemHelper;

/**
 * All kind of file name handling stuff. This class gives you platform
 * independent file name handling.
 *
 * @author Philip Helger
 */
@Immutable
public final class FilenameHelper
{
  /** The file extension separation character. */
  public static final char EXTENSION_SEPARATOR = '.';

  /** The replacement character used for illegal file name characters. */
  public static final char ILLEGAL_FILENAME_CHAR_REPLACEMENT = '_';

  /** Special name of the current path */
  public static final String PATH_CURRENT = ".";

  /** Special name of the parent path */
  public static final String PATH_PARENT = "..";

  /** The Unix path separator character. */
  public static final char UNIX_SEPARATOR = '/';

  /** The Unix path separator string. */
  public static final String UNIX_SEPARATOR_STR = Character.toString (UNIX_SEPARATOR);

  /** The Windows separator character. */
  public static final char WINDOWS_SEPARATOR = '\\';

  /** The Windows separator string. */
  public static final String WINDOWS_SEPARATOR_STR = Character.toString (WINDOWS_SEPARATOR);

  /** The prefix to identify UNC paths on Unix based systems */
  public static final String UNIX_UNC_PREFIX = "//";

  /** The prefix to identify UNC paths on Windows based systems */
  public static final String WINDOWS_UNC_PREFIX = "\\\\";

  /**
   * The prefix to identify local UNC paths on Windows based systems.
* Those no longer work in files in certain Java versions. See * https://bugs.openjdk.org/browse/JDK-8285445 */ public static final String WINDOWS_UNC_PREFIX_LOCAL1 = "\\\\.\\"; /** * The prefix to identify local UNC paths on Windows based systems
* Those no longer work in files in certain Java versions. See * https://bugs.openjdk.org/browse/JDK-8285445 */ public static final String WINDOWS_UNC_PREFIX_LOCAL2 = "\\\\?\\"; /** The prefix used for Unix hidden files */ public static final char HIDDEN_FILE_PREFIX = '.'; private static final Logger LOGGER = LoggerFactory.getLogger (FilenameHelper.class); /** * Illegal characters in Windows file names.
* see http://en.wikipedia.org/wiki/Filename */ private static final char [] ILLEGAL_CHARACTERS_WINDOWS = { 0, '<', '>', '?', '*', ':', '|', '"' }; private static final char [] ILLEGAL_CHARACTERS_OTHERS = { 0, '<', '>', '?', '*', '|', '"' }; // separate by OS - allow ":" as name part on Linux private static final char [] ILLEGAL_CHARACTERS = EOperatingSystem.getCurrentOS ().isWindowsBased () ? ILLEGAL_CHARACTERS_WINDOWS : ILLEGAL_CHARACTERS_OTHERS; /** * see http://www.w3.org/TR/widgets/#zip-relative
* see http://forum.java.sun.com/thread.jspa?threadID=544334&tstart=165
* see http://en.wikipedia.org/wiki/Filename */ private static final String [] ILLEGAL_PREFIXES = { "CLOCK$", "CON", "PRN", "AUX", "NUL", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; private static final char [] ILLEGAL_SUFFIXES = { '.', ' ', '\t' }; static { if (!isSecureFilenameCharacter (ILLEGAL_FILENAME_CHAR_REPLACEMENT)) throw new InitializationException ("The illegal filename replacement character must be a valid ASCII character!"); } @PresentForCodeCoverage private static final FilenameHelper INSTANCE = new FilenameHelper (); private FilenameHelper () {} /** * Returns the index of the last extension separator character, which is a * dot. *

* This method also checks that there is no directory separator after the last * dot. To do this it uses {@link #getIndexOfLastSeparator(String)} which will * handle a file in either Unix or Windows format. *

* The output will be the same irrespective of the machine that the code is * running on. * * @param sFilename * The filename to find the last path separator in. May be * null. * @return the index of the last separator character, or * {@link CGlobal#ILLEGAL_UINT} if there is no such character or the * input parameter is null. * @see #getIndexOfLastSeparator(String) */ public static int getIndexOfExtension (@Nullable final String sFilename) { if (sFilename == null) return CGlobal.ILLEGAL_UINT; final int nExtensionIndex = sFilename.lastIndexOf (EXTENSION_SEPARATOR); final int nLastSepIndex = getIndexOfLastSeparator (sFilename); return nLastSepIndex > nExtensionIndex ? CGlobal.ILLEGAL_UINT : nExtensionIndex; } /** * Get the name of the passed file without the extension. If the file name * contains a leading absolute path, the path is returned as well. * * @param aFile * The file to extract the extension from. May be null. * @return An empty string if no extension was found, the extension without * the leading dot otherwise. If the input file is null * the return value is null. * @see #getWithoutExtension(String) */ @Nullable public static String getWithoutExtension (@Nullable final File aFile) { return aFile == null ? null : getWithoutExtension (aFile.getPath ()); } /** * Get the passed filename without the extension. If the file name contains a * leading absolute path, the path is returned as well. * * @param sFilename * The filename to extract the extension from. May be null * or empty. * @return An empty string if no extension was found, the extension without * the leading dot otherwise. If the input string is null * the return value is null. * @see #getIndexOfExtension(String) */ @Nullable public static String getWithoutExtension (@Nullable final String sFilename) { final int nIndex = getIndexOfExtension (sFilename); return nIndex == CGlobal.ILLEGAL_UINT ? sFilename : sFilename.substring (0, nIndex); } /** * Get the extension of the passed file. * * @param aFile * The file to extract the extension from. May be null. * @return An empty string if no extension was found, the extension without * the leading dot otherwise. Never null. * @see #getExtension(String) */ @Nonnull public static String getExtension (@Nullable final File aFile) { return aFile == null ? "" : getExtension (aFile.getName ()); } /** * Get the extension of the passed filename. * * @param sFilename * The filename to extract the extension from. May be null * or empty. * @return An empty string if no extension was found, the extension without * the leading dot otherwise. Never null. * @see #getIndexOfExtension(String) */ @Nonnull public static String getExtension (@Nullable final String sFilename) { final int nIndex = getIndexOfExtension (sFilename); if (nIndex == CGlobal.ILLEGAL_UINT) return ""; return sFilename.substring (nIndex + 1); } /** * Check if the passed file has one of the passed extensions. The comparison * is done case insensitive even on Unix machines. * * @param aFile * The file to check the extension from. May be null or * empty. * @param aExtensions * An array of extensions (without the leading dot) which are matched * case insensitive. May not be null. * @return true if the file has one of the passed extensions, * else false. * @see #getExtension(File) */ public static boolean hasExtension (@Nullable final File aFile, @Nonnull final String... aExtensions) { ValueEnforcer.notNull (aExtensions, "Extensions"); // determine current extension. final String sExt = getExtension (aFile); for (final String sExtension : aExtensions) if (sExt.equalsIgnoreCase (sExtension)) return true; return false; } /** * Check if the passed filename has one of the passed extensions. The * comparison is done case insensitive even on Unix machines. * * @param sFilename * The filename to check the extension from. May be null * or empty. * @param aExtensions * An array of extensions (without the leading dot) which are matched * case insensitive. May not be null. * @return true if the filename has one of the passed extensions, * else false. * @see #getExtension(String) */ public static boolean hasExtension (@Nullable final String sFilename, @Nonnull final String... aExtensions) { ValueEnforcer.notNull (aExtensions, "Extensions"); // determine current extension. final String sExt = getExtension (sFilename); for (final String sExtension : aExtensions) if (sExt.equalsIgnoreCase (sExtension)) return true; return false; } /** * Returns the index of the last directory separator character. This method * will handle a file in either Unix or Windows format. The position of the * last forward or backslash is returned. The output will be the same * irrespective of the machine that the code is running on. * * @param sFilename * The filename to find the last path separator in, null * returns {@link CGlobal#ILLEGAL_UINT}. * @return The index of the last separator character, or * {@link CGlobal#ILLEGAL_UINT} if there is no such character */ public static int getIndexOfLastSeparator (@Nullable final String sFilename) { return sFilename == null ? CGlobal.ILLEGAL_UINT : Math.max (sFilename.lastIndexOf (UNIX_SEPARATOR), sFilename.lastIndexOf (WINDOWS_SEPARATOR)); } /** * Get the name of the passed file without any eventually leading path. Note: * if the passed file is a directory, the name of the directory is returned. * * @param aFile * The file. May be null. * @return The name only or null if the passed parameter is * null. */ @Nullable public static String getWithoutPath (@Nullable final File aFile) { return aFile == null ? null : aFile.getName (); } /** * Get the name of the passed file without any eventually leading path. * * @param sAbsoluteFilename * The fully qualified file name. May be null. * @return The name only or null if the passed parameter is * null. * @see #getIndexOfLastSeparator(String) */ @Nullable public static String getWithoutPath (@Nullable final String sAbsoluteFilename) { /** * Note: do not use new File (sFilename).getName () since this * only invokes the underlying FileSystem implementation which handles path * handling only correctly on the native platform. Problem arose when * running application on a Linux server and making a file upload from a * Windows machine. */ if (sAbsoluteFilename == null) return null; final int nLastSepIndex = getIndexOfLastSeparator (sAbsoluteFilename); return nLastSepIndex == CGlobal.ILLEGAL_UINT ? sAbsoluteFilename : sAbsoluteFilename.substring (nLastSepIndex + 1); } /** * Get the path of the passed file name without any eventually contained * filename. * * @param sAbsoluteFilename * The fully qualified file name. May be null. * @return The path only including the last trailing path separator character. * Returns null if the passed parameter is * null. * @see #getIndexOfLastSeparator(String) */ @Nullable public static String getPath (@Nullable final String sAbsoluteFilename) { /** * Note: do not use new File (sFilename).getPath () since this * only invokes the underlying FileSystem implementation which handles path * handling only correctly on the native platform. Problem arose when * running application on a Linux server and making a file upload from a * Windows machine. */ if (sAbsoluteFilename == null) return null; final int nLastSepIndex = getIndexOfLastSeparator (sAbsoluteFilename); return nLastSepIndex == CGlobal.ILLEGAL_UINT ? "" : sAbsoluteFilename.substring (0, nLastSepIndex + 1); } /** * Get the passed filename without path and without extension.
* Example: /dir1/dir2/file.txt becomes file * * @param aFile * The file to get the base name from. May be null. * @return The base name of the passed parameter. May be null if * the parameter was null. * @see #getWithoutExtension(String) */ @Nullable public static String getBaseName (@Nullable final File aFile) { return aFile == null ? null : getWithoutExtension (aFile.getName ()); } /** * Get the passed filename without path and without extension.
* Example: /dir1/dir2/file.txt becomes file * * @param sAbsoluteFilename * The filename to get the base name from. May be null. * @return The base name of the passed parameter. May be null if * the parameter was null. * @see #getWithoutPath(String) * @see #getWithoutExtension(String) */ @Nullable public static String getBaseName (@Nullable final String sAbsoluteFilename) { return getWithoutExtension (getWithoutPath (sAbsoluteFilename)); } /** * Ensure that the path (not the absolute path!) of the passed file is using * the Unix style separator "/" instead of the Operating System dependent one. * * @param aFile * The file to use. May be null * @return null if the passed file is null. * @see #getPathUsingUnixSeparator(String) */ @Nullable public static String getPathUsingUnixSeparator (@Nullable final File aFile) { return aFile == null ? null : getPathUsingUnixSeparator (aFile.getPath ()); } /** * Ensure that the passed path is using the Unix style separator "/" instead * of the Operating System dependent one. * * @param sAbsoluteFilename * The file name to use. May be null * @return null if the passed path is null. * @see #getPathUsingUnixSeparator(File) */ @Nullable public static String getPathUsingUnixSeparator (@Nullable final String sAbsoluteFilename) { return sAbsoluteFilename == null ? null : StringHelper.replaceAll (sAbsoluteFilename, WINDOWS_SEPARATOR, UNIX_SEPARATOR); } /** * Ensure that the path (not the absolute path!) of the passed file is using * the Windows style separator "\" instead of the Operating System dependent * one. * * @param aFile * The file to use. May be null * @return null if the passed file is null. * @see #getPathUsingWindowsSeparator(String) */ @Nullable public static String getPathUsingWindowsSeparator (@Nullable final File aFile) { return aFile == null ? null : getPathUsingWindowsSeparator (aFile.getPath ()); } /** * Ensure that the passed path is using the Windows style separator "\" * instead of the Operating System dependent one. * * @param sAbsoluteFilename * The file name to use. May be null * @return null if the passed path is null. * @see #getPathUsingWindowsSeparator(File) */ @Nullable public static String getPathUsingWindowsSeparator (@Nullable final String sAbsoluteFilename) { return sAbsoluteFilename == null ? null : StringHelper.replaceAll (sAbsoluteFilename, UNIX_SEPARATOR, WINDOWS_SEPARATOR); } /** * Check whether the two passed file names are equal, independent of the used * separators (/ or \). * * @param sAbsoluteFilename1 * First file name. May be null. * @param sAbsoluteFilename2 * Second file name. May be null. * @return true if they are equal, false otherwise. * @see #getPathUsingUnixSeparator(String) */ public static boolean isEqualIgnoreFileSeparator (@Nullable final String sAbsoluteFilename1, @Nullable final String sAbsoluteFilename2) { return EqualsHelper.equals (getPathUsingUnixSeparator (sAbsoluteFilename1), getPathUsingUnixSeparator (sAbsoluteFilename2)); } /** * Avoid 0 byte attack. E.g. file name "test.java\u0000.txt" is internally * represented as "test.java" but ends with ".txt".
* Note: the passed file name is NOT decoded (e.g. %20 stays %20 and * will not be converted to a space). * * @param sFilename * The file name to check. May be null. * @return null if the input string is null or * everything up to the 0-byte. */ @Nullable public static String getSecureFilename (@Nullable final String sFilename) { if (sFilename == null) return null; final int nIdx0 = sFilename.indexOf ('\0'); return nIdx0 == -1 ? sFilename : sFilename.substring (0, nIdx0); } /** * Check if the passed file name is valid. It checks for illegal prefixes that * affects compatibility to Windows, illegal characters within a filename and * forbidden suffixes. This method fits only for filenames on one level. If * you want to check a full path, use * {@link #isValidFilenameWithPaths(String)}. * * @param sFilename * The filename to check. May be null. * @return false if the passed filename is null or * empty or invalid. true if the filename is not empty * and valid. * @see #containsPathSeparatorChar(String) */ public static boolean isValidFilename (@Nullable final String sFilename) { // empty not allowed if (StringHelper.hasNoText (sFilename)) return false; // path separator chars are not allowed in filenames! if (containsPathSeparatorChar (sFilename)) return false; // check for illegal last characters if (StringHelper.endsWithAny (sFilename, ILLEGAL_SUFFIXES)) return false; // Check if file name contains any of the illegal characters for (final char cIllegal : ILLEGAL_CHARACTERS) if (sFilename.indexOf (cIllegal) != -1) return false; // check prefixes directly for (final String sIllegalPrefix : ILLEGAL_PREFIXES) if (sFilename.equalsIgnoreCase (sIllegalPrefix)) return false; // check if filename is prefixed with it // Note: we can use the default locale, since all fixed names are pure ANSI // names final String sUCFilename = sFilename.toUpperCase (SystemHelper.getSystemLocale ()); for (final String sIllegalPrefix : ILLEGAL_PREFIXES) if (sUCFilename.startsWith (sIllegalPrefix + ".")) return false; return true; } /** * Check if the passed filename path is valid. In contrast to * {@link #isValidFilename(String)} this method can also handle filenames * including paths. * * @param sFilename * The filename to be checked for validity. * @return true if all path elements of the filename are valid, * false if at least one element is invalid * @see #isValidFilename(String) */ public static boolean isValidFilenameWithPaths (@Nullable final String sFilename) { if (StringHelper.hasNoText (sFilename)) return false; // Iterate filename path by path File aFile = new File (sFilename); while (aFile != null) { final String sCurFilename = aFile.getName (); final File aParentFile = aFile.getParentFile (); if (sCurFilename.length () == 0 && aParentFile == null) { // The last part of an absolute path can be skipped! break; } if (!isValidFilename (sCurFilename)) return false; aFile = aParentFile; } return true; } /** * Convert the passed filename into a valid filename by performing the * following actions: *

    *
  1. Remove everything after a potential \0 character
  2. *
  3. Remove all characters that are invalid at the end of a file name
  4. *
  5. Replace all characters that are invalid inside a filename with a * underscore
  6. *
  7. If the filename is invalid on Windows platforms it is prefixed with an * underscore.
  8. *
* Note: this method does not handle Windows full path like * "c:\autoexec.bat"
* * @param sFilename * The filename to be made value. May be null. * @return null if the input filename was null or if * it consisted only of characters invalid for a filename; the * potentially modified filename otherwise but never an empy * string. * @see #getSecureFilename(String) */ @Nullable @Nonempty public static String getAsSecureValidFilename (@Nullable final String sFilename) { // First secure it, by cutting everything behind the '\0' String ret = getSecureFilename (sFilename); // empty not allowed if (StringHelper.hasText (ret)) { // Remove all trailing invalid suffixes while (ret.length () > 0 && StringHelper.endsWithAny (ret, ILLEGAL_SUFFIXES)) ret = ret.substring (0, ret.length () - 1); // Replace all characters that are illegal inside a filename for (final char cIllegal : ILLEGAL_CHARACTERS) ret = StringHelper.replaceAll (ret, cIllegal, ILLEGAL_FILENAME_CHAR_REPLACEMENT); // Check if a file matches an illegal prefix final String sTempRet = ret; if (ArrayHelper.containsAny (ILLEGAL_PREFIXES, sTempRet::equalsIgnoreCase)) ret = ILLEGAL_FILENAME_CHAR_REPLACEMENT + ret; // check if filename is prefixed with an illegal prefix // Note: we can use the default locale, since all fixed names are pure // ANSI names final String sUCFilename = ret.toUpperCase (SystemHelper.getSystemLocale ()); if (ArrayHelper.containsAny (ILLEGAL_PREFIXES, x -> sUCFilename.startsWith (x + "."))) ret = ILLEGAL_FILENAME_CHAR_REPLACEMENT + ret; } // Avoid returning an empty string as valid file name return StringHelper.hasNoText (ret) ? null : ret; } /** * Check if the passed character is secure to be used in filenames. Therefore * it must be ≥ 0x20 and < 0x80. * * @param c * The character to check * @return true if it is valid, false if not */ public static boolean isSecureFilenameCharacter (final char c) { return c >= 0x20 && c < 0x80; } /** * Replace all non-ASCII characters from the filename (e.g. German Umlauts) * with underscores. Before replacing non-ASCII characters the filename is * made valid using {@link #getAsSecureValidFilename(String)}. * * @param sFilename * Input file name. May not be null. * @return null if the input filename was null. The * file name containing only ASCII characters. The returned value is * never an empty String. * @see #getAsSecureValidASCIIFilename(String, char) */ @Nullable @Nonempty public static String getAsSecureValidASCIIFilename (@Nullable final String sFilename) { return getAsSecureValidASCIIFilename (sFilename, ILLEGAL_FILENAME_CHAR_REPLACEMENT); } /** * Replace all non-ASCII characters from the filename (e.g. German Umlauts) * with a replacement char. Before replacing non-ASCII characters the filename * is made valid using {@link #getAsSecureValidFilename(String)}. * * @param sFilename * Input file name. May not be null. * @param cReplacementChar * The replacement character to be used for insecure filenames. * @return null if the input filename was null. The * file name containing only ASCII characters. The returned value is * never an empty String. * @see #getAsSecureValidASCIIFilename(String) * @see #getAsSecureValidFilename(String) * @see #isSecureFilenameCharacter(char) */ @Nullable @Nonempty public static String getAsSecureValidASCIIFilename (@Nullable final String sFilename, final char cReplacementChar) { // Make it valid according to the general rules final String sValid = getAsSecureValidFilename (sFilename); if (sValid == null) return null; // Start replacing all non-ASCII characters with '_' final StringBuilder ret = new StringBuilder (sValid.length ()); for (final char c : sValid.toCharArray ()) if (isSecureFilenameCharacter (c)) ret.append (c); else ret.append (cReplacementChar); return ret.toString (); } /** * Check if the passed character is a path separation character. This method * handles both Windows- and Unix-style path separation characters. * * @param c * The character to check. * @return true if the character is a path separation character, * false otherwise. */ public static boolean isPathSeparatorChar (final char c) { return c == UNIX_SEPARATOR || c == WINDOWS_SEPARATOR; } /** * Check if the passed character sequence starts with a path separation * character. * * @param s * The character sequence to check. May be null or empty. * @return true if the character sequences starts with a Windows- * or Unix-style path character. * @see #isPathSeparatorChar(char) */ public static boolean startsWithPathSeparatorChar (@Nullable final CharSequence s) { return isPathSeparatorChar (StringHelper.getFirstChar (s)); } /** * Check if the passed character sequence ends with a path separation * character. * * @param s * The character sequence to check. May be null or empty. * @return true if the character sequences ends with a Windows- * or Unix-style path character. * @see #isPathSeparatorChar(char) */ public static boolean endsWithPathSeparatorChar (@Nullable final CharSequence s) { return isPathSeparatorChar (StringHelper.getLastChar (s)); } /** * Check if the passed String contains at least one path separator char * (either Windows or Unix style). * * @param s * The string to check. May be null. * @return true if the passed string is not null and * contains at least one separator. */ public static boolean containsPathSeparatorChar (@Nullable final String s) { // This is a tick faster than iterating the s.toCharArray() chars return s != null && (s.indexOf (UNIX_SEPARATOR) >= 0 || s.indexOf (WINDOWS_SEPARATOR) >= 0); } /** * Check if the passed file is a system directory. A system directory is * either {@value #PATH_CURRENT} or {@value #PATH_PARENT}. * * @param aFile * The file to be checked. May be null. * @return true if the passed file name (not the path) matches * any of the special directory names, false of the * passed file is null or does not denote a special * directory. * @see #isSystemInternalDirectory(CharSequence) */ public static boolean isSystemInternalDirectory (@Nullable final File aFile) { return aFile != null && isSystemInternalDirectory (aFile.getName ()); } /** * Check if the passed file is a system directory. A system directory is * either {@value #PATH_CURRENT} or {@value #PATH_PARENT}. * * @param aFile * The file to be checked. May be null. * @return true if the passed file name (not the path) matches * any of the special directory names, false of the * passed file is null or does not denote a special * directory. * @see #isSystemInternalDirectory(CharSequence) */ public static boolean isSystemInternalDirectory (@Nullable final Path aFile) { if (aFile == null) return false; final Path aPureFile = aFile.getFileName (); return aPureFile != null && isSystemInternalDirectory (aPureFile.toString ()); } /** * Check if the passed string is a system directory. A system directory is * either {@value #PATH_CURRENT} or {@value #PATH_PARENT}. * * @param s * The value to be checked. May be null. * @return true if the passed string matches any of the special * directory names, false of the passed string is * null or does not denote a special directory. * @see #isSystemInternalDirectory(File) */ public static boolean isSystemInternalDirectory (@Nullable final CharSequence s) { return s != null && (s.equals (PATH_CURRENT) || s.equals (PATH_PARENT)); } /** * Check if the passed file is an UNC path. UNC paths are identified by * starting with "//" or "\\". * * @param aFile * The file to be checked. May not be null. * @return true if the file points to an UNC path, * false if not. * @see #isUNCPath(String) */ public static boolean isUNCPath (@Nonnull final File aFile) { final String sPath = aFile.getAbsolutePath (); return isUNCPath (sPath); } /** * Check if the passed file is an UNC path. UNC paths are identified by * starting with "//" or "\\". * * @param sFilename * The absolute filename to be checked. May not be null. * @return true if the file points to an UNC path, * false if not. * @see #isUNCPath(File) */ public static boolean isUNCPath (@Nonnull final String sFilename) { return sFilename.startsWith (WINDOWS_UNC_PREFIX) || sFilename.startsWith (UNIX_UNC_PREFIX); } /** * Check if the passed file is a Windows local UNC path. This type is * identified by starting with "\\?\" or "\\.\". * * @param aFile * The file to be checked. May not be null. * @return true if the file points to an UNC path, * false if not. * @see #isWindowsLocalUNCPath(String) */ public static boolean isWindowsLocalUNCPath (@Nonnull final File aFile) { final String sPath = aFile.getAbsolutePath (); return isWindowsLocalUNCPath (sPath); } /** * Check if the passed file is a Windows local UNC path. This type is * identified by starting with "\\?\" or "\\.\". * * @param sFilename * The absolute filename to be checked. May not be null. * @return true if the file points to a Windows local UNC path, * false if not. * @see #isWindowsLocalUNCPath(File) */ public static boolean isWindowsLocalUNCPath (@Nonnull final String sFilename) { return sFilename.startsWith (WINDOWS_UNC_PREFIX_LOCAL1) || sFilename.startsWith (WINDOWS_UNC_PREFIX_LOCAL2); } /** * Get a clean path of the passed file resolving all "." and ".." paths.
* Note: in case {@link FileHelper#getCanonicalPath(File)} fails, * {@link #getCleanPath(String)} is used as a fallback.
* Note 2: no cleansing operations beside "." and ".." are returned. You need * to ensure yourself, that the returned file name is valid! * * @param aFile * The file to be cleaned. May not be null. * @return The cleaned path and never null. * @see #getPathUsingUnixSeparator(String) * @see #getCleanPath(String) */ @Nonnull public static String getCleanPath (@Nonnull final File aFile) { ValueEnforcer.notNull (aFile, "File"); // getCanoncialPath fails for Windows local UNC paths if (!isUNCPath (aFile)) try { // This works only if the file exists // Note: getCanonicalPath may be a bottleneck on Unix based file // systems! return getPathUsingUnixSeparator (FileHelper.getCanonicalPath (aFile)); } catch (final IOException ex) { // Use our method // This is most likely an IOException from "File.getCanonicalPath ()" // stating "Invalid file path" LOGGER.warn ("Using getCleanPath on an invalid file '" + aFile + "' - " + ex.getMessage ()); } // Fallback: do it manually return getCleanPath (aFile.getAbsolutePath ()); } /** * Clean the path by removing all ".." and "." path elements. * * @param sPath * The path to be cleaned. * @return The cleaned path or null if the input parameter was * null. * @see #getSecureFilename(String) * @see #getPathUsingUnixSeparator(String) */ @Nullable public static String getCleanPath (@Nullable final String sPath) { if (sPath == null) return null; // Remove all relative paths and insecure characters String sPrefix = ""; String sPathToUse = getSecureFilename (sPath); boolean bForceWindowsSeparator = false; if (isUNCPath (sPathToUse)) { // It's an UNC path sPrefix += sPathToUse.substring (0, 2); sPathToUse = sPathToUse.substring (2); bForceWindowsSeparator = sPrefix.startsWith (WINDOWS_SEPARATOR_STR); } // Strip prefix from path to analyze, to not treat it as part of the // first path element. This is necessary to correctly parse paths like // "file://core/../core/io/Resource.class", where the ".." should just // strip the first "core" directory while keeping the "file:" prefix. // The same applies to http:// addresses where the domain should be kept! final int nProtoIdx = sPathToUse.indexOf ("://"); boolean bPrefixIsAbsolute = false; if (nProtoIdx > -1) { // Keep protocol and server name // Start searching for the first slash after "://" (length=3) final int nPrefixIndex = sPathToUse.indexOf ('/', nProtoIdx + 3); if (nPrefixIndex >= 0) { sPrefix += sPathToUse.substring (0, nPrefixIndex + 1); sPathToUse = sPathToUse.substring (nPrefixIndex + 1); bPrefixIsAbsolute = true; } else { // We have e.g. a URL like "http://www.helger.com" // -> Nothing to return sPathToUse; } } else { // Keep volume or protocol prefix final int nPrefixIndex = sPathToUse.indexOf (':'); if (nPrefixIndex >= 0) { sPrefix += sPathToUse.substring (0, nPrefixIndex + 1); sPathToUse = sPathToUse.substring (nPrefixIndex + 1); bPrefixIsAbsolute = true; } } // Unify all remaining path separators to be "/" // Unify after the prefixes where removed sPathToUse = getPathUsingUnixSeparator (sPathToUse); // Is it an absolute Path? if (StringHelper.startsWith (sPathToUse, UNIX_SEPARATOR)) { // If no other prefix is present yet, this seems to be an absolute path! if (sPrefix.length () == 0) bPrefixIsAbsolute = true; sPrefix += bForceWindowsSeparator ? WINDOWS_SEPARATOR : UNIX_SEPARATOR; sPathToUse = sPathToUse.substring (1); } // Start splitting into paths final ICommonsList aElements = new CommonsArrayList <> (); int nParentFolders = 0; final String [] aPathArray = StringHelper.getExplodedArray (UNIX_SEPARATOR, sPathToUse); // from right to left for (int i = aPathArray.length - 1; i >= 0; i--) { final String sElement = aPathArray[i]; // Empty paths and paths reflecting the current directory (".") are // ignored if (sElement.length () == 0 || PATH_CURRENT.equals (sElement)) continue; if (PATH_PARENT.equals (sElement)) { // Remember that we have a parent path to skip nParentFolders++; } else if (nParentFolders > 0) { // Merging path element with element corresponding to top path. nParentFolders--; } else { // Normal path element found. aElements.add (0, sElement); } } if (!bPrefixIsAbsolute) { // Remaining top paths need to be retained. for (int i = 0; i < nParentFolders; i++) aElements.add (0, PATH_PARENT); } return sPrefix + StringHelper.getImploded (bForceWindowsSeparator ? WINDOWS_SEPARATOR : UNIX_SEPARATOR, aElements); } /** * Concatenate a base URL and a sub path incl. the path cleansing. More or * less the same as calling getCleanPath (sURL + "/" + sPath) * * @param sURL * The base URL. May not be null. * @param sPath * The path to append. May not be null. * @return The combined, cleaned path. * @see #getCleanPath(String) */ @Nonnull public static String getCleanConcatenatedUrlPath (@Nonnull final String sURL, @Nonnull final String sPath) { ValueEnforcer.notNull (sURL, "URL"); ValueEnforcer.notNull (sPath, "Path"); // If nothing is to be appended, just clean the base URL if (StringHelper.hasNoText (sPath)) return getCleanPath (sURL); final String sRealURL = StringHelper.endsWith (sURL, UNIX_SEPARATOR) ? sURL : sURL + UNIX_SEPARATOR; final String sRealPath = StringHelper.startsWith (sPath, UNIX_SEPARATOR) ? sPath.substring (1) : sPath; return getCleanPath (sRealURL + sRealPath); } /** * Ensure that the passed path starts with a directory separator character. If * the passed path starts with either {@value #WINDOWS_SEPARATOR} or * {@value #UNIX_SEPARATOR} no changes are performed. * * @param sPath * The path to be checked. * @return The path that is ensured to start with the directory separator of * the current operating system. * @see #startsWithPathSeparatorChar(CharSequence) */ @Nullable @CheckReturnValue public static String ensurePathStartingWithSeparator (@Nullable final String sPath) { if (sPath == null) return null; return startsWithPathSeparatorChar (sPath) ? sPath : File.separator + sPath; } /** * Ensure that the passed path does NOT end with a directory separator * character. Any number of trailing {@value #WINDOWS_SEPARATOR} or * {@value #UNIX_SEPARATOR} are removed. * * @param sPath * The path to be checked. * @return The path that is ensured to NOT end with the directory separator. * @see #endsWithPathSeparatorChar(CharSequence) */ @Nullable @CheckReturnValue public static String ensurePathEndingWithoutSeparator (@Nullable final String sPath) { if (sPath == null) return null; String sRet = sPath; while (endsWithPathSeparatorChar (sRet)) sRet = sRet.substring (0, sRet.length () - 1); return sRet; } /** * Ensure that the passed path ends with a directory separator character. If * the passed path ends with either {@value #WINDOWS_SEPARATOR} or * {@value #UNIX_SEPARATOR} no changes are performed. * * @param sPath * The path to be checked. * @return The path that is ensured to end with the directory separator of the * current operating system. * @see #endsWithPathSeparatorChar(CharSequence) */ @Nullable @CheckReturnValue public static String ensurePathEndingWithSeparator (@Nullable final String sPath) { if (sPath == null) return null; return endsWithPathSeparatorChar (sPath) ? sPath : sPath + File.separator; } /** * Tries to express the passed file path relative to the passed parent * directory. If the parent directory is null or not actually a parent of the * passed file, the passed file name will be returned unchanged. * * @param aFile * The file which is to be described relatively. May not be * null. * @param aParentDirectory * The parent directory of the file to which the relative path * expression will relate to. May be null. * @return The relative path or the unchanged absolute file path using Unix * path separators instead of Operating System dependent separator. Or * null if the passed file contains a path traversal at * the beginning * @see #getCleanPath(File) * @see #startsWithPathSeparatorChar(CharSequence) */ @Nullable public static String getRelativeToParentDirectory (@Nonnull final File aFile, @Nullable final File aParentDirectory) { ValueEnforcer.notNull (aFile, "File"); final String sCleanedFile = getCleanPath (aFile); if (aParentDirectory == null) return sCleanedFile; String sRelative = StringHelper.trimStart (sCleanedFile, getCleanPath (aParentDirectory)); if (sRelative.equals (sCleanedFile)) { // The passed file contains a path traversal! return null; } if (startsWithPathSeparatorChar (sRelative)) { // Ignore any leading path separator char sRelative = sRelative.substring (1); } return sRelative; } /** * Get a concatenated absolute path consisting of the parent directory and the * file path. It is ensured that the resulting (cleaned) filename is still the * same or a child of the passed parent directory. If the file path contains * some directory traversal elements (e.g. starting with "..") * null is returned. * * @param aParentDirectory * The parent directory to be ensured. May not be null. * @param sFilePath * The file path to be appended to the passed parent directory. May not * be null. * @return null if the parent directory would be changed with the * passed file path - the concatenated cleaned path otherwise (using * Unix separators). * @see #getRelativeToParentDirectory(File, File) * @see #getCleanPath(File) */ @Nullable public static String getAbsoluteWithEnsuredParentDirectory (@Nonnull final File aParentDirectory, @Nonnull final String sFilePath) { ValueEnforcer.notNull (aParentDirectory, "ParentDirectory"); ValueEnforcer.notNull (sFilePath, "FilePath"); final File aSubFile = new File (sFilePath); String sRelativeSubPath = sFilePath; // if sub path is absolute, must contain parent path! if (aSubFile.isAbsolute ()) { if (!aParentDirectory.isAbsolute ()) { LOGGER.error ("Cannot express absolute child file ('" + aSubFile + "') relative to a relative parent file ('" + aParentDirectory + "')!"); return null; } sRelativeSubPath = getRelativeToParentDirectory (aSubFile, aParentDirectory); } if (sRelativeSubPath == null) return null; // Merge the files together as usual, and clean all "." and ".." up final String sCleanParentPath = getCleanPath (aParentDirectory); final String sEstimatedPath = getCleanPath (new File (sCleanParentPath, sRelativeSubPath)); if (!sEstimatedPath.startsWith (sCleanParentPath)) { // Seems like there is a path traversal at the beginning of the passed // file path return null; } return sEstimatedPath; } /** * Check if the passed filename is a Unix hidden filename. * * @param aFile * The file to check. May be null. * @return true if the file is not null and the name * starts with a dot. * @see #isHiddenFilename(String) */ public static boolean isHiddenFilename (@Nullable final File aFile) { return aFile != null && isHiddenFilename (aFile.getName ()); } /** * Check if the passed filename is a Unix hidden filename. * * @param sFilename * The filename to check. May be null. * @return true if the filename is neither null nor * empty and starts with a dot. * @see #isHiddenFilename(File) */ public static boolean isHiddenFilename (@Nullable final String sFilename) { return StringHelper.hasText (sFilename) && sFilename.charAt (0) == HIDDEN_FILE_PREFIX; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy