1. Para que sirve un hex‑dump: por que print(data) no ayuda
Cuando ves Data por primera vez, parece que bastaria con hacer print(data) — y todo quedaria claro. Pero Data se comporta como una "caja negra": guarda bytes, y no tiene por que mostrarlos "bonitos". Al final, o bien veras algo como "N bytes", o una descripcion que no ayuda a entender la estructura.
Hex‑dump es como una linterna: no explica el formato del archivo, pero permite ver rapidamente los primeros bytes y entender si "en general parece correcto" o si estas leyendo algo equivocado.
Empecemos con una pequena demostracion: que solemos ver sin hex‑dump, y por que eso no basta.
import Foundation
let data = Data([0, 1, 2, 255])
print(data) // 4 bytes
print(Array(data)) // [0, 1, 2, 255]
Array(data) ya es mejor, pero los numeros en decimal son dificiles de comparar con la documentacion y las firmas de formatos. La mayoria de los "bytes magicos" (firmas de formato) se suelen mirar en hexadecimal: FF, 0A, 89 50 4E 47 y asi sucesivamente. Por eso, hoy nuestro objetivo es aprender a convertir bytes en texto hex comprensible e imprimir los primeros N bytes de forma ordenada y predecible.
2. Hex en palabras simples: un byte — dos digitos
Hex (sistema hexadecimal) es una forma de escribir numeros en un sistema de base 16. Estamos acostumbrados a la base 10 (digitos 0-9), y en hex hacen falta 16 digitos, por lo que despues de 9 aparecen las letras A–F.
Parece "el alfabeto raro de los programadores", pero en realidad es muy practico: un byte es un valor de 0 a 255, y en hex siempre cabe exactamente en dos digitos: de 00 a FF. Este formato es comodisimo de leer y comparar con la vista.
Aqui tienes una pequena tabla de "un digito hex" (a veces se llama "nibble", pero la palabra asusta — puedes no memorizarla):
| Valor | Digito hex |
|---|---|
| 0…9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 | |
| 14 | |
| 15 | |
La clave esta en que cualquier byte puede descomponerse en dos partes: "digito hex alto" y "digito hex bajo". En aritmetica se ve de forma muy simple: hi = byte / 16, lo = byte % 16. Y luego tomamos caracteres de la cadena "0123456789ABCDEF" (el conjunto clasico de digitos hex) y ensamblamos el resultado.
3. Escribimos hexByte(_:): UInt8 → 00…FF
Ahora hagamos la pieza mas importante: una funcion que toma un byte y devuelve una cadena de dos simbolos hex.
La escribimos "a pelo" a proposito, para que sea comprensible incluso para quien todavia no se maneja con las operaciones a nivel de bits (y aqui no las necesitamos). Esta sera nuestra pequena "tipografia", que imprime un byte con un tipo bonito.
import Foundation
func hexByte(_ b: UInt8) -> String {
let digits = Array("0123456789ABCDEF")
let hi = Int(b) / 16
let lo = Int(b) % 16
return String([digits[hi], digits[lo]])
}
Probemos con valores simples, para que el cerebro crea lo que esta pasando.
import Foundation
print(hexByte(0)) // 00
print(hexByte(15)) // 0F
print(hexByte(16)) // 10
print(hexByte(255)) // FF
Fijate en 0F: el cero inicial no es un adorno. Es disciplina. Si no imprimes el cero inicial, el byte 0F se convertira en F, y tu salida quedara "rota": algunos bytes tendran un solo simbolo y otros dos. Leer eso es incomodo; compararlo, aun mas.
4. Hex‑dump de los primeros N bytes: prefix(maxBytes) y ensamblado de la cadena
Ahora que ya sabemos imprimir un byte, tenemos que aprender a imprimir los primeros N bytes de Data.
Idea importante: el hex‑dump casi siempre se limita. Imprimir todo el archivo es como intentar entender un libro tirando todas las paginas sobre la mesa y mezclandolas. Por eso tomamos data.prefix(maxBytes) y trabajamos solo con ese fragmento.
Hagamos una funcion que devuelva una cadena del tipo "48 69 21" — bytes separados por espacios.
import Foundation
func hexDump(_ data: Data, maxBytes: Int) -> String {
var parts: [String] = []
for byte in data.prefix(maxBytes) {
parts.append(hexByte(byte))
}
return parts.joined(separator: " ")
}
Probemos con datos que entendemos: 72 105 33 — eso es ASCII "Hi!".
import Foundation
let bytes = Data([72, 105, 33])
print(hexDump(bytes, maxBytes: 16)) // 48 69 21
Aqui se puede atrapar un momento agradable: 48 69 21 es precisamente 'H' 'i' '!' en ASCII/UTF‑8. Y asi ya "ves" el texto, incluso sin decodificarlo como cadena. Esto es muy util cuando la decodificacion de texto no funciona y quieres entender rapido: "eso se parece a texto o es claramente binario?".
5. Formato comodo: 16 bytes por linea y la parte ASCII
Una cadena corta como "48 69 21" esta bien para ejemplos, pero cuando hay mas bytes, la salida se convierte en una papilla.
El hex‑dump clasico se imprime en lineas de 16 bytes (a veces de 8), y a la derecha se anade la representacion ASCII: los caracteres imprimibles se muestran tal cual, y los no imprimibles se sustituyen por un punto .. No es una descompresion del formato, pero si una forma muy rapida de entender si hay fragmentos de texto dentro.
Primero hagamos una pequena funcion que convierte un byte en un caracter ASCII o en un punto.
import Foundation
func asciiCharOrDot(_ b: UInt8) -> Character {
if (32...126).contains(b), let scalar = UnicodeScalar(Int(b)) {
return Character(scalar)
}
return "."
}
Ahora hagamos la impresion por lineas. Para que el ejemplo no se convierta en 60 lineas, ensamblaremos una sola linea de salida para cada bloque de 16 bytes: bytes hex a la izquierda, ASCII a la derecha.
import Foundation
func hexDumpLines(_ data: Data, maxBytes: Int) -> [String] {
let bytesPerLine = 16
var lines: [String] = []
let slice = data.prefix(maxBytes)
var i = 0
while i < slice.count {
let chunk = slice.dropFirst(i).prefix(bytesPerLine)
let hexPart = chunk.map { hexByte($0) }.joined(separator: " ")
let asciiPart = chunk.map { asciiCharOrDot($0) }.map(String.init).joined()
lines.append("\(hexPart) |\(asciiPart)|")
i += bytesPerLine
}
return lines
}
Probemos con una mezcla de bytes "visibles" e "invisibles":
import Foundation
let demo = Data([72, 105, 33, 0, 10, 255, 65, 66, 67])
for line in hexDumpLines(demo, maxBytes: 64) {
print(line)
}
// 48 69 21 00 0A FF 41 42 43 |Hi!...ABC|
Si, ya tiene pinta de herramienta de diagnostico. Ves enseguida que los tres primeros bytes son texto legible, luego hay bytes "de servicio" (00, 0A, FF), y luego vuelve el texto legible ABC.
Si quieres anadir un desplazamiento (offset) a la izquierda, puedes mostrar el numero de la primera posicion de la linea. De momento no vamos a complicar el formato y simplemente mostraremos el offset decimal.
import Foundation
func printHexDump(_ data: Data, maxBytes: Int) {
let bytesPerLine = 16
let slice = data.prefix(maxBytes)
var i = 0
while i < slice.count {
let chunk = slice.dropFirst(i).prefix(bytesPerLine)
let hexPart = chunk.map { hexByte($0) }.joined(separator: " ")
print("\(i): \(hexPart)")
i += bytesPerLine
}
}
6. Hex‑dump en un archivo real
La funcion de hex‑dump en si es agradable, pero el valor real aparece cuando puedes aplicarla rapidamente a un archivo real. Por ejemplo, guardas datos de la aplicacion junto al proyecto, intentas leer un archivo, recibes un error o nil al hacer String(data:encoding: ) — y quieres entender: "¿que demonios hay escrito ahi?".
En esos momentos, el hex‑dump es como un botiquin: mejor que este ahi y no haga falta, que al reves.
Usarlo en una aplicacion CLI
Hagamos un escenario minimo, sin "analisis real de CLI" (lo estudiaremos en otros dias). Que el programa pida una ruta y un numero de bytes.
import Foundation
print("Enter file path:")
let path = readLine() ?? ""
print("Max bytes (e.g. 64):")
let maxBytes = Int(readLine() ?? "") ?? 64
let url = URL(fileURLWithPath: path)
do {
let data = try Data(contentsOf: url)
for line in hexDumpLines(data, maxBytes: maxBytes) {
print(line)
}
} catch {
print("Can't read file:", error)
}
Fijate en dos cosas. Primero, Data(contentsOf: ) lanza errores, y eso es correcto: el archivo puede no existir, puede haber problemas de permisos, o la ruta puede ser incorrecta. Segundo, mostramos solo los primeros maxBytes, porque imprimir todo el archivo es un mal estilo y una mala UX (tanto para el usuario como para ti como desarrollador).
Como "leer" los primeros bytes
El hex‑dump no te convierte en un experto en formatos binarios. Pero te permite sacar varias conclusiones muy utiles incluso sin documentacion.
Si los primeros bytes parecen texto legible (48 65 6C 6C 6F → “Hello”), hay posibilidades de que el archivo sea de texto o contenga encabezados textuales. Si al principio hay muchos ceros y patrones repetidos, puede que sea un formato binario o una estructura con campos de longitud fija.
Un escenario muy comun: pensabas que habias guardado texto UTF‑8, pero en realidad has guardado otra cosa. Por ejemplo, escribiste una cadena en la codificacion equivocada, anadiste bytes de mas, o incluso guardaste "la descripcion de un objeto" en lugar de los datos reales. El hex‑dump ayuda a no discutir con la realidad: en programacion, la realidad a menudo parece 00 00 00 00 y no se avergüenza.
Otra conclusion practica: si ves firmas "magicas" conocidas (el formato empieza con bytes concretos), entiendes rapidamente si es el archivo correcto. Aunque no recuerdes la firma de memoria, puedes comparar dos archivos: "este empieza igual, y este es completamente distinto". Para diagnosticar "por que mi codigo no lee el archivo", esto a menudo ahorra horas.
7. Errores tipicos
Error #1: imprimir todo el archivo sin limite.
A veces parece: "bueno, solo estoy depurando". Y de repente haces un hex‑dump de un archivo de 200 MB, el terminal se convierte en lava, el IDE se cuelga, y empiezas a dudar del sentido de la vida y de print. La regla es simple: el hex‑dump casi siempre debe tener un parametro maxBytes, y los valores tipicos son 16, 32, 64, 256.
Error #2: mostrar bytes sin ceros iniciales.
Si imprimes el byte 0x0A como A, rompes la "cuadricula": los bytes dejan de tener la misma anchura, las lineas son dificiles de leer y es imposible comparar rapidamente los desplazamientos con la vista. En un buen dump, cada byte tiene exactamente dos digitos hex, aunque el primero sea 0.
Error #3: confundir bytes y caracteres.
Es facil equivocarse: "si en la cadena hay 10 caracteres, entonces hay 10 bytes". Eso es falso en Unicode y UTF‑8: un caracter puede ocupar varios bytes. Por eso, el hex‑dump trata siempre de Data.count y del tamano real en bytes, no de la "longitud de la cadena".
Error #4: intentar interpretar cualquier Data como texto UTF‑8.
Si haces String(data: data, encoding: .utf8) y obtienes nil, no es un "error de Swift", sino una senal normal: los bytes no forman un texto UTF‑8 valido. Precisamente para eso sirve el hex‑dump: para no entrar en panico en ese momento, sino mirar el contenido con calma y entender que, tal vez, sea un formato binario.
Error #5: olvidar que el hex‑dump es diagnostico, no parser de formato.
A veces, despues de un primer dump exitoso, aparece la tentacion: "Oh, veo los bytes, asi que ya entiendo el formato". Normalmente es una trampa. El hex‑dump muestra "como estan colocados los bytes", pero no explica donde estan los limites de los campos, como se construyen los numeros, cual es el orden de bytes, si hay compresion, etc. Usalo como una linterna, no como sustituto de la especificacion.
GO TO FULL VERSION