1/6/10

Trabajando con archivos binarios en PHP

Aunque PHP no es un lenguaje de programación muy adecuado para trabajar con archivos binarios, en determinadas circunstancias puede ser de utilidad. Y explicaré aquí algunos detalles a tener en cuenta al trabajar con archivos binarios y técnicas para hacerlo con sencillez.

fopen. Flag 'b' para binarios en windows.
A la hora de abrir archivos, existen diversos modos: de escritura (write only), de lectura (read only), de lectura/escritura (read/write), de escritura situando el cursor al final del archivo (append). Además hay un flag que permite especifica si el archivo a abrir se usará como un archivo de texto o como uno binario. En modo texto es posible que un salto de línea (\n) se guarde como dos bytes \r\n dependiendo del sistema operativo. Y esto puede causar archivos corruptos en muchos casos. Nota: El flag opuesto a 'b' es 't'. Puede producir problemas también con ftell. Se recomiendo usar siempre el flag 'b' para que la compatibilidad sea la misma indistintamente de la plataforma.

pack, chr / unpack + list, ord
Estas funciones sirven para empaquetar y desempaquetar valores numéricos y cadenas en datos binarios. Y son el pilar fundamental de una codificación/decodificación fácil y cómoda:

ord (ordinal) obtiene el valor numérico que tiene un carácter. Se le pasa una cadena y lo calcula a partir del primer byte de la cadena
chr (character) obtiene el carácter de un valor numérico (la función inversa a ord).
Ambas funciones trabajan a nivel de byte y únicamente trabajan con el rango de 00-FF.

chr(ord('a')) == 'a'

pack permite empaquetar una serie de valores en un formato binario. La función proviene de perl. Se le pasa una cadena con el formato de empaquetado y luego una sucesión de parámetros con los valores a empaquetar. El formato de empaquetado contempla valores con signo y sin signo de 8, 16 y 32 bits pudiendo especificar el endian: usando el de la máquina actual, o usando little o big endian. También permite empaquetar cadenas terminadas con '\0's o con espacios.

unpack permite desempaquetar una serie de valores a partir de una cadena binaria. Si no se especifican nombres para los datos desempaquetados, se devuelve un array de claves numéricas empezando por la clave 1 (en vez de por la clave 0). Para extraer valores en variables con comodidad se puede usar la estructura del lenguaje "list". Teniendo en cuenta que el casting de array a bool es siempre true, se puede usar una asignación intrínseca y el operador ternario para hacer una extracción en una única expresión.
list(, $valor) = unpack('s', "\xFF\xFF");
list($valor) = array_values(unpack('s', "\xFF\xFF"));
$valor = ($v = unpack('s', "\xFF\xFF")) ? $v[1] : false;
echo $valor;


Tip:
pack('H*', '736f7977697a')
Las funciones pack usando H*, permiten convertir datos en hexadecimal a cadenas binarias y viceversa. (ver funciones hex/unhex de las funcionesde utilidad del final del artículo)

fseek (archivos grandes)


PHP trabaja con dos tipos numéricos: signed int (32 bits) y double (64 bits) de los cuales se pueden conseguir 53 bits para valores enteros (sin decimales) que son unos 16 digitos decimales.
$a = 0x7FFFFFFF; var_dump($a); $a++; var_dump($a); var_dump((int)$a);

int(2147483647)
float(2147483648)
int(-2147483648)
El problema es que fseek trabaja con enteros de 32 bits y el direccionamiento de fseek y ftell de archivos está limitado a eso en PHP. Que son 2GB sin signo y 4GB con signo. Diría que se puede superar este límite haciendo varias llamadas con SEEK_CUR.


substr

La función substr nos permite extraer determinados bytes de una cadena binaria como si fuese una cadena de texto normal. Donde cada caracter es un byte.

Nota:
En la configuración de PHP hay una opción llamada "mbstring.func_overload" que permite susitutir las funciones normales. Esto permite hacer que las funciones normales para trabajar con cadenas se sustituyan por sus equivalentes mb_*. Esto es un problema que puede llevar a verdaderos quebraderos de cabeza cuando se trabaja con cadenas binarias.

Una solución pasa por:
$back_encoding = mb_internal_encoding();

mb_internal_encoding('8bit');
// Hacer cosas.
mb_internal_encoding($back_encoding);

O directamente utilizar las funciones mb_* usando 8bit como el encoding normal.
Así que para extraer los primeros 10 bytes:
substr($cadena, 0, 10);
->
mb_substr($cadena, 0, 10, '8bit');

Con el resto de funciones de cadena tipo strlen pasa exáctamente lo mismo.

acceso como array [] {}
PHP permite acceder a las cadenas como si fuesen arrays. Tanto para lectura como para escritura.
Así que:
$str = 'abc';
$str[1] == substr($str, 1, 1);

$str[1] = 'a';
$str = substr_replace($str, 'a', 1, 1);

$str == 'aac'

streams: php://memory, php://temp, data://, file_get_contents
En lenguajes que soportan slicing de streams, leer y procesar archivos binarios suele ser bastante mas cómodo. PHP no soporta slicing de streams directamente, y aunque se puede hacer un apaño, no se soporta nativamente. Sin embargo la lectura secuencial de datos y el "consumo" de datos es un patrón básico en el procesado de archivos binarios medianamente complejos. Los streams son muy cómodos para la consumición de datos ya que tiene un cursor y un torrente de datos y cada vez que lees, se actualiza ese cursor. En determinadas ocasiones tendremos los datos que queremos consumir en una cadena binaria, por ejemplo tras obtenerlos directamente con file_get_contents, tras generarlos por otro medio o al leer un subtream en una cadena.
Una función que puede permitir la consumición de datos en una cadena podría ser esta:
function fread_str(&$str, $len) { $data = substr($str, 0, $len); $str = substr($str, $len); return $data; }
Aunque esta forma de procesar datos es muy poco eficiente. Porque estás reconstruyendo una cadena todo el rato, copiando datos contínuamente y en cadenas grandes puede ser un proceso muy costoso.
otra forma es tener un cursor, de forma que evitamos tocar la cadena y únicamente extraemos la parte que nos interesa:

function fread_str_cur(&$cur, &$str, $len) { $data = substr($str, $cur, $len); $cur += $len; return $data; }
En PHP se puede generar un stream a partir de una cadena con relativa facilidad. Hay diversas formas:
$f = fopen('data://text/plain;base64,' . base64_encode($data), 'r+');
$f = fopen('php://memory', 'w+'); fwrite($f, $data); fseek($f, 0);



rtrim+\0
Al trabajar con archivos binarios, suele trabajarse con stringz muy amenudo o con cadenas que tienen un right padding de o bien espacios o bien el carácter 0.
La función rtrim nos puede ayudar a eliminar esos caracteres sobrantes de la cadena. rtrim tiene un segundo parámetro opcional que permite especificar los caracteres a eliminar. En nuestro caso \0. Por defecto elimina espacios, tabuladores, saltos de línea y el carácter \0. Pero en estos casos nos interesa únicamente que elimine el carácter de padding:
rtrim("hola\0\0\0", "\0") == 'hola'

Algunas funciones de utilidad:
function fread1($f) { @list(, $r) = unpack('C', fread($f, 1)); return $r; }
// Little Endian.
function fread2le($f) { @list(, $r) = unpack('v', fread($f, 2)); return $r; }
function fread4le($f) { @list(, $r) = unpack('V', fread($f, 4)); return $r; }
function freadsz($f, $l) { return rtrim(fread($f, $l), "\0"); }

function hex  ($str) { return strtoupper(($v = unpack('H*', $str)) ? $v[1] : ''); }
function unhex($str) { return pack('H*', $str); }
unhex(hex('prueba')) == 'prueba';


function fread_str(&$str, $len) { $data = substr($str, 0, $len); $str = substr($str, $len); return $data; }


Variantes:

function fread1($f) { return ord(fread($f, 1)); }
function fread2le($f) { return ($v = unpack('v', fread($f, 2))) ? $v[1] : false; }
function fread4le($f) { return ($v = unpack('V', fread($f, 4))) ? $v[1] : false; }


function freadsz($f, $l = false) {
if ($l === false) {
$s = '';
while (!feof($f)) {
$c = fread($f, 1);
if ($c == '' || $c == "\0") break;
$s .= $c;
}
return $s;
} else {
return rtrim(fread($f, $l), "\0");
}
}


2 comentarios:

  1. Como hago para forzar a escribir un archivo en binario en linux? o sea con windows se utiliza la bandera "b", pero en linux hay chances de hacer esto?

    ResponderEliminar
  2. En linux se consideran binarios en cualquier caso; pongas o no pongas el 'b'.

    ResponderEliminar