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/unhexde 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;  
    }  
    return rtrim(fread($f, $l), "\0");  
}