PHP: Working with binary files on PHP

Jun 02 2010

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"); } ```