Decodificação Base64 em Swift: um guia completo
Em algum lugar do seu pipeline, seus dados estão de disfarce: um token escondido em um header HTTP, um avatar se escondendo dentro de um campo JSON, um arquivo .b64 que você prometeu analisar na semana passada, um anexo de e-mail que chegou como uma muralha de letras. Tirar esses disfarces no Swift é um dos trabalhos mais agradáveis da linguagem: um framework, um inicializador e um manual de regras curto o suficiente para caber em um post-it.
A página inicial deste site já cobre o formato em si (64 caracteres imprimíveis, seis bits por caractere, no máximo dois caracteres = de padding no grupo final), então não vamos repetir essa história aqui. Só guarde dois fatos no bolso. Primeiro, base64 é uma forma de vestir bytes como texto, não uma fechadura. Segundo, toda viagem de base64 no Swift passa por um único tipo, Data, e o decodificador mora nele como um inicializador falível. Esse único fato molda o resto deste artigo, porque um inicializador falível muda a forma como você escreve cada linha que vem depois dele.
Um único tipo domina o trabalho inteiro
O Swift não espalha helpers de base64 por uma dúzia de módulos, e não obriga você a instalar nada. O decodificador é Data(base64Encoded:options:) no Foundation, e faz parte da plataforma desde os primeiros dias do framework (a Apple lista o inicializador desde o iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 e visionOS 1.0; as opções de comprimento de linha do lado da codificação já remontam ao iOS 7.0). No Linux e no Windows, o mesmo Foundation vem com a toolchain de código aberto, então o código abaixo se comporta igual num app de iPhone, num worker de servidor e num script no seu terminal.
Existe um inicializador irmão, Data(base64Encoded: Data, options:), para o caso em que o seu base64 chega como bytes ASCII brutos em vez de string. Os dois aceitam um argumento options que por padrão é []. E os dois compartilham um traço de personalidade que importa mais do que qualquer opção: eles são falíveis.
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!
A documentação da Apple para o inicializador é lindamente direta: ele "retorna nil quando a entrada não é reconhecida como Base-64 válida". Sem exceções, sem erros lançados, sem spam de log. Só um nil tranquilo e a responsabilidade de decidir o que isso significa para o seu usuário. Se você for lembrar de uma coisa sobre base64 no Swift, que seja essa: o decodificador nunca dá crash e nunca reclama. Ele simplesmente recusa.
O veredito do decodificador: uma tabela de sim e não
Então o que significa "válido" para este decodificador? Acontece que é uma lista curta de regras duras, e essa lista é a diferença entre "funciona na demo" e "sobrevive em produção". Cada linha da tabela abaixo é comportamento real do inicializador numa toolchain atual, então você pode citá-la direto nas suas mensagens de erro:
| Entrada | Veredito | Por quê |
|---|---|---|
TWFu |
Man |
um grupo completo de quatro caracteres não precisa de nenhum padding |
TQ== |
M |
um byte mais dois pads, o caso de manual |
SGVsbG8h |
Hello! |
oito caracteres é múltiplo de quatro, então nenhum pad é necessário |
==== |
Data vazio |
padding com nada atrás é legal e decodifica para zero bytes |
| a string vazia | Data vazio |
nada entra, nada sai, e o optional ainda assim tem sucesso |
TQ |
nil |
comprimento dois: um grupo de quatro foi prometido mas nunca entregue |
T |
nil |
um caractere carrega seis bits e um byte precisa de oito |
SGVsbG8hTQ |
nil |
dez caracteres: o grupo final fica pendurado sem seus pads |
TQ=== |
nil |
três pads: o terceiro não tem mais nada para padding |
TQ==TQ |
nil |
dados depois do padding é um não definitivo |
SGVs bG8h |
nil |
um único espaço está fora do alfabeto, e o modo estrito não mostra piedade |
SGVsbG8h mais um newline no final |
nil |
a quebra de linha no fim de um arquivo que você acabou de ler conta como ruído |
Três linhas merecem uma segunda olhada. A linha ==== significa que a checagem if let passa e o seu código segue viagem com zero bytes, então se um payload vazio não é um estado válido no seu app, confira a contagem logo depois da decodificação. A linha da string vazia é o mesmo truque com menos maquiagem. E a linha do newline final é a razão mais comum para um arquivo base64 que codificou perfeitamente de manhã se recusar a decodificar à tarde: algo no caminho adicionou um fim de linha, e o decodificador estrito leva para o pessoal.
Existe também um ponto fraco famoso que a tabela não consegue mostrar. Compare TQ== e TS==: os dois decodificam para o mesmo byte, M, porque os dois bits mais baixos daquele caractere final são descartados antes de serem inspecionados. Aponte Tg== em vez disso e você recebe N sem luta. O decodificador fiscaliza os caracteres e deixa os bits finais escaparem. Essa tolerância não é um bug, mas significa que duas strings diferentes podem significar os mesmos dados, e isso começa a importar no momento em que o seu sistema compara, deduplica ou guarda em cache valores base64 (mais sobre isso na seção de segurança).
Quando a entrada é mais barulhenta do que você pensa
O base64 do mundo real raramente chega como uma única linha imaculada. Anexos de e-mail são quebrados a cada 76 caracteres, com um carriage return e um line feed após cada linha, um hábito herdado da especificação MIME de 1996, e arquivos de certificado são quebrados a cada 64. O decodificador tem exatamente uma opção para lidar com esse ruído, e ela é das grandes:
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.
O .ignoreUnknownCharacters está documentado como um decodificador que "ignora bytes não-Base-64 desconhecidos, incluindo caracteres de fim de linha", e para esse trabalho é a ferramenta certa: o ruído é apagado, o alfabeto sobrevive e o payload sai inteiro. Mas a opção tem um ponto cego, e é aquele que mais morde desenvolvedores Swift: ele apaga todo caractere fora do alfabeto, incluindo o - e o _ do base64url. Ele não os converte em + e /; ele simplesmente joga fora. Dependendo do que essa exclusão deixa para trás, você recebe um nil (quando os sobreviventes já não formam grupos inteiros) ou, pior, uma resposta confiante com o número errado de bytes. Uma string base64url de 16 caracteres que codifica 12 bytes pode voltar do decodificador tolerante como 9 bytes diferentes, sem erro e sem pedido de desculpas.
A regra para guardar: .ignoreUnknownCharacters é para ruído de transporte (quebras de linha, espaços perdidos de um copiar-e-colar), nunca para diferenças de alfabeto. Se o payload pode ser base64url, converta os caracteres você mesmo primeiro, exatamente como a próxima seção mostra, e entregue ao decodificador uma string padrão limpa.
O alfabeto de URL
A seção 5 do RFC 4648 define o primo do alfabeto padrão que você vem encontrando: o base64url, onde + vira -, / vira _ e o padding de = é normalmente descartado. O motivo é o mesmo que mantém as suas URLs honestas: numa query string, um + é lido como espaço pela análise de formulário, um / é um separador de caminho e um = separa chaves de valores. O RFC é direto sobre a relação entre os dois: a variante de URL "não deve ser considerada a mesma que a codificação base64". JWTs, mensagens Web Push, IDs de vídeo do YouTube e a maioria dos identificadores de API modernos falam base64url, então espere encontrá-lo no seu primeiro dia.
No lado da decodificação, a receita tem dois movimentos: traduza o alfabeto e depois complete o padding, porque o decodificador estrito ainda quer o seu múltiplo de quatro.
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
}
A linha do módulo é o truque inteiro: payloads base64url normalmente chegam sem padding, e um ou dois caracteres = (nunca três) restauram o grupo de quatro que o decodificador espera. Você vai encontrar uma versão dessa extensão de cinco linhas em um número surpreendente de codebases Swift, e com boa razão. Existe um motivo pelo qual ela ficará mais curta no futuro: os SDKs mais novos da Apple (26.4 e acima, até a data desta escrita) ganharam uma opção nativa .base64URLAlphabet para o codificador, com as opções de decodificação correspondentes ainda amadurecendo no Foundation de código aberto atrás de um marcador de disponibilidade para uma toolchain posterior. Até isso chegar ao seu alvo mínimo de deploy, a extensão é a resposta portátil, e continuará funcionando em todas as versões por construção.
Bytes primeiro, palavras depois
Aqui está a decisão que o decodificador não pode tomar por você: ele entrega um Data, um saco de bytes sem a menor ideia de em qual conjunto de caracteres o payload original foi escrito. Se o payload era texto, escolher esse conjunto de caracteres é o seu trabalho, e o Swift te dá duas portas de saída do mundo dos bytes, com temperamentos bem diferentes.
String(data:encoding:)é a porta estrita. Ela retorna um optional e respondenilquando os bytes não são válidos na codificação que você nomeou. Ideal para validação, perigoso se você fizer force-unwrap da resposta.String(decoding:as:)é a porta nunca-recusa. Ela sempre retorna uma string, substituindo pelo caractere de substituição U+FFFD tudo o que não consegue interpretar. Ideal para logs e pré-visualizações, perigoso se você guarda o resultado e chama de dados.
import Foundation
let bytes = Data([0xC3, 0xA5]) // a grafia UTF-8 da letra a com anel
print(String(data: bytes, encoding: .utf8) ?? "?") // a com anel, lida corretamente
print(String(data: bytes, encoding: .isoLatin1) ?? "?") // duas letras confusas, mesmos bytes
print(String(decoding: bytes, as: UTF8.self)) // a com anel, e nunca dá crash
A receita que cobre quase tudo: tente primeiro UTF-8 estrito, porque é o que as APIs modernas quase sempre querem dizer; volte para ISO Latin-1 só quando o contrato estiver em silêncio e você prefira legível-mas-errado em vez de silêncio; reserve a porta nunca-recusa para saída de depuração. E um intruso invisível para conferir: se o payload começa com um BOM UTF-8 (os três bytes EF BB BF), a conversão estrita o mantém, e a sua string agora começa com um caractere U+FEFF invisível que silenciosamente quebra checagens de igualdade e idas-e-voltas de JSON. Remova-o com uma checagem de prefixo quando a especificação não promete um.
Abrindo arquivos
O trabalho "tem um arquivo .b64, me dá o que ele esconde" é uma leitura, um trim, uma decodificação e uma escrita. O trim não é enfeite; ele é a diferença entre um arquivo que abre e um arquivo que retorna nil, porque ferramentas, clientes de e-mail e editores adoram deixar uma quebra de linha no 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")
}
Se o arquivo for embrulhado em MIME (quebras de linha a cada 76 caracteres), você tem duas fugas limpas: decodifique com .ignoreUnknownCharacters e deixe a opção engolir os fins de linha, ou remova-os você mesmo com replacingOccurrences antes de uma decodificação estrita. As duas têm uma linha cada. Para arquivos que são simplesmente grandes, decodifique em grupos alinhados em vez de ler a coisa inteira: cada grupo de quatro caracteres decodifica por conta própria, então você pode carregar apenas o grupo atual mais um pequeno resto entre fronteiras de leitura.
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)
}
}
A memória fica plana não importa o quão grande o arquivo seja: um buffer de leitura, um fragmento sobrando e o resultado que você está construindo. O mesmo loop lida com um download que chega como base64 pelo cabo, um arquivo de log que na verdade é um stream codificado, ou qualquer payload grande demais para caber na sua mão.
JWTs: lendo os três pontos
Um JSON Web Token compacto é três partes base64url unidas por pontos, e as duas primeiras dessas partes são JSON puro vestindo um sobretudo. Elas chegam sem padding, que é exatamente a combinação que o decodificador estrito rejeita à primeira vista, então o seu helper dataFromBase64URL() da seção de URL faz todo o trabalho 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"}
Duas lembranças vêm junto. Um JWT é assinado, não criptografado: o header e o payload são informação pública, que é exatamente por isso que senha nunca deve morar em um (o primo criptografado, JWE, é outra especificação inteira). E a terceira parte separada por ponto é uma assinatura criptográfica, não um documento, então decodifique as partes um e dois e deixe o resto quieto.
Data URIs: o arquivo atrás da vírgula
APIs web adoram esconder binário dentro de texto com o esquema data:: um PNG num campo de perfil, uma fonte num blob de CSS, um código QR num arquivo de configuração. O formato é data:{mime};base64,{payload}, e descascar o payload fica a um split de distância:
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")
}
O exemplo usa o famoso GIF transparente de 42 bytes, a menor imagem do formato, que é por isso que os seus caracteres de abertura aparecem em mais codebases do que quase qualquer outra string base64 na internet. Nas plataformas da Apple o pipeline termina com uma linha só: o mesmo Data que você acabou de decodificar alimenta diretamente o UIImage(data:) ou o NSImage(data:), que é por isso que "exibir um avatar de uma API" é um recurso pequeno e não um projeto.
HTTP: o header Basic e seus amigos
O antigo header Authorization: Basic é um nome de usuário e uma senha, unidos por dois-pontos, empacotados com base64 padrão para a viagem (não o dialeto de URL: este mora num header onde + e / são perfeitamente inofensivos). Desempacotá-lo é um split e uma decodificação:
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")
}
Mantenha a nota de rodapé de segurança em alto e bom som, porque ela vale para todo base64 que você jamais vai encontrar: isso é empacotamento, não proteção. Basic auth só é aceitável sobre HTTPS, onde o TLS faz a guarda de verdade e o base64 apenas impede que os bytes quebrem a gramática do header. O mesmo raciocínio explica os tokens Authorization: Bearer: o token em si é um JWT, então a receita de decodificação da seção de JWT se aplica a ele sem mudanças.
E-mail: o hábito dos 76 caracteres
Um anexo de e-mail codificado em base64 é quebrado a cada 76 caracteres, com fins de linha CRLF, exatamente o ruído para o qual a opção tolerante existe. Os headers MIME brutos te dizem qual alfabeto e qual quebra de linha o remetente usou (Content-Transfer-Encoding: base64), e a correção é um flag só:
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.
Se você está escrevendo um recurso de e-mail em vez de lendo um, lembre-se que a quebra de 76 caracteres também te custa: com uma quebra de linha a cada 76 caracteres, o texto codificado chega perto de 137 por cento do tamanho original, que é por isso que antigos engenheiros de e-mail estimavam tamanhos de anexo com o atalho "multiplique o original por 1.37 e some mais ou menos 800 bytes de headers". O número é folclore hoje, mas a aritmética continua sendo a aritmética.
O payload dobradamente embrulhado
O chamado mais comum de "meus dados estão corrompidos" na terra do base64 é dado que foi empacotado duas vezes: uma camada de integração o codificou, e uma segunda camada que nunca leu a documentação codificou o resultado. O movimento defensivo é decodificar uma vez, olhar o que você recebeu, e se o resultado em si é uma string limpa com cara de base64 (comprimento certo, alfabeto certo, nada surpreendente), decodificá-la mais uma vez, com intenção, e parar. Não escreva um loop que decodifica até falhar. Um loop desses come com alegria um arquivo perfeitamente bom cujo conteúdo por acaso tem cara de base64, e depois que ele roda, ninguém consegue dizer onde os dados originais começavam.
import Foundation
func unwrapOnce(_ packed: String) -> Data? {
let cleaned = packed.trimmingCharacters(in: .whitespacesAndNewlines)
return Data(base64Encoded: cleaned)
}
let suspicious = "WVdKag==" // já parece empacotado
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
Dois descascamentos, duas decisões conscientes, e um payload que finalmente é só abc de novo.
Fazendo o nil significar algo
Como o decodificador responde nil em vez de lançar, o estilo de tratamento de erro do seu código base64 é uma escolha que você faz, e a escolha de que você vai se alegrar depois é um pequeno wrapper que transforma a recusa silenciosa em um erro alto e 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
}
O wrapper também vira o único lugar onde mora a normalização: o trim, qualquer tradução de alfabeto, qualquer complemento de padding. Quem chama recebe uma função, um significado para falha e nenhum ! à vista. Fazer force-unwrap de Data(base64Encoded:)! é assim que um payload ruim vira um app com crash, e o wrapper é o seguro barato contra isso. O mesmo padrão funciona na linha de comando, onde um script com CommandLine.arguments e uma escrita via FileHandle transforma "decodificar este arquivo do shell" num utilitário de cinco linhas em vez de um desvio de copiar-e-colar por um site.
Segurança, medida em bytes
- Não é criptografia. Base64 é um reempacotamento reversível, legível na hora. Se o seu modelo de ameaças inclui um humano com um navegador e cinco segundos, você tem proteção zero, e cada header de JWT prova o ponto todo dia.
- Ponha na forma canônica antes de comparar. Como
TQ==eTS==decodificam para os mesmos bytes, dois sistemas podem guardar grafias diferentes dos mesmos dados. Um paper de 2022, "Maleabilidade do Base64 na Prática", documentou o que essa garantia de unicidade quebrada faz no mundo real: logs que não batem, ataques de negação de serviço e entradas duplicadas no banco de dados. Se o seu app Swift coloca em cache, deduplica ou compara valores base64, rode uma decodificação canônica (ou um reempacotamento canônico) no portão. - Limite a entrada antes da decodificação. Decodificar N caracteres aloca mais ou menos três quartos de N bytes enquanto você ainda segura a string de entrada. Um cliente hostil pode enviar 100 megabytes da letra
Ae assistir sua memória subir antes do decodificador dizer não. Confira o comprimento primeiro, a baixo custo, e rejeite o que for grande demais. - Cuide-se da opção tolerante como filtro.
.ignoreUnknownCharactersapaga caracteres. Uma passada de "sanitização" por ele pode transformar um payload base64url válido em dados diferentes sem nenhum erro. É um filtro de ruído para quebras de linha, não um validador. - Mantenha fora de URLs onde puder. Payloads base64 grandes em query strings ou caminhos estouram comprimentos de URL confortáveis e ficam amassados por proxies. Coloque-os em corpos de requisição, arquivos ou tokens, em vez disso.
Desempenho, rapidamente
O decodificador é um passeio por tabela de consulta: cada caractere é indexado numa pequena tabela e alguns bits são deslocados e or'd em bytes de saída. Numa toolchain atual isso é rápido o suficiente para qualquer coisa que caiba em memória, e o número para lembrar é a proporção de saída: os bytes decodificados são mais ou menos três quartos do comprimento da entrada, então uma string de 4 megabytes te custa em torno de 3 megabytes de resultado por cima da string que você já segura. Se você está num caminho onde o próprio Foundation não é permitido (um alvo profundamente embutido, um bundle WebAssembly), o pacote da comunidade swift-extras-base64 é a alternativa notável: Swift puro sem dependência do Foundation, um codificador e um decodificador conformes com o RFC 4648 com opções de base64url e padding, e benchmarks que o colocam várias vezes mais rápido que o Foundation. Uma implementação anterior do mesmo pacote até vem dentro do suporte a WebSocket do swift-nio, que é o mais próximo de nível de produção que um projeto de lado consegue chegar. Para um app ou script comum, é bagagem desnecessária; para o canto restrito do Swift, é a resposta padrão.
Uma década de desembrulhamento
O Swift não inventou nada disso, e vale a pena saber de onde veio cada peça da caixa de ferramentas:
- Anos 1980, a era da mesma máquina. As codificações mais antigas dessa família (uuencode no UNIX, BinHex no TRS-80 e no Mac clássico) moviam arquivos entre máquinas que assumiam que a outra ponta era igual à delas. O uuencode usava um alfabeto de letras maiúsculas, dígitos e pontuação, e suas letras ocupam posições ASCII consecutivas, então codificar era questão de somar 32, sem nenhuma tabela de consulta. Decodificadores dessa época podiam assumir muito, e no momento em que dados cruzavam ecossistemas, tudo caía.
- 1987, o alfabeto ganha um endereço. O RFC 989 (Privacy-Enhanced Mail, fevereiro de 1987) padronizou o alfabeto de 64 caracteres, quebrou linhas em exatamente 64 caracteres e usou
=para padding e*para marcar dados codificados-mas-não-criptografados. Todo bloco no estilo PEM é descendente daquele documento. - 1996, a era liberal. O MIME (RFC 2045) levou o alfabeto para o e-mail, moveu a quebra para 76 caracteres e mandou decodificadores conformes ignorar qualquer caractere fora do alfabeto, como as quebras de linha CRLF. Essa é a era que treinou uma geração a esperar decodificadores perdoadores, e a era cujas expectativas o padrão estrito do Swift quebra de propósito.
- 2003 a 2006, as regras endurecem. O RFC 3548 (2003) deu o primeiro golpe para unificar a família; o RFC 4648 (outubro de 2006) resolveu, codificou as regras de padding e adicionou o alfabeto seguro para URL. Seu parágrafo de decodificador é o que o Swift segue: rejeite caracteres fora do alfabeto, a menos que o formato que você atende diga explicitamente para ignorá-los, como o MIME faz.
- 2013 a 2014, a API espera ao fundo do palco. A classe
NSDatada Apple empacotava e desempacotava base64 há anos, e a API baseada em opções com sua opção de decodificação chegou no iOS 7, em 2013, um ano antes do Swift existir. Quando o Swift 1.0 saiu em 9 de setembro de 2014, o decodificador entrou com a linguagem e mantém a mesma personalidade desde então: núcleo estrito, um único botão tolerante, inicializador falível. - 3 de dezembro de 2015, o Linux ganha um decodificador. O Swift foi liberado como open-source nesse dia, e com ele o base64 do Foundation cruzou para o Linux e, depois, para o Windows. "Decodificação de base64 em Swift numa máquina que não é da Apple" não tem nem uma década: um convidado atrasado numa festa que começou em 1987.
- 2023 a 2026, a reescrita. A reescrita do Foundation (o projeto swift-foundation) moveu o
Datapara um núcleo de Swift puro, e em 2025 uma proposta da comunidade adicionou opções nativas de base64url e de omissão de padding. Até a data desta escrita, os betas mais novos dos SDKs e a toolchain de código aberto trazem as opções de codificação, enquanto as opções de decodificação ainda amadurecem na toolchain de código aberto, então as extensões feitas à mão continuam sendo a resposta universal no intervalo.
Pequenas maravilhas
====é entrada legal. Quatro pads e nenhum dado decodificam para umDatavazio, a única string base64 cujo conteúdo inteiro é "não tem nada aqui", e o Swift concorda com ela.- A polícia de caracteres do decodificador não confere o trabalho da polícia de bits:
TS==eTQ==te entregamMnos dois casos, enquantoTg==te entregaN. Mesma gramática, bits diferentes, sem perguntas feitas. - O
Datado Swift pode decodificar base64 que chegou como bytes em vez de string, pela varianteData(base64Encoded: Data), então um payload que cruzou a rede como ASCII pode pular a ida-e-volta de string por completo. - A palavra que é base64 desde o nascimento dos vetores de teste é
foobar, e ela empacota paraZm9vYmFy. Se você já viu um exemplo de base64 no mundo real, há uma chance bem boa de que foobar estivesse envolvida. - O famoso GIF transparente 1x1 é exatamente 42 bytes e começa com a palavra mágica
GIF89a, que é por isso que os seus primeiros oito caracteres codificados aparecem em mais codebases do que quase qualquer outro prefixo base64 na Terra. - O decodificador de código aberto moderno faz a sua checagem de caractere inválido com uma comparação única: ele or's juntos quatro valores de consulta por posição e testa o resultado contra um sentinela, então uma única branch decide o destino de um grupo inteiro de quatro caracteres. A implementação mais antiga fazia o mesmo trabalho com uma tabela de 128 bytes onde qualquer valor em 0x80 ou acima significava "não é uma letra".
- BOMs UTF-8 são invisíveis:
EF BB BFno começo de um payload vira um caractere U+FEFF que sobrevive à conversão estrita e depois quebra checagens de igualdade algumas linhas de código adiante. - O Swift é 27 anos mais novo que o alfabeto que ele decodifica. A linguagem saiu em 2014; as 64 letras que ela lida foram padronizadas em 1987 e não mudaram desde então.
Essa é a caixa de ferramentas inteira de decodificação: um inicializador falível com um manual de regras curto, um botão tolerante com ponto cego documentado, um helper de base64url de cinco linhas, uma decisão de conjunto de caracteres que é sua, um loop em blocos para os arquivos grandes e um wrapper que faz o nil significar algo. Decodificação é onde o base64 morde, e agora você conhece o nome de cada dente. Quando o trabalho inverte e você começa a empacotar bytes para a viagem em vez de desempacotá-los, a sobretaxa de mais ou menos 33 por cento toma o comando e as opções de quebra de linha aparecem. O artigo de codificação relacionado cobre essa metade da ida-e-volta por completo, então siga para lá quando estiver pronto para enviar na outra direção.
Última atualização: 2026-09-08
Artigo relacionado: Codificação Base64 em Swift: um guia completo