Як я можу безпечно кодувати рядок у Java, щоб використовувати її як ім'я файлу?


117

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

    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 (правильно) кидається.

Як я можу безпечно кодувати String, щоб його можна було використовувати як ім'я файлу?

Редагувати: Я сподіваюся, що це дзвінок 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
Яка мета кодування рядка?
Стівен C

3
@Stephen C: мета кодування рядка - зробити придатним для використання як ім'я файлу, як це робиться java.net.URLEncoder для URL-адрес.
Стів Маклауд

1
О Я бачу. Чи потрібно кодування бути оборотним?
Стівен C

@Stephen C: Ні, його не потрібно реверсувати, але я хотів би, щоб результат був максимально схожий на початковий рядок.
Стів Маклауд,

1
Чи потрібно кодування затінювати оригінальну назву? Чи потрібно це бути 1-до-1; тобто зіткнення в порядку?
Стівен C

Відповіді:


17

Якщо ви хочете, щоб результат нагадував оригінальний файл, 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 працює, але він недоліком є ​​те, що він кодує безліч символів легальних імен файлів.

Якщо ви хочете не гарантовано бути реверсивним рішенням, просто видаліть «погані» символи, а не замінюючи їх послідовностями втечі.


Реверс вищевказаного кодування повинен бути однаково прямим для реалізації.


105

Моя пропозиція - скористатися підходом до "білого списку", тобто не намагайтеся відфільтрувати погані символи. Замість цього визначте, що добре. Ви можете або відхилити ім'я файлу, або відфільтрувати його. Якщо ви хочете відфільтрувати його:

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

Це означає, що замінює будь-який символ, який не є цифрою, буквою чи підкресленням нічим. Можна також замінити їх іншим символом (наприклад, підкресленням).

Проблема полягає в тому, що якщо це спільний каталог, ви не хочете зіткнення імен файлів. Навіть якщо області зберігання користувачів розділені користувачем, ви можете зіткнути назву файлу, просто відфільтрувавши погані символи. Ім’я, яке вводить користувач, часто корисне, якщо вони коли-небудь хочуть також його завантажити.

З цієї причини я, як правило, дозволяю користувачеві вводити те, що він хоче, зберігати ім'я файлу на основі схеми, яку я вибрав (наприклад, userId_fileId), а потім зберігати ім'я файлу користувача в таблиці бази даних. Таким чином ви можете відобразити його назад користувачеві, зберігати речі, як ви хочете, і ви не порушуєте безпеку або видаляєте інші файли.

Ви також можете хеш-файл (наприклад, хеш MD5), але тоді ви не можете перелічити файли, які вводиться користувачем (все одно не зі значущим іменем).

EDIT: виправлений регулярний вираз для Java


Я не думаю, що спочатку надати погане рішення. Крім того, MD5 - це майже зламаний хеш-алгоритм. Я рекомендую принаймні SHA-1 або краще.
vog

19
Для цілей створення унікального імені файлу кого цікавить, чи алгоритм "зламаний"?
клетус

3
@cletus: проблема полягає в тому, що різні рядки будуть зіставляти однакове ім’я файлу; тобто зіткнення
Стівен C

3
Зіткнення повинно бути навмисним, оригінальне запитання не говорить про те, що ці нитки обирає зловмисник.
tialaramex

8
Вам потрібно використовувати "\\W+"для regexp на Java. Зворотний проріз спочатку застосовується до самої рядку, і \Wне є дійсною послідовністю відходу. Я спробував відредагувати відповідь, але схоже, що хтось відхилив мою
редакцію

35

Це залежить від того, чи має бути кодування реверсивним чи ні.

Оборотна

Використовуйте кодування 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!?");
    }
}


2
Уявлення URLEncoder про те, що є особливим символом, може бути невірним.
Стівен C

4
@vog: URLEncoder не вдається "". і "..". Вони повинні бути закодовані, інакше ви зіткнетеся з записами в $ HOME
Stephen C

6
@vog: "*" дозволено лише у більшості файлових систем на базі Unix, NTFS та FAT32 не підтримують його.
Джонатан

1
"." і ".." можна вирішити, перемістивши точки до% 2E, коли рядок є лише крапками (якщо ви хочете мінімізувати послідовності евакуації). '*' також можна замінити на "% 2A".
viphe

1
зауважте, що будь-який підхід, який подовжує ім'я файлу (змінивши окремі символи на% 20 чи що завгодно), призведе до недійсності деяких імен файлів, близьких до межі довжини (255 символів для систем Unix)
smcg

24

Ось що я використовую:

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

Для цього потрібно замінити кожен символ, який не є літерою, цифрою, підкресленням або крапкою, підкресленням, використовуючи регулярний вираз.

Це означає, що щось на кшталт "Як перетворити £ в $" стане "How_to_convert___to__". Правда, цей результат не дуже зручний у користуванні, але він безпечний, і отримані імена каталогів / файлів гарантовано працюють скрізь. У моєму випадку результат не відображається користувачеві, і, таким чином, це не є проблемою, але ви можете змінити регулярний вираз на більш дозвільний характер.

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

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


6
Ще одна проблема полягає в тому, що вона характерна для мов, які використовують символи ASCII. Для інших мов це призведе до назви файлів, що складаються з нічого, крім підкреслення.
Енді Томас

13

Для тих, хто шукає загального рішення, це можуть бути звичайні критери:

  • Ім'я файлу має нагадувати рядок.
  • Кодування повинно бути зворотним, де це можливо.
  • Імовірність зіткнень повинна бути зведена до мінімуму.

Щоб досягти цього, ми можемо використовувати регулярний вираз, щоб відповідати незаконним символам, відсотковим кодувати їх, а потім обмежувати довжину кодованого рядка.

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_\\-]");

Замість того, щоб використовувати білий список, ви можете вибрати чорний список зарезервованих символів для вашої конкретної файлової системи. EG Цей регекс підходить для файлових систем FAT32:

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

Довжина

На Android 127 безпечних меж - 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");

Обмеження

Оскільки довші рядки усічені, існує можливість зіткнення імен при кодуванні або пошкодження при розшифровці.


1
Posix дозволяє дефісам - вам слід додати його до візерунка -Pattern.compile("[^A-Za-z0-9_\\-]")
mkdev

Додані дефіси. Дякую :)
SharkAlley

Я не думаю, що відсоткове кодування буде добре працювати на Windows, враховуючи, що це зарезервований символ ..
Amalgovinus

1
Не враховує не-англійські мови.
NateS

5

Спробуйте скористатися наступним регулярним виразом, який замінює кожен недійсний символ імені файлу пробілом:

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

Простіри неприємні для CLI; розглянути можливість заміни на _або -.
sdgfsdh


2

Це, мабуть, не найефективніший спосіб, але показує, як це зробити за допомогою трубопроводів 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, тому вам не доведеться привчати кожного символу легкої ваги до важкої ваги.


-1

Ви можете видалити недійсні символи ('/', '\', '?', '*'), А потім використовувати його.


1
Це ввело б можливість називання конфліктів. Тобто, "tes? T", "tes * t" і "test" піде той самий файл "test".
vog

Правда. Потім замініть їх. Наприклад, '/' -> коса риса, '*' -> зірка ... або використовувати хеш як запропоновано vog.
Беркхард

4
Ви завжди відкриті до можливості називати конфлікти
Brian Agnew

2
"?" та "*" дозволені символи в іменах файлів. Їх потрібно уникати лише в командах оболонки, тому що зазвичай використовується глобалізація. На рівні API файлів, однак, немає жодної проблеми.
vog

2
@Brian Agnew: насправді не так. Схеми, що кодують недійсні символи за допомогою зворотної схеми проходження, не зіткнуться.
Стівен C
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.