01 enero 2013

Generando archivos Auto-Extraibles en PHP para Windows (o Linux)

Para abrir el año y considerando que tuve que pasar en casa el año nuevo en casa debido a situaciones fuera de mi control, decidí que podía hacer algo interesante para aprovechar el tiempo libre.

Hace varios días había un pequeño tema en SVCommunity preguntando como hacer para generar "auto-extraibles" vía PHP. Luego de buscar un poco en Google y leer un par de respuestas un tanto "pedantes" en StackOverflow donde sugerían que lo mejor era cambiar de proveedor de hosting, decidí que tal vez había una forma sencilla de hacer un autoextraible en Linux que funcionara en Windows que no implicara correr comandos externos.

Una idea bastante simple


Mi idea es bastante sencilla: En vez de hacer o utilizar un programa complicado que se re-compile para generar un auto-extraible, ¿Por qué no mejor agregar los datos al final de un ejecutable? Así, si el programa en tiempo de ejecución detecta que es más grande de lo que era al momento de compilarse, tratará de descomprimir los datos que tiene "extra" al final de si mismo.



De esta manera en linux no es necesario ejecutar ningún codigo ni recompilar nada, podemos agregar los datos al final del ejecutable con el comando "cat" o si dado el caso nuestro proveedor de hosting no nos lo permite, podemos utilizar directamente las funciones de lectura de archivos en PHP para generar el "autoextraible".

Comenzando por lo más dificil, descomprimiendo el Zip


La ventaja de la fuente abierta es que nos ahorra el problema de reinventar la rueda, luego de googlear un poco encontré que la opción más sencilla era utilizar "MiniZip". Esta es una pequeñísima utilidad de línea de comandos que descomprime archivos en formato Zip, pueden descargar el código fuente para compilar la aplicación desde la siguiente dirección:


Obviamente el problema es que para hacer un ejecutable que funcione en Windows necesitaba compilarlo en Windows o en su defecto hacer una "compilación cruzada". Como no me quería complicar demasiado, instalé el cygwin incluyendo gcc, make y zlib (necesario para compilar MiniZip).

Tuve que realizar algunas pequeñas modificaciones al código original. La más relevante es cambiar todas las llamadas de "fopen64" a "fopen" ya que por defecto cygwin utiliza fopen64 y el código genera errores de referencia si se llama de la primera manera. Luego hay que hacer algunas modificaciones al Makefile para que encuentre las bibliotecas de zlib.

En el código podrán encontrar las modificaciones realizadas a las fuentes originales de MiniZip.

Detectando el cambio del tamaño del ejecutable


Esta es la parte más fácil y a la vez mas confusa de como funciona el archivo autoextraible, la lógica utilizada es sumamente sencilla:
  1. Verificar el tamaño del archivo ejecutable y comparar con el tamaño "original"
  2. Si el tamaño es igual, terminar la ejecución del programa.
  3. Si el tamaño es mayor, copiar los datos excedentes a un archivo temporal y descomprimir.
Lo anterior nos lleva al problema del "huevo o la gallina": ¿Cómo se que tamaño va a tener el ejecutable antes de compilarlo? La respuesta es sencilla: Simplemente no lo se, el tamaño de compilación depende de las versiones de las bibliotecas utilizadas y las optimizaciones que realice el compilador (en este caso GCC).

Para realizar la compilación realizo un procedimiento que podría sonar hasta un poco tonto:
  1. Compilo el programa
  2. Verifico el tamaño del archivo
  3. Modifico la fuente y lo vuelvo a recompilar.
  4. Verifico si el tamaño no ha cambiado.
Lo anterior funciona porque el espacio en bytes asignado para la variable que guarda el tamaño del archivo compilado siempre es el mismo, lo que significa que cambiar su valor antes de compilar no debería cambiar el tamaño final del archivo compilado.

Poniendo todo junto


Modificando un poco MiniZip removí todas las operaciones originales y la reemplazé por mi algoritmo de identificación de cambio de tamaño y así nacio MiniSXF. El nombre viene derivado de MiniZip y Self eXtracting File, además que me pareció que ese nombre no es usado por nadie más.

Si estan interesados en ojear un poco el código fuente pueden accederlo por medio de GitHub en la siguiente dirección:


Ejemplos de uso en PHP


Los siguientes ejemplos asumen que tienen el archivo MiniSXF.exe y el archivo ZIP en la misma carpeta, para el ejemplo mi archivo ZIP se llama "Payload-TEST.zip".

El primer ejemplo es el más sencillo que utiliza shell_exec para llamar al comando "cat":

<?php
header("Content-Description: File Transfer");
header("Content-Disposition: attachment; filename=autoextraible.exe");
header("Content-Type: application/octet-stream");
header("Content-Transfer-Encoding: binary");

echo shell_exec("/bin/cat MiniSXF.exe Payload-TEST.zip");

exit();

El segundo ejemplo utiliza la funcion "readfile" que lee un archivo completo y lo envía a la salida:
<?php
header("Content-Description: File Transfer");
header("Content-Disposition: attachment; filename=autoextraible.exe");
header("Content-Type: application/octet-stream");
header("Content-Transfer-Encoding: binary");

readfile("MiniSXF.exe");
readfile("Payload-TEST.zip");
exit();

El tercer y último ejemplo intenta emular la función "readfile" pero haciendo uso de las funciones fopen, este lo pueden utilizar en caso de que el servicio de hosting no les permita utilizar el comando "readfile" (muy poco probable):
<?php
header("Content-Description: File Transfer");
header("Content-Disposition: attachment; filename=autoextraible.exe");
header("Content-Type: application/octet-stream");
header("Content-Transfer-Encoding: binary");

function custom_readfile($filename) {
  $f = fopen($filename,"rb");
  while(!feof($f)) {
    echo fread($f,512);
  }
  fclose($f);
}

custom_readfile("MiniSXF.exe");
custom_readfile("Payload-TEST.zip");
exit();

Puedes compilar tu código bajando las fuentes del repositorio o puedes descargar el ejecutable ya compilado de la siguiente dirección (la opción más prudente):


¿Interesado en colaborar? 


Realmente este código lo hice entre el día de ayer y hoy así que la funcionalidad es sumamente limitada, sin embargo sería bonito que tuviera algunas otras funciones mas interesantes como comprimir los archivos en otro formato diferente a ZIP. Tal vez una interfaz gráfica o simplemente que se pueda pre-configurar el directorio destino.

Si tienes conocimientos en C y estás dispuesto a colaborar, solo tienes que clonar del repositorio git en la siguiente dirección:


No habiendo nada mas que decir, solo espero que esta sencilla aplicación les sea de muchísima utilidad y por supuesto ¡¡¡Feliz 2013!!!

+Modificado 01/01/13: Gracias al comentario del Sr. Perez se corrigio a donde decia "Payload-Test.exe" a lo correcto "Payload-TEST.zip".

2 comentarios:

Sr Perez dijo...

excelente aplicacion, me sacara de aguas para un proyecto que tengo. solo tengo 2 comentarios:
1- donde pusiste: "Los siguientes ejemplos asumen que tienen el archivo MiniSXF.exe y el archivo ZIP en la misma carpeta, para el ejemplo mi archivo ZIP se llama "Payload-TEST.exe".
en la ultima frase Creo que es "Payload-TEST.zip" en lugar de exe.
2-En el ejemplo 2 y 3 leer el/los archivos y enviarlos a la salida.
Aca no se debe usar el comando flush o algo parecido para vaciar el cache antes de poner ahi los archivos? no se mucho de php pero lei que se debe vaciar el cache antes de poner datos, asi que es mas una consulta que una observacion.

Mario Gómez dijo...

¡Hola Sr. Perez!

Leyendo un poco la documentación de PHP sobre el comportamiento de la función flush, esta solo sirve para obligar a que se "transmitan" los datos que estan almacenados en el cache antes de enviarse. Para este caso el script no escribe nada previamente al cache, solo los datos que lee de los archivos. Es más, para este caso en particular ya que estamos enviando un "ejecutable" si enviaramos cualquier tipo de dato antes el ejecutable se corrompería.
Me imagino que el uso de la función flush resulta más útil por ejemplo cuando queremos enviar un archivo multimedia que esperamos que se lea como un stream, así por ejemplo si ponemos un flush despues de cada fread estaríamos obligando a que se enviara el paquete de 512 bytes luego de cada lectura.
Para este caso en particular no debería ser necesario ya que no necesitamos que el usuario vaya recibiendo el archivo por partes, así que el manejo que PHP haga del cache no debería afectar la descarga del archivo.