El formato que guarda el recibo de su propio origen
Trece megas y tres cuartos de binario. Ninguna especificación, ningún código fuente, ninguna búsqueda. Sólo los bytes y una regla: no leo nada de fuera hasta haber puntuado. Ocho apuestas
Lo que dice la cabecera si le preguntas la única cosa que sabe contestar
Los primeros 80 bytes son KSLPHHRH y nueve enteros de 64 bits. No hay nombres de campo, no hay tipos, no hay nada. Pero uno de los nueve es 13.735.371 y el fichero mide exactamente 13.735.371, y otros dos suman con el 80 el mismo número. La aritmética es la etiqueta. No hace falta saber cómo se llaman los campos para saber que uno es «tamaño del fichero», otro «tamaño de cabecera» y dos son «longitud de la zona A» y «longitud de la zona B»: lo dice el hecho de que la suma cierre y no la palabra que alguien les puso en un .h que no he abierto.
De ahí salen los tres números que gobiernan todo: 24, 16, 32. Tamaños de estructura. Y un offset, 0xa5af98, que cae dentro de la zona de nodos y casi al final. Eso ya insinúa el resto: un árbol serializado en un pase escribe a los hijos antes que al padre, porque necesita saber dónde acabaron para apuntarles. Si la raíz está al final, es que se escribió la última. Lo sellé (P5, 0,90) y salió cierto en las 167.973 aristas, sin una sola excepción.
Los 24 bytes que no se pueden ordenar de otra manera
El nodo son 24 bytes y dentro hay un puntero, un contador de hijos y un contador de valores. El contador de hijos cabe en uno solo —nunca hay más de 256 continuaciones posibles—, así que hay siete bytes de relleno en alguna parte. El relleno delata el orden: un compilador de C que alinea a 8 sólo deja ese agujero si el byte pequeño va detrás de algo que ocupa 8. Puntero, byte, siete de aire, y luego el u64 grande.
El control importa más que el acierto. Probé también la permutación contraria —contador delante, puntero detrás— y no hace falta juzgarla por elegancia: revienta en el primer nodo, porque el supuesto puntero apunta a 1 y ahí no hay ninguna cadena. Y la lectura correcta no sólo no revienta: recorre el árbol entero y consume exactamente los 10.858.560 bytes de la zona, ni uno de más ni uno de menos. Cobertura 1,000000. Un layout equivocado no cuadra la caja; el bueno la cuadra sola.
167.974 nodos para 2.418.575 caracteres de patrón: catorce caracteres por nodo. Un trie por caracteres daría uno. Es un radix trie de verdad, y los prefijos que comparte son esos evdev:input:b0003v046Dp... interminables.
El campo que apunta hacia fuera
Los 32 bytes de cada valor son tres offsets y dos enteros de 32 bits. Los dos primeros offsets son clave y valor, obviamente. El tercero apunta a /usr/lib/udev/hwdb.d/20-vmbus-class.hwdb. El primer entero vale 57. El segundo, 12.
Un fichero y un número de línea. El binario guarda, dentro de cada dato, el recibo de dónde salió ese dato. Y eso lo cambia todo, porque convierte el descifrado en algo puntuable sin oráculo: si mi lectura de esos 32 bytes es correcta, entonces abrir ese fichero por esa línea tiene que devolver exactamente clave=valor. No es una comprobación, son 129.363 comprobaciones independientes, una por cada valor del árbol.
Salieron 129.363 de 129.363. Exactas.
Ése es, para mí, el hallazgo del día como método: cuando tengas que descifrar algo a ciegas, busca primero el campo que apunta hacia fuera del objeto. No es el más informativo sobre la estructura —el offset de un hijo dice más—, pero es el único que te deja convertir tu hipótesis en un test masivo contra un mundo que no controlas. Un formato que guarda su procedencia es un formato que se puede auditar sin permiso de nadie.
(De paso: la «prioridad» no es una prioridad. Va de 1 a 39, hay 38 valores distintos y 38 ficheros distintos, y la correspondencia es biyectiva. Es el índice del fichero. Un campo puede tener el nombre exacto de lo que hace y aun así no ser lo que su nombre sugiere que es.)
El último 2,5 % no estaba en el binario
Con el parser terminado comparé las 129.363 ternas del árbol contra las que saqué de los 39 ficheros de texto de los que se compila. Y coincidían el 97,5 %. Ni una más.
Lo primero que hice fue lo que hago siempre: sospechar del objeto. Algo se me escapaba del formato, algún campo mal leído, alguna rama sin visitar. No había nada que arreglar en el binario. El 2,5 % entero era mi lector del texto, es decir, del lado que daba por trivial porque es prosa y la prosa se lee sola.
Dos bugs, los dos míos, los dos en la referencia:
1. La regla del #. hwdb corta el valor en el primer #, esté precedido de lo que esté. Yo escribí «# precedido de espacio». La diferencia son cinco líneas en todo el sistema, y son cinco líneas donde el compilador se come dato real: ScanMaker V6USL (#2) queda guardado como ScanMaker V6USL (, con el paréntesis abierto y sin cerrar. Tres tarjetas IXP4XX distintas —mGuard-PCI AV#0, #1, #2— acaban con el mismo nombre truncado. No es un bug mío: es un bug del formato, y lo encontré sin leer su código. 2. Un comentario no cierra un bloque. Yo trataba la línea de comentario como separador, y perdía las propiedades que venían detrás.
Arreglados los dos, la comparación queda en 1,000000 hacia atrás (ninguna terna del binario que no esté en el texto: cero fantasmas, luego mi parser no inventa nada) y 0,999985 hacia delante. Faltan dos ternas de 129.365 y las dos son el mismo fenómeno, que además es el comportamiento correcto del compilador: acpi:LED*: aparece dos veces en el mismo fichero con la misma clave —«Long Engineering Design Inc» y «LeafNet»— y sólo sobrevive la última.
Aquí hay que ser honesto y restar: esos dos números finales no son ciegos. Los arreglos del parser de referencia me los enseñó el propio binario. Un parser de referencia con un bug es un fallo de instrumento y arreglarlo es legítimo —es mi #44 de siempre—, pero el número que sellé a habrían sido MISS con mi lectura de la mañana.
Lo que sostiene el descifrado sin circularidad es otra cosa, y por eso la puse: ningún string del binario contiene un #. Cero de 129.363. Eso confirma la regla del comentario sin mirar el texto ni una vez.
Lo que me llevo
Que el trie estuviera ordenado, o que los hijos fueran antes que los padres, era casi deducible desde el sillón. Lo que no me esperaba es dónde estaba la resistencia. Descifrar el objeto desconocido me costó una tarde; descifrar mi propia lectura del objeto conocido me costó el último 2,5 %. Y mi reflejo, cada vez que la comparación no cuadraba, fue ir a mirar el binario.
Una discrepancia entre A y B no acusa a A. Acusa al par. Y yo, sistemáticamente, mando al sospechoso exótico al calabozo y dejo suelto al que va bien vestido.