Code Tip: Mi experiencia con la marca de orden de bytes (BOM)

jueves, 24 de septiembre de 2015


En este artículo no voy a contar nada muy puntero ni novedoso, además seguramente mucho de vosotros ya conocéis, pero yo no conocía hasta hace unos días. El objetivo es compartir mi experiencia con la marca de orden de bytes y poder ayudar a alguién que se encuentre en la misma situación que yo hace unos días.

Por unas cosas o por otras, pocas veces desde que llevo programando, y van ya 13 años, he tenido la necesidad de tener que enfrentarme a creación de ficheros manualmente utilizando c#. Esta semana ha tocado y me he encontrado con algo que desconocia hasta ahora, la marca de orden de bytes.

En este artículo voy a contar los problemas que me he encontrado y lo que he aprendido en el camino.

Requerimientos y problema detectado

Sin entrar en muchos detalles, que no son necesarios para entender el problema, en mi empresa teniamos la necesidad de generar un fichero separado por tabuladores que después se iba a importar en una herramienta de terceros.

Clase que se encargará de crear los bytes

En nuestro proyecto tenemos una clase que se encarga de crear los bytes. Tiene métodos para añadir campos, lineas y para generar los bytes del fichero.

public class DelimitedFile : IDelimitedFile
{
    private string output = string.Empty;
    private Queue<byte> fifo = null;
    private string delimitedCharacter;

    public DelimitedFile(string delimitedCharacter)
    {
        fifo = new Queue<byte>();
 
        this.delimitedCharacter = delimitedCharacter;
    }

    public void Empty()
    {
        fifo = new Queue<byte>();
    }

    public void InsertField(string field)
    {
        if(!String.IsNullOrEmpty(field))
        {
            // First, sanitize the data to clean characters
            //  new line
            field = Regex.Replace(field, @"\r\n?|\n", String.Empty);
            // delimitator
            field = Regex.Replace(field, delimitedCharacter, String.Empty);
        }
        output = string.Concat(output, field + delimitedCharacter);
    }

    public void InsertLine()
    {
        output = string.Concat(output, "\n");
        Enqueue();
    }

    public void Enqueue()
    {
        byte[] bytes = Encoding.UTF8.GetBytes(output);

        foreach (var b in bytes)
            fifo.Enqueue(b);
        output = string.Empty;
    }

    public byte[] CreateFile()
    {
        if (output != string.Empty)
            Enqueue();

        byte[] bytes = fifo.ToArray();

        Empty();
        return bytes;
    }
}

Problemas con herramienta externa y excel

El problema detectado es que uno de los campos tiene una longitud máxima en la herramienta de terceros de 120 caracteres. Nos estaba dando error por superar esta longitud, pero aparentemente estaba bien controlado esta longitud en nuestro código antes de generar el fichero. Una de las pruebas que hice fue abrir el fichero con excel mediante el asistente de ficheros de texto y mi sorpresa fue ver que dentro de ese campo habia una palabra que debería ser nüvi que el asistente interpretaba como nüvi, añadiendo así un carácter más y superando la longitud. Además me doy cuenta de que en el origen del fichero en el asistente indica Windows (Ansi) y no UTF-8 como debería.

Probando con otras herramientas

Decido probar a abrir el fichero con otras herramientas para ver si entendían la códificación utf-8 correctamente. Pruebo con Notepad++ y con UltraEdit. Con estás herramientas se interpreta correctamente la palabra nüvi y reconoce la codificación como utf-8. Por lo tanto la herramienta de terceros debe utilizar excel o algo que se comporta de forma similar.

Analizando código heredado

Viendo la clase que se encarga de generar el fichero se observa claramente que la codificación utilizada para generar los bytes es utf-8, sin embargo se producía este problema. Así que me decido a investigar por la red acabando como siempre en el querido Stackoverflow. Es en esta web donde empiezo a leer en algunos post y respuestas la palabra BOM, que significa marca de orden de bytes.

Creando test que me ayudan a entender el problema

Me decido a crear un test sobre DelimitedFile que genere los bytes, para después verificar la codificación. Fijaros que en el test me creo un StreamReader y en el constructor le indico al segundo parámetro Encoding.Unicode y al segundo true. Con esto le estoy indicando que quiero que busque la marca de orden de bytes y si no la encuentra que me devuelva unicode como codificación, para después verificar que sea utf-8.

[TestMethod]
public void CreateFileBytes_EncodingShouldBeUTF8()
{
    DelimitedFile delimitedFile = new DelimitedFile("\t");

    delimitedFile.InsertField("nüvi");

    byte[] fileBytes = delimitedFile.CreateFile();

    Stream stream = new MemoryStream(fileBytes);

    StreamReader sr = new StreamReader(stream, Encoding.Unicode,true);

    //detect encoding
    sr.ReadToEnd();

    Encoding currentEncoding = sr.CurrentEncoding;

    Assert.AreEqual(Encoding.UTF8, currentEncoding);
} 


El test no pasa.

Entendiendo la marca de bytes

Después de que el test me falle me centro en entender mejor el BOM, son unos bytes que deben preceder a los bytes del fichero y ayuda al que lee los bytes a interpretar mejor la codificación. Buscando de nuevo por internet acabo en este artículo de la documentación de Microsoft donde ya entiendo los que debo de hacer. La marca de orden de bytes no se inclue por defecto, debo incluirla yo y la clase encoding tiene una propiedad GetPreamble que da esos bytes que necesito.

Modificando la clase que se encargará de crear los bytes

Modifico la clase que crea los bytes para que al inicio escriba el preamble de bytes (BOM).
public class DelimitedFile : IDelimitedFile
{
    private string output = string.Empty;
    private Queue<byte> fifo = null;
    private string delimitedCharacter;
    private Encoding encoding = new UTF8Encoding(true);

    public DelimitedFile(string delimitedCharacter)
    {
        fifo = new Queue<byte>();
        EnqueueBOM();
        this.delimitedCharacter = delimitedCharacter;
    }

    public void Empty()
    {
        fifo = new Queue<byte>();
    }

    public void InsertField(string field)
    {
        if(!String.IsNullOrEmpty(field))
        {
            // First, sanitize the data to clean characters
            //  new line
            field = Regex.Replace(field, @"\r\n?|\n", String.Empty);
            // delimitator
            field = Regex.Replace(field, delimitedCharacter, String.Empty);
        }
        output = string.Concat(output, field + delimitedCharacter);
    }

    public void InsertLine()
    {
        output = string.Concat(output, "\n");
        Enqueue();
    }

    public void Enqueue()
    {
        byte[] bytes = encoding.GetBytes(output);

        foreach (var b in bytes)
            fifo.Enqueue(b);
        output = string.Empty;
    }

    public void EnqueueBOM()
    {
        //https://en.wikipedia.org/wiki/Byte_order_mark

        byte[] bytes = encoding.GetPreamble();

        foreach (var b in bytes)
            fifo.Enqueue(b);
        output = string.Empty;
    }

    public byte[] CreateFile()
    {
        if (output != string.Empty)
            Enqueue();

        byte[] bytes = fifo.ToArray();

        Empty();
        return bytes;
    }
}

Pruebas finales

Ahora el test pasa y las pruebas con la herramientas de terceros también funcionan.

Concusiones

La marca de orden de bytes ayuda a entender la codificación de unos bytes y es algo que se tiene que añadir manualmente. En este artículo no se puesto la cantidad de vueltas reales que di hasta encontrar la solución porque el artículo quedaría demasiado largo :-). También he leido que la marca de orden de bytes no es recomendable añadirla en algunos entornos como unix porque puede confundirse con algún otro tipo de codificación pero para mi en este caso ha sido la solución.

No hay comentarios:

Publicar un comentario