Как я могу безопасно кодировать строку в Java для использования в качестве имени файла?

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

    String s = ... // comes from external source
    File currentFile = new File(System.getProperty("user.home"), s);
    PrintWriter currentWriter = new PrintWriter(currentFile);

Если s содержит недопустимый символ, например "/" в ОС на основе Unix, тогда вызывается java.io.FileNotFoundException(правильно).

Как я могу безопасно кодировать строку, чтобы ее можно было использовать как имя файла?

Изменить: на что я надеюсь - это вызов API, который делает это для меня.

Я могу это сделать:

    String s = ... // comes from external source
    File currentFile = new File(System.getProperty("user.home"), URLEncoder.encode(s, "UTF-8"));
    PrintWriter currentWriter = new PrintWriter(currentFile);

Но я не уверен, является ли URLEncoder надежным с этой целью.

Ответ 1

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

Вместо этого вы хотите что-то вроде этого.

char fileSep = '/'; // ... or do this portably.
char escape = '%'; // ... or some other legal char.
String s = ...
int len = s.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
    char ch = s.charAt(i);
    if (ch < ' ' || ch >= 0x7F || ch == fileSep || ... // add other illegal chars
        || (ch == '.' && i == 0) // we don't want to collide with "." or ".."!
        || ch == escape) {
        sb.append(escape);
        if (ch < 0x10) {
            sb.append('0');
        }
        sb.append(Integer.toHexString(ch));
    } else {
        sb.append(ch);
    }
}
File currentFile = new File(System.getProperty("user.home"), sb.toString());
PrintWriter currentWriter = new PrintWriter(currentFile);

Это решение дает обратимое кодирование (без коллизий), где закодированные строки в большинстве случаев напоминают исходные строки. Я предполагаю, что вы используете 8-битные символы.

URLEncoder работает, но имеет тот недостаток, что он кодирует множество логических символов имени файла.

Если вы хотите не гарантированное, чтобы быть обратимым решением, просто удалите "плохие" символы, а не замените их escape-последовательностями.

Ответ 2

Мое предложение - принять подход "белого списка", то есть не пытаться фильтровать плохие символы. Вместо этого определите, что в порядке. Вы можете либо отклонить имя файла, либо отфильтровать его. Если вы хотите отфильтровать его:

String name = s.replaceAll("\\W+", "");

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

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

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

Вы также можете хэш файл (например, хеш файл MD5), но затем вы не можете перечислить файлы, которые пользователь вложил (не имея значимого имени).

EDIT: исправлено регулярное выражение для java

Ответ 3

Это зависит от того, должна ли кодировка быть обратимой или нет.

Реверсивный

Используйте кодировку URL (java.net.URLEncoder) для замены специальных символов с помощью %xx. Обратите внимание, что вы заботитесь о специальных случаях, когда строка равна ., равна .. или пуста! ¹ Многие программы используют кодировку URL для создания имен файлов, поэтому это стандартная методика, которую все понимают.

Необратимые

Используйте хэш (например, SHA-1) данной строки. Современные алгоритмы хеширования ( не MD5) можно считать беспорядочными. Фактически, если вы обнаружите столкновение, вы получите прорыв в криптографии.


<Суб > ¹ Вы можете обрабатывать все 3 специальных случая элегантно, используя префикс, например "myApp-". Если вы поместите файл непосредственно в $HOME, вам все равно придется это делать, чтобы избежать конфликтов с существующими файлами, такими как ".bashrc".
public static String encodeFilename(String s)
{
    try
    {
        return "myApp-" + java.net.URLEncoder.encode(s, "UTF-8");
    }
    catch (java.io.UnsupportedEncodingException e)
    {
        throw new RuntimeException("UTF-8 is an unknown encoding!?");
    }
}

Ответ 4

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

public String sanitizeFilename(String inputName) {
    return inputName.replaceAll("[^a-zA-Z0-9-_\\.]", "_");
}

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

Это означает, что что-то вроде "Как конвертировать £ в $" станет "How_to_convert___to__". По общему признанию, этот результат не очень удобен для пользователя, но он безопасен, и результирующие имена файлов/файлов гарантированно работают повсюду. В моем случае результат не отображается пользователю и, следовательно, не является проблемой, но вы можете изменить регулярное выражение на более разрешительный.

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

Ответ 5

Для тех, кто ищет общее решение, это могут быть общие критерии:

  • Имя файла должно напоминать строку.
  • Кодировка по возможности должна быть обратимой.
  • Вероятность столкновения должна быть минимизирована.

Для этого мы можем использовать регулярное выражение для соответствия недопустимым символам percent-encode их, а затем ограничить длину закодированной строки.

private static final Pattern PATTERN = Pattern.compile("[^A-Za-z0-9_\\-]");

private static final int MAX_LENGTH = 127;

public static String escapeStringAsFilename(String in){

    StringBuffer sb = new StringBuffer();

    // Apply the regex.
    Matcher m = PATTERN.matcher(in);

    while (m.find()) {

        // Convert matched character to percent-encoded.
        String replacement = "%"+Integer.toHexString(m.group().charAt(0)).toUpperCase();

        m.appendReplacement(sb,replacement);
    }
    m.appendTail(sb);

    String encoded = sb.toString();

    // Truncate the string.
    int end = Math.min(encoded.length(),MAX_LENGTH);
    return encoded.substring(0,end);
}

Шаблоны

Вышеупомянутый шаблон основан на консервативном подмножестве допустимых символов в спецификации POSIX.

Если вы хотите разрешить символ точки, используйте:

private static final Pattern PATTERN = Pattern.compile("[^A-Za-z0-9_\\-\\.]");

Просто будьте осторожны с строками типа "." и ".."

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

private static final Pattern PATTERN = Pattern.compile("[^a-z0-9_\\-]");

Или пропустите строчные буквы:

private static final Pattern PATTERN = Pattern.compile("[^a-z0-9_\\-]");

Вместо использования белого списка вы можете выбрать черный список зарезервированных символов для вашей конкретной файловой системы. НАПРИМЕР. Это регулярное выражение подходит для файловых систем FAT32:

private static final Pattern PATTERN = Pattern.compile("[%\\.\"\\*/:<>\\?\\\\\\|\\+,\\.;=\\[\\]]");

Длина

В Android, 127 символов является безопасным лимитом. Многие файловые системы допускают 255 символов.

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

// Truncate the string.
int start = Math.max(0,encoded.length()-MAX_LENGTH);
return encoded.substring(start,encoded.length());

Декодирование

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

URLDecoder.decode(filename, "UTF-8");

Ограничения

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

Ответ 7

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

public static String toValidFileName(String input)
{
    return input.replaceAll("[:\\\\/*\"?|<>']", " ");
}

Ответ 8

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

private static String sanitizeFileName(String name) {
    return name
            .chars()
            .mapToObj(i -> (char) i)
            .map(c -> Character.isWhitespace(c) ? '_' : c)
            .filter(c -> Character.isLetterOrDigit(c) || c == '-' || c == '_')
            .map(String::valueOf)
            .collect(Collectors.joining());
}

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

Ответ 9

Вы можете удалить недопустимые символы ('/', '\', '?', '*'), а затем использовать его.

Ответ 10

Просто используйте:

IOHelper.toFileSystemSafeName ( "Iblabla/блабла" );

превратится в "Iblablablabla"