Файл.listFiles () искажает имена Юникода с помощью JDK 6 (проблемы нормализации Юникода)

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

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

вот программа для демонстрации. Он создает файл с именем Unicode, а затем распечатывает URL-encoded версии имен файлов, полученных из непосредственно созданного файла, и тот же файл, указанный в Родительском каталоге (вы должны запустить этот код в пустом каталоге). Результаты показывают различную кодировку, возвращаемую 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 переменные среды. Ничто не помогает, и я не хочу прибегать к таким хитростям.

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

6 ответов


используя Unicode, существует несколько допустимых способов представления одной и той же буквы. Символы, которые вы используете в своем хитром имени, - это "Латинская маленькая буква i с окружностью "и"Латинская маленькая буква a с кольцом выше".

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

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

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

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

см. статью Википедии на Эквивалентности Юникод для общего обсуждения составленных vs разложенных форм, в которых упоминается OS X конкретно.

см. технические вопросы и ответы Apple QA1235 (в Objective-C к сожалению) для получения информации о преобразовании форм.

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

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


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

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

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

// 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 реализация и что-то вроде этого отражение на основе hack см. также как это нормализует работу функции?

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

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

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

  • строковые литералы Unicode, такие как содержащийся в примере кода (и полученные через 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. Это потенциально противный попался. Это, конечно, gotchme.

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

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

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


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

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

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

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

надеюсь, что это помогает!


в файловой системе Unix имя файла действительно является байтом с нулевым завершением[]. Таким образом, среда выполнения java должна выполнять преобразование из java.ленг.Строка в байт[] во время операции createNewFile (). Преобразование char-to-byte регулируется языковым стандартом. Я тестировал настройки LC_ALL to en_US.UTF-8 и en_US.ISO-8859-1 и получил внятные результаты. Это с Солнцем (...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) и последовательность i+\u0302 (69 CC 82 в UTF-8). \u0302-комбинирующий диакритический знак (комбинирующий окружной акцент). Во время создания файла произошла некоторая нормализация. Я не уверен, сделано ли это во время выполнения Java или в ОС.

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


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

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

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

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


альтернативным решением является использование новой java.НИО.Путь api вместо java.Ио.Файл api, который работает отлично.