Even if PHP is not the greatest programming language to work with binary files,
in some circumstances it might be useful.
Here I'm going to explain a few details to have into account when working with binary files
and some techniques to do it easily.
### fopen. 'b' flag for binary files on windows.
When opening files, there are several modes: for writing (write only), for reading (read only),
and both (read/write), or writing by appending to the end of the file (append).
In addition to those there is a flag that allows to specify wether the file will be opened as file
or as binary. In text mode, it is possible that a line break (\n) is stored as two bytes \r\n
depending on the operating system. And this can cause corrupt files in lots of cases.
Note: The opposite flag to 'b' is 't'. It can produce problems also with ftell.
It is recommended to use always the 'b' flag so the compatibility will be the same
independently to the platform.
### pack, chr / unpack + list, ord
These functions allows to pack and unpack numeric values and string into/from binary data.
And are the fundamental pillars for an easy coding/encoding:
**[ord](http://es.php.net/ord)** (ORDinal) obtains the numeric value of a character.
You have to pass a string and it is computed from the first byte of the string.
**[chr](http://draft.blogger.com/goog_1966246566)** (CHaRacter) obtains the character from a numeric value
(the opposite function to ord).
Both functions work at the byte level and only works in the range of 00-FF.
```php
chr(ord('a')) == 'a'
```
**[pack](http://php.net/pack)** allows to pack a set of values in a binary format.
That function comes from perl. You can pass a string with the packed format and then a sequence
of parameters with the values to pack. The packing format have into account values signed and unsigned
from 8, 16 and 32 bits, allowing to specify the endian: using the endian from the current machine,
and little or big endian. It also allows to pack strings ending with '\0' or with spaces.
**[unpack](http://php.net/unpack)** allows to unpack a ser of values from a binary string.
If you are not specifying names for the unpacked data, it returns an array of numeric keys starting with
key 1 (instead of key 0). In order to extract values into variables easily, you can use the language structure
`list`. Having into account that casting an array into a bool is always true,
it is possible to make an intrinsecal assign and the ternary operator to make an extraction in a single expression.
```php
list(, $value) = unpack('s', "\xFF\xFF");
list($value) = array_values(unpack('s', "\xFF\xFF"));
$valor = ($v = unpack('s', "\xFF\xFF")) ? $v[1] : false;
echo $value;
```
Tip:
```php
pack('H*', '736f7977697a')
```
Pack functions with H*, allows to convert hexadecimal data to binary strings and viceversa.
(check out functions **hex**/**unhex** from utility functions from the end of the article)
### fseek (big files)
PHP works with two kind of numeric types: signed int (32 bits) and double (64 bits).
For doubles, you can get up to 53 bits for integral values (no decimal) that are about 16 decimal digits.
```php
$a = 0x7FFFFFFF; var_dump($a); $a++; var_dump($a); var_dump((int)$a);
```
```php
int(2147483647)
float(2147483648)
int(-2147483648)
```
The problem of fseek is that it works with 32-bit integers, and the fseek and ftell file addressing
is limited to that in PHP. With sign 2GB and without it 4GB. But you can overcome that limit
by fseek several times with SEEK_CUR.
### substr
substr function allows us to extract some bytes from a binary string as if they were normal texts,
where each character is a byte.
Note:
With PHP configuration, there is an option called "mbstring.func_overload" that allows to replace
normal string functions.
This allows that normal functions with work with strings to be replaced with their equivalents `mb_*`.
This a problem that could lead to really painful situations when working with binary strings.
One solution could be:
```php
$back_encoding = mb_internal_encoding();
mb_internal_encoding('8bit');
// Hacer cosas.
mb_internal_encoding($back_encoding);
```
Or directly to use `mb_*` functions using 8bit as normal encoding.
So in order to extract the first 10 bytes:
```php
substr($string, 0, 10);
// ->
mb_substr($string, 0, 10, '8bit');
```
With the rest of the string functions like `strlen` you can do exactly the same.
### accessing as an array [] {}
PHP allows to access strings as if they were arrays. As for reading as well as for writing.
So:
```php
$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
In languages that allows stream slicing, reading and processing binary files, is usually much more convenient.
PHP doesn't support stream slicing directly, though even if you can make some tricks, it is not natively supported.
However sequencial reading of data and the draining of data is a basic pattern when working with
binary files more or less complex. Streams are pretty conveniente to consume data since you get a cursor
and a flow of data and every time you read i, that cursor is updated. In some situations
we will have the data that we want to consume in a binary string, for example after obtaining it directly
from `file_get_contents`, after generating by other means or from reading a substream in a string.
A function that allows to consume the data from a binary string could be this one:
```php
function fread_str(&$str, $len) {
$data = substr($str, 0, $len);
$str = substr($str, $len);
return $data;
}
```
Though this way of processing data is too ineficient, since you are rebuilding a string everytime,
copying the data everytime and for long strings this is very costly.
another way it o have a cursor, this way we avoid to change the string and we limit ourselves to the part
we are interested in:
```php
function fread_str_cur(&$cur, &$str, $len) {
$data = substr($str, $cur, $len);
$cur += $len;
return $data;
}
```
With PHP you can generate a stream from a string pretty easily. You can do it in several ways:
```php
$f = fopen('data://text/plain;base64,' . base64_encode($data), 'r+');
```
```php
$f = fopen('php://memory', 'w+'); fwrite($f, $data); fseek($f, 0);
```
### rtrim+\0
When working with binary data, there are a lot of situations where the strings are stored with a single
or several \0 bytes (or spaces) padding at the end.
rtrim function could help us to remove those extra characters from the string. rtrim has a
secondary optional parameter that allows us to specify the caracters we want to remove. In our case
\0. By default it removes spaces, tabs, line endings and the character \0. But in these cases
we are just interested in removing the padding character:
```php
rtrim("hola\0\0\0", "\0") == 'hola';
```
### Some useful functions:
```php
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"); }
```
```php
function hex ($str) { return strtoupper(($v = unpack('H*', $str)) ? $v[1] : ''); }
function unhex($str) { return pack('H*', $str); }
unhex(hex('prueba')) == 'prueba';
```
```php
function fread_str(&$str, $len) { $data = substr($str, 0, $len); $str = substr($str, $len); return $data; }
```
Variants:
```php
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");
}
```
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](http://es.php.net/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](http://draft.blogger.com/goog_1966246566)** (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.
```php
chr(ord('a')) == 'a'
```
**[pack](http://php.net/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](http://php.net/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.
```php
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:
```php
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.
```php
$a = 0x7FFFFFFF; var_dump($a); $a++; var_dump($a); var_dump((int)$a);
```
```php
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:
```php
$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:
```php
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:
```php
$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:
```php
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:
```php
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:
```php
$f = fopen('data://text/plain;base64,' . base64_encode($data), 'r+');
```
```php
$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:
```php
rtrim("hola\0\0\0", "\0") == 'hola';
```
### Algunas funciones de utilidad:
```php
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"); }
```
```php
function hex ($str) { return strtoupper(($v = unpack('H*', $str)) ? $v[1] : ''); }
function unhex($str) { return pack('H*', $str); }
unhex(hex('prueba')) == 'prueba';
```
```php
function fread_str(&$str, $len) { $data = substr($str, 0, $len); $str = substr($str, $len); return $data; }
```
Variantes:
```php
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");
}
```