File.listFiles () transforma nombres Unicode con JDK 6 (problemas de normalización Unicode)

Estoy luchando con un extraño problema de encoding de nombre de archivo al enumerar los contenidos del directorio en Java 6 tanto en OS X como en Linux: los File.listFiles() y los métodos relacionados parecen devolver nombres de archivos en una encoding diferente que el rest del sistema.

Tenga en cuenta que no es solo la visualización de estos nombres de archivo lo que me está causando problemas. Principalmente estoy interesado en hacer una comparación de nombres de archivos con un sistema de almacenamiento de archivos remoto, por lo que me preocupa más el contenido de las cadenas de nombre que la encoding de caracteres utilizada para imprimir el resultado.

Aquí hay un progtwig para demostrar. Crea un archivo con un nombre Unicode y luego imprime versiones codificadas en URL de los nombres de archivo obtenidos del archivo creado directamente, y el mismo archivo cuando aparece en un directorio principal (debe ejecutar este código en un directorio vacío). Los resultados muestran la diferente encoding devuelta por el método 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")); } 

Esto es lo que obtengo cuando ejecuto este código de prueba en mis sistemas. Tenga en cuenta las representaciones de caracteres %CC versus %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 (ejecutándose en una VM en el mismo sistema 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) 

He intentado varios hacks para que las cadenas estén de acuerdo, incluida la configuración de la propiedad del sistema file.encoding y varias variables de entorno LC_CTYPE y LANG . Nada ayuda, ni quiero recurrir a tales hacks.

A diferencia de esta pregunta (¿algo relacionada?) , Puedo leer datos de los archivos enumerados a pesar de los nombres impares.

Usando Unicode, hay más de una forma válida de representar la misma letra. Los caracteres que está usando en su nombre Tricky son una “letra pequeña latina i con circunflejo” y una “letra pequeña latina con un anillo arriba”.

Usted dice “Note las representaciones de caracteres %CC versus %C3 “, pero mirando más de cerca lo que ve son las secuencias

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

Es decir, la primera letra i seguida por 0xCC82 que es la encoding UTF-8 del \u0302 “acentuar circunflejo combinado” de Unicode \u0302 mientras que la segunda es UTF-8 por “letra pequeña latina i con circunflejo”. De forma similar para el otro par, el primero es la letra a seguida de 0xCC8A, el carácter “anillo de combinación arriba” y el segundo es “letra pequeña latina a con anillo arriba”. Ambas son codificaciones UTF-8 válidas de cadenas de caracteres Unicode válidas, pero una está en “compuesto” y la otra en formato “descompuesto”.

Los volúmenes OS X HFS Plus almacenan cadenas (por ejemplo, nombres de archivos) como “completamente descompuestos”. Un sistema de archivos Unix realmente se almacena de acuerdo con la forma en que el controlador del sistema de archivos elija almacenarlo. No puede hacer ninguna instrucción general en diferentes tipos de sistemas de archivos.

Vea el artículo de Wikipedia sobre Equivalencia Unicode para una discusión general de formas compuestas frente a descompuestas, que menciona OS X específicamente.

Consulte el QA1235 de Apple Tech ( desafortunadamente en Objective-C) para obtener información sobre la conversión de formularios.

Un hilo de correo electrónico reciente en la lista de correo java-dev de Apple podría ser de alguna ayuda para usted.

Básicamente, debe normalizar la forma descompuesta en una forma compuesta antes de poder comparar las cadenas.

Solución extraída de la pregunta:

Gracias a Stephen P por ponerme en el camino correcto.

La solución primero, para los impacientes. Si está comstackndo con Java 6, puede usar la clase java.text.Normalizer para normalizar cadenas en una forma común de su elección, por ej.

 // 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; } 

Como java.text.Normalizer solo está disponible en Java 6 y versiones posteriores, si necesita comstackr con Java 5 puede que tenga que recurrir a la implementación sun.text.Normalizer y algo así como este hack basado en la reflexión. Consulte también Cómo se normaliza esto funciona el trabajo?

Esto solo es suficiente para que yo decida que no apoyaré la comstackción de mi proyecto con Java 5: |

Aquí hay otras cosas interesantes que aprendí en esta sórdida aventura.

  • La confusión se debe a que los nombres de los archivos se encuentran en una de las dos formas de normalización que no se pueden comparar directamente: Formulario de normalización Descomposición canónica (NFD) o Forma de normalización Composición canónica (NFC). El primero tiende a tener letras ASCII seguidas por “modificadores” para agregar acentos, etc., mientras que el último solo tiene los caracteres extendidos sin el carácter principal ACSCII. Lea la página wiki Stephen P referencias para una mejor explicación.

  • Los literales de cadena Unicode como el que figura en el código de ejemplo (y los recibidos a través de HTTP en mi aplicación real) están en el formulario NFD, mientras que los nombres de archivo devueltos por el método File.listFiles() son NFC. El siguiente mini-ejemplo demuestra las diferencias:

     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")); 

    Salida:

     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 
  • Si construye un objeto File con un nombre de cadena, el método File.getName() devolverá el nombre en cualquier forma que le haya dado originalmente . Sin embargo, si llama a los métodos de File que descubren los nombres por su cuenta, parecen devolver nombres en formato NFC. Este es un juego potencialmente desagradable. Ciertamente, gotchme.

  • De acuerdo con la siguiente cita de la documentación de Apple, los nombres de los archivos se almacenan en forma descompuesta (NFD) en el sistema de archivos HFS Plus:

    Cuando trabaje en Mac OS, se encontrará usando una mezcla de Unicode precompuesto y descompuesto. Por ejemplo, HFS Plus convierte todos los nombres de archivo a Unicode descompuesto, mientras que los teclados de Macintosh generalmente producen Unicode precompuesto.

    Así que el método File.listFiles() amablemente (?) Convierte los nombres de los archivos a la forma (pre) compuesta (NFC).

He visto algo similar antes. Las personas que suben archivos desde su Mac a una aplicación web utilizan nombres de archivos con é.

a) En SO, ese carácter es normal e + “signo para” aplicado al carácter anterior ”

b) En Windows es un char especial: é

Ambos son Unicode. Entonces … Entiendo que pasas la opción (b) a Crear archivo y en algún momento Mac OS la convierte a la opción (a). Tal vez si encuentra el problema de la doble representación en Internet, puede obtener una forma de manejar ambas situaciones con éxito.

¡Espero eso ayude!

En el sistema de archivos Unix, un nombre de archivo es realmente un byte terminado en nulo []. Por lo tanto, el tiempo de ejecución de java debe realizar la conversión de java.lang.String a byte [] durante la operación createNewFile (). La conversión de char-to-byte se rige por la configuración regional. He estado probando la configuración de LC_ALL en en_US.UTF-8 y en_US.ISO-8859-1 y obtuve resultados coherentes. Esto es con Sun (… Oracle) java 1.6.0_20. Sin embargo, para LC_ALL=en_US.POSIX , el resultado es:

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

3F es un signo de interrogación. Me dice que la conversión no fue exitosa para el personaje que no es ASCII. Por otra parte, todo es como se esperaba.

Pero la razón por la cual sus dos cadenas son diferentes es debido a la equivalencia entre el carácter \ u00EE (o C3 AE en UTF-8) y la secuencia i + \ u0302 ( 69 CC 82 en UTF-8). \ u0302 es una marca diacrítica combinada (combinando acento circunflejo). Algún tipo de normalización ocurrió durante la creación del archivo. No estoy seguro si se hace en el tiempo de ejecución de Java o en el sistema operativo.

NOTA: Me tomé un tiempo para averiguarlo, ya que el fragmento de código que ha publicado no tiene una marca diacrítica combinada, pero tiene el carácter equivalente î (por ejemplo, \u00ee ). Deberías haber incrustado la secuencia de escape de Unicode en el literal de la cadena (pero es fácil decir eso después …).

Sospecho que solo tiene que indicarle a javac qué encoding usar para comstackr el archivo .java que contiene los caracteres especiales, ya que lo ha codificado en el archivo fuente. De lo contrario, se usará la encoding predeterminada de la plataforma, que puede no ser UTF-8 en absoluto.

Puede usar el argumento VM -encoding para esto.

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

De esta forma, el archivo .class resultante terminará conteniendo los caracteres correctos y también podrá crear y listar el nombre de archivo correcto.

Una solución alternativa es usar la nueva API java.nio.Path en lugar de la API java.io.File que funciona perfectamente.

Intereting Posts