Ir para o conteúdo
AZ Tools

Decodificador BSON e visualizador de Extended JSON

BSON é a forma binária que o MongoDB armazena e fala: uma contagem de bytes int32 em little-endian, uma sequência de elementos — cada um com um código de tipo, um nome de campo terminado em NUL e um valor cujo comprimento o tipo decide — e um único byte zero para fechar. Este decodificador lê esses bytes diretamente no seu navegador e mostra o documento de três formas ao mesmo tempo: uma árvore de campos que nomeia o tipo de cada elemento junto com o seu byte de tipo e diz quanto ele custa em bytes, Extended JSON canônico e Extended JSON relaxado. Os vinte e um tipos de elemento que a especificação define são todos tratados, inclusive os obsoletos que só aparecem em dados antigos: undefined (0x06), DBPointer (0x0C), symbol (0x0E) e o subtipo binário 0x02, que carrega um segundo comprimento dentro da própria carga e entrega quatro bytes de lixo a qualquer leitor que o esqueça. Os números são onde um decodificador dentro do navegador costuma errar. int64, a data UTC e o timestamp interno do MongoDB são de 64 bits, e um número JavaScript só guarda inteiros exatos até 9.007.199.254.740.991, então 9223372036854775807 vira silenciosamente 9223372036854775808 assim que passa por um; aqui os três são lidos e impressos a partir de um BigInt. O decimal128 é decodificado do próprio coeficiente de 128 bits e do expoente enviesado direto para uma cadeia decimal exata, sem nunca ser aproximado por um double — que é exatamente o motivo pelo qual o tipo existe para dinheiro. A árvore ainda abre um ObjectId nas três partes que ele de fato tem — quatro bytes de data de criação, cinco aleatórios e um contador de três — e mostra uma data UTC tanto como a contagem de milissegundos com sinal quanto como data, de modo que os números negativos das datas anteriores a 1970 deixam de surpreender. O que ele não faz é adivinhar: um documento cujo comprimento declarado não bate com o arquivo, um que nunca alcança o terminador, um com um byte de tipo que a especificação não define, ou um arquivo JSON entregue por engano, é recusado com nome e deslocamento em bytes em vez de virar meia árvore plausível.

Como usar

  1. Solte um arquivo .bson na caixa: a saída do mongodump, um documento avulso salvo por um driver ou qualquer coisa escrita por uma ferramenta do MongoDB.
  2. Leia a árvore. Cada linha é um elemento com o nome do tipo BSON, o byte de tipo como ele trafega, o valor e os bytes que custa, de modo que um documento grande demais se explica sozinho.
  3. Use o Extended JSON canônico quando os tipos precisarem sobreviver — $numberInt e $numberLong não são a mesma coisa — e o relaxado quando você só quiser ler os dados.
  4. Veja a linha extra sob um ObjectId para a data de criação, os bytes aleatórios e o contador, e a de uma data UTC para a data por trás da contagem de milissegundos.
  5. Se o arquivo for recusado, repare em qual erro é: um comprimento que não bate, um terminador ausente e um byte de tipo indefinido são três falhas diferentes, e o deslocamento diz onde olhar.

Perguntas frequentes

Por que um array em BSON ocupa muito mais que o mesmo array em JSON?
Porque BSON não tem um tipo array próprio. Um array é guardado como um documento comum cujos nomes de campo são os índices decimais "0", "1", "2" e assim por diante, cada um uma cadeia real terminada em NUL. Mil inteiros de 32 bits custam então um byte de tipo, um nome de índice de um a três caracteres, um terminador e quatro bytes de dado cada: cerca de 8,9 KB onde os valores sozinhos são 4 KB. É também por isso que as chaves de índice podem estar erradas sem ninguém notar: os drivers leem arrays por posição, então um com chaves "0", "2", "x" decodifica em três elementos e só se comporta mal quando algo o reserializa. Esta ferramenta sinaliza esse caso em vez de disfarçá-lo.
Qual é a diferença entre o Extended JSON canônico e o relaxado?
Extended JSON é a forma padrão de escrever BSON como texto, e existe em duas variantes porque servem a duas tarefas diferentes. O canônico envolve cada valor em um marcador de tipo — {"$numberInt": "1"} não é {"$numberLong": "1"} nem {"$numberDouble": "1.0"} — de modo que um documento pode sair para texto e voltar exatamente com os mesmos bytes. O relaxado tira o invólucro sempre que o JSON simples consegue segurar o valor: números viram números e uma data dentro da faixa do ISO 8601 vira um instante legível. O relaxado lê-se muito melhor e é o que o mongoexport escreve por padrão, mas perde informação: quando um 1 é só um 1, nada diz de qual dos três tipos numéricos ele veio.
Quais valores do BSON o JavaScript não consegue representar, e o que se faz com eles aqui?
Três. int64 e a data UTC são inteiros de 64 bits com sinal, e o timestamp interno são duas metades de 32 bits sem sinal em uma palavra de 64, enquanto um número JavaScript é um double e só guarda inteiros exatos até 9.007.199.254.740.991. Qualquer valor maior é arredondado na entrada, e é assim que 9223372036854775807 vira 9223372036854775808 em tantos visualizadores. Este decodificador lê os três com BigInt e imprime os dígitos a partir do BigInt, de modo que nada passa por um double. Com decimal128 é ainda pior: é um tipo decimal justamente para que 0,1 e 2,675 continuem exatos, e lê-lo em um double anula o motivo de tê-lo escolhido. Aqui ele é decodificado do coeficiente de 128 bits e do expoente enviesado direto para uma cadeia decimal.
Por que uma data anterior a 1970 aparece como número negativo?
Porque é exatamente o que o BSON guarda. O tipo data UTC são milissegundos int64 desde a época Unix, e o inteiro tem sinal, então qualquer instante anterior a 1970-01-01 é uma contagem negativa: uma data de nascimento de 1953 fica perto de −526.608.000.000. É legal e todos os drivers devolvem isso intacto, mas um número surpreendente de ferramentas trata o campo como sem sinal ou o corta em zero, e é assim que uma data de 1953 acaba em 1970 ou salta para o ano 292 milhões. A faixa que o tipo aceita é muito mais larga que o calendário que a maioria dos softwares suporta, por isso aqui a contagem de milissegundos aparece ao lado da data e fica dito com clareza quando um valor sai do que uma data de calendário consegue expressar.
O que é o subtipo binário 0x02 e por que o chamam de armadilha de parser?
É o arranjo binário original, hoje obsoleto. Todo campo binário é escrito como um comprimento int32, um byte de subtipo e então a carga — exceto o subtipo 0x02, cuja carga começa com um segundo int32 repetindo o comprimento, quatro a menos que o primeiro. Um leitor que o trate como qualquer outro subtipo devolve um blob com quatro bytes extras colados na frente, e como esses bytes parecem dados binários plausíveis ninguém reclama; a corrupção só aparece quando alguém tenta usar o valor. Coleções antigas da era anterior à 2.0 ainda os contêm, então um decodificador que diz ler BSON precisa tratar esse caso à parte. Este trata, e recusa o campo por inteiro quando o comprimento interno não concorda com o externo.
O que existe dentro de um ObjectId? É um UUID?
Não: são 12 bytes com estrutura, e conhecer essa estrutura costuma ser o motivo de olhar para um. Os quatro primeiros bytes são um timestamp Unix em segundos, big-endian, e é por isso que ordenar por _id ordena mais ou menos por data de criação e dá para filtrar por data sem um campo separado. Os cinco seguintes são aleatórios por processo, e os três últimos um contador que começa em um valor aleatório e aumenta a cada documento. Versões antigas do MongoDB punham no meio um identificador de máquina e um id de processo, o que vazava um pouco sobre o servidor; isso mudou na 3.4. Não há soma de verificação nem campo de versão, então qualquer cadeia de 24 caracteres hexadecimais é um ObjectId sintaticamente válido, e só o timestamp embutido diz se ele é plausível.

Ferramentas relacionadas