Hoy vamos a experimentar con la libreria PhysFS, tambien denominada PhysicsFS. Que, a pesar de lo que pueda inducirnos su nombre, no nos servirá para resolver ningún problema de física.

La función principal de esta libreria es la de darnos acceso a archivos creando “directorios virtuales” a partir de archivos comprimidos, de forma que podremos acceder desde nuestra aplicación a esos archivos como si estuvieran en una carpeta real de nuestro sistema. Algo asi como enlaces a rutas pero con capacidad para trabajar con archivos comprimidos de forma transparente.

Introducción a PhysFS

PhysFS nos permite trabajar con diversos formatos de archivos comprimidos, como los conocidos ZIP o los 7z, aunque tambien nos permitirán trabajar con formatos de archivos de algunos videojuegos como DOOM, Quake, o Descent, entre otros. La ventaja principal de PhysFS es que nos permitirá empaquetar los archivos de datos de nuestras aplicaciones en uno o varios archivos organizandolos por carpetas, a los cuales podremos acceder como si fuera un unico sistema de archivos, dandonos acceso a los mismos de una forma sencilla. Bueno, antes de empezar con el codigo os haré una breve descripción del funcionamiento de PhysFS. Para empezar imaginaremos a PhysFS como una unidad de disco virtual o una ruta del sistema “especial”. Lo de “especial” lo resalto ya que no va a ser una unidad o ruta del sistema más, no la podremos ver en el sistema como tal a traves del administrador de archivos, sino que va a se un espacio propio de la aplicación a la que solo se podra acceder desde la aplicación que la esté utilizando y con unas funciones propias para la lectura y escritura de datos, aunque, como veremos, existen algunos trucos para poder utilizar los archivos de las unidades de Phys FS de forma más sencilla.

Uso de la libreria

Lo primero que habrá que realizar en todas nuestras aplicaciones será inicializar la libreria, con lo que se nos creará la unidad de PhysFS. Esto se realiza con la función Fs.PHYSFS_init(null) .

Una vez inicializada la libreria ya podremos empezar a adjuntar los datos de los archivos comprimidos o incluso añadir carpetas del sistema de archivos del ordenador.

A medida que vayamos haciendo esto se irán adjuntando todos los contenidos de los archivos y sus correspondientes carpetas como si fueran una unica unidad, permitiendonos leer cada uno de los archivos que contengan nuestros archivos de datos como si estuvieran en el mismo sitio.

Para explicarlo algo mejor imaginemos que tenemos dos archivos comprimidos y una carpeta.

El primer archivo contiene dos carpetas con un archivo distinto cada una de ellas:

/musica/musica1.mid
/imagen/dibu1.png

El segundo archivo contiene otras dos carpetas con otros dos archivos:

/sonido/bum.wav
/imagen/dibu2.png

Mientras que la carpeta del sistema contiene lo siguiente:

/sonido/bam.wav
/imagen/dibu3.png
/texto.txt

Supongamos ahora que vamos adjuntando los dos archivos comprimidos y la carpeta a nuestra unidad virtual, lo que obtendremos será la mezcla de todo como un unico espacio don diversas carpetas y archivos.

/musica/musica1.mid
/sonido/bum.wav
/sonido/bam.wav
/imagen/dibu1.png
/imagen/dibu2.png
/imagen/dibu3.png
/texto.txt

Si tuvieramos que modificar cualquier archivo PhysFS se encargará de comprimirlo y guardarlo donde corresponda de forma transparente para nosotros.

La versión de PhysFS para la plataforma .NET viene dentro del paquete TaoFramowork, por lo que si quereis utilizarlo debereis descargarlo aquí.

Para poder seguir este articulo supondré que el lector tiene unos conocimientos minimos de C#, por lo que algunas cosas las obviaré.

Una vez tengamos el paquete de Tao Framework instalado o descomprimido, según sea el caso, procederemos a crear un nuevo proyecto de aplicación de consola con nuestro entorno preferido.

En mi caso voy a utilizar el visual c# express, por comodidad mas que otra cosa aunque vosotros podeis utilizar el que más rabia os de.

Inicialización de la libreria

Una vez tenemos el proyecto creado, se supone que debemos tener un archivo de codigo donde aparecerá entre otras cosas la función main(), en caso contrario creadlo y escribid lo siguiente:

  1. //Programa.cs
  2. using System;
  3.  
  4. namespace PhysFS_1
  5. {
  6.     class Program
  7.     {
  8.         static void Main(string[] args)
  9.         {
  10.         }
  11.     }
  12. }

A continuación deberemos añadir una referencia a la libreria Tao.PhysFS y ya de paso asegurarnos de que se ha hecho referencia a System. Hecho esto empezaremos con el código para manejar la libreria en cuestión. Lo primero incluiremos la sentencia using siguiente para no tener que escribir más de la cuenta:

  1. using Tao.PhysFs;

Una vez hecho esto empezaremos a modificar la función main() para activar la libreria. Escribiremos dentro se la función main la sentencia de inicialización de PhysFS y ya que estamos escribiremos la sentencia de desinicialización.

  1.         static void Main(string[] args)
  2.         {
  3.          Fs.PHYSFS_init(null); //Esta es la funcion para inicializar PhysFS.
  4.  
  5.          //Aqui iria el codigo de las cosas que queremos hacer
  6.  
  7.          Fs.PHYSFS_Fs.PHYSFS_deinit(); //Desinicializamos la libreria antes de cerrar la aplicación
  8.         }

Bien, con esto en teoria ya podriamos compilar la aplicación y no deberia darnos ningun problema, aunque es posible que si ejecutamos la aplicación está nos pueda dar un error informandonos de que no es posible encontrar el archivo ‘physfs.dll’ o algo por el estilo.

Este error es debido a que TaoFramework no es realmente una implementación para .NET de la libreria, sino es lo que en inglés se denomina un warpper, es decir, una especie de puente entre .NET y el binario de la libreria original.

El codigo que realmente se ejecuta cuando se hace una llamada a cualquiera de las funciones de PhysFS se encuentra previamente compilado en los archivos de la libreria dinamica de PhysFS y habrá que tener en cuenta que aquello que se ejecute llamando a las funciones de TaoFramework no va a ser codigo protegido, sino codigo propietario del sistema en el que se esté ejecutando la aplicación.

Asi pues, para que funcione nuestra aplicación habrá que incluir los archivos de las librerias originales en la misma carpeta que nuestro ejecutable o bien en alguna ruta por defecto del sistema como c:\windows\system32.

En el caso de los sistemas windows el archivo preciso se encuentra en la carpeta /lib dentro de la ruta de instalación del TaoFramework.

En el caso de los sistemas linux, la mayoria de ellos ya disponen de esta libreria instalada por defecto, aunque en caso de que no funcione el tema podeis acudir a la pagina oficial de la libreria, aqui. Si es preciso copiaremos el archivo en la carpeta del binario de nuestro proyecto y eso deberia resolver el problema.

Bien, supongamos que nuestra aplicación ya compila y se ejecuta. Lo normal por ahora es que no haga nada de particular relevancia por lo que si no nos aparece ningún mensaje de error podremos darnos por contentos.

Añadiendo archivos y carpetas

A continuación lo que debemos hacer es añadir aquellas rutas o archivos que desemos incluir en nuestra aplicación. Esto lo realizaremos mediante la función Fs.PHYSFS_addToSearchPath(string newDir, int appendToPath). Los valores que acepta son: Por un lado newDir, donde especificaremos la ruta o el archivo a incluir dentro de nuestra “unidad virtual”. La segunda variable que acepta es appendToPath, que especifica donde deseamos que añadamos la ruta para que, en el caso de archivos duplicados tenga prevalencia uno sobre otro al buscarlos posteriormente. Un 1 colocará el archivo o ruta al final de la lista de busqueda, mientras que un 0 (cero) lo pondrá al principio. La función devolverá un valor entero distinto de 0 si no ha habido ningún problema y un 0 en el caso de que la función haya tenido algun problema.

Para averiguar que problema ha tenido podremos utilizar la función Fs.PHYSFS_getLastError() que nos devolverá una cadena de texto describiendo el error ocurrido.

Supongamos en este caso que queremos añadir dentro de nuestra “unidad virtual” el contenido de la carpeta /datos que está en la misma ruta que el ejecutable y de los archivos datos1.zip y datos2.zip . Si descargais los ejemplos que es encuentran al final de este articulo encontrareis los archivos en cuestión para hacer pruevas.

Es posible que la primera carpeta que nos interese añadir sea la ruta de ejecución de nuestra aplicación. Para saber cual es, podriamos acudir a la propiedad, Application.StartupPath, de la libreria System.Windows.Forms. Pero como en este caso no estamos utilizando dicha libreria podemos utilizar una función auxiliar de PhysFS. La función Fs.PHYSFS_getBaseDir().

Otras funciones auxiliares interesantes que nos ofrece PhysFS son:

Fs.PHYSFS_getUserDir() nos dará la ruta a los archivos del usuario actual.

Fs.PHYSFS_getCdRomDirs() nos dará un array de strings con la ruta a las distintas unidades de CD/DVD del sistema (util para utilizarlos de forma transparente).

PHYSFS_getDirSeparator() nos dará el caracter o la cadena que sirve para separar los nombres de las distintas carpetas dentro de una ruta según el sistema (en unix será “/”, mientras que en windows será “\\” y en MacOS “:”). Hay que tener en cuenta que PhysFS utiliza como separador la barra inclinada a la derecha “/”, por lo que esta función solo nos será util para manejar archivos del sistema.

Sigamos pues con el codigo añadiendo algunas lineas más a nuestra función principal y veamos como añadir archivos y carpetas.

  1.         static void Main(string[] args)
  2.         {
  3.             int result; //variable para guardar el resultado de las funciones
  4.             string dirSep; //variable para guardar la cadena de separación de las carpetas
  5.  
  6.             result=Fs.PHYSFS_init(null); //Esta es la funcion para inicializar PhysFS.
  7.  
  8.             dirSep = Fs.PHYSFS_getDirSeparator(); //guardamos el caracter separador
  9.             if (result == 0) { Console.Write(Fs.PHYSFS_getLastError()); Fs.PHYSFS_deinit(); return; }
  10.  
  11.             //ahora añadiremos la carpeta /datos de la ruta de nuestra aplicación
  12.             result = Fs.PHYSFS_addToSearchPath(Fs.PHYSFS_getBaseDir() + dirSep + "datos", 1);
  13.             if (result == 0) { Console.Write("ERROR: " + Fs.PHYSFS_getLastError() + "\n"); }
  14.  
  15.             //añadimos los archivos de datos (datos1.zip y datos2.zip)
  16.             result = Fs.PHYSFS_addToSearchPath("datos1.zip", 1);
  17.             if (result == 0) { Console.Write("ERROR: " + Fs.PHYSFS_getLastError() + "\n"); }
  18.             result = Fs.PHYSFS_addToSearchPath("datos2.zip", 1);
  19.             if (result == 0) { Console.Write("ERROR: " + Fs.PHYSFS_getLastError() + "\n"); }
  20.  
  21.             //Ahora seguiremos con más funciones
  22.  
  23.             //Y finalmente cerraremos la libreria.
  24.           Fs.PHYSFS_Fs.PHYSFS_deinit(); //Desinicializamos la libreria antes de cerrar la aplicación
  25.         }

Con esto se supone que habremos añadido los archivos y la carpeta en cuestión a nuestra aplicación gracias a la función Fs.PHYSFS_addToSearchPath() Evidentemente si el archivo o carpeta que queremos añadir no existe habrá que tratar el problema convenientemente. Aqui unicamente escribimos en la consola un mensaje de error.

Si quisieramos eliminar cualquiera de estos archivos o rutas podriamos utlizar la función Fs.PHYSFS_removeFromSearchPath(string path).  Ahora, para comprobar que es lo que hemos añadido a la unidad virtual añadiremos una nueva función.

  1. static void ImprimeAdjuntos()
  2.         {
  3.             string[] adjuntos = Fs.PHYSFS_getSearchPath();
  4.             foreach (string str in adjuntos)
  5.             {
  6.                 Console.Write(str + "\n");
  7.             }
  8.         }

Y en la función main() añadiremos la llamada a esta funcion:

Dentro de main()
  1.             //Ahora seguiremos con más funciones
  2.             ImprimeAdjuntos();
  3.             Console.Write("\n\n");

Esto imprimirá los archivos y las rutas que hemos añadido al sistema de archivos virtual. Si os fijais utilizamos la función Fs.PHYSFS_getSearchPath() que nos devuelve un array con los nombres de los archivos o carpetas que hemos añadido. Si no hubieramos añadido nada el array no tendria entradas.

Para saber el contenido de estos archivos y rutas crearemos otra función.

  1. static void EnumeraArchivos(string ruta)
  2.         {
  3.  
  4.             if (Fs.PHYSFS_exists(ruta) == 0)
  5.             {
  6.                 Console.Write("¡La carpeta '" + ruta + "' no existe! \n");
  7.                 return;
  8.             }
  9.             Console.Write("Archivos en " +  ruta + ": \n");
  10.             string[] a = Fs.PHYSFS_enumerateFiles(ruta);
  11.             foreach (string str in a)
  12.             {
  13.                 if(Fs.PHYSFS_isDirectory(ruta + str)!=0)
  14.                     Console.Write("    [" + str + "]\n");
  15.                 else if(Fs.PHYSFS_isSymbolicLink(ruta + str)!=0)
  16.                     Console.Write("    <" + str + ">\n");
  17.                 else
  18.                     Console.Write("    " + str + "\n");
  19.             }
  20.             Console.Write("\n\n");
  21.         }

Y para probarla añadiremos unas cuantas lineas más a la función main().

Dentro de main()
  1.             //Ahora seguiremos con más funciones
  2.             ImprimeAdjuntos();
  3.             Console.Write("\n\n");
  4.             EnumeraArchivos("");
  5.             EnumeraArchivos("graficos");
  6.             EnumeraArchivos("textos");
  7.             EnumeraArchivos("carpeta1");
  8.             EnumeraArchivos("carpeta1/carpeta2");
  9.             EnumeraArchivos("noexiste");

La función Fs.PHYSFS_exists(string ruta) nos devuelve 0 (cero) si la ruta o el archivo que le solicitemos no existe y cualquier otro valor si existe.

Una vez sepamos si la carpeta que deseamos listar existe utilizamos la función Fs.PHYSFS_enumerateFiles(ruta) para que nos devuelva un array con los nombres de los contenidos, ya sean archivos o carpetas.

Para distinguir entre archivos, carpetas o enlaces simbolicos podemos utilizar las funciones Fs.PHYSFS_isDirectory() o Fs.PHYSFS_isSymbolicLink(). Que nos devolverán 0 (cero) como resultado falso y cualquier otro valor como resultado verdadero. Los Enlaces simbolicos son una referencia a otro archivo o carpeta, pero que actuarán como si este estuvieran en ese mismo lugar a efectos de apertura y lectura. Para poder trabajar con enlaces simbolicos deberiamos utilizar previamente la función Fs.PHYSFS_permitSymbolicLinks(1) para habilitarlos. Con Fs.PHYSFS_permitSymbolicLinks(0) los deshabilitariamos. Por defecto suelen estar deshabilitados.

Leyendo archivos

Bien, hasta aqui hemos aprendido a añadir y quitar archivos y carpetas  a nuestra unidad virtual, a listar estos archivos y su contenido y ahora vamos a ver como podemos leer y escribir en estos archivos. PhysFS nos permite Leer archivos, ya estén en una carpeta de nuestro sistema o en un archivo comprimido, no obstante solo nos va a permitir crear o modificar archivos y carpetas en una carpeta del sistema ya que no puede modificar los archivos comprimidos, aunque eso ya lo veremos más adelante. Ahora veremos como leer de un archivo existente dentro nuestras rutas virtuales.

Lo primero,  vamos a crear una nueva función que nos abra el archivo y nos devuelva su contenido en forma de array de bytes. Luego, veremos como transformar el array a un objeto String o a un objeto Image, e incluso a una estructura definida por nosotros.

Por ahora veamos como abrir un archivo y leer los datos del mismo. La función siguiente abrirá el archivo especificado y nos devolverá un array con los datos del mismo, o en el caso de que el archivo no exista o esté bloqueado por cualquier otra operación devolverá un array vacio.  Para saber el motivo del error utilizaremos la ya conocida función Fs.PHYSFS_getLastError().

  1.   static byte[] CargaArchivo(string nombreArchivo)
  2.         {
  3.             //Primero comprovamos si el archivo en cuestión existe.
  4.             //Si no existe volvemos con un array vacio.
  5.             if (Fs.PHYSFS_exists(nombreArchivo) == 0) return new byte[0];
  6.  
  7.             //Ahora abrimos el archivo para leer
  8.             IntPtr miArchivo = Fs.PHYSFS_openRead(nombreArchivo);
  9.             if (miArchivo == IntPtr.Zero) return new byte[0]; //Si no puede abrir el archivo devuelve el array vacio
  10.             //Miramos el tamaño del archivo porque lo vamos a necesitar
  11.             long tamArchivo = Fs.PHYSFS_fileLength(miArchivo);
  12.  
  13.             //Y finalmente copiamos el contenido de todo el archivo a un array.
  14.             //Hay que tener cuidado con esto ya que podemos quedarnos sin memoria si el archivo es grande
  15.             byte[] array;
  16.             Fs.PHYSFS_read(miArchivo, out array, 1, (uint)tamArchivo);
  17.             //Finalmente cerramos el archivo
  18.             Fs.PHYSFS_close(miArchivo);
  19.             //Y devolvemos el array
  20.             return array;
  21.         }

Y para ver el funcionamiento de la función, en la función main() insertaremos la siguiente linea:

  1. byte[] datos = CargaArchivo("textos/TEST1.txt");

Con esto obtendremos en el array los datos contenidos en el archivo solicitado.

Hay que tener en cuenta que esta funcion devuelve el contenido de TODO el archivo, por lo que si intentamos abrir un archivo grande podemos tener algún que otro problema de memoria. Lo ideal seria abrir el archivo e ir leyendo los datos de forma controlada. Para hacer esto añadiremos una sobrecarga a la función de forma que nos permita especificar que trocito queremos del archivo.

  1.   static byte[] CargaArchivo(string nombreArchivo, ulong inicio, ulong tamBloque)
  2.         {
  3.             //Primero comprovamos si el archivo en cuestión existe.
  4.             //Si no existe volvemos con un array vacio.
  5.             if (Fs.PHYSFS_exists(nombreArchivo) == 0) return new byte[0];
  6.  
  7.             //Ahora abrimos el archivo para leer
  8.             IntPtr miArchivo = Fs.PHYSFS_openRead(nombreArchivo);
  9.             if (miArchivo == IntPtr.Zero) return new byte[0]; //Si no puede abrir el archivo devuelve el array vacio
  10.             //Miramos el tamaño del archivo
  11.             long tamArchivo = Fs.PHYSFS_fileLength(miArchivo);
  12.             if ((ulong)tamArchivo < inicio) return new byte[0]; //El archivo es menor que el punto de inicio
  13.             if ((ulong)tamArchivo < (inicio + tamBloque)) tamBloque = (ulong)tamArchivo - inicio; //Miramos el tamaño del bloque para que no sea mayor que lo que vamos a leer
  14.  
  15.             //Y finalmente copiamos el contenido de todo el archivo a un array.
  16.             //Hay que tener cuidado con esto ya que podemos quedarnos sin memoria si el archivo es grande
  17.             byte[] array;
  18.             Fs.PHYSFS_seek(miArchivo, inicio); //Colocamos el punto de lectura
  19.             Fs.PHYSFS_read(miArchivo, out array, 1, (uint)tamBloque); //Leemos el archivo
  20.             //Finalmente cerramos el archivo
  21.             Fs.PHYSFS_close(miArchivo);
  22.             //Y devolvemos el array
  23.             return array;
  24.         }

La única diferencia con el anterior es que posicionamos el punto de lectura con Fs.PHYSFS_seek() y luego leemos solo los bytes que nos interesan.

A estas alturas talvez os hayais fijado en que estoy declarando todas las funciones como static. Para que lo sepais, el motivo no es otro que la pereza. Podria haberlo trabajado un poco más, pero como el objetivo de este articulo es simplemente mostrar un ejemplo del uso de la libreria no tiene mayor importancia. Talvéz más adelante construya una clase para manejar más facilmente esta libreria. Ya veremos…

Archivos de texto

Ahora, para aquel que no sepa como hacerlo, veremos como transformar los datos que tenemos en nuestro array a tipos más utiles, como cadenas de texto, estructuras o graficos. Lo primero que veremos serán las cadenas de texto, porque es lo mas sencillo.
El texto normalmente suele tener una codificación concreta que nos permitirá transformar los bytes de nuestro array a caracteres.  Saber que tipo de codificación tiene nuestro documento es muy importante ya que de intentar transformar los bytes con una codificación incorrecta obtendremos textos sin sentido. La codificación estandard para la plataforma . NET es la Unicode que nos permite gran cantidad de caracteres distintos. Mientras que muchos archivos de texto todavia utilizan la codificación ASCII que tan solo nos permite utilizar 256 caracteres distintos (de hecho son algunos menos porque los primeros 32 suelen ser codigos de control especiales).

Asi mismo el formato unicode tiene distintas formas de codificar los datos; seguro que os suenan los formatos UTF-8, UTF-16 o UTF-32.; estos formatos encapsulan los caracteres para minimizar el tamaño de los archivos.  La forma en como se codifican los datos no es algo que interese para seguir este articulo asi que si quereis saber más os remito a la wikipedia donde se explica bastante mejor de lo que podria hacerlo yo.

Para saber si un archivo de texto está codificado con el juego de caracteres unicode o ascii observaremos los primeros bytes del archivo para ver si hay algún indicio del formato del archivo. En ocasiones, los archivos de texto guardados con caracteres unicode tienen una pequeña cabecera que nos indican la codificación de la misma. Esta cabecera se denomina BOM.

Leeremo pues los primeros bytes del archivo, si estos son 0xEF, 0xBB y 0xBF el archivo muy probablemente estará codificado en formato UTF-8, si son 0xFF y 0xFE el archivo muy probablemente estará codificado en formato UTF-16 Little Endian (con el byte menos significativo primero, utilizado en procesadores intel), si son 0xFE y 0xFF el archivo muy probablemente estará codificado en formato UTF-16 Big Endian (con el byte más significativo primero, utilizado en procesadores motorola y risc en general) , si son 0xFF, 0xFE, 0×00 y 0×00 el formato el archivo muy probablemente estará codificado en formato UTF-32 Little Endian, y f finalmente, si los bytes son 0×00, 0×00, 0xFE y 0xFF el formato el archivo muy probablemente estará codificado en formato UTF-32 Big Endian.

Si os fijais en todos los casos he especificado que “probablemente” estén en ese formato ya que en ocasiones los archivos están codificados en cualquiera de estos formatos, pero no se especifica la cabecera, con lo que deberemos saber de entemano cual es el formato del archivo. Para el siguiente ejemplo supondremos que el archivo está bien formado y tiene su cabecera. En caso contrario entenderemos que es un archivo de texto estandard en formato ASCII.

  1.   static string decoTXT(byte[] datos)
  2.         {
  3.             ASCIIEncoding ascii = new ASCIIEncoding();
  4.             UTF8Encoding utf8 = new UTF8Encoding();
  5.             UnicodeEncoding utf16_le = new UnicodeEncoding(false, true);
  6.             UnicodeEncoding utf16_be = new UnicodeEncoding(true, true);
  7.             UTF32Encoding utf32_le = new UTF32Encoding(false, true);
  8.             UTF32Encoding utf32_be = new UTF32Encoding(true, true);
  9.             string cadenaEstandard;
  10.             //Ahora miramos cual es el formato del archivo y lo descodificamos en consecuencia
  11.             if (datos[0] == 0×00 && datos[1] == 0×00 && datos[2] == 0xFE && datos[3] == 0xFF) //UTF-32 Big Endian
  12.             { cadenaEstandard = utf32_be.GetString(datos).Substring(1); }
  13.             else if (datos[0] == 0xFF && datos[1] == 0xFE && datos[2] == 0×00 && datos[3] == 0×00) //UTF-32 Little Endian lo ponemos primero para no confundirlo con el UTF-16 Little Endian
  14.             { cadenaEstandard = utf32_le.GetString(datos).Substring(1); }
  15.             else if (datos[0] == 0xFE && datos[1] == 0xFF) //UTF-16 Big Endian
  16.             { cadenaEstandard = utf16_be.GetString(datos).Substring(1); }
  17.             else if (datos[0] == 0xFF && datos[1] == 0xFE) //UTF-16 Little Endian (la mayoria de los archivos unicode utilizan este formato)
  18.             { cadenaEstandard = utf16_le.GetString(datos).Substring(1); }//Copiamos la cadenaEstandard eliminando eliminando primer caracter que es la cabecera
  19.             else if (datos[0] == 0xEF && datos[1] == 0xBB && datos[2] == 0xBF) //UTF-8 (el habitual de las páginas web ultimamente)
  20.             { cadenaEstandard = utf8.GetString(datos).Substring(1); }
  21.             else //Seguramente será ascii
  22.             { cadenaEstandard = ascii.GetString(datos); }
  23.             return cadenaEstandard;
  24.         }

El funcionamiento es muy simple. unicamente hay que pasarle el array de bytes a la función y nos devolverá una cadena de texto correctamente formateada.

  1.  byte[] datos = CargaArchivo("textos/TEST1.txt");
  2.  string cadena = decoTXT(datos);

Por cierto, las clases AsciiEncoding, UnicodeEncoding y compañia las encontamos en System.Text, por lo que habrá que incluir la clausula using correspondiente al principio del archivo.

using System.Text;
  1. <p>Las funciones inversas para pasar de una cadena de texto a un array de bytes tampoco son tan complicadas, y nos serán utiles más adelante cuando queramos escribir cualquier cosa en un archivo.</p>
  2. <pre lang="csharp">
  3.         static byte[] codificaTXT_UTF8(string texto)
  4.         {
  5.             UTF8Encoding unicode = new UTF8Encoding();
  6.             return unicode.GetBytes(texto);
  7.  
  8.         }
  9.  
  10.         static byte[] codificaTXT_UTF16(string texto)
  11.         {
  12.             return codificaTXT_UTF16(texto, false);
  13.         }
  14.  
  15.         static byte[] codificaTXT_UTF16(string texto, bool bigEndian)
  16.         {
  17.             UnicodeEncoding unicode = new UnicodeEncoding(bigEndian, false);
  18.             return unicode.GetBytes(texto);
  19.         }
  20.  
  21.         static byte[] codificaTXT_UTF32(string texto)
  22.         {
  23.             return codificaTXT_UTF32(texto, false);
  24.         }
  25.  
  26.         static byte[] codificaTXT_UTF32(string texto, bool bigEndian)
  27.         {
  28.             UTF32Encoding unicode = new UTF32Encoding(bigEndian, false);
  29.             return unicode.GetBytes(texto);
  30.         }
  31.  
  32.         static byte[] codificaTXT_ASCII(string texto)
  33.         {
  34.             ASCIIEncoding ascii = new ASCIIEncoding();
  35.             return ascii.GetBytes(texto);
  36.         }

Archivos de imagen

Bueno, despues de ver como pasar los datos del array de bytes a una cadena de texto veamos como pasar esos datos a un objeto Image.

Lo primero de todo es definir un par de clausulas using para poder trabajar con las clases Image y MemoryStream. Y en el caso de que no lo hayamos hecho antes, añadir la referencia al proyecto de la libreria System.Drawing.

using System.Drawing;
  1. using System.IO;

Ahora, lo unico que hará falta, es crear un objeto MemoryStream a partir del array con el contenido completo del archivo grafico y despues crear un archivo Image a partir de ese objeto. No hará falta descodificar el archivo ni nada por el estilo ya que la clase Image lo hará por nosotros gracias a la función Image.FromStream() como si estuviera abriendo el archivo directamente del disco.

  1. static Image byteArrayAImage(byte[] byteArrayIn)
  2.             {
  3.                  MemoryStream ms = new MemoryStream(byteArrayIn);
  4.                  Image returnImage = Image.FromStream(ms);
  5.                  return returnImage;
  6.             }

Y al igual que con el texto, la función para pasar de una imagen a un archivo grafico:

  1. public byte[] imageAByteArray(System.Drawing.Image imageIn)
  2.             {
  3.              MemoryStream ms = new MemoryStream();
  4.              imageIn.Save(ms,System.Drawing.Imaging.ImageFormat.Png);
  5.              return  ms.ToArray();
  6.             }

En este caso se crearia un archivo PNG, pero podriamos grabarlo en cualquier formato aceptado por la clase Image como Gif, Jpg, Bmp o Tiff.

Estructuras de datos

Finalmente, lo más interesante de la lectura de archivos es poder pasar los datos leidos directamente del array a una estructura. No obstante, esto es algo más complejo que requiere tener en cuenta ciertos aspectos.

Existen dos formas de pasar un array de bytes a una estructura. La primera, la artesana, consistiria en ir leyendo e interpretando poco a poco cada uno de los bytes para ir colocandolos en su variable correspondiente de la estructura. No hay que decir que este procedimiento puede ser muy lento y pesado a la hora de escribir el codigo, pero nos permite una mayor flexibilidad a la hora de leer los archivos.

El segundo sistema, que es el que trataremos aqui, consiste simplemente en copiar el contenido del array de bytes directamente a una estructura sin interpretar los datos. Lo que haremos será copiar el contenido del espacio de memoria del array al espacio de memoria de la estructura y es por eso que la estructura debe tener unas caracteristicas determinadas.

Para empezar la estructura debe tener un tamaño fijo, es decir, no puede especificarse simplemente un string, debe especificarse tambien que tamaño tiene ese string, o en caso contrario los valores del array no encajarán dentro de la estructura. Si queremos insertar strings de longitud variable deberemos utilizar el metodo “artesano”.

Como segunda condición los valores de la estructura deberán tener un orden determinado por el mismo motivo que antes, porque sino no encajarian los datos. Es decir, habrá que especificar que la estructura es secuencial para que no se nos cambie el orden de los mismos.

En ocasiones, algunos valores de la estructura podrán tener formatos especificos de codificación que habrá que definir para que se pueda descodificar el array correctamente. ¿Y como hacerlo? Pues con una serie de directivas indicadas entre corchetes.

Para empezar definiremos una estructura. En esta ocasión voy a rellenarla con bastantes tipos de datos para ver como hacer en cada caso.

  1. [StructLayout(LayoutKind.Sequential)]
  2.         struct MiEstructura
  3.         {
  4.             public int entero32bits;
  5.             public long entero64bits;
  6.             public float numeroConDecimales; //32bits
  7.             public double numeroConDecimalesDoble; //64bits
  8.  
  9.             public char caracter; //Atención que son 2bytes por caracter al ser unicode
  10.             public DateTime fechaYhora;
  11.             [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)]
  12.             public string texto50caracteres;
  13.             [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
  14.             public byte[] array8bytes;
  15.         }

Esto es solo un ejemplo. para que se vea mas o menos como definir la estructura. para ver que otros parametros podemos utilizar en la sentencia MarshallAs podemos ir a la documentación de Microsoft aqui o aqui.

Una vez tengamos la estructura utilizaremos las siguientes funciones para pasar del array a la estructura o viceversa.

  1. public static object RawDeserialize(byte[] rawData, int position, Type anyType)
  2.         {
  3.             int rawsize = Marshal.SizeOf(anyType);
  4.             if (rawsize > rawData.Length)
  5.                 return null;
  6.             IntPtr buffer = Marshal.AllocHGlobal(rawsize);
  7.             Marshal.Copy(rawData, position, buffer, rawsize);
  8.             object retobj = Marshal.PtrToStructure(buffer, anyType);
  9.             Marshal.FreeHGlobal(buffer);
  10.             return retobj;
  11.         }
  12.  
  13.         public static byte[] RawSerialize(object anything)
  14.         {
  15.             int rawSize = Marshal.SizeOf(anything);
  16.             IntPtr buffer = Marshal.AllocHGlobal(rawSize);
  17.             Marshal.StructureToPtr(anything, buffer, false);
  18.             byte[] rawDatas = new byte[rawSize];
  19.             Marshal.Copy(buffer, rawDatas, 0, rawSize);
  20.             Marshal.FreeHGlobal(buffer);
  21.             return rawDatas;
  22.         }

He de decir que las dos últimas funciones no son mias, las encontré hace tiempo en un foro y no sabria decir exactamente quien las escribió ya que las he visto copiadas en diversos sitios con diversos autores. No obstante, para el caso nos servirán y yo no las podria haber hecho mejor.

Su uso es bastante simple:

  1.             MiEstructura estructura;
  2.             estructura.entero32bits =32;
  3.             estructura.entero64bits=64;
  4.             estructura.numeroConDecimales=32.0F;
  5.             estructura.numeroConDecimalesDoble=64.0;
  6.             estructura.caracter='c';
  7.             estructura.fechaYhora = new DateTime(2009, 6, 15);
  8.             estructura.texto50caracteres = "Hola".PadRight(50, ' ');
  9.             estructura.array8bytes = new byte[8];
  10.  
  11.             byte[] datosStruct = RawSerializa(estructura);
  12.  
  13.             MiEstructura estructura2 = (MiEstructura)RawDeserializa(datosStruct, 0, typeof(MiEstructura));

Con este ejemplo creamos una estructura, que rellenamos con los datos que necesitemos y luego la pasamos a un array de bytes para finalmente volver a pasarlo a otra estructura.

Escritura de archivos

Ya he mencionado antes que PhysFS, no nos permite crear ni modificar archivos dentro de un archivo comprimido. Únicamente nos permitirá trabajar con archivos o carpetas dentro de una ruta del sistema.
Para poder hacer eso, primero de todo deberemos especificar en que carpeta del sistema deseamos escribir, para ello utilizaremos la función la función Fs.PHYSFS_setWriteDir(string newDir) donde en newDir especificaremos la ruta completa a la carpeta donde deseemos escribir.

Los usuarios de sistemas operativos con permisos de escritura especificos para cada directorio deberán tener en cuenta que la aplicación debe tener permisos para el directorio seleccionado, o de lo contrario no funcionará.

Lo habitual es utilizar esa función acompañada de Fs.PHYSFS_getBaseDir() para definir como directorio de escritura el directorio desde el que se ejecuta la aplicación aunque podemos realmente elegir cualquier carpeta aunque no esté agregada a la unidad virtual.

También podremos crear directorios nuevos con la función Fs.PHYSFS_mkdir(string newDir). Este directorio se creará dentro de la carpeta especificada con Fs.PHYSFS_setWriteDir(string newDir) y lamentablemente no podremos ni modificar su nombre ni eliminarlo a través de PhysFS ya que la libreria no dispone de esas funciones, por lo que deberemos acudir a las funciones de la clase estandard System.IO.Directory.

Una vez seleccionado el directorio de escritura procederemos a abrir el archivo a modificar. Para ello tenemos dos funciones:

Fs.PHYSFS_openAppend(string Archivo) abrirá un archivo existente sin modificarlo o lo creará si este no existiera y pondrá el cursor de escritura al final del archivo.

Fs.PHYSFS_openWrite(string Archivo)creará un archivo nuevo o abrirá uno existente eliminando todo su contenido poniendo el cursor de escritura al principio del mismo.

Si la apertura del archivo ha ido bien las funciones de apertura nos devolverán un puntero al archivo en cuestión. Si ha habido algún problema nos devolverá un PtrInt.Zero

A partir de ahi ya podemos empezar a escribir los datos.

Para ello, PhysFS dispone de diversas funciones según los datos que deseemos escribir.

Las mas especificas nos permitirán escribir valores numéricos con distinto tamaño en bits (16,23,64), orden de los mismos (Big endian, low endian) y si tienesn o no signo. Los nombres de dichas funciones empiezan siempre por Fs.PHYSFS_write, a continuación el numero de bits, luego se identifica si tiene signo con una ‘S’ o si no, con una ‘U’ y finalmente el orden de los bytes con ‘LE’ para low endian o ‘BE’ para big endian. Por ejemplo Fs.PHYSFS_writeULE16(archivo, valor) o Fs.PHYSFS_writeSBE32(archivo, valor).

No obstante, a efectos prácticos la función generica Fs.PHYSFS_write() es mucho más util ya que nos permitirá escribir arrays de bytes directamente en el archivo. El último ejemplo nos muestra como:

  1.  
  2.            string s ="¡Hola!";
  3.  
  4.            //Creamos los objetos para descodificar
  5.             UnicodeEncoding unicode = new UnicodeEncoding();
  6.             ASCIIEncoding ascii = new ASCIIEncoding();
  7.  
  8.             result = Fs.PHYSFS_setWriteDir(Fs.PHYSFS_getBaseDir());
  9.             if (result != 0)
  10.             {
  11.                 byte[] caractersU = unicode.GetBytes(s); //Primero transformamos el texto a un array de bytes
  12.                 GCHandle gch = GCHandle.Alloc(caractersU, GCHandleType.Pinned);
  13.                 IntPtr buffer = gch.AddrOfPinnedObject();
  14.  
  15.                 IntPtr hArchivo = Fs.PHYSFS_openWrite("a.txt");
  16.                 Fs.PHYSFS_write(hArchivo, buffer, 1, (uint)caractersU.Length);
  17.                 Fs.PHYSFS_close(hArchivo);
  18.  
  19.                 gch.Free();
  20.             }

En este ejemplo vemos como utilizar el Garbage Colector para bloquear el array de bytes y obtener su puntero, que luego utilizaremos en la función Fs.PHYSFS_write()

Una vez hayamos terminado con la escritura no hay que olvidarse de cerrar el archivo con la función Fs.PHYSFS_close(archivo) y si hemos utilizado la clase GCHandle de liberar el objeto creado.

Y con esto por ahora terminamos este articulo, que ya tenia yo ganas.

Descarga los ejemplos aqui

Por ahora los ejemplos son muy basicos. Con algo más de tiempo posiblemente los ponga más bonitos y claros.