Как проверить, является ли данная строка допустимым / допустимым именем файла в Windows?

Я хочу включить функцию переименования пакетного файла в моем приложении. Пользователь может ввести шаблон имени файла назначения и (после замены некоторых шаблонов в шаблоне) мне нужно проверить, будет ли это допустимое имя файла в Windows. Я пытался использовать регулярное выражение, как [a-zA-Z0-9_]+ но он не включает много национальных символов из разных языков (например, умлаутов и т. д.). Каков наилучший способ сделать такую ​​проверку?

26 ответов

Решение

Вы можете получить список недопустимых символов из Path.GetInvalidPathChars а также GetInvalidFileNameChars,

UPD: см . Предложение Стива Купера о том, как использовать их в регулярном выражении.

UPD2: обратите внимание, что согласно разделу "Примечания" в MSDN "Массив, возвращаемый этим методом, не обязательно содержит полный набор символов, недопустимых в именах файлов и каталогов". Ответ, предоставленный Sixlettervaliables, входит в более подробную информацию.

В MSDN "Наименование файла или каталога" приведены общие соглашения о том, что является допустимым именем файла в Windows:

Вы можете использовать любой символ в текущей кодовой странице (Unicode/ANSI выше 127), кроме:

  • <>:"/\|?*
  • Символы, целочисленные представления которых 0-31 (меньше, чем ASCII-пробел)
  • Любой другой символ, который целевая файловая система не допускает (скажем, конечные точки или пробелы)
  • Любое из имен DOS: CON, PRN, AUX, NUL, COM0, COM1, COM2, COM3, COM4, ​​COM5, COM6, COM7, COM8, COM9, LPT0, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9 (и избегайте AUX.txt и т. Д.)
  • Имя файла - все периоды

Некоторые дополнительные вещи, чтобы проверить:

  • Пути к файлам (включая имя файла) могут содержать не более 260 символов (которые не используют \?\ префикс)
  • Пути к файлам Unicode (включая имя файла), содержащие более 32 000 символов при использовании \?\ (обратите внимание, что префикс может расширять компоненты каталога и приводить к переполнению лимита 32 000)

Для .Net Framework до 3.5 это должно работать:

Соответствие регулярных выражений поможет вам в этом. Вот фрагмент с использованием System.IO.Path.InvalidPathChars постоянная;

bool IsValidFilename(string testName)
{
    Regex containsABadCharacter = new Regex("[" 
          + Regex.Escape(System.IO.Path.InvalidPathChars) + "]");
    if (containsABadCharacter.IsMatch(testName)) { return false; };

    // other checks for UNC, drive-path format, etc

    return true;
}

Для .Net Frameworks после 3.0 это должно работать:

http://msdn.microsoft.com/en-us/library/system.io.path.getinvalidpathchars(v=vs.90).aspx

Соответствие регулярных выражений поможет вам в этом. Вот фрагмент с использованием System.IO.Path.GetInvalidPathChars() постоянная;

bool IsValidFilename(string testName)
{
    Regex containsABadCharacter = new Regex("["
          + Regex.Escape(new string(System.IO.Path.GetInvalidPathChars())) + "]");
    if (containsABadCharacter.IsMatch(testName)) { return false; };

    // other checks for UNC, drive-path format, etc

    return true;
}

После того, как вы это узнаете, вам также следует проверить различные форматы, например: c:\my\drive а также \\server\share\dir\file.ext

Попытайтесь использовать это, и заманивайте в ловушку для ошибки. Разрешенный набор может меняться в разных файловых системах или в разных версиях Windows. Другими словами, если вы хотите знать, нравится ли Windows имя, передайте ему имя и дайте ему сказать.

Этот класс очищает имена файлов и пути; используйте это как

var myCleanPath = PathSanitizer.SanitizeFilename(myBadPath, ' ');

Вот код:

/// <summary>
/// Cleans paths of invalid characters.
/// </summary>
public static class PathSanitizer
{
    /// <summary>
    /// The set of invalid filename characters, kept sorted for fast binary search
    /// </summary>
    private readonly static char[] invalidFilenameChars;
    /// <summary>
    /// The set of invalid path characters, kept sorted for fast binary search
    /// </summary>
    private readonly static char[] invalidPathChars;

    static PathSanitizer()
    {
        // set up the two arrays -- sorted once for speed.
        invalidFilenameChars = System.IO.Path.GetInvalidFileNameChars();
        invalidPathChars = System.IO.Path.GetInvalidPathChars();
        Array.Sort(invalidFilenameChars);
        Array.Sort(invalidPathChars);

    }

    /// <summary>
    /// Cleans a filename of invalid characters
    /// </summary>
    /// <param name="input">the string to clean</param>
    /// <param name="errorChar">the character which replaces bad characters</param>
    /// <returns></returns>
    public static string SanitizeFilename(string input, char errorChar)
    {
        return Sanitize(input, invalidFilenameChars, errorChar);
    }

    /// <summary>
    /// Cleans a path of invalid characters
    /// </summary>
    /// <param name="input">the string to clean</param>
    /// <param name="errorChar">the character which replaces bad characters</param>
    /// <returns></returns>
    public static string SanitizePath(string input, char errorChar)
    {
        return Sanitize(input, invalidPathChars, errorChar);
    }

    /// <summary>
    /// Cleans a string of invalid characters.
    /// </summary>
    /// <param name="input"></param>
    /// <param name="invalidChars"></param>
    /// <param name="errorChar"></param>
    /// <returns></returns>
    private static string Sanitize(string input, char[] invalidChars, char errorChar)
    {
        // null always sanitizes to null
        if (input == null) { return null; }
        StringBuilder result = new StringBuilder();
        foreach (var characterToTest in input)
        {
            // we binary search for the character in the invalid set. This should be lightning fast.
            if (Array.BinarySearch(invalidChars, characterToTest) >= 0)
            {
                // we found the character in the array of 
                result.Append(errorChar);
            }
            else
            {
                // the character was not found in invalid, so it is valid.
                result.Append(characterToTest);
            }
        }

        // we're done.
        return result.ToString();
    }

}

Это то, что я использую:

    public static bool IsValidFileName(this string expression, bool platformIndependent)
    {
        string sPattern = @"^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\"";|/]+$";
        if (platformIndependent)
        {
           sPattern = @"^(([a-zA-Z]:|\\)\\)?(((\.)|(\.\.)|([^\\/:\*\?""\|<>\. ](([^\\/:\*\?""\|<>\. ])|([^\\/:\*\?""\|<>]*[^\\/:\*\?""\|<>\. ]))?))\\)*[^\\/:\*\?""\|<>\. ](([^\\/:\*\?""\|<>\. ])|([^\\/:\*\?""\|<>]*[^\\/:\*\?""\|<>\. ]))?$";
        }
        return (Regex.IsMatch(expression, sPattern, RegexOptions.CultureInvariant));
    }

Первый шаблон создает регулярное выражение, содержащее недопустимые / недопустимые имена файлов и символы только для платформ Windows. Второй делает то же самое, но гарантирует, что имя является законным для любой платформы.

Нужно помнить один угловой случай, который удивил меня, когда я впервые узнал об этом: Windows позволяет вводить пробелы в именах файлов! Например, ниже приведены все допустимые и разные имена файлов в Windows (без кавычек):

"file.txt"
" file.txt"
"  file.txt"

Один из выводов из этого: будьте осторожны при написании кода, который удаляет начальные / конечные пробелы из строки имени файла.

Упрощение ответа Евгения Каца:

bool IsFileNameCorrect(string fileName){
    return !fileName.Any(f=>Path.GetInvalidFileNameChars().Contains(f))
}

Или же

bool IsFileNameCorrect(string fileName){
    return fileName.All(f=>!Path.GetInvalidFileNameChars().Contains(f))
}

Microsoft Windows: ядро ​​Windows запрещает использование символов в диапазоне 1-31 (т. Е. 0x01-0x1F) и символов " *: < >? \ |. Хотя NTFS позволяет каждому компоненту пути (каталогу или имени файла) иметь длину 255 символов и длина путей до 32767 символов, ядро ​​Windows поддерживает только пути длиной до 259. Кроме того, Windows запрещает использование имен устройств MS-DOS AUX, CLOCK$, COM1, COM2, COM3, COM4, ​​COM5, COM6, COM7, COM8, COM9, CON, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9, NUL и PRN, а также эти имена с любым расширением (например, AUX.txt), кроме случаев использования Длинные UNC-пути (например, \.\C:\nul.txt или \?\D:\aux\con). (На самом деле, CLOCK $ может использоваться, если предоставляется расширение.) Эти ограничения применяются только к Windows - Linux, например, позволяет использовать "*: <>? \ | даже в NTFS.

Источник: http://en.wikipedia.org/wiki/Filename

Вместо того, чтобы явно включать все возможные символы, вы можете выполнить регулярное выражение, чтобы проверить наличие недопустимых символов, и затем сообщить об ошибке. В идеале ваше приложение должно называть файлы в точности так, как пожелает пользователь, и кричать нечестно, только если наткнется на ошибку.

Я использую это, чтобы избавиться от недопустимых символов в именах файлов без исключения:

private static readonly Regex InvalidFileRegex = new Regex(
    string.Format("[{0}]", Regex.Escape(@"<>:""/\|?*")));

public static string SanitizeFileName(string fileName)
{
    return InvalidFileRegex.Replace(fileName, string.Empty);
}

Вопрос в том, пытаетесь ли вы определить, является ли имя пути допустимым путем окон или оно разрешено в системе, где выполняется код.? Я думаю, что последнее более важно, поэтому лично я, вероятно, разложил бы полный путь и попытался бы использовать _mkdir для создания каталога, к которому принадлежит файл, а затем попытаться создать файл.

Таким образом, вы узнаете не только, содержит ли путь только допустимые символы Windows, но и действительно ли он представляет путь, который может быть записан этим процессом.

Также CON, PRN, AUX, NUL, COM# и некоторые другие никогда не являются допустимыми именами файлов в любом каталоге с любым расширением.

Из MSDN, вот список символов, которые не допускаются:

Используйте почти любой символ в текущей кодовой странице для имени, включая символы Unicode и символы в расширенном наборе символов (128–255), за исключением следующего:

  • Следующие зарезервированные символы недопустимы: < >: " / \ |? *
  • Символы, чьи целочисленные представления находятся в диапазоне от нуля до 31, не допускаются.
  • Любой другой символ, который целевая файловая система не позволяет.

Чтобы дополнить другие ответы, вот пара дополнительных крайних случаев, которые вы, возможно, захотите рассмотреть.

  • В Excel могут возникнуть проблемы, если вы сохраните книгу в файл, имя которого содержит символы "[" или "]". См. http://support.microsoft.com/kb/215205 для получения подробной информации.

  • Sharepoint имеет целый дополнительный набор ограничений. См. http://support.microsoft.com/kb/905231 для получения подробной информации.

Это вопрос, на который уже дан ответ, но только ради "других вариантов", вот неидеальный:

(неидеально, потому что использование исключений в качестве управления потоком, как правило, является "плохой вещью")

public static bool IsLegalFilename(string name)
{
    try 
    {
        var fileInfo = new FileInfo(name);
        return true;
    }
    catch
    {
        return false;
    }
}

Регулярные выражения излишни для этой ситуации. Вы можете использовать String.IndexOfAny() метод в сочетании с Path.GetInvalidPathChars() а также Path.GetInvalidFileNameChars(),

Также обратите внимание, что оба Path.GetInvalidXXX() методы клонируют внутренний массив и возвращают клон. Поэтому, если вы собираетесь делать это много раз (тысячи и тысячи раз), вы можете кэшировать копию недопустимого массива chars для повторного использования.

Также важна файловая система назначения.

Под NTFS некоторые файлы не могут быть созданы в определенных каталогах. EG $ загрузка в корне

Если вы пытаетесь проверить, не содержит ли строка, содержащая имя / путь вашего файла, недопустимые символы, самый быстрый способ, который я нашел, это использовать Split() разбить имя файла на массив частей, где есть недопустимый символ. Если результатом является только массив 1, недопустимые символы отсутствуют.:-)

var nameToTest = "Best file name \"ever\".txt";
bool isInvalidName = nameToTest.Split(System.IO.Path.GetInvalidFileNameChars()).Length > 1;

var pathToTest = "C:\\My Folder <secrets>\\";
bool isInvalidPath = pathToTest.Split(System.IO.Path.GetInvalidPathChars()).Length > 1;

Я попытался запустить этот и другие методы, упомянутые выше, для имени файла / пути 1 000 000 раз в LinqPad.

С помощью Split() составляет всего ~850 мс.

С помощью Regex("[" + Regex.Escape(new string(System.IO.Path.GetInvalidPathChars())) + "]") составляет около 6 секунд.

Справедливость более сложных регулярных выражений НАМНОГО хуже, как и некоторых других опций, таких как использование различных методов в Path класс для получения имени файла и выполнения их внутренней проверки (скорее всего из-за накладных расходов на обработку исключений).

Конечно, не очень часто нужно проверять 1 миллион имен файлов, так что в любом случае для большинства этих методов подходит одна итерация. Но это все еще довольно эффективно и эффективно, если вы ищете только недопустимые символы.

Моя попытка:

using System.IO;

static class PathUtils
{
  public static string IsValidFullPath([NotNull] string fullPath)
  {
    if (string.IsNullOrWhiteSpace(fullPath))
      return "Path is null, empty or white space.";

    bool pathContainsInvalidChars = fullPath.IndexOfAny(Path.GetInvalidPathChars()) != -1;
    if (pathContainsInvalidChars)
      return "Path contains invalid characters.";

    string fileName = Path.GetFileName(fullPath);
    if (fileName == "")
      return "Path must contain a file name.";

    bool fileNameContainsInvalidChars = fileName.IndexOfAny(Path.GetInvalidFileNameChars()) != -1;
    if (fileNameContainsInvalidChars)
      return "File name contains invalid characters.";

    if (!Path.IsPathRooted(fullPath))
      return "The path must be absolute.";

    return "";
  }
}

Это не идеально, потому что Path.GetInvalidPathChars не возвращает полный набор символов, которые недопустимы в именах файлов и каталогов, и, конечно, есть еще много тонкостей.

Поэтому я использую этот метод в качестве дополнения:

public static bool TestIfFileCanBeCreated([NotNull] string fullPath)
{
  if (string.IsNullOrWhiteSpace(fullPath))
    throw new ArgumentException("Value cannot be null or whitespace.", "fullPath");

  string directoryName = Path.GetDirectoryName(fullPath);
  if (directoryName != null) Directory.CreateDirectory(directoryName);
  try
  {
    using (new FileStream(fullPath, FileMode.CreateNew)) { }
    File.Delete(fullPath);
    return true;
  }
  catch (IOException)
  {
    return false;
  }
}

Он пытается создать файл и вернуть false, если есть исключение. Конечно, мне нужно создать файл, но я думаю, что это самый безопасный способ сделать это. Также обратите внимание, что я не удаляю каталоги, которые были созданы.

Вы также можете использовать первый метод для выполнения базовой проверки, а затем тщательно обрабатывать исключения, когда используется путь.

Я получил эту идею от кого-то. - не знаю кто. Пусть ОС сделает тяжелую работу.

public bool IsPathFileNameGood(string fname)
{
    bool rc = Constants.Fail;
    try
    {
        this._stream = new StreamWriter(fname, true);
        rc = Constants.Pass;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Problem opening file");
        rc = Constants.Fail;
    }
    return rc;
}

Многие из этих ответов не будут работать, если имя файла слишком длинное и работает в среде, предшествующей Windows 10. Точно так же подумайте о том, что вы хотите сделать с периодами - разрешить начальное или конечное значение технически допустимо, но это может создать проблемы, если вы не хотите, чтобы файл трудно было увидеть или удалить соответственно.

Это атрибут проверки, который я создал для проверки правильности имени файла.

public class ValidFileNameAttribute : ValidationAttribute
{
    public ValidFileNameAttribute()
    {
        RequireExtension = true;
        ErrorMessage = "{0} is an Invalid Filename";
        MaxLength = 255; //superseeded in modern windows environments
    }
    public override bool IsValid(object value)
    {
        //http://stackru.com/questions/422090/in-c-sharp-check-that-filename-is-possibly-valid-not-that-it-exists
        var fileName = (string)value;
        if (string.IsNullOrEmpty(fileName)) { return true;  }
        if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) > -1 ||
            (!AllowHidden && fileName[0] == '.') ||
            fileName[fileName.Length - 1]== '.' ||
            fileName.Length > MaxLength)
        {
            return false;
        }
        string extension = Path.GetExtension(fileName);
        return (!RequireExtension || extension != string.Empty)
            && (ExtensionList==null || ExtensionList.Contains(extension));
    }
    private const string _sepChar = ",";
    private IEnumerable<string> ExtensionList { get; set; }
    public bool AllowHidden { get; set; }
    public bool RequireExtension { get; set; }
    public int MaxLength { get; set; }
    public string AllowedExtensions {
        get { return string.Join(_sepChar, ExtensionList); } 
        set {
            if (string.IsNullOrEmpty(value))
            { ExtensionList = null; }
            else {
                ExtensionList = value.Split(new char[] { _sepChar[0] })
                    .Select(s => s[0] == '.' ? s : ('.' + s))
                    .ToList();
            }
    } }

    public override bool RequiresValidationContext => false;
}

и тесты

[TestMethod]
public void TestFilenameAttribute()
{
    var rxa = new ValidFileNameAttribute();
    Assert.IsFalse(rxa.IsValid("pptx."));
    Assert.IsFalse(rxa.IsValid("pp.tx."));
    Assert.IsFalse(rxa.IsValid("."));
    Assert.IsFalse(rxa.IsValid(".pp.tx"));
    Assert.IsFalse(rxa.IsValid(".pptx"));
    Assert.IsFalse(rxa.IsValid("pptx"));
    Assert.IsFalse(rxa.IsValid("a/abc.pptx"));
    Assert.IsFalse(rxa.IsValid("a\\abc.pptx"));
    Assert.IsFalse(rxa.IsValid("c:abc.pptx"));
    Assert.IsFalse(rxa.IsValid("c<abc.pptx"));
    Assert.IsTrue(rxa.IsValid("abc.pptx"));
    rxa = new ValidFileNameAttribute { AllowedExtensions = ".pptx" };
    Assert.IsFalse(rxa.IsValid("abc.docx"));
    Assert.IsTrue(rxa.IsValid("abc.pptx"));
}

Я предлагаю просто использовать Path.GetFullPath()

string tagetFileFullNameToBeChecked;
try
{
  Path.GetFullPath(tagetFileFullNameToBeChecked)
}
catch(AugumentException ex)
{
  // invalid chars found
}

Эта проверка

static bool IsValidFileName(string name)
{
    return
        !string.IsNullOrWhiteSpace(name) &&
        name.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 &&
        !Path.GetFullPath(name).StartsWith(@"\\.\");
}

отфильтровывает имена с неверными символами (<>:"/\|?* и ASCII 0-31), а также зарезервированные устройства DOS (CON, NUL, COMx). Это позволяет начальные пробелы и все-точечные имена, в соответствии с Path.GetFullPath, (Создание файла с начальными пробелами успешно в моей системе).


Используется.NET Framework 4.7.1, протестировано на Windows 7.

На мой взгляд, единственный правильный ответ на этот вопрос - попытаться использовать путь и позволить ОС и файловой системе проверить его. В противном случае вы просто повторно реализуете (и, вероятно, плохо) все правила проверки, которые уже используют ОС и файловая система, и если эти правила будут изменены в будущем, вам придется изменить свой код, чтобы он соответствовал им.

Имена файлов Windows довольно неограничены, так что на самом деле это может даже не быть проблемой. Символы, которые запрещены Windows:

\ / : * ? " < > |

Вы можете легко написать выражение, чтобы проверить, присутствуют ли эти символы. Тем не менее, лучшим решением было бы попытаться назвать файлы так, как хочет пользователь, и предупредить их, когда имя файла не совпадает.

Один лайнер для проверки нелегальных символов в строке:

public static bool IsValidFilename(string testName) => !Regex.IsMatch(testName, "[" + Regex.Escape(new string(System.IO.Path.InvalidPathChars)) + "]");
Другие вопросы по тегам