Codificação Base64 em Swift: um guia completo
Você tem algo que precisa viajar, e o caminho é só de texto: uma API JSON que se recusa a aceitar bytes brutos, um canal de e-mail que lembra das suas origens de 7 bits, uma URL que engasga com qualquer coisa que não consiga nomear, um arquivo de configuração que só aceita os caracteres mais simples. Bem-vindo ao lado do empacotamento do base64, onde o Swift transforma seus bytes em uma muralha amigável de letras com uma única chamada de método, uma sobretaxa de mais ou menos um caractere extra a cada três bytes e algumas opções de quebra de linha que existem porque duas décadas diferentes tinham opiniões sobre o comprimento das linhas.
A página inicial deste site já explica o formato em detalhe (64 caracteres imprimíveis, quatro deles a cada três bytes de entrada, no máximo dois caracteres = de padding no grupo final), então a palestra sobre o formato acaba antes de começar. Dois fatos para levar a este artigo: base64 é empacotamento, não fechadura, e o empacotamento expande seus dados em mais ou menos 33 por cento, o que importa sempre que você está perto de um limite de tamanho. No Swift, o trabalho inteiro passa por um único tipo, Data, e um método total, base64EncodedString(options:). A única habilidade de verdade necessária é saber o que acontece nos dois passos em volta desse método, porque o método em si nunca falha. São os passos que falham.
Um método, zero desculpas
Tudo que é base64 no Swift mora no Data do framework Foundation, e mora lá desde os primeiros lançamentos da linguagem (a Apple lista o método desde o iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 e visionOS 1.0). O pipeline é sempre os mesmos três passos: coloque o seu conteúdo num Data, chame o método, envie a string.
import Foundation
let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=
Dois detalhes nessas três linhas merecem uma olhada mais de perto. Primeiro, Data(note.utf8) é o passo silencioso: a vista utf8 pode representar qualquer escalar Unicode por definição, então nunca falha, que é por isso que é o padrão na maioria dos exemplos. O primo falível, note.data(using:), pode e de fato responde nil para algumas codificações, e toda aquela decisão de "quais bytes" ganha a sua própria seção abaixo, porque é o primeiro lugar onde os seus dados podem sumir. Segundo, o método em si é total: ele sempre responde, não tem caso de erro, e a única pergunta que ele faz é qual quebra de linha você quer. Existe também um irmão, base64EncodedData(options:), que retorna o resultado empacotado como Data de bytes ASCII em vez de string, para pipelines onde a próxima parada é uma API binária em vez de um campo de texto.
E como metade de você chegou via "meu app Swift precisa de uma dependência base64": não há nada para instalar. Base64 faz parte do Foundation, o Foundation faz parte da toolchain, e a toolchain chega do mesmo jeito em todas as plataformas. No macOS é o Xcode ou as ferramentas de linha de comando; no Linux e no Windows é o instalador do swift.org, onde a linha estável atual, até a data desta escrita, é a 6.3.x e o gerenciador de versões Swiftly é a porta de entrada recomendada; e as imagens Docker oficiais cobrem a galera de contêiner. O seu Package.swift fica vazio, e deve ficar.
A primeira decisão de verdade: quais bytes?
Antes de um único caractere base64 ser produzido, você já tomou a decisão que mais importa, porque base64 empacota bytes, e uma string é só uma string até você escolher a sua forma em bytes. UTF-8 é o padrão sensato e a resposta certa para quase tudo, mas no momento em que os seus dados vêm de um sistema legado, de um protocolo binário ou de um canto do Unicode, a escolha para de ser invisível:
import Foundation
let phrase = "héllo"
print(phrase.data(using: .utf8)?.count ?? -1) // 6
print(phrase.data(using: .ascii) == nil) // true
print(phrase.data(using: .utf16)?.count ?? -1) // 12
print(phrase.data(using: .utf16LittleEndian)?.count ?? -1) // 10
print(phrase.data(using: .utf8)!.base64EncodedString())
// aMOpbGxv
print(phrase.data(using: .utf16LittleEndian)!.base64EncodedString())
// aADpAGwAbABvAA==
| Conversão | Bytes de "héllo" | O que o base64 carrega |
|---|---|---|
.utf8 |
6 | aMOpbGxv, a grafia que as APIs modernas esperam |
.ascii |
falha com nil |
o caractere acentuado fica acima de 0x7F e o ASCII se recusa |
.utf16 |
12 | o dobro do tamanho do UTF-8, mais uma marca de ordem de bytes de dois bytes viajando na frente |
.utf16LittleEndian |
10 | a mesma palavra sem a tag BOM: 10 bytes, ainda assim a mais pesada entre as opções sem BOM listadas aqui |
Três lições se escondem nessa saída. A forma data(using:) é falível e .ascii é um candidato número um a falhar, então fazer force-unwrap dela é assim que uma frase perfeitamente boa vira um app com crash. A conversão simples .utf16 acrescenta na frente uma marca de ordem de bytes de dois bytes (FF FE numa máquina little-endian), e esse BOM viaja para a sua saída empacotada e confunde qualquer decodificador que não o esperava. E a conta de tamanho não perdoa: uma escolha descuidada de conjunto de caracteres te custa a sobretaxa base64 sobre o dobro dos dados, então a pergunta nunca é "isso vai codificar?" mas "o que a outra ponta vai esperar encontrar quando desempacotar?". A regra de ouro: as duas pontas da viagem devem concordar sobre a forma em bytes antes do base64 começar, porque o decodificador não tem como adivinhar o que você escolheu e não vai perguntar.
Quebra de linha: dois hábitos, um parâmetro
As opções do método são todas sobre quebra de linha, e todas existem porque dois formatos do século XX não conseguiam concordar sobre o quão longa uma linha de letras deveria ser. O MIME, o padrão de e-mail de 1996, quebra o base64 a cada 76 caracteres com CRLF no fim de cada linha. O PEM, a linhagem do Privacy-Enhanced Mail de 1987, quebra a cada 64 caracteres, e essa é a forma que você encontra dentro de certificados e chaves, os blocos -----BEGIN CERTIFICATE----- que os seus servidores guardam num diretório de configuração.
import Foundation
let certBytes = Data((0..<300).map { UInt8($0 % 256) })
let raw = certBytes.base64EncodedString()
let pemStyle = certBytes.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed])
let mimeStyle = certBytes.base64EncodedString(options: [.lineLength76Characters,
.endLineWithCarriageReturn, .endLineWithLineFeed])
print(raw.count) // 400 caracteres numa linha só
print(pemStyle.components(separatedBy: "\n").count) // 7 linhas de no máximo 64
print(mimeStyle.components(separatedBy: "\r\n").count) // 6 linhas de no máximo 76
| Opção | Trabalho | Atenção |
|---|---|---|
.lineLength64Characters |
corta a linha depois de 64 caracteres, o hábito do PEM | o fim de linha é CRLF, a menos que você diga o contrário |
.lineLength76Characters |
corta a linha depois de 76 caracteres, o hábito do MIME | mesmo padrão de CRLF |
.endLineWithCarriageReturn |
inclui um carriage return no fim da linha | sozinha, isso é só CR, estilo Mac antigo, e raramente é o que você quer |
.endLineWithLineFeed |
inclui um line feed no fim da linha | passe as duas opções quando quiser CRLF |
Agora o padrão que surpreende as pessoas: peça qualquer opção .lineLength sem escolher um fim de linha, e o fim de linha que você recebe é CRLF, o par completo de carriage-return-mais-line-feed. O método tem um estilo da casa, e o estilo da casa é 1996. Quer só LF? Pague por ele explicitamente com .endLineWithLineFeed e nada mais. Mais uma regra da casa para o registro: a última linha nunca ganha um fim de linha no final. Um resultado quebrado termina com o seu último caractere de dados ou com os seus pads =, não importa quais opções você escolheu, então você pode concatenar e colar sem uma linha em branco órfã no fim. E sem nenhuma opção, a saída é uma única linha ininterrupta, que é a forma certa para corpos JSON, URLs e payloads de API: o trabalho que um app Swift moderno realmente faz a maior parte do tempo.
Base64url: uma string que pode viajar
O alfabeto padrão é um bom cidadão do JSON e um cidadão terrível de uma URL. Numa query string, + é lido como espaço pela análise de formulário, / é um separador de caminho e = separa chaves de valores, que é por isso que fazer percent-encoding do alfabeto padrão o deixa mais longo e mais feio em vez de mais curto. A seção 5 do RFC 4648 existe para corrigir exatamente isso: o "Alfabeto Seguro para URL e Nome de Arquivo", onde + vira -, / vira _ e o padding de = é normalmente descartado, porque um pad numa URL tipicamente vira %3D, o que derruba o objetivo. O RFC acrescenta um aviso digno de moldura: esta codificação "não deve ser considerada a mesma que a codificação base64". IDs de vídeo do YouTube, JWTs e a maioria dos identificadores de API modernos falam ele, então espere usá-lo.
import Foundation
extension Data {
var base64URLEncoded: String {
base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
let tricky = Data("The + / and = trio goes home.".utf8)
print(tricky.base64EncodedString())
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4=
print(tricky.base64URLEncoded)
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4
Olhe para essa saída de perto: este payload em particular, por acaso, não produziu nem um + nem um /, então as duas grafias diferem só pelo padding descartado. Mude um byte e elas vão divergir no alfabeto, que é o ponto inteiro. Duas regras de engajamento. Escolha o dialeto uma vez, na fronteira onde os seus dados encontram o mundo exterior, e nunca misture alfabetos dentro do mesmo documento: um decodificador padrão que recebe base64url (ou vice-versa) vai rejeitar a entrada ou, em modos tolerantes, apagar os caracteres estrangeiros e te entregar bytes errados. E dê um nome honesto ao seu helper, para o próximo desenvolvedor saber que a string é base64url e não um erro de digitação. A mesma extensão pode ficar mais curta em toolchains mais novas: os betas mais novos dos SDKs agora incluem uma opção nativa .base64URLAlphabet que faz a troca de alfabeto dentro do framework, com uma opção correspondente .omitPaddingCharacter, e o Foundation de código aberto traz as mesmas opções atrás de um marcador de disponibilidade para toolchains posteriores. Até elas chegarem ao seu alvo mínimo de deploy, a extensão de quatro linhas é a resposta portátil, e continuará funcionando em todas as plataformas por construção.
JSON e APIs: o base64 que você nunca pediu
Esse aqui é o que mais surpreende entre as pessoas que trabalham com Codable, então ganha a sua própria seção: a estratégia padrão do JSONEncoder para uma propriedade Data já é base64. Se um struct Codable tem um campo Data, o codificador o empacota com base64 padrão automaticamente, e o JSONDecoder o desempacota automaticamente no caminho de volta. Sem opção, sem configuração, sem cerimônia.
import Foundation
struct Snapshot: Codable {
let name: String
let icon: Data
}
let snap = Snapshot(name: "cat", icon: Data("🐱".utf8))
let json = try JSONEncoder().encode(snap)
print(String(decoding: json, as: UTF8.self))
// o ícone cruzou a rede como "8J+QsQ=="
A propriedade icon cruzou a rede como 8J+QsQ== porque esse é o estilo da casa. Existem alternativas, e as duas que você vai de fato encontrar são .custom, que te entrega os dados e um codificador e deixa você decidir a representação, e o mais novo .deferredToData, que delega para a própria instância de dados. No momento em que uma API quer base64url em vez do padrão, .custom é onde a sua extensão da seção anterior se encaixa:
import Foundation
extension Data {
var base64URLEncoded: String {
base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
struct Snapshot: Codable {
let name: String
let icon: Data
}
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .custom { data, enc in
var container = enc.singleValueContainer()
try container.encode(data.base64URLEncoded)
}
let json = try encoder.encode(Snapshot(name: "cat", icon: Data("🐱".utf8)))
print(String(decoding: json, as: UTF8.self))
// o ícone cruzou a rede como "8J-QsQ"
Um aviso que separa um recurso que funciona de um incidente de produção: uma string JSON não pode conter uma quebra de linha crua. Se você quebrar um payload com uma opção .lineLength e interpolar o resultado num documento JSON sem escapar, você não fez nenhum valor JSON; você fez um erro de sintaxe com um sotaque base64, e o parser vai provar. Saída quebrada pertence a corpos de e-mail e a arquivos de certificado. Tudo que vive dentro de JSON, URLs ou query strings ganha a string simples não quebrada.
Data URIs: a imagem dentro de uma string
O truque favorito da web é embutir os bytes de um arquivo diretamente numa URL: data:{mime};base64,{payload}. Montar um no Swift é uma leitura, uma codificação e uma concatenação de strings:
import Foundation
let gif = Data(base64Encoded: "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")!
let uri = "data:image/gif;base64," + gif.base64EncodedString()
print(uri.hasPrefix("data:image/gif;base64,R0lGODlh")) // true
print(gif.count) // 42
O exemplo reconstrói o famoso GIF transparente de 42 bytes (existem menores que não são transparentes, mas este é o que todo mundo embute) num data URI que um navegador vai renderizar sem um segundo request. Nas plataformas da Apple a direção inversa é uma linha só: o mesmo Data que você empacotou alimenta diretamente o UIImage(data:) ou o NSImage(data:). O trade-off é o tamanho, e ele se acumula: uma imagem de 100 quilobytes vira uma string de mais de 133.000 caracteres antes mesmo de você adicionar o prefixo data:image/png;base64,. Data URIs brilham para ícones, avatares e assets minúsculos, e incham a largura de banda em silêncio para fotos de destaque, então guarde-os para as coisas pequenas.
JWTs: selando as duas primeiras partes
O lado da codificação de um JSON Web Token é dois selamentos mais uma assinatura, e o selamento é a sua extensão base64url com o padding descartado, que é exatamente o que o formato pede. O header e o payload são documentos JSON, e as duas partes recebem o mesmo tratamento:
import Foundation
extension Data {
var base64URLEncoded: String {
base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
func seal(_ text: String) -> String {
Data(text.utf8).base64URLEncoded
}
let header = seal(#"{"alg":"HS256","typ":"JWT"}"#)
let claims = seal(#"{"sub":"42","role":"editor"}"#)
print("\(header).\(claims).signature-here")
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJlZGl0b3IifQ.signature-here
Duas lembranças. A terceira parte separada por ponto é uma assinatura criptográfica calculada sobre as duas primeiras, e é a única parte do token que dá alguma garantia: o header e os claims são JSON puro vestindo um sobretudo, então segredos nunca moram neles. E note como o padding desaparece no seal(): decodificadores JWT na outra ponta (incluindo o do artigo irmão) o colocam de volta com um complemento por módulo, então as duas direções da viagem se encontram em terreno comum.
Headers HTTP: Basic e o resto
O antigo header Authorization: Basic quer um nome de usuário e uma senha, unidos por dois-pontos, empacotados com base64 padrão, porque num header + e / são inofensivos e a pergunta do dialeto nem surge:
import Foundation
let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==
A mesma nota de rodapé em alto e bom som como em todo lugar: o empacotamento não fornece nenhuma segurança por si só, e o header é seguro apenas na medida da conexão HTTPS que o carrega. O primo moderno, Authorization: Bearer, carrega um JWT em vez disso, então a receita de selamento da seção de JWT é o que vai para a rede ali. O único lugar onde a pergunta do dialeto surge de verdade no HTTP é a query string: se a sua API deixa um identificador viajar numa URL, esse identificador deve ser base64url, ou no mínimo base64 padrão percent-encoded, nunca o alfabeto padrão cru com o seu + deixado para ser lido como espaço.
Anexos de e-mail: o contrato dos 76 caracteres
Quando o seu app produz um anexo que precisa sobreviver às origens de 7 bits do SMTP, o contrato é o do MIME: base64 quebrado a cada 76 caracteres com fins de linha CRLF, e um header Content-Transfer-Encoding: base64 dizendo ao receptor o que esperar. A seção de opções já mostrou a grafia; aqui está a forma completa de um corpo quebrado:
import Foundation
let attachment = Data((0..<400).map { UInt8(65 + $0 % 26) })
let body = attachment.base64EncodedString(options: [.lineLength76Characters,
.endLineWithCarriageReturn, .endLineWithLineFeed])
let lines = body.components(separatedBy: "\r\n")
print(lines.count) // 8 linhas
print(lines.map { $0.count }.max() ?? 0) // 76, a mais longa
print(body.hasSuffix("\r\n")) // false, a última linha fica limpa
A conta de tamanho desse dialeto é a famosa: o imposto de alfabeto 4/3 mais uma quebra de linha a cada 76 caracteres chega perto de 137 por cento do original, e o atalho antigo da engenharia de e-mail "multiplique o original por 1.37 e some mais ou menos 800 bytes de headers" ainda funciona para estimar tamanhos de anexo num cliente de e-mail. É folclore com aritmética correta, e é o único lugar neste artigo onde a sobretaxa de 33 por cento ganha um segundo número depois da vírgula.
Configuração, ambiente e bancos de dados: escondendo o sublinhado
Existe uma classe silenciosa de trabalhos onde a única virtude do base64 é que a sua saída é um conjunto de caracteres pequeno e previsível: esconder um blob binário ou um valor estruturado num lugar que quer texto simples. Variáveis de ambiente que precisam sobreviver a um arquivo de configuração de shell, colunas num banco de dados que é mais feliz com varchar do que com blob, um arquivo LDAP com a sua marca base64, um código QR que lê letras com mais confiabilidade do que bits. O padrão é o mesmo em todo lugar: decida os bytes, codifique, guarde a string, decodifique na outra ponta.
import Foundation
struct FeatureFlags: Codable {
var betaToolbar: Bool
var maxRetries: Int
}
do {
let flags = FeatureFlags(betaToolbar: true, maxRetries: 5)
let json = try JSONEncoder().encode(flags)
let storable = json.base64EncodedString()
print(storable)
guard let packed = Data(base64Encoded: storable) else {
print("decode failed, that is odd")
exit(1)
}
let restored = try JSONDecoder().decode(FeatureFlags.self, from: packed)
print(restored.betaToolbar, restored.maxRetries)
} catch {
print(error)
}
Duas armadilhas moram aqui. A primeira é o embrulho duplo: duas camadas de integração que "ajudam" codificando, então o valor que você guarda é base64 de base64, e o leitor que decodifica uma vez recebe uma muralha de letras e acha que o recurso está quebrado. Codifique exatamente uma vez, numa fronteira exata, e diga isso num comentário. A segunda é a deriva de dialeto por ambiente: se o valor um dia viajar por uma URL, um campo de formulário ou um shell que amassa + e /, guarde a grafia base64url em vez disso, porque o conjunto de caracteres é o ponto inteiro do formato.
Arquivos: a ida-e-volta do .b64
O trabalho "transformar este arquivo num arquivo de texto .b64" é uma leitura, uma chamada e uma escrita:
import Foundation
let source = URL(fileURLWithPath: "photos/cat.png")
let archive = URL(fileURLWithPath: "photos/cat.b64")
let bytes = try Data(contentsOf: source)
try Data(bytes.base64EncodedString().utf8).write(to: archive)
// depois, possivelmente em outro processo
let packed = try String(contentsOf: archive, encoding: .utf8)
let restored = Data(base64Encoded:
packed.trimmingCharacters(in: .whitespacesAndNewlines))
if let restored = restored {
try restored.write(to: URL(fileURLWithPath: "photos/cat-copy.png"))
} else {
print("the .b64 file was not base64 after all")
}
O trimmingCharacters na viagem de volta está ali porque o que quer que tenha escrito o arquivo pode ter adicionado um fim de linha, e o decodificador estrito trata um newline final como um veredito de nil. Essa ida-e-volta volta byte a byte, o que você deve verificar na primeira vez que colocar em produção. Para arquivos grandes o suficiente para tornar o uso de memória interessante, não codifique o buffer inteiro de uma vez. O Base64 tem uma propriedade adorável que torna o stream exato: a cada três bytes de entrada são produzidos quatro caracteres de saída independentes, então, desde que cada bloco que você codifica seja um múltiplo de três bytes, a saída concatenada é idêntica a codificar o arquivo inteiro de uma vez. Quebre o alinhamento e a saída muda, porque uma fronteira de bloco corta um grupo de três bytes no meio do stream:
import Foundation
func streamEncode(_ input: InputStream, output: OutputStream, lineLength: Int = 76) throws {
input.open()
output.open()
defer { input.close(); output.close() }
var buffer = [UInt8](repeating: 0, count: 65_536)
var pending = [UInt8]()
var line = ""
var lineCount = 0
func addText(_ text: String) {
line += text
while line.count > lineLength {
if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
_ = output.write(Array(String(line.prefix(lineLength)).utf8), maxLength: lineLength)
line = String(line.dropFirst(lineLength))
lineCount += 1
}
}
func flushGroup(_ group: [UInt8]) {
addText(Data(group).base64EncodedString())
}
while input.hasBytesAvailable {
let n = input.read(&buffer, maxLength: buffer.count)
if n < 0 { throw CocoaError(.fileReadUnknown) }
if n == 0 { break }
pending.append(contentsOf: buffer[0..<n])
let groups = pending.count / 3
if groups > 0 {
flushGroup(Array(pending[0..<(groups * 3)]))
pending.removeFirst(groups * 3)
}
}
if !pending.isEmpty {
flushGroup(pending)
}
if !line.isEmpty {
if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
_ = output.write(Array(line.utf8), maxLength: line.utf8.count)
}
}
A memória de pico é um buffer de leitura mais a linha atual, não importa o tamanho do arquivo, e a saída quebrada casa exatamente com a grafia de uma vez só do .lineLength76Characters. A mesma regra de múltiplo de três, com os papéis invertidos, é a que o decodificador em stream do artigo irmão usa como base, então os dois lados da viagem compartilham uma única verdade aritmética.
Payloads grandes e a conta de memória
Vamos fazer a aritmética que você vai precisar da próxima vez que alguém perguntar "dá para base64 isso?". A cada três bytes de entrada correspondem quatro caracteres de saída, então o tamanho multiplica por 4/3: um arquivo de 100 quilobytes vira uma string de 133.336 caracteres, um arquivo de 10 megabytes vira 13.333.336 caracteres, e assim por diante. O padding acrescenta no máximo dois caracteres no finalzinho, um erro de arredondamento para qualquer coisa maior que alguns bytes, e a entrada vazia é a única isenção, onde o fisco concede uma única passagem gratuita e o resultado é a string vazia. Três consequências práticas. Primeira, faça o orçamento antes de começar: se o seu payload já está perto de um limite (a zona de conforto de mais ou menos 2.000 caracteres de uma URL, o contrato de um campo JSON, a largura de uma coluna de banco de dados), divida o limite por 1.33 antes de codificar, não depois (e por 1.37 quando há quebra de linha envolvida). Segunda, enquanto você empacota, segura os bytes originais e a string empacotada ao mesmo tempo, então o conjunto de trabalho é mais ou menos 2.33 vezes o original, e as funções em stream acima são a fuga quando esse número para de ser confortável. Terceira, o imposto é de mão única na prática: você paga quando empacota e os seus bytes voltam para casa quando alguém desempacota, então a pergunta de verdade nunca é "base64 é caro?" mas "a estrada só de texto em que estou exige isso?".
Os erros que mordem
- O passo falível do conjunto de caracteres.
String.data(using:)pode respondernil(tente.asciicom um caractere acentuado), e fazer force-unwrap dela é a atualização clássica de uma entrada ruim para um app com crash. Proteja a conversão, não só a chamada base64, que é a parte fácil. - O estilo da casa CRLF. Uma opção
.lineLengthsem uma opção de fim de linha produz CRLF por padrão. Se o seu formato quer só LF e você esqueceu a opção, a sua saída carrega carriage returns que nunca deveria ter. - A armadilha de só CR.
.endLineWithCarriageReturnsozinho produz fins de linha só de CR, estilo Mac antigo. Se você quis CRLF (e para MIME você quis), passe as duas opções de fim de linha. - Quebra de linha dentro de JSON. Uma quebra de linha crua dentro de uma string JSON é JSON inválido, ponto final. Base64 quebrado interpolado num documento é um erro de sintaxe com um sotaque base64. Mantenha saída quebrada em corpos de e-mail e a arquivos de certificado.
- O carona BOM. A conversão simples
.utf16acrescenta na frente um BOM de dois bytes que viaja para a sua saída empacotada e confunde decodificadores que não o esperavam. Use.utf16LittleEndianou.utf16BigEndianquando precisar de UTF-16 sem a tag. - A deriva de dialeto. Padrão e base64url são alfabetos diferentes, e o RFC diz isso por escrito. Um
+que sobrevive até uma query string vira um espaço; um-que chega a um decodificador padrão tolerante é apagado. Escolha o dialeto na fronteira e mantenha-o. - Caixa é letra. O alfabeto distingue
Adea. Um copiar-e-colar com caixa unificada ou uma chamada entusiasmada de maiúsculas corrompe os dados em silêncio, porque as duas versões ainda passam em toda checagem de alfabeto. Base64 é sensível a maiúsculas e minúsculas do jeito que um número de passaporte é. - A regra de alinhamento. Codificadores em stream devem cortar blocos em múltiplos de três bytes. Um bloco desalinhado muda a saída, e a mudança é silenciosa: a string ainda decodifica, para os dados errados.
- O embrulho duplo. Duas camadas que codificam produzem base64 de base64. O leitor que decodifica uma vez vê letras onde bytes deveriam estar, e o incidente se escreve sozinho.
- O muro de disponibilidade. As novas opções nativas (
.base64URLAlphabet,.omitPaddingCharacter) existem nos betas mais novos dos SDKs e no Foundation de código aberto atrás de um marcador de disponibilidade, mas não em toda toolchain que a sua CI vai tocar. Se você adotá-las, proteja com checagens de disponibilidade para o mesmo código compilar no Xcode mais antigo e no Linux. Na toolchain estável atual, a extensão de quatro linhas compila em todo lugar onde as opções não compilam. - Base64 não é criptografia. Se o requisito é confidencialidade, você escolheu a ferramenta errada por uma categoria inteira. O trabalho do base64 é fazer bytes viajarem, e ele faz exatamente esse trabalho, nem mais.
Como colocar no ar
- Codifique bytes, não desejos. Decida a forma em bytes antes de chamar o método, UTF-8 por padrão e nomeado explicitamente quando não for, e proteja o passo falível
data(using:), porque é ali que dados realmente somem. - Sem quebra por padrão, quebrado por contrato. A saída simples de uma linha só é a certa para JSON, APIs e a maioria dos bancos de dados; vá de opções de quebra 64/76 só quando o formato receptor exigir, e pague pelas duas opções de fim de linha quando quiser CRLF.
- Um dialeto por fronteira. Base64 padrão para destinos centrados em texto, base64url para qualquer coisa que vai tocar uma URL ou um nome de arquivo, nunca os dois no mesmo documento. Escreva a conversão uma vez, dê um nome honesto e reutilize.
- Orce a sobretaxa. Multiplique por 4/3 antes de começar (por 1.37 quando há quebra de linha em jogo), e faça stream com blocos alinhados a 3 bytes quando o payload for grande o suficiente para deixar o conjunto de trabalho desconfortável.
- Não use fita adesiva como fechadura. Se o requisito é segredo, pare na prateleira do base64 e pegue criptografia em vez disso.
Uma breve história do empacotamento
O alfabeto com que você empacota e os comprimentos de linha aos quais você quebra são fósseis de quatro décadas de argumentos sobre quanto binário consegue sobreviver a uma estrada só de texto, e a posição do Swift nessa história é curta mas interessante:
- Anos 1980, a era da mesma máquina. Os primeiros codificadores dessa família existiam para mover arquivos por dial-up entre sistemas que assumiam que a outra ponta era uma máquina igual à deles. O uuencode no UNIX usava letras maiúsculas, dígitos e pontuação, e seus designers encontraram um truque que economizava poder de computação: o alfabeto ocupa posições ASCII consecutivas, então codificar era literalmente "somar 32", sem tabela de consulta. O BinHex, o primo que nasceu no TRS-80 em 1981, pulou para o Apple II, virou o formato do Macintosh clássico em 1984 e fez uma aposta diferente: seus 64 caracteres omitem
7,O,W,g,oe quase metade das minúsculas. - 1987, o alfabeto ganha um endereço. O RFC 989, a primeira especificação do Privacy-Enhanced Mail, padronizou os exatos 64 caracteres que você digita hoje, quebrou a saída a 64 caracteres por linha e usou
=para padding e*para marcar dados codificados-mas-não-criptografados. Todo bloco no estilo PEM que você já colou numa configuração de servidor é descendente deste documento. - 1996, a era liberal. O MIME (RFC 2045) levou o alfabeto para anexos de e-mail e moveu a quebra para 76 caracteres, adicionando a regra que tornou a quebra segura para produzir: decodificadores devem ignorar as quebras de linha. Codificadores aprenderam a quebrar; decodificadores aprenderam a perdoar. A opção de 76 caracteres do Swift é uma lembrança viva de exatamente esse argumento.
- 2003 a 2006, as regras endurecem. O RFC 3548 (2003) declarou que o padding não pode ser pulado (a menos que um formato diga o contrário) e que decodificadores devem rejeitar caracteres fora do alfabeto; o RFC 4648 (outubro de 2006) resolveu a família e adicionou o alfabeto seguro para URL, explicitamente para que identificadores longos pudessem morar em URLs sem percent-escapar cada caractere especial. A convenção de "sem padding no dialeto de URL" nasceu no mesmo documento, porque um caractere de pad numa URL tipicamente vira
%3D, o que derruba o objetivo. - 2013 a 2014, a API já está aqui. A classe
NSDatada Apple empacotava base64 há anos, e a API baseada em opções com as quatro opções de quebra chegou no iOS 7, em 2013, antes do Swift existir de qualquer forma. Quando o Swift 1.0 chegou em 9 de setembro de 2014, ele herdou um codificador total com quatro opções de quebra e um alfabeto de 64 letras de 1987, e a personalidade não mudou desde então. - 3 de dezembro de 2015, a toolchain deixa o prédio. O Swift foi liberado como open-source nesse dia, e o base64 do Foundation cruzou para o Linux e, depois, para o Windows com ele. "Codificação de base64 em Swift longe de uma máquina da Apple" não tem nem uma década: um convidado muito jovem numa festa que começou em 1987.
- 2023 a 2026, a reescrita e o dialeto de URL. 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, o resto da família amadurece no Foundation de código aberto atrás de marcadores de disponibilidade, e a extensão da comunidade continua sendo a ponte portátil no intervalo.
Pequenos encantos
- Um megabyte empacota para exatamente 1.333.336 caracteres base64, o imposto de 4/3 mais dois caracteres de padding, até o dígito. A única entrada que escapa do imposto por completo é a vazia: nada entra, nada sai.
- O codificador é total de um jeito que o decodificador não é. Ele nunca retorna nil, nunca lança, nunca recusa. A única falha em todo o pipeline mora a montante, no passo do conjunto de caracteres, que é por isso que o método se sente muito mais calmo que o primo dele.
- Codifique a palavra
hélloem UTF-8 e ela viraaMOpbGxv; codifique em UTF-16 little-endian e ela viraaADpAGwAbABvAA==. Mesma palavra, dois passaportes diferentes, ambos válidos, nenhum intercambiável. - A palavra de teste do mundo base64 é
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 é 42 bytes e começa com a palavra mágica
GIF89a, que é por isso que o prefixoR0lGODlhaparece em mais codebases na Terra do que quase qualquer outra string base64. - O seu struct
Codableprovavelmente está enviando base64 há anos sem você perceber: a estratégia padrão deDatadoJSONEncoderempacota com base64 padrão, que é por isso que um campoDatacruza a rede como uma string com padding em vez de um array de números. - O padding nunca excede dois caracteres, nunca. Um payload de 1 byte termina em
==, um payload de 2 bytes termina em=, e um payload de 3 bytes termina em nada. A gramática inteira do grupo final cabe na unha. - O Swift é 27 anos mais novo que o alfabeto com que ele empacota. A linguagem saiu em 2014; as 64 letras foram padronizadas em 1987 e não mudaram desde então.
Essa é a caixa de ferramentas completa de empacotamento: um método total, um passo falível que vem antes dele, quatro opções de quebra de linha com um estilo da casa CRLF, uma extensão base64url de quatro linhas, uma regra de alinhamento de 3 bytes para stream e uma sobretaxa de 4/3 que é a taxa de entrada da estrada só de texto. Codificação é onde você paga a conta do base64, e agora você conhece cada item do extrato antes de assinar. No momento em que você inverte a viagem e começa a abrir o que outras pessoas empacotaram, os retornos nil, os vereditos de espaços em branco e o ponto cego do botão tolerante assumem o palco. O artigo de decodificação relacionado faz o show completo nessa metade da ida-e-volta, então quando as letras começarem a chegar, você já vai saber exatamente como abri-las.
Última atualização: 2026-09-08
Artigo relacionado: Decodificação Base64 em Swift: um guia completo