Decodificación Base64 en Swift: una guía completa
En algún punto de tu pipeline, los datos llevan un disfraz: un token metido en una cabecera HTTP, un avatar escondido dentro de un campo JSON, un archivo .b64 al que prometiste mirar la semana pasada, un adjunto de correo que llegó como un muro de letras. Quitar esos disfraces en Swift es una de las tareas más agradables del lenguaje: un framework, un inicializador y un reglamento tan corto que cabe en una nota adhesiva.
La página de inicio de este sitio ya cubre el formato en sí (64 caracteres imprimibles, seis bits por carácter, hasta dos caracteres = de padding en el grupo final), así que no vamos a contar esa historia otra vez aquí. Solo guarda dos hechos en el bolsillo. Primero, base64 es una forma de vestir bytes como texto, no una cerradura. Segundo, cada viaje base64 en Swift pasa por un único tipo, Data, y el decodificador vive en él como un inicializador que puede fallar. Ese único hecho marca el resto de este artículo, porque un inicializador que puede fallar cambia cómo escribes cada línea que le sigue.
Un tipo se queda con todo el trabajo
Swift no dispersa los helpers de base64 por una docena de módulos, y no te obliga a instalar nada. El decodificador es Data(base64Encoded:options:) en Foundation, y lleva siendo parte de la plataforma desde los primeros días del framework (Apple lista el inicializador desde iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 y visionOS 1.0; las opciones de longitud de línea del lado de la codificación llegan incluso hasta iOS 7.0). En Linux y Windows, el mismo Foundation se distribuye con el toolchain de código abierto, así que el código de abajo se comporta igual en una app de iPhone, un worker de servidor y un script en tu terminal.
Hay un inicializador hermano, Data(base64Encoded: Data, options:), para el caso en que tu base64 llega como bytes ASCII en bruto en vez de una cadena. Ambos toman un argumento options que por defecto vale []. Y ambos comparten un rasgo de personalidad que pesa más que cualquier opción: pueden fallar.
import Foundation
let packed = "SGVsbG8sIFN3aWZ0IQ=="
if let data = Data(base64Encoded: packed) {
let text = String(data: data, encoding: .utf8)
print(text ?? "not text after all")
} else {
print("that was not base64")
}
// Hello, Swift!
La documentación de Apple para el inicializador es hermosamente directa: "devuelve nil cuando la entrada no se reconoce como Base-64 válido". Sin excepciones, sin errores lanzados, sin spam en los logs. Solo una nil silenciosa y la responsabilidad de decidir qué significa eso para tu usuario. Si recuerdas una sola cosa sobre base64 en Swift, que sea esta: el decodificador nunca se estrella y nunca se queja. Simplemente se niega.
El veredicto del decodificador: una tabla de síes y noes
Entonces, ¿qué significa "válido" para este decodificador? Resulta que es una lista corta de reglas duras, y esa lista es la diferencia entre "funciona en la demo" y "sobrevive en producción". Cada fila de la tabla de abajo es comportamiento real del inicializador en un toolchain actual, así que puedes citarla tal cual en tus mensajes de error:
| Entrada | Veredicto | Por qué |
|---|---|---|
TWFu |
Man |
un grupo completo de cuatro caracteres no necesita padding en absoluto |
TQ== |
M |
un byte más dos pads, el caso del libro de texto |
SGVsbG8h |
Hello! |
ocho caracteres es un múltiplo de cuatro, así que no hacen falta pads |
==== |
un Data vacío |
padding sin nada detrás es legal y se decodifica a cero bytes |
| la cadena vacía | un Data vacío |
nada entra, nada sale, y el optional sigue teniendo éxito |
TQ |
nil |
longitud dos: se prometió un grupo de cuatro y nunca llegó |
T |
nil |
un carácter lleva seis bits y un byte necesita ocho |
SGVsbG8hTQ |
nil |
diez caracteres: el grupo final cuelga sin sus pads |
TQ=== |
nil |
tres pads: al tercero no le queda nada que rellenar |
TQ==TQ |
nil |
datos después del padding es un no tajante |
SGVs bG8h |
nil |
un solo espacio está fuera del alfabeto, y el modo estricto no muestra misericordia |
SGVsbG8h más un salto de línea al final |
nil |
el salto de línea al final de un archivo que acabas de leer cuenta como ruido |
Tres filas merecen una segunda mirada. La fila de ==== significa que la comprobación if let pasa y tu código sigue navegando con cero bytes, así que si un payload vacío no es un estado válido en tu app, comprueba el recuento justo después de decodificar. La fila de la cadena vacía es el mismo truco con menos maquillaje. Y la fila del salto de línea final es la razón más común de que un archivo base64 que se codificó perfectamente por la mañana se niegue a decodificarse por la tarde: algo por el camino añadió un fin de línea, y el decodificador estricto se lo toma a título personal.
Hay además un punto débil famoso que la tabla no puede mostrar. Compara TQ== y TS==: ambos se decodifican al mismo byte, M, porque los dos bits más bajos de ese carácter final se descartan antes de que nadie los inspeccione. Apunta Tg== en su lugar y obtienes N sin pelear. El decodificador vigila los caracteres y deja pasar los bits colgantes. Esa tolerancia no es un error, pero sí significa que dos cadenas distintas pueden significar los mismos datos, y empieza a importarte en el momento en que tu sistema compara, deduplica o cachea valores base64 (más sobre eso en la sección de seguridad).
Cuando la entrada tiene más ruido del que crees
El base64 del mundo real rara vez llega como una línea intacta. Los adjuntos de correo se envuelven a los 76 caracteres con un retorno de carro y un salto de línea después de cada línea, un hábito heredado de la especificación MIME de 1996, y los archivos de certificado se envuelven a los 64. El decodificador tiene exactamente una opción para manejar ese ruido, y es una opción importante:
import Foundation
let mimeBody = "SGVs\r\nbG8sIG1h\naWwgbm9pc2Uu"
if let data = Data(base64Encoded: mimeBody, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// Hello, mail noise.
.ignoreUnknownCharacters está documentado como un decodificador que "ignora los bytes no Base-64 desconocidos, incluidos los caracteres de fin de línea", y para ese trabajo es la herramienta correcta: el ruido se elimina, el alfabeto sobrevive y el payload sale entero. Pero la opción tiene un punto ciego, y es el que más muerde a los desarrolladores de Swift: elimina cada carácter fuera del alfabeto, incluidos los - y _ de base64url. No los traduce a + y /; simplemente los tira. Dependiendo de lo que esa eliminación deje atrás, obtienes una nil (cuando los supervivientes ya no forman grupos completos) o, peor, una respuesta confiada con el número equivocado de bytes. Una cadena base64url de 16 caracteres que codifica 12 bytes puede volver del decodificador permisivo como 9 bytes distintos, sin error y sin disculpa.
La regla que hay que guardar: .ignoreUnknownCharacters es para ruido de transporte (saltos de línea, espacios sueltos de un copiar-pegar), nunca para diferencias de alfabeto. Si el payload puede ser base64url, convierte los caracteres tú primero, exactamente como muestra la siguiente sección, y entrégale al decodificador una cadena estándar limpia.
El alfabeto URL
La sección 5 del RFC 4648 define al primo del alfabeto estándar que has ido conociendo: base64url, donde + pasa a ser -, / pasa a ser _ y el padding de = suele descartarse. La razón es la misma que mantiene tus URLs honestas: en una cadena de consulta, un + se lee como un espacio por el parseo de formularios, un / es un separador de ruta y un = separa las claves de los valores. El RFC es directo sobre la relación entre los dos: la variante URL "no debe considerarse la misma que la codificación base64". Los JWT, los mensajes de Web Push, los IDs de vídeo de YouTube y la mayoría de los identificadores de API modernos hablan base64url, así que espera toparte con él el primer día.
Del lado de la decodificación, la receta tiene dos movimientos: traduce el alfabeto y luego completa el padding, porque el decodificador estricto sigue pidiendo su múltiplo de cuatro.
import Foundation
extension String {
func dataFromBase64URL() -> Data? {
var fixed = self
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
return Data(base64Encoded: fixed)
}
}
let tokenPart = "0S__zMWaTC-iVgJ-"
if let bytes = tokenPart.dataFromBase64URL() {
print(bytes.count) // 12
}
La línea del módulo es todo el truco: los payloads base64url suelen llegar sin padding, y uno o dos caracteres = (nunca tres) restauran el grupo de cuatro que el decodificador espera. Encontrarás una versión de esta extensión de cinco líneas en un número sorprendente de codebases de Swift, y con buena razón. Hay una razón por la que será más corta en el futuro: los SDK más nuevos de Apple (26.4 y superiores, a fecha de escribir esto) crecieron una opción nativa .base64URLAlphabet para el codificador, con las opciones de decodificación correspondientes aún madurando en Foundation de código abierto detrás de un marcador de disponibilidad para un toolchain posterior. Hasta que eso llegue a tu objetivo mínimo de despliegue, la extensión es la respuesta portable, y por construcción seguirá funcionando en todas las versiones.
Bytes primero, palabras después
Aquí está la decisión que el decodificador no puede tomar por ti: te entrega un Data, una bolsa de bytes sin la más mínima idea de en qué juego de caracteres se escribió el payload original. Si el payload era texto, elegir ese juego de caracteres es tu trabajo, y Swift te da dos puertas fuera del mundo de los bytes con temperamentos muy distintos.
String(data:encoding:)es la puerta estricta. Devuelve un optional y contestanilcuando los bytes no son válidos en la codificación que nombraste. Ideal para validar, peligroso si desenvuelves la respuesta a la fuerza.String(decoding:as:)es la puerta que nunca se niega. Siempre devuelve una cadena, sustituyendo por el carácter de reemplazo U+FFFD todo lo que no consigue entender. Ideal para logs y previsualizaciones, peligroso si guardas el resultado y lo llamas datos.
import Foundation
let bytes = Data([0xC3, 0xA5]) // la grafía UTF-8 de la letra a con anillo
print(String(data: bytes, encoding: .utf8) ?? "?") // a con anillo, leída correctamente
print(String(data: bytes, encoding: .isoLatin1) ?? "?") // dos letras confundidas, los mismos bytes
print(String(decoding: bytes, as: UTF8.self)) // a con anillo, y nunca se estrella
La receta que cubre casi todo: prueba primero UTF-8 estricto, porque es lo que las API modernas casi siempre quieren decir; cae a ISO Latin-1 solo cuando el contrato calla y prefieres algo legible pero equivocado antes que el silencio; reserva la puerta que nunca se niega para la salida de depuración. Y un intruso invisible que debes comprobar: si el payload empieza con un BOM UTF-8 (los tres bytes EF BB BF), la conversión estricta lo conserva, y tu cadena ahora empieza con un carácter U+FEFF invisible que rompe silenciosamente las comprobaciones de igualdad y los recorridos de ida y vuelta de JSON. Quítalo con una comprobación de prefijo cuando la especificación no prometa uno.
Abriendo archivos
El trabajo de "hay un archivo .b64, dame lo que esconde" es una lectura, un recorte, una decodificación y una escritura. El recorte no es decoración; es la diferencia entre un archivo que se abre y un archivo que devuelve nil, porque las herramientas, los clientes de correo y los editores adoran dejar un salto de línea al final:
import Foundation
let inbox = URL(fileURLWithPath: "Downloads/avatar.b64")
let outbox = URL(fileURLWithPath: "Downloads/avatar.png")
let raw = try String(contentsOf: inbox, encoding: .utf8)
if let data = Data(base64Encoded:
raw.trimmingCharacters(in: .whitespacesAndNewlines)) {
try data.write(to: outbox)
} else {
print("the file was not base64 after all")
}
Si el archivo está envuelto en MIME (saltos de línea cada 76 caracteres), tienes dos salidas limpias: decodifica con .ignoreUnknownCharacters y deja que la opción se coma los fines de línea, o quítalos tú con replacingOccurrences antes de una decodificación estricta. Cada una es una sola línea. Para archivos que simplemente son grandes, decodifica en grupos alineados en vez de leerlo todo: cada grupo de cuatro caracteres se decodifica por su cuenta, así que solo necesitas llevar el grupo actual más un pequeño resto a través de los límites de lectura.
import Foundation
func decodeBase64Chunks(_ stream: InputStream, into result: inout Data) throws {
let chunkSize = 65_536
var buffer = [UInt8](repeating: 0, count: chunkSize)
var leftover = ""
result = Data()
stream.open()
defer { stream.close() }
while stream.hasBytesAvailable {
let read = stream.read(&buffer, maxLength: chunkSize)
if read < 0 { throw CocoaError(.fileReadUnknown) }
if read == 0 { break }
var text = String(decoding: buffer[0..<read], as: UTF8.self)
text = text.replacingOccurrences(of: "\r", with: "")
.replacingOccurrences(of: "\n", with: "")
text = leftover + text
if text.count % 4 != 0 {
let whole = text.count - (text.count % 4)
leftover = String(text.suffix(text.count - whole))
text = String(text.prefix(whole))
} else {
leftover = ""
}
guard !text.isEmpty else { continue }
guard let part = Data(base64Encoded: text) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
if !leftover.isEmpty {
guard let part = Data(base64Encoded: leftover) else {
throw CocoaError(.fileReadCorruptFile)
}
result.append(part)
}
}
La memoria se mantiene plana sin importar el tamaño del archivo: un buffer de lectura, un fragmento sobrante y el resultado que estás construyendo. El mismo bucle maneja una descarga que llega como base64 por la red, un archivo de log que en realidad es un stream codificado, o cualquier payload demasiado grande para tenerlo entre las manos.
JWTs: leyendo los tres puntos
Un JSON Web Token compacto son tres partes base64url unidas por puntos, y las dos primeras de esas partes son JSON plano con abrigo de trinchera. Llegan sin padding, que es exactamente la combinación que el decodificador estricto rechaza a primera vista, así que tu helper dataFromBase64URL() de la sección de URL hace todo el trabajo pesado:
import Foundation
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
func openPart(_ part: String) -> String? {
var fixed = part
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let missing = fixed.count % 4
if missing > 0 {
fixed += String(repeating: "=", count: 4 - missing)
}
guard let data = Data(base64Encoded: fixed) else { return nil }
return String(data: data, encoding: .utf8)
}
let pieces = token.split(separator: ".")
print(openPart(String(pieces[0])) ?? "?")
// {"alg":"HS256","typ":"JWT"}
print(openPart(String(pieces[1])) ?? "?")
// {"sub":"1234567890","name":"John Doe"}
Dos recordatorios van de gratis. Un JWT está firmado, no cifrado: la cabecera y el payload son información pública, y por eso exactamente una contraseña nunca debe vivir dentro de uno (el primo cifrado, JWE, es otra especificación completamente distinta). Y la tercera parte separada por puntos es una firma criptográfica, no un documento, así que decodifica las partes uno y dos y deja el resto en paz.
Data URIs: el archivo detrás de la coma
A las API web les encanta esconder binario dentro de texto con el esquema data:: un PNG en un campo de perfil, una fuente en un blob de CSS, un código QR en un archivo de configuración. El formato es data:{mime};base64,{payload}, y pelar el payload está a un split de distancia:
import Foundation
let uri = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
let payload = uri.components(separatedBy: ",").last ?? ""
if let bytes = Data(base64Encoded: payload) {
print(String(decoding: bytes.prefix(6), as: UTF8.self)) // GIF89a
print(bytes.count) // 42
} else {
print("not a base64 data uri")
}
El ejemplo usa el famoso GIF transparente de 42 bytes, la imagen más pequeña del formato, y por eso sus caracteres de apertura aparecen en más codebases que casi cualquier otra cadena base64 de internet. En las plataformas de Apple el pipeline termina en una línea: el mismo Data que acabas de decodificar se alimenta directamente a UIImage(data:) o NSImage(data:), y por eso "mostrar un avatar desde una API" es una función pequeña y no un proyecto.
HTTP: la cabecera Basic y sus amigos
La antigua cabecera Authorization: Basic es un nombre de usuario y una contraseña, unidos por un dos puntos, empacados con base64 estándar para el viaje (no el dialecto URL: esta vive en una cabecera donde + y / son perfectamente inofensivos). Desempacarla es un split y una decodificación:
import Foundation
let header = "Basic ZWRpdG9yOnMzY3JldA=="
let packed = header.replacingOccurrences(of: "Basic ", with: "")
if let creds = Data(base64Encoded: packed) {
print(String(data: creds, encoding: .utf8) ?? "") // editor:s3cret
} else {
print("malformed header")
}
Deja la nota al pie de seguridad bien alta, porque se aplica a cada base64 que vayas a encontrar: esto es empacar, no proteger. El Basic auth solo es aceptable sobre HTTPS, donde TLS hace la guardia de verdad y base64 solo evita que los bytes rompan la gramática de la cabecera. El mismo razonamiento explica los tokens Authorization: Bearer: el token en sí es un JWT, así que la receta de decodificación de la sección de JWT se le aplica sin cambios.
Correo: el hábito de los 76 caracteres
Un adjunto de correo codificado en base64 se envuelve a los 76 caracteres con fines de línea CRLF, exactamente el ruido para el que existe la opción permisiva. Las cabeceras MIME en bruto te dicen qué alfabeto y qué envoltura usó el remitente (Content-Transfer-Encoding: base64), y la solución es un flag:
import Foundation
let attachment = "VGhpcyBhdHRhY2htZW50IHN1cnZpdmVk\r\nIHRoZSA3Ni1jaGFyYWN0ZXIgaGFiaXQu"
if let data = Data(base64Encoded: attachment, options: .ignoreUnknownCharacters) {
print(String(data: data, encoding: .utf8) ?? "")
}
// This attachment survived the 76-character habit.
Si estás escribiendo una función de correo en vez de leyendo una, recuerda que la envoltura de 76 caracteres también te cuesta: con un salto de línea cada 76 caracteres, el texto codificado llega a cerca del 137 por ciento del tamaño original, y por eso los antiguos ingenieros de correo estimaban el tamaño de los adjuntos a ojo con el atajo "multiplica el original por 1.37, añade unos 800 bytes de cabeceras". El número es folclore ahora, pero la aritmética sigue siendo la aritmética.
El payload doblemente envuelto
El ticket más común de "mis datos están corruptos" en el mundo base64 son datos que se empacaron dos veces: una capa de integración los codificó, y una segunda capa que nunca leyó la documentación codificó el resultado. La jugada defensiva es decodificar una vez, mirar lo que obtuviste, y si el resultado es él mismo una cadena limpia que parece base64 (longitud correcta, alfabeto correcto, nada sorprendente), decodificarlo una vez más, a propósito, y parar. No escribas un bucle que decodifique hasta que falle. Un bucle así se traga felizmente un archivo perfectamente bueno cuyo contenido resulta por casualidad parecido a base64, y después de que corra, nadie puede decir dónde empezaban los datos originales.
import Foundation
func unwrapOnce(_ packed: String) -> Data? {
let cleaned = packed.trimmingCharacters(in: .whitespacesAndNewlines)
return Data(base64Encoded: cleaned)
}
let suspicious = "WVdKag==" // ya parece empaquetado
if let first = unwrapOnce(suspicious) {
let inner = String(data: first, encoding: .utf8) ?? ""
if let second = unwrapOnce(inner) {
print("it was wrapped twice:", String(data: second, encoding: .utf8) ?? "?")
}
}
// it was wrapped twice: abc
Dos desenvolvimientos, dos decisiones conscientes, y un payload que por fin vuelve a ser solo abc.
Hacer que la nil signifique algo
Como el decodificador contesta nil en vez de lanzar errores, el estilo de manejo de errores de tu código base64 es una elección que haces tú, y la elección de la que estarás agradecido más adelante es un envoltorio pequeño que convierte el rechazo silencioso en un error ruidoso y específico:
import Foundation
enum Base64Failure: Error, CustomStringConvertible {
case notBase64(Int)
var description: String {
switch self {
case .notBase64(let length):
return "input of \(length) characters is not valid base64"
}
}
}
func decodeStrict(_ text: String) throws -> Data {
let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard let data = Data(base64Encoded: cleaned) else {
throw Base64Failure.notBase64(cleaned.count)
}
return data
}
do {
let bytes = try decodeStrict("c3ludGF4IGVycm")
print(String(data: bytes, encoding: .utf8) ?? "?")
} catch {
print(error) // input of 14 characters is not valid base64
}
El envoltorio se convierte además en el único lugar donde vive la normalización: el recorte, cualquier traducción de alfabeto, cualquier relleno de padding. Los que llaman obtienen una función, un único significado para el fallo, y ni una ! a la vista. Desenvolver a la fuerza Data(base64Encoded:)! es la forma en que un payload malo se convierte en una app caída, y el envoltorio es el seguro barato contra eso. El mismo patrón funciona en la línea de comandos, donde un script con CommandLine.arguments y una escritura por FileHandle convierte "decodificar este archivo desde el shell" en una utilidad de cinco líneas en vez de un rodeo de copiar-pegar por una web.
Seguridad, medida en bytes
- No es cifrado. Base64 es un reempaquetado reversible y legible al instante. Si tu modelo de amenazas incluye a un humano con un navegador y cinco segundos, tienes cero protección, y cada cabecera JWT lo demuestra a diario.
- Normaliza antes de comparar. Como
TQ==yTS==se decodifican a los mismos bytes, dos sistemas pueden guardar grafías distintas de los mismos datos. Un paper de 2022, "La maleabilidad de Base64 en la práctica", documentó lo que esa garantía de unicidad rota hace en el mundo real: desajustes en los logs, ataques de denegación de servicio y entradas duplicadas en la base de datos. Si tu app de Swift cachea, deduplica o compara valores base64, ejecuta una decodificación canónica (o un recodificado canónico) en la puerta. - Limita la entrada antes de decodificar. Decodificar N caracteres asigna aproximadamente tres cuartas partes de N bytes mientras sigues sosteniendo la cadena de entrada. Un cliente hostil puede enviar 100 megabytes de la letra
Ay ver cómo sube tu memoria antes de que el decodificador diga que no. Comprueba primero la longitud, a bajo coste, y rechaza lo que sea demasiado grande. - Cuidado con la opción permisiva como filtro.
.ignoreUnknownCharacterselimina caracteres. Un pase de "sanitización" a través de ella puede convertir un payload base64url válido en unos datos distintos sin ningún error. Es un filtro de ruido para saltos de línea, no un validador. - Mantenlo fuera de las URLs cuando puedas. Los payloads base64 grandes en cadenas de consulta o rutas superan las longitudes cómodas de URL y se destrozan al pasar por los proxies. Mételos en cuerpos de petición, archivos o tokens en su lugar.
Rendimiento, en breves
El decodificador es un recorrido por tabla de búsqueda: cada carácter se consulta en una tabla pequeña y unos pocos bits se desplazan y se OR-ean en los bytes de salida. En un toolchain actual es lo bastante rápido para cualquier cosa que quepa en memoria, y el número a recordar es la proporción de salida: los bytes decodificados son aproximadamente tres cuartas partes de la longitud de entrada, así que una cadena de 4 megabytes te cuesta unos 3 megabytes de resultado encima de la cadena que ya sostienes. Si vas por un camino donde Foundation en sí no está permitido (un objetivo profundamente embebido, un bundle de WebAssembly), el paquete de la comunidad swift-extras-base64 es la alternativa notable: Swift puro sin dependencia de Foundation, un codificador y decodificador compatibles con RFC 4648 con opciones base64url y de padding, y benchmarks que lo sitúan varias veces más rápido que Foundation. Una implementación anterior del mismo paquete va incluso incluida en el soporte WebSocket de swift-nio, que es tan cercano al grado de producción como llega a estar un proyecto paralelo. Para una app o script corriente, es equipaje innecesario; para el rincón restringido de Swift, es la respuesta estándar.
Una década de desembalaje
Swift no inventó nada de esto, y vale la pena saber de dónde salió cada pieza de la caja de herramientas:
- Los años 80, la era de la misma máquina. Las codificaciones más antiguas de esta familia (uuencode en UNIX, BinHex en el TRS-80 y el Mac clásico) movían archivos entre máquinas que daban por hecho que el otro extremo era igual a la suya. uuencode usaba un alfabeto de mayúsculas, dígitos y puntuación, y sus letras ocupan posiciones ASCII consecutivas, así que codificar era cuestión de sumar 32 sin ninguna tabla de búsqueda. Los decodificadores de esta época podían suponer mucho, y en el momento en que los datos cruzaban ecosistemas, se caían.
- 1987, el alfabeto recibe una dirección. RFC 989 (Correo con refuerzo de privacidad, febrero de 1987) estandarizó el alfabeto de 64 caracteres, envolvió las líneas a exactamente 64 caracteres, y usó
=para el padding y*para marcar datos codificados pero no cifrados. Cada bloque estilo PEM es un descendiente de ese documento. - 1996, la era liberal. MIME (RFC 2045) tomó el alfabeto para el correo, movió la envoltura a 76 caracteres, y dijo a los decodificadores conformes que ignoraran cualquier carácter fuera del alfabeto, como los saltos de línea CRLF. Esta es la era que entrenó a una generación a esperar decodificadores perdonadores, y la era cuyas expectativas el valor estricto por defecto de Swift rompe a propósito.
- 2003 a 2006, las reglas se endurecen. RFC 3548 (2003) dio el primer golpe para unificar la familia; RFC 4648 (octubre de 2006) lo resolvió, codificó las reglas de padding, y añadió el alfabeto URL-safe. Su párrafo sobre decodificadores es el que Swift sigue: rechaza los caracteres fuera del alfabeto, a menos que el formato que estés sirviendo diga explícitamente ignorarlos, como hace MIME.
- 2013 a 2014, la API espera en los bastidores. La clase
NSDatade Apple llevaba años empacando y desempacando base64, y la API basada en opciones con su opción de decodificación llegó en iOS 7, en 2013, un año antes de que Swift existiera. Cuando Swift 1.0 salió el 9 de septiembre de 2014, el decodificador entró con el lenguaje y ha mantenido el mismo carácter desde entonces: núcleo estricto, un mando permisivo, inicializador que puede fallar. - 3 de diciembre de 2015, Linux recibe un decodificador. Swift se abrió ese día, y con él el base64 de Foundation cruzó a Linux y, más tarde, a Windows. "Decodificar base64 en Swift en una máquina que no es de Apple" tiene apenas una década: un invitado tarde a una fiesta que empezó en 1987.
- 2023 a 2026, la reescritura. La reescritura de Foundation (el proyecto swift-foundation) movió
Dataa un núcleo de Swift puro, y en 2025 una propuesta de la comunidad añadió opciones nativas base64url y de omisión de padding. A fecha de escribir esto, las betas más nuevas de los SDK y el toolchain de código abierto distribuyen las opciones de codificación mientras las opciones de decodificación aún maduran en el toolchain de código abierto, así que las extensiones hechas a mano siguen siendo la respuesta universal mientras tanto.
Pequeñas maravillas
====es entrada legal. Cuatro pads y sin datos se decodifican a unDatavacío, la única cadena base64 cuyo contenido entero es "no hay nada aquí", y Swift está de acuerdo con ella.- La policía de caracteres del decodificador no revisa el trabajo de la policía de bits:
TS==yTQ==ambos te entreganM, mientras queTg==te entregaN. Misma gramática, bits distintos, sin preguntas. - El
Datade Swift puede decodificar base64 que llegó como bytes en vez de una cadena, a través de la varianteData(base64Encoded: Data), así que un payload que cruzó el cable como ASCII puede omitir el recorrido de ida y vuelta por cadena por completo. - La palabra que lleva siendo base64 desde que nacieron los vectores de prueba es
foobar, y se empaqueta aZm9vYmFy. Si alguna vez has visto un ejemplo base64 en el mundo real, hay buenas probabilidades de que foobar estuviera involucrado. - El famoso GIF transparente de 1x1 es exactamente 42 bytes y abre con la palabra mágica
GIF89a, y por eso sus primeros ocho caracteres codificados aparecen en más codebases que casi cualquier otro prefijo base64 de la Tierra. - El decodificador de código abierto moderno hace su comprobación de carácter inválido con una comparación única: OR-a juntos cuatro valores de búsqueda por posición y prueba el resultado contra un centinela, así que una sola rama decide el destino de un grupo completo de cuatro caracteres. La implementación anterior hacía el mismo trabajo con una tabla de 128 bytes donde cualquier valor en o por encima de 0x80 significaba "no es una letra".
- Los BOM UTF-8 son invisibles:
EF BB BFal principio de un payload se convierte en un carácter U+FEFF que sobrevive a la conversión estricta y luego rompe comprobaciones de igualdad unas líneas de código después. - Swift es 27 años más joven que el alfabeto que decodifica. El lenguaje salió en 2014; las 64 letras que maneja se estandarizaron en 1987 y no han cambiado desde entonces.
Esa es la caja de herramientas de decodificación completa: un inicializador que puede fallar con un reglamento corto, un mando permisivo con un punto ciego documentado, un helper base64url de cinco líneas, una decisión de juego de caracteres que te pertenece, un bucle por bloques para los archivos grandes y un envoltorio que hace que la nil signifique algo. La decodificación es donde base64 muerde, y ahora conoces los nombres de cada diente. Cuando el trabajo se da la vuelta y empiezas a empacar bytes para el viaje en vez de desempacarlos, la sobrecarga de aproximadamente 33 por ciento toma el mando y aparecen las opciones de envoltura. El artículo relacionado de codificación cubre esa mitad del recorrido de ida y vuelta por completo, así que pasa por allí cuando estés listo para enviar en la otra dirección.
Última actualización: 2026-09-08
Artículo relacionado: Codificación Base64 en Swift: una guía completa