Подтвердить что ты не робот

File.listFiles() управляет именами Unicode с JDK 6 (проблемы с нормализацией Unicode)

Я борюсь со странной проблемой кодирования имени файла при перечислении содержимого каталога в Java 6 как для OS X, так и для Linux: File.listFiles() и связанные с ним методы, похоже, возвращают имена файлов в другой кодировке, чем остальные системы.

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

Вот программа для демонстрации. Он создает файл с именем Unicode и затем выводит URL-кодированные версии имен файлов, полученные из непосредственно созданного файла, и тот же файл, когда он указан в родительском каталоге (вы должны запустить этот код в пустой каталог). Результаты показывают различную кодировку, возвращаемую методом File.listFiles().

String fileName = "Trîcky Nåme";
File file = new File(fileName);
file.createNewFile();
System.out.println("File name: " + URLEncoder.encode(file.getName(), "UTF-8"));

// Get parent (current) dir and list file contents
File parentDir = file.getAbsoluteFile().getParentFile();
File[] children = parentDir.listFiles();
for (File child: children) {
    System.out.println("Listed name: " + URLEncoder.encode(child.getName(), "UTF-8"));
}

Вот что я получаю, когда запускаю этот тестовый код в своих системах. Обратите внимание на представления символов %CC против %C3.

OS X Snow Leopard:

File name: Tri%CC%82cky+Na%CC%8Ame
Listed name: Tr%C3%AEcky+N%C3%A5me

$ java -version
java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02-279-10M3065)
Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01-279, mixed mode)

KUbuntu Linux (работает в виртуальной машине в той же системе OS X):

File name: Tri%CC%82cky+Na%CC%8Ame
Listed name: Tr%C3%AEcky+N%C3%A5me

$ java -version
java version "1.6.0_18"
OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1)
OpenJDK Client VM (build 16.0-b13, mixed mode, sharing)

Я попробовал различные хаки, чтобы согласовать строки, включая установку системного свойства file.encoding и различные переменные среды LC_CTYPE и LANG. Ничто не помогает, и я не хочу прибегать к таким хакам.

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

4b9b3361

Ответ 1

Используя Unicode, существует более одного действительного способа представления одной и той же буквы. Персонажи, которые вы используете в своем Tricky Name, являются "латинской маленькой буквой я с округлой линией" и "латинской маленькой буквой a с кольцом выше".

Вы говорите "Обратите внимание на представления символов %CC против %C3", но, глядя ближе, вы видите последовательности

i 0xCC 0x82 vs. 0xC3 0xAE
a 0xCC 0x8A vs. 0xC3 0xA5

То есть первая буква i, за которой следует 0xCC82, которая является кодировкой UTF-8 Unicode \u0302 "комбинируя символ округлого акцента", а второй - UTF-8 для \u00EE "латинская маленькая буква я с обводкой". Аналогично для другой пары первая - это буква a, за которой следует 0xCC8A, символ "комбинирование кольца выше", а вторая - "латинская маленькая буква a с кольцом выше". Оба они являются допустимыми кодировками UTF-8 действительных символов Unicode, но один из них "составлен", а другой - в "разложенном" формате.

В томах OS X HFS Plus хранятся строки (например, имена файлов) как "полностью разложенные". Файловая система Unix действительно хранится в соответствии с тем, как драйвер файловой системы решает сохранить его. Вы не можете делать какие-либо общие заявления в разных типах файловых систем.

См. статью Wikipedia по Unicode Equivalence для общего обсуждения составленных или разложенных форм, в которых конкретно упоминается OS X.

См. Apple Tech Q & A QA1235 (в Objective-C к сожалению) для получения информации о преобразовании форм.

A недавний поток электронной почты в списке рассылки Apple java-dev может вам помочь.

В принципе, вам нужно нормализовать разложенную форму в сформованную форму, прежде чем вы сможете сравнить строки.

Ответ 2

Решение, извлеченное из вопроса:

Спасибо Стивену Р за то, что он поставил меня на правильный путь.

Исправить сначала, для нетерпеливого. Если вы компилируете с Java 6, вы можете использовать класс java.text.Normalizer для нормализации строк в общую форму по вашему выбору, например

// Normalize to "Normalization Form Canonical Decomposition" (NFD)
protected String normalizeUnicode(String str) {
    Normalizer.Form form = Normalizer.Form.NFD;
    if (!Normalizer.isNormalized(str, form)) {
        return Normalizer.normalize(str, form);
    }
    return str;
}

Так как java.text.Normalizer доступен только в Java 6 и более поздних версиях, если вам нужно скомпилировать Java 5, вам может потребоваться реализация sun.text.Normalizer и что-то вроде этого анализ на основе отражения См. также Как эта функция нормализации работает?

Этого достаточно для меня, чтобы решить, что я не буду поддерживать компиляцию моего проекта с помощью Java 5: |

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

  • Путаница вызвана тем, что имена файлов находятся в одной из двух форм нормализации, которые нельзя сравнивать напрямую: форма нормализации каноническая декомпозиция (NFD) или форма нормализации каноническая композиция (NFC). Первые имеют буквы ASCII, а затем "модификаторы", чтобы добавить акценты и т.д., В то время как последний имеет только расширенные символы без главного символа ACSCII. Прочитайте страницу вики. Стивен П ссылается на лучшее объяснение.

  • Строковые литералы из Юникода, подобные тому, который содержится в примере кода (и те, которые были получены через HTTP в моем реальном приложении), находятся в форме NFD, а имена файлов, возвращаемые методом File.listFiles(), - это NFC. Следующий мини-пример демонстрирует различия:

    String name = "Trîcky Nåme";
    System.out.println("Original name: " + URLEncoder.encode(name, "UTF-8"));
    System.out.println("NFC Normalized name: " + URLEncoder.encode(
        Normalizer.normalize(name, Normalizer.Form.NFC), "UTF-8"));
    System.out.println("NFD Normalized name: " + URLEncoder.encode(
        Normalizer.normalize(name, Normalizer.Form.NFD), "UTF-8"));
    

    Вывод:

    Original name: Tri%CC%82cky+Na%CC%8Ame
    NFC Normalized name: Tr%C3%AEcky+N%C3%A5me
    NFD Normalized name: Tri%CC%82cky+Na%CC%8Ame
    
  • Если вы построите объект File с именем строки, метод File.getName() вернет имя в любой форме, которую вы дали первоначально. Однако, если вы вызываете методы File, которые сами обнаруживают имена, они, похоже, возвращают имена в форме NFC. Это потенциально неприятный прием. Это наверняка получило.

  • Согласно приведенной ниже цитате из Документация Apple имена файлов хранятся в форме разложенного (NFD) в файловой системе HFS Plus

    При работе в Mac OS вы обнаружите, что используете смесь предварительно скомпонованного и разложенного Юникода. Например, HFS Plus преобразует все имена файлов в разложенные Unicode, в то время как клавиатура Macintosh обычно создает предустановленный Unicode.

    Таким образом, метод File.listFiles() помогает (?) преобразует имена файлов в форму (pre), составленную (NFC).

Ответ 3

Я видел нечто подобное раньше. Люди, которые загружают файлы с их Mac на webapp, использовали имена файлов с é.

a) В ОС, что char является нормальным знаком e + "для ', примененного к предыдущему char"

b) В Windows это специальный char: é

Оба являются Unicode. Итак... Я понимаю, что вы передаете (b) вариант создания файла, и в какой-то момент Mac OS преобразует его в (a). Возможно, если вы обнаружите проблему двойного представления в Интернете, вы можете получить способ успешно справиться с обеими ситуациями.

Надеюсь, что это поможет!

Ответ 4

В файловой системе Unix имя файла действительно является байтом с нулевым завершением []. Таким образом, среда выполнения java должна выполнять преобразование из java.lang.String в байт [] во время операции createNewFile(). Преобразование char -to-byte регулируется языковой версией. Я тестировал установку LC_ALL на en_US.UTF-8 и en_US.ISO-8859-1 и получил согласованные результаты. Это с Sun (... Oracle) java 1.6.0_20. Однако для LC_ALL=en_US.POSIX результат:

File name:   Tr%C3%AEcky+N%C3%A5me
Listed name: Tr%3Fcky+N%3Fme

3F - знак вопроса. Он говорит мне, что преобразование не было успешным для символа, отличного от ASCII. И снова все будет как ожидается.

Но причина, по которой ваши две строки отличаются друг от друга, связана с эквивалентностью символа \u00EE (или C3 AE в UTF-8) и последовательностью я +\u0302 (69 CC 82 в UTF-8).\u0302 представляет собой комбинированную диакритическую метку (сочетающую округлый акцент). Во время создания файла произошла некоторая нормализация. Я не уверен, что это было сделано во время выполнения Java или в ОС.

ПРИМЕЧАНИЕ. Я взял некоторое время, чтобы понять это, поскольку фрагмент кода, который вы опубликовали, не имеет комбинированной диакритической метки, но эквивалентного символа î (например, \u00ee). Вы должны были внедрить escape-последовательность Unicode в строковый литерал (но это легко сказать, что потом...).

Ответ 5

Я подозреваю, что вам просто нужно указать javac, какую кодировку использовать для компиляции файла .java, содержащего специальные символы, с тех пор, как вы жестко закодировали его в исходном файле. В противном случае будет использоваться кодировка по умолчанию платформы, которая вообще не может быть UTF-8.

Для этого можно использовать аргумент VM -encoding.

javac -encoding UTF-8 com/example/Foo.java

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

Ответ 6

Альтернативным решением является использование нового java.nio.Path api вместо java.io.File api, который отлично работает.