ADRIANOLAUREANO← Artigos

Como um computador sabe qual programa deve abrir um arquivo?

Descubra como sistemas operacionais identificam arquivos, por que extensões não são suficientes e como criar seu próprio formato de arquivo do zero em Rust.

foto.png
89504E470D0A1A0A
visualizador
O nome sugere. Os bytes confirmam. O programa interpreta.
4 bytesassinatura SLAI
16 bytescabeçalho
8 módulosformato, parser, CLI e viewer
1 formatocriado do zero

01 / O PRIMEIRO DUPLO CLIQUE

Você clica em uma foto. Em menos de um segundo, várias decisões acontecem.

Como o computador sabe abrir um PNG? Como reconhece um PDF? A extensão realmente importa? O que acontece entre o segundo clique e a janela aparecer? A resposta rápida costuma ser: “ele olha o final do nome”. Essa resposta ajuda, mas é incompleta. Ela mistura duas perguntas diferentes.

PERGUNTA 1

Quem deve receber o arquivo?

É uma decisão do sistema operacional baseada em associações e preferências.

PERGUNTA 2

O que existe dentro dele?

É uma decisão do programa baseada nos bytes e na estrutura do formato.

Imagine foto.png. O gerenciador de arquivos percebe .png, consulta qual aplicativo está associado a esse sufixo e inicia o visualizador. Em seguida, o visualizador abre o arquivo, lê seus primeiros bytes, confere se parecem um PNG e tenta interpretar seus blocos. A extensão escolheu o candidato; o conteúdo provou — ou desmentiu — a hipótese.

A IDEIA CENTRAL

O sistema escolhe um programa. O programa valida e interpreta o formato.

Essa separação parece pequena, mas explica por que um arquivo pode ter o ícone errado e ainda abrir, por que dois computadores podem escolher programas diferentes e por que confiar apenas na extensão é perigoso em um upload.

02 / A PISTA NO NOME

Uma extensão é texto depois do último ponto. Nada dentro do arquivo muda.

.png, .jpg, .exe, .pdf,.zip e .mp3 são convenções acrescentadas ao nome. Elas permitem que pessoas e programas façam uma classificação rápida. O sistema de arquivos não precisa conhecer fotografia, música ou documento. Para ele, normalmente há um nome, metadados e uma sequência de bytes.

Raio-X de um arquivo renomeado

Renomear foto.png para foto.txt troca alguns caracteres no registro do diretório. A imagem comprimida não vira parágrafos. A paleta não desaparece. Os bytes de assinatura continuam sendo os de PNG. Um editor de texto pode ser escolhido porque agora o nome sugere texto, mas receberá dados que não sabe transformar em letras legíveis.

CamadaExemploResponsabilidade
Nomefoto.pngIdentificação humana e associação
Extensão.pngPista convencional sobre o tipo
MIMEimage/pngRótulo usado por aplicações e protocolos
Conteúdo89 50 4E 47…A estrutura que o decoder realmente lê

Há nomes com vários pontos, como relatorio.final.pdf. Normalmente a parte relevante para associação é o último sufixo. Há também arquivos sem extensão, como muitos executáveis e arquivos de configuração no Linux. E há formatos cujas extensões variam: JPEG aparece como .jpg ou.jpeg. Tudo isso mostra que extensão é convenção, não lei física.

ATENÇÃO

Trocar a extensão não converte. Converter exige decodificar um formato e codificar outro.

03 / QUEM ABRE O QUÊ

Windows, Linux e macOS resolvem a mesma tarefa com catálogos diferentes.

A associação responde: “quando o usuário acionar um arquivo desse tipo, qual aplicativo devo iniciar?”. Ela não pertence ao arquivo. É uma preferência registrada no computador, muitas vezes por usuário. Por isso seu PDF pode abrir no navegador enquanto o mesmo PDF abre em outro leitor no computador de alguém.

WINDOWS

Extensões e ProgIDs

O sistema consulta associações mantidas no Registro. Uma extensão aponta para um identificador de classe; a classe descreve ícone, comandos e aplicativo. As preferências do usuário têm prioridade sobre padrões sugeridos pelo app.

LINUX

MIME e Freedesktop

Ambientes gráficos seguem especificações compartilhadas. Bancos de MIME relacionam nomes e conteúdo a tipos; arquivos .desktop descrevem aplicativos; associações definem o manipulador preferido.

MACOS

UTType e Launch Services

O macOS trabalha com identificadores de tipo que podem representar extensões, MIME types e relações entre formatos. Launch Services combina declarações dos aplicativos com a escolha do usuário.

Os nomes técnicos mudam, mas o fluxo é semelhante: observar pistas, transformar a pista em um tipo conhecido, consultar preferências, iniciar o aplicativo e entregar o caminho ou uma referência ao arquivo. O aplicativo então faz sua própria leitura.

UM DETALHE IMPORTANTE

“Abrir com” muda a associação. Não reescreve um único byte do arquivo.

MIME type: um rótulo que viaja

MIME nasceu para descrever conteúdo em mensagens, mas tornou-se linguagem comum na web e em desktops. image/png, application/pdf eaudio/mpeg têm uma categoria antes da barra e um subtipo depois. Em uma resposta HTTP, Content-Type: image/png orienta o navegador. Porém esse cabeçalho também é uma afirmação de quem enviou. O navegador pode aplicar regras de segurança, conferir o conteúdo ou se recusar a “adivinhar”.

Então qual fonte é verdadeira: extensão, MIME ou bytes? Nenhuma isoladamente resolve todos os casos. Sistemas robustos acumulam evidências e escolhem o nível de confiança adequado ao risco.

04 / O ARQUIVO SE APRESENTA

Magic bytes são pequenas assinaturas colocadas em posições previsíveis.

Quando a extensão está ausente ou errada, alguns programas ainda reconhecem o arquivo porque começam pelo conteúdo. Eles leem uma sequência curta — geralmente no offset zero — e comparam com assinaturas conhecidas. O nome “mágico” é brincalhão; não existe magia. Existe uma escolha deliberada dos autores do formato.

FormatoHexadecimalLeitura
PNG89 50 4E 47 0D 0A 1A 0Abyte especial + PNG + controles
JPEGFF D8 FFinício de imagem + marcador
GIF47 49 46 38 37 61GIF87a
ZIP50 4B 03 04PK + cabeçalho local
RAR 552 61 72 21 1A 07 01 00Rar! + controles e versão
PDF25 50 44 46 2D%PDF-
ELF7F 45 4C 46DEL + ELF
PE/EXE4D 5AMZ, cabeçalho histórico
WAV52 49 46 46 … 57 41 56 45RIFF … WAVE
MP349 44 33 ou FF Extag ID3 ou frame MPEG

Observe o PNG. 89 é maior que a faixa ASCII comum e ajuda a revelar caminhos que removem o bit mais alto. 50 4E 47 são as letrasPNG. 0D 0A representam uma quebra de linha tradicional;1A já foi usado como fim de arquivo; 0A fecha a sequência. A assinatura foi desenhada para detectar não apenas “parece PNG”, mas também algumas corrupções causadas por transporte de texto.

A assinatura PNG vista byte por byte

JPEG é diferente: seus dados são organizados em marcadores. FF D8significa Start Of Image, e o próximo FF inicia outro marcador. WAV pertence à família RIFF, por isso não basta olhar os quatro primeiros bytes: é preciso conferir RIFF no início e WAVE mais adiante. MP3 também não tem uma única abertura universal: pode começar por metadados ID3 ou diretamente por um frame de áudio.

NÃO CONFUNDA EVIDÊNCIA COM GARANTIA

Uma assinatura curta reduz dúvidas; o parser completo é quem prova se a estrutura é válida.

Quatro bytes têm apenas 32 bits. Um arquivo malicioso pode copiá-los. Um arquivo corrompido pode preservar o cabeçalho e quebrar no meio. Alguns documentos são “poliglotas”: obedecem parcialmente a mais de um formato. Detecção séria avança camada por camada, valida limites e nunca trata o magic como autorização para executar conteúdo.

05 / ENXERGANDO OS BYTES

Um editor hexadecimal não mostra outra versão do arquivo. Mostra os mesmos bytes com outra representação.

Abra um arquivo em um editor hexadecimal e você verá três regiões comuns: offsets à esquerda, bytes no centro e uma aproximação ASCII à direita. Offset é a distância, em bytes, desde o início. O byte no offset 0x00 é o primeiro; o byte no 0x10 vem depois de dezesseis bytes.

Hexadecimal usa dezesseis símbolos: 0–9 e A–F. Um dígito hexadecimal guarda quatro bits; dois dígitos descrevem exatamente um byte. FF equivale a 255 porque o primeiro F representa quinze grupos de dezesseis e o segundo representa quinze unidades: 15 × 16 + 15.

BINÁRIO0101 0000difícil de percorrer
HEX50compacto para bytes
DECIMAL80familiar para contar
ASCIIPinterpretação como caractere

Little endian e big endian sem mistério

Um byte não tem ordem interna discutível. O problema aparece quando um número ocupa vários bytes. O valor hexadecimal 0x1234 precisa de12 e 34. Em big endian, o byte mais significativo vem primeiro: 12 34. Em little endian, o menos significativo vem primeiro: 34 12.

VALOR0x1234
BIG ENDIAN12 34“grande” primeiro
LITTLE ENDIAN34 12“pequeno” primeiro

Nenhuma ordem é universalmente correta. O formato deve escolher uma e documentar. O erro acontece quando quem grava escolhe uma ordem e quem lê supõe outra. No nosso projeto, usaremos little endian e chamaremos explicitamenteto_le_bytes e from_le_bytes. Assim a intenção fica no código e o resultado independe da CPU.

06 / UMA GRAMÁTICA PARA BYTES

Um formato de arquivo é um acordo sobre o significado de cada parte.

Bytes não carregam etiquetas. A sequência 00 10 pode ser o número 4096 em big endian, 16 em little endian, duas cores, parte de uma instrução ou simplesmente dados comprimidos. O significado vem da especificação e da posição. Um formato é uma gramática: diz o que pode aparecer, em qual ordem, quanto mede e como validar.

Os bytes só ganham significado quando passam por uma especificação
01Cabeçalhoidentidade e parâmetros
02Versãocomo interpretar
03Metadadoscontexto e descrição
04Dadosconteúdo principal
05Checksumindício de integridade
06Finalizadorencerramento ou índice

Nem todo formato possui todas essas áreas, e elas nem sempre aparecem nessa ordem. Um formato simples pode ter tamanho fixo. Outro usa blocos com identificador e comprimento. Um terceiro termina com um índice porque só então conhece a posição final de tudo.

A engenharia começa nas perguntas incômodas. Qual o maior tamanho permitido? Como ignorar uma seção desconhecida? O que acontece se a largura multiplicada pela altura estourar o inteiro? Uma versão antiga pode abrir a nova? O parser deve aceitar bytes extras? Um formato não é apenas a gravação do caso feliz; é o contrato para rejeitar o impossível.

07 / ANATOMIA DO PNG

PNG organiza a imagem em chunks: pequenos envelopes que dizem tipo, tamanho e integridade.

Depois da assinatura de oito bytes, o PNG apresenta uma sequência de chunks. Cada chunk carrega comprimento, tipo de quatro letras, dados e um CRC. Essa estrutura resolve dois problemas: permite dividir responsabilidades e possibilita que leitores reconheçam partes importantes sem conhecer cada extensão futura.

4 BYTESTamanhoquantos bytes há nos dados
4 BYTESTipoIHDR, IDAT, IEND…
VARIÁVELDadosconteúdo do chunk
4 BYTESCRCtipo + dados

IHDR é o cartão de identidade da imagem e deve vir cedo. Informa largura, altura, profundidade de bits, tipo de cor, compressão, filtro e entrelaçamento. Antes de reservar memória, o decoder já sabe a forma geral do trabalho.

Uma imagem PNG atravessando o decoder

PLTE guarda uma paleta quando a imagem usa cores indexadas. Em vez de repetir RGB em cada pixel, o dado pode dizer “cor 7”. A posição 7 na paleta fornece os canais. IDAT carrega o fluxo de pixels comprimido; pode aparecer em vários chunks consecutivos, que juntos formam o mesmo fluxo. IEND sinaliza que a sequência terminou.

ASSINATURAisto pretende ser PNG
IHDRforma e modo da imagem
PLTE?cores indexadas
IDAT + IENDpixels e encerramento

Por que não colocar simplesmente largura, altura e pixels? Porque um formato mundial precisa sobreviver a transmissão, corrupção parcial, recursos opcionais e evolução. Chunks tornam extensões localizadas. O custo é um parser mais cuidadoso: ele deve verificar comprimentos, ordem, CRC e limites antes de alocar.

08 / ANATOMIA DO ZIP

ZIP é uma pequena biblioteca: dados perto de cada entrada e um catálogo no final.

Um ZIP contém vários arquivos, seus nomes, métodos de compressão, datas, CRCs e posições. Antes de cada conteúdo há um cabeçalho local. Perto do fim existe o diretório central, que repete metadados úteis e aponta para os cabeçalhos locais. Um registro final diz onde o diretório começa e quantas entradas possui.

LOCAL 1nome + método
DADOS 1arquivo comprimido
LOCAL 2nome + método
DADOS 2arquivo comprimido
DIRETÓRIO CENTRALíndice das entradas
FIMposição do índice

Colocar o índice no fim tem uma vantagem histórica e prática: é possível gravar entradas sequencialmente e montar o catálogo depois. Para listar o conteúdo, o leitor procura o final, encontra o diretório e evita descomprimir tudo.

A assinatura PK 03 04 que lembramos como “ZIP” é, mais precisamente, a assinatura de um cabeçalho local. Um arquivo vazio ou estruturas especiais podem começar por outro registro PK. Além disso, formatos como DOCX, XLSX, EPUB e APK usam ZIP como contêiner. Os primeiros bytes dizem “família ZIP”; os nomes e arquivos internos revelam a aplicação específica.

CURIOSIDADE

Um .docx moderno é um pacote ZIP com XML, relações e recursos organizados por convenção.

09 / ANATOMIA DO PDF

PDF descreve objetos e relações, não apenas uma sequência de páginas prontas.

O início costuma trazer %PDF- e uma versão. O corpo contém objetos numerados: catálogos, árvores de páginas, fontes, imagens, instruções de desenho e fluxos comprimidos. Um objeto pode referenciar outro sem precisar estar ao lado. Isso permite compartilhar recursos e construir uma estrutura complexa.

Para localizar objetos rapidamente, PDFs tradicionais usam uma tabela de referências cruzadas. O trailer aponta para informações essenciais, inclusive o objeto raiz. Perto do fim, startxref ajuda a encontrar essa tabela. Versões modernas podem guardar informações equivalentes em streams, mas a ideia permanece: oferecer um mapa das posições.

1 0 objCatálogoentrada do documento
2 0 objÁrvoreorganiza páginas
3 0 objPáginamídia e conteúdo
xrefMapaobjeto → offset
trailerRaizcomo começar a leitura

Essa arquitetura também permite atualizações incrementais: certas alterações podem ser anexadas, com uma nova seção de referências apontando para o estado anterior. É por isso que “o último pedaço” pode ser tão importante quanto o primeiro. Magic bytes iniciam a conversa; a estrutura inteira conta a história.

10 / NOSSO PRÓPRIO FORMATO

Vamos criar o SLAIMG: uma imagem indexada pequena, explícita e evolutiva.

Agora saímos do papel de observador. Nosso formato terá a extensão.slaimg, mas seu nome interno será SLAIMG. Ele guardará uma imagem retangular. Cada cor da paleta possui três bytes — vermelho, verde e azul — e cada pixel possui um byte que aponta para uma posição dessa paleta.

Escolhemos imagem indexada porque ela expõe decisões interessantes sem exigir algoritmos enormes. Precisamos de dimensões, paleta, pixels, validação, versão e checksum. Ao final, teremos exatamente o que programas reais têm: um encoder que produz o contrato e um decoder desconfiado que o verifica.

O arquivo SLAIMG, do primeiro ao último byte
OFFSETTAMANHOCAMPOREGRA
0x004MagicASCII SLAI
0x041Versão1
0x051Flags0 nesta versão
0x062Largurau16 little endian
0x082Alturau16 little endian
0x0A2Cores1 a 256
0x0C4Reservadozeros
0x103 × coresPaletaRGB, três u8
variávellargura × alturaPixelsíndices u8
final − 44ChecksumCRC-32 little endian

Por que cada decisão existe?

SLAI é curto, legível em ASCII e diferente dos exemplos comuns. A versão permite recusar contratos futuros em vez de interpretá-los errado. Flags reservam oito decisões booleanas futuras. Dimensões em u16 suportariam até 65.535, mas nosso parser limitará cada lado a 4.096 para controlar memória.

O número de cores ocupa u16 porque precisamos representar 256; umu8 termina em 255. Cada pixel continua sendo u8 e portanto aceita índices de 0 a 255. Os quatro bytes reservados devem ser zero: uma versão futura poderá lhes dar significado sem mover todo o cabeçalho. O CRC-32 detecta alterações acidentais; não oferece autenticidade contra um atacante, pois qualquer pessoa pode recalculá-lo.

ESPECIFICAÇÃO É UMA PROMESSA

Se encoder e decoder só concordam por acidente, você ainda não tem um formato estável.

11 / ORGANIZANDO O PROJETO

Separaremos dados, gravação, leitura, erros e apresentação.

Crie o projeto com cargo new slaimg. O código completo acompanha o artigo dentro de projects/slaimg. A biblioteca contém o formato; o binário oferece comandos. Assim testes podem chamar encoder e decoder sem abrir uma janela.

slaimg/
├── Cargo.toml
├── README.md
├── src/
│   ├── lib.rs
│   ├── main.rs
│   ├── format.rs
│   ├── encoder.rs
│   ├── decoder.rs
│   ├── checksum.rs
│   ├── error.rs
│   └── viewer.rs
└── tests/
    └── roundtrip.rs

format.rs define o vocabulário. encoder.rs transforma uma Image em bytes. decoder.rs percorre o caminho inverso. checksum.rs isola o CRC.error.rs descreve falhas esperadas. viewer.rs converte pixels em uma janela. main.rs apenas liga as peças.

A pequena fábrica do nosso formato
pub const MAGIC: [u8; 4] = *b"SLAI";
pub const VERSION: u8 = 1;
pub const HEADER_LEN: usize = 16;
pub const CHECKSUM_LEN: usize = 4;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Color {
    pub red: u8,
    pub green: u8,
    pub blue: u8,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Pixel(pub u8);

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Image {
    pub width: u16,
    pub height: u16,
    pub palette: Vec<Color>,
    pub pixels: Vec<Pixel>,
}

As quatro constantes congelam o contrato básico. [u8; 4] é um array de exatamente quatro bytes; *b"SLAI" produz esses bytes em tempo de compilação. usize é usado para índices e capacidades na memória; u8 e u16 são usados no arquivo porque seu tamanho é estável.

Color contém os três canais. Cada um varia de 0 a 255.Pixel(pub u8) é um newtype: em memória ainda custa um byte, mas no código deixa claro que o número é índice de cor, não intensidade.Image reúne dimensões, paleta dinâmica e pixels dinâmicos.

Os derive pedem ao compilador implementações úteis.Debug facilita diagnóstico. Clone permite cópia explícita. PartialEq e Eq deixam o teste comparar a imagem decodificada com a original. Copy aparece apenas nos tipos pequenos; copiar uma imagem inteira silenciosamente seria caro.

PROJETO COMPLETO

O código exibido na próxima seção também está no repositório completo no GitHub. Não há módulos escondidos nem trechos substituídos por reticências.

Ver o projeto SLAIMG completo no GitHub

11.1 / CÓDIGO COMPLETO

Vamos montar o projeto inteiro, em uma ordem que o compilador consegue acompanhar.

Execute cargo new slaimg, entre na nova pasta e crie os arquivos mostrados abaixo. Começamos pelo pacote e pelos tipos, adicionamos erros e CRC, implementamos encoder e decoder, conectamos o visualizador e terminamos com a CLI e os testes.

cargo new slaimg
cd slaimg

# substitua Cargo.toml e src/main.rs
# crie os demais módulos dentro de src/
# crie tests/roundtrip.rs
cargo check
cargo test

Rode cargo check depois de cada arquivo. No começo alguns módulos ainda estarão ausentes; isso é esperado. Quando lib.rs, os tipos e os módulos referenciados existirem, cada erro restante apontará uma conexão concreta que falta.

01
Cargo.toml

O pacote e suas duas dependências

[package]
name = "slaimg"
version = "0.1.0"
edition = "2024"
description = "Formato de imagem binário didático criado para o artigo do slaureano.com.br"

[dependencies]
crc32fast = "1.5"
minifb = "0.28"

Cargo.toml identifica o projeto, seleciona Rust 2024 e declara apenas o necessário: crc32fast calcula CRC-32 e minifb cria uma janela nativa simples. As versões ficam registradas para que outra pessoa monte o mesmo projeto.

  • package descreve nome, versão, edição e finalidade.
  • crc32fast implementa o algoritmo de integridade; não o formato SLAIMG.
  • minifb recebe um buffer de pixels e o coloca em uma janela.
  • Cargo resolve e compila as dependências com cargo build ou cargo run.
02
src/lib.rs

A API pública do formato

pub mod checksum;
pub mod decoder;
pub mod encoder;
pub mod error;
pub mod format;

pub use decoder::decode;
pub use encoder::encode;
pub use error::SlaError;
pub use format::{Color, Image, Pixel};

A biblioteca reúne as regras independentes de interface gráfica. pub mod carrega cada módulo; pub use oferece os tipos e funções essenciais diretamente em slaimg. Testes e o executável usam a mesma API.

  • checksum, decoder, encoder, error e format são módulos da biblioteca.
  • viewer não aparece aqui porque pertence somente ao executável gráfico.
  • decode e encode são reexportados para reduzir caminhos longos.
  • Color, Image e Pixel formam o modelo que usuários da biblioteca constroem.
03
src/format.rs

O contrato em memória

pub const MAGIC: [u8; 4] = *b"SLAI";
pub const VERSION: u8 = 1;
pub const HEADER_LEN: usize = 16;
pub const CHECKSUM_LEN: usize = 4;
pub const MAX_SIDE: u16 = 4096;
pub const MAX_COLORS: u16 = 256;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Color {
    pub red: u8,
    pub green: u8,
    pub blue: u8,
}

impl Color {
    pub const fn new(red: u8, green: u8, blue: u8) -> Self {
        Self { red, green, blue }
    }

    pub const fn to_u32(self) -> u32 {
        ((self.red as u32) << 16) | ((self.green as u32) << 8) | self.blue as u32
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Pixel(pub u8);

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Image {
    pub width: u16,
    pub height: u16,
    pub palette: Vec<Color>,
    pub pixels: Vec<Pixel>,
}

impl Image {
    pub fn new(
        width: u16,
        height: u16,
        palette: Vec<Color>,
        pixels: Vec<Pixel>,
    ) -> Self {
        Self { width, height, palette, pixels }
    }
}

Este arquivo congela magic, versão, tamanhos e limites. Depois modela cor, índice de pixel e imagem. Os tipos não sabem ler disco: eles representam somente uma imagem SLAIMG válida ou em processo de validação.

  • Color ocupa três canais u8 e to_u32 prepara a cor para minifb.
  • Pixel é um newtype: o u8 representa um índice da paleta.
  • Image guarda dimensões, paleta e pixels em ordem de leitura.
  • MAX_SIDE e MAX_COLORS limitam alocação e combinações inválidas.
04
src/checksum.rs

CRC-32 em uma única função

pub fn calculate(bytes: &[u8]) -> u32 {
    crc32fast::hash(bytes)
}

O módulo esconde a dependência externa atrás de calculate. Encoder e decoder não precisam saber qual crate executa CRC-32; eles apenas concordam que a função recebe bytes e devolve u32.

  • &[u8] empresta os bytes sem copiá-los.
  • crc32fast::hash calcula o mesmo valor para a mesma sequência.
  • Isolar o algoritmo facilita trocar a implementação no futuro.
  • CRC detecta corrupção acidental, não adulteração maliciosa.
05
src/error.rs

Falhas como dados estruturados

use std::{fmt, io};

#[derive(Debug)]
pub enum SlaError {
    Io(io::Error),
    InvalidMagic([u8; 4]),
    UnsupportedVersion(u8),
    InvalidDimensions { width: u16, height: u16 },
    InvalidPaletteSize(u16),
    InvalidPixelIndex { index: u8, palette_len: usize },
    UnexpectedLength { expected: usize, actual: usize },
    ChecksumMismatch { expected: u32, actual: u32 },
}

impl fmt::Display for SlaError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(error) => write!(f, "erro de entrada ou saída: {error}"),
            Self::InvalidMagic(found) => write!(f, "assinatura inválida: {found:02X?}"),
            Self::UnsupportedVersion(version) => write!(f, "versão {version} não suportada"),
            Self::InvalidDimensions { width, height } => {
                write!(f, "dimensões inválidas: {width}x{height}")
            }
            Self::InvalidPaletteSize(size) => write!(f, "paleta inválida: {size} cores"),
            Self::InvalidPixelIndex { index, palette_len } => {
                write!(f, "pixel aponta para a cor {index}, mas a paleta possui {palette_len} cores")
            }
            Self::UnexpectedLength { expected, actual } => {
                write!(f, "tamanho inválido: esperado {expected}, recebido {actual}")
            }
            Self::ChecksumMismatch { expected, actual } => {
                write!(f, "checksum inválido: esperado {expected:08X}, calculado {actual:08X}")
            }
        }
    }
}

impl std::error::Error for SlaError {}

impl From<io::Error> for SlaError {
    fn from(error: io::Error) -> Self {
        Self::Io(error)
    }
}

SlaError enumera problemas de entrada e de estrutura. Display escreve mensagens compreensíveis; Error integra o tipo ao padrão Rust; From<io::Error> permite propagar falhas de leitura, escrita e janela com o operador ?.

  • Cada variante preserva valores úteis para diagnóstico.
  • InvalidMagic e UnsupportedVersion impedem interpretar outro contrato.
  • UnexpectedLength detecta truncamento e bytes excedentes.
  • ChecksumMismatch mostra o valor armazenado e o calculado.
06
src/encoder.rs

Uma Image vira bytes

use crate::{
    checksum,
    error::SlaError,
    format::{CHECKSUM_LEN, HEADER_LEN, Image, MAGIC, MAX_COLORS, MAX_SIDE, VERSION},
};

pub fn encode(image: &Image) -> Result<Vec<u8>, SlaError> {
    validate(image)?;

    let pixel_count = image.width as usize * image.height as usize;
    let palette_bytes = image.palette.len() * 3;
    let capacity = HEADER_LEN + palette_bytes + pixel_count + CHECKSUM_LEN;
    let mut output = Vec::with_capacity(capacity);

    output.extend_from_slice(&MAGIC);
    output.push(VERSION);
    output.push(0);
    output.extend_from_slice(&image.width.to_le_bytes());
    output.extend_from_slice(&image.height.to_le_bytes());
    output.extend_from_slice(&(image.palette.len() as u16).to_le_bytes());
    output.extend_from_slice(&[0; 4]);

    for color in &image.palette {
        output.extend_from_slice(&[color.red, color.green, color.blue]);
    }

    output.extend(image.pixels.iter().map(|pixel| pixel.0));

    let checksum = checksum::calculate(&output);
    output.extend_from_slice(&checksum.to_le_bytes());
    Ok(output)
}

fn validate(image: &Image) -> Result<(), SlaError> {
    if image.width == 0 || image.height == 0 || image.width > MAX_SIDE || image.height > MAX_SIDE {
        return Err(SlaError::InvalidDimensions { width: image.width, height: image.height });
    }

    let palette_len = image.palette.len();
    if palette_len == 0 || palette_len > MAX_COLORS as usize {
        return Err(SlaError::InvalidPaletteSize(palette_len as u16));
    }

    let expected = image.width as usize * image.height as usize;
    if image.pixels.len() != expected {
        return Err(SlaError::UnexpectedLength { expected, actual: image.pixels.len() });
    }

    for pixel in &image.pixels {
        if pixel.0 as usize >= palette_len {
            return Err(SlaError::InvalidPixelIndex { index: pixel.0, palette_len });
        }
    }

    Ok(())
}

encode valida primeiro e só então reserva o tamanho exato aproximado. O cabeçalho é escrito campo por campo, a paleta vira trios RGB, pixels viram índices e o CRC cobre tudo que veio antes dele.

  • validate impede dimensões, paleta, contagem e índices incoerentes.
  • Vec::with_capacity reduz realocações sem alterar o comprimento.
  • to_le_bytes fixa a ordem de width, height e palette_len.
  • A quantidade de pixels deve ser width multiplicado por height.
  • O resultado é Vec<u8>, que main pode gravar onde quiser.
07
src/decoder.rs

Bytes não confiáveis viram uma Image

use crate::{
    checksum,
    error::SlaError,
    format::{
        CHECKSUM_LEN, Color, HEADER_LEN, Image, MAGIC, MAX_COLORS, MAX_SIDE, Pixel, VERSION,
    },
};

pub fn decode(bytes: &[u8]) -> Result<Image, SlaError> {
    if bytes.len() < HEADER_LEN + CHECKSUM_LEN {
        return Err(SlaError::UnexpectedLength {
            expected: HEADER_LEN + CHECKSUM_LEN,
            actual: bytes.len(),
        });
    }

    let magic: [u8; 4] = bytes[0..4].try_into().expect("a fatia possui quatro bytes");
    if magic != MAGIC {
        return Err(SlaError::InvalidMagic(magic));
    }

    let version = bytes[4];
    if version != VERSION {
        return Err(SlaError::UnsupportedVersion(version));
    }

    let width = u16::from_le_bytes([bytes[6], bytes[7]]);
    let height = u16::from_le_bytes([bytes[8], bytes[9]]);
    if width == 0 || height == 0 || width > MAX_SIDE || height > MAX_SIDE {
        return Err(SlaError::InvalidDimensions { width, height });
    }

    let palette_len = u16::from_le_bytes([bytes[10], bytes[11]]);
    if palette_len == 0 || palette_len > MAX_COLORS {
        return Err(SlaError::InvalidPaletteSize(palette_len));
    }

    let palette_bytes = palette_len as usize * 3;
    let pixel_count = width as usize * height as usize;
    let expected_len = HEADER_LEN + palette_bytes + pixel_count + CHECKSUM_LEN;
    if bytes.len() != expected_len {
        return Err(SlaError::UnexpectedLength { expected: expected_len, actual: bytes.len() });
    }

    let data_end = bytes.len() - CHECKSUM_LEN;
    let expected_checksum = u32::from_le_bytes(
        bytes[data_end..].try_into().expect("a fatia possui quatro bytes"),
    );
    let actual_checksum = checksum::calculate(&bytes[..data_end]);
    if expected_checksum != actual_checksum {
        return Err(SlaError::ChecksumMismatch {
            expected: expected_checksum,
            actual: actual_checksum,
        });
    }

    let mut palette = Vec::with_capacity(palette_len as usize);
    for chunk in bytes[HEADER_LEN..HEADER_LEN + palette_bytes].chunks_exact(3) {
        palette.push(Color::new(chunk[0], chunk[1], chunk[2]));
    }

    let pixels_start = HEADER_LEN + palette_bytes;
    let mut pixels = Vec::with_capacity(pixel_count);
    for &index in &bytes[pixels_start..data_end] {
        if index as usize >= palette.len() {
            return Err(SlaError::InvalidPixelIndex { index, palette_len: palette.len() });
        }
        pixels.push(Pixel(index));
    }

    Ok(Image::new(width, height, palette, pixels))
}

decode faz o caminho inverso sem confiar no arquivo. Confere tamanho mínimo, identidade, versão, limites, tamanho total e CRC antes de construir paleta e pixels. Cada índice é comparado ao tamanho real da paleta.

  • O cálculo expected_len prova onde cada região deve terminar.
  • from_le_bytes reconstrói os u16 segundo a especificação.
  • chunks_exact(3) transforma RGB RGB RGB em objetos Color.
  • A fatia de pixels termina antes dos quatro bytes do checksum.
  • Somente após todas as provas a função devolve Image.
08
src/viewer.rs

Da paleta para uma janela

use minifb::{Key, ScaleMode, Window, WindowOptions};
use slaimg::{Image, SlaError};

pub fn show(image: &Image) -> Result<(), SlaError> {
    let width = image.width as usize;
    let height = image.height as usize;
    let buffer: Vec<u32> = image
        .pixels
        .iter()
        .map(|pixel| image.palette[pixel.0 as usize].to_u32())
        .collect();

    let mut window = Window::new(
        "Visualizador SLAIMG — ESC para sair",
        width.max(320),
        height.max(240),
        WindowOptions {
            resize: true,
            scale_mode: ScaleMode::AspectRatioStretch,
            ..WindowOptions::default()
        },
    )
    .map_err(|error| std::io::Error::other(error.to_string()))?;

    while window.is_open() && !window.is_key_down(Key::Escape) {
        window
            .update_with_buffer(&buffer, width, height)
            .map_err(|error| std::io::Error::other(error.to_string()))?;
    }

    Ok(())
}

O visualizador resolve cada índice de pixel para uma cor u32, cria uma janela redimensionável e envia repetidamente o mesmo buffer enquanto ela estiver aberta. Escape encerra o laço.

  • map converte Pixel em Color e depois em 0xRRGGBB.
  • collect materializa o buffer contíguo esperado por minifb.
  • width.max e height.max evitam uma janela minúscula.
  • AspectRatioStretch preserva a proporção da imagem.
  • Erros gráficos são convertidos para io::Error e depois SlaError.
09
src/main.rs

Create, info e view

mod viewer;

use std::{env, fs, path::Path};

use slaimg::{Color, Image, Pixel, SlaError, decode, encode};

fn main() -> Result<(), SlaError> {
    let arguments: Vec<String> = env::args().collect();
    match arguments.get(1).map(String::as_str) {
        Some("create") => {
            let path = arguments.get(2).map(String::as_str).unwrap_or("example.slaimg");
            let image = create_example();
            fs::write(path, encode(&image)?)?;
            println!("Arquivo criado em {path}");
        }
        Some("info") => {
            let path = required_path(&arguments)?;
            let image = decode(&fs::read(path)?)?;
            println!(
                "{}: {}x{} pixels, {} cores",
                path.display(),
                image.width,
                image.height,
                image.palette.len()
            );
        }
        Some("view") => {
            let path = required_path(&arguments)?;
            let image = decode(&fs::read(path)?)?;
            viewer::show(&image)?;
        }
        _ => print_help(),
    }
    Ok(())
}

fn required_path(arguments: &[String]) -> Result<&Path, SlaError> {
    arguments
        .get(2)
        .map(Path::new)
        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "informe o arquivo"))
        .map_err(SlaError::from)
}

fn create_example() -> Image {
    let width = 64;
    let height = 64;
    let palette = vec![
        Color::new(8, 12, 14),
        Color::new(237, 107, 50),
        Color::new(128, 203, 208),
        Color::new(241, 235, 224),
    ];

    let mut pixels = Vec::with_capacity(width as usize * height as usize);
    for y in 0..height {
        for x in 0..width {
            let index = if x == y || x + y == width - 1 {
                3
            } else if (16..48).contains(&x) && (16..48).contains(&y) {
                1
            } else if (x / 8 + y / 8) % 2 == 0 {
                2
            } else {
                0
            };
            pixels.push(Pixel(index));
        }
    }

    Image::new(width, height, palette, pixels)
}

fn print_help() {
    println!("SLAIMG — formato de imagem didático");
    println!("  cargo run -- create [arquivo.slaimg]");
    println!("  cargo run -- info <arquivo.slaimg>");
    println!("  cargo run -- view <arquivo.slaimg>");
}

O executável interpreta a linha de comando. create monta uma imagem de exemplo e chama encode; info decodifica e imprime metadados; view decodifica e entrega a imagem validada ao visualizador.

  • env::args coleta os argumentos sem biblioteca de CLI.
  • required_path evita executar info ou view sem arquivo.
  • create_example desenha um padrão usando apenas índices de paleta.
  • main retorna Result para que ? propague qualquer SlaError.
  • A lógica binária permanece na biblioteca, não no match da CLI.
10
tests/roundtrip.rs

O contrato provado automaticamente

use slaimg::{Color, Image, Pixel, decode, encode};

#[test]
fn encodes_and_decodes_the_same_image() {
    let image = Image::new(
        2,
        2,
        vec![Color::new(0, 0, 0), Color::new(255, 255, 255)],
        vec![Pixel(0), Pixel(1), Pixel(1), Pixel(0)],
    );

    let bytes = encode(&image).expect("a imagem válida deve ser codificada");
    let decoded = decode(&bytes).expect("o arquivo gerado deve ser decodificado");
    assert_eq!(decoded, image);
}

#[test]
fn rejects_a_file_with_the_wrong_magic() {
    let image = Image::new(1, 1, vec![Color::new(0, 0, 0)], vec![Pixel(0)]);
    let mut bytes = encode(&image).unwrap();
    bytes[0] = b'X';
    assert!(decode(&bytes).is_err());
}

O primeiro teste constrói uma imagem pequena, codifica, decodifica e exige igualdade total. O segundo altera a assinatura e prova que o decoder recusa o arquivo. Eles exercitam a API como um usuário real.

  • Uma imagem 2x2 permite conferir mentalmente os quatro pixels.
  • expect documenta por que aquela operação deve funcionar.
  • assert_eq compara dimensões, paleta e todos os pixels.
  • O teste de magic mostra que extensão e nome não substituem validação.
COMO ESTUDAR SEM VIRAR COPIA E COLA

Digite primeiro format.rs, desenhe o cabeçalho em papel e preveja os bytes de uma imagem 1x1. Depois implemente encode, abra o resultado em hexadecimal e só então escreva decode.

12 / ESCREVENDO O ARQUIVO

Serializar manualmente é escolher, byte por byte, como o valor vive fora da memória.

Não podemos gravar a memória bruta de Image. UmVec guarda ponteiro, tamanho e capacidade; esses valores só fazem sentido no processo atual. Padding e layout também podem variar. Serialização manual extrai somente os dados definidos pela especificação.

let mut output = Vec::with_capacity(capacity);

output.extend_from_slice(&MAGIC);
output.push(VERSION);
output.push(0); // flags
output.extend_from_slice(&image.width.to_le_bytes());
output.extend_from_slice(&image.height.to_le_bytes());
output.extend_from_slice(&(image.palette.len() as u16).to_le_bytes());
output.extend_from_slice(&[0; 4]); // reservado

Vec::with_capacity cria um vetor vazio e reserva o tamanho previsto. Isso não insere bytes; reduz realocações. extend_from_slice copia uma fatia inteira. push adiciona um byte. A largura é um número, entãoto_le_bytes a divide nos dois bytes little endian prometidos.

O cast da quantidade de cores para u16 só é seguro porque validamos antes que a paleta tenha no máximo 256 entradas. Os quatro zeros reservados são gravados mesmo sem uso. Espaço reservado não é “lixo”: é parte fixa e previsível do cabeçalho.

for color in &image.palette {
    output.extend_from_slice(&[
        color.red,
        color.green,
        color.blue,
    ]);
}

output.extend(image.pixels.iter().map(|pixel| pixel.0));

let checksum = crc32fast::hash(&output);
output.extend_from_slice(&checksum.to_le_bytes());

std::fs::write("example.slaimg", output)?;

O for pega cada cor por referência, evitando mover a paleta. Construímos uma pequena fatia temporária com R, G e B e a copiamos. Depois,map transforma cada Pixel em seu índiceu8; extend consome esse iterador e anexa os bytes.

O checksum é calculado sobre tudo o que veio antes dele. Só depois seus quatro bytes são acrescentados. std::fs::write cria ou substitui o arquivo e escreve a fatia completa. Internamente ele oferece a simplicidade que obteríamos abrindo um File e usando write_all.

O que write_all garante?

Uma chamada de escrita de baixo nível pode aceitar menos bytes que o solicitado. Isso não é necessariamente erro. write_all repete as escritas até enviar toda a fatia ou encontrar uma falha. Ele não garante que os dados já chegaram fisicamente ao disco e também não torna a substituição atômica. Para um editor real, gravaríamos em arquivo temporário, sincronizaríamos e renomearíamos.

VALIDAR ANTES DE GRAVAR

O melhor decoder não compensa um encoder autorizado a produzir arquivos impossíveis.

13 / LENDO SEM CONFIAR

Um decoder é uma fronteira de segurança: todo byte externo pode estar errado.

Ao ler nosso próprio arquivo de exemplo, sabemos sua origem. Ao publicar o formato, perdemos esse conforto. O decoder pode receber arquivo truncado, dimensões gigantes, índice fora da paleta, checksum falso ou operações aritméticas que estouram. A regra é validar antes de fatiar, multiplicar, alocar ou indexar.

let magic: [u8; 4] = bytes[0..4]
    .try_into()
    .expect("a fatia possui quatro bytes");

if magic != MAGIC {
    return Err(SlaError::InvalidMagic(magic));
}

let version = bytes[4];
if version != VERSION {
    return Err(SlaError::UnsupportedVersion(version));
}

let width = u16::from_le_bytes([bytes[6], bytes[7]]);
let height = u16::from_le_bytes([bytes[8], bytes[9]]);
let palette_len = u16::from_le_bytes([bytes[10], bytes[11]]);

Antes desse trecho, o código exige pelo menos vinte bytes: dezesseis de cabeçalho e quatro de checksum. Por isso bytes[0..4] é seguro.try_into converte a fatia em array e o expect documenta uma invariável já garantida — não uma esperança sobre entrada externa.

Comparamos o magic antes de interpretar dimensões. Em seguida recusamos versões desconhecidas. from_le_bytes recompõe os números exatamente na ordem da especificação. Depois validamos largura e altura entre 1 e 4.096 e quantidade de cores entre 1 e 256.

Calcular o tamanho esperado antes de recortar

O tamanho deve ser: cabeçalho + três bytes por cor + um byte por pixel + checksum. Com os limites atuais, a multiplicação cabe em usize nas plataformas suportadas. Um formato mais amplo usaria checked_mul echecked_add. Se o arquivo tiver um byte a menos ou a mais, recusamos. Ser estrito simplifica a primeira versão.

let palette_bytes = palette_len as usize * 3;
let mut palette = Vec::with_capacity(palette_len as usize);

for chunk in bytes[HEADER_LEN..HEADER_LEN + palette_bytes]
    .chunks_exact(3)
{
    palette.push(Color::new(chunk[0], chunk[1], chunk[2]));
}

chunks_exact(3) divide a região da paleta em grupos RGB sem sobras. A conta anterior garantiu que a região existe. Vec::with_capacityreserva exatamente a quantidade de cores. Cada grupo vira um Color.

Depois percorremos os pixels. Antes de criar Pixel(index), conferimos se index as usize < palette.len(). Sem isso, o viewer poderia indexar fora do vetor e encerrar. Por fim, lemos os quatro últimos bytes, calculamos CRC sobre o restante e comparamos.

1Tamanho mínimopodemos ler o cabeçalho?
2Identidademagic e versão são conhecidos?
3Limitesdimensões e paleta são aceitáveis?
4Estruturao tamanho calculado combina?
5Referênciaspixels apontam para cores válidas?
6Integridadeo CRC ainda é o mesmo?

Note a ordem. Se alocássemos width × height antes de aplicar limites, quatro bytes controlados pelo arquivo poderiam pedir gigabytes. Se indexássemos a paleta antes da validação, um único pixel criaria panic. Segurança em parser não vem de um grande if; vem de não usar um valor antes de provar o que ele pode ser.

14 / TRANSFORMANDO ÍNDICES EM LUZ

O visualizador não “abre .slaimg”. Ele recebe uma Image já validada.

Essa divisão importa. O decoder conhece bytes, mas não janelas. O viewer conhece pixels, mas não offsets. Se amanhã criarmos uma versão web, reutilizaremos o decoder. Se trocarmos o formato da janela, não mexeremos na segurança do parser.

let buffer: Vec<u32> = image
    .pixels
    .iter()
    .map(|pixel| image.palette[pixel.0 as usize].to_u32())
    .collect();

while window.is_open() && !window.is_key_down(Key::Escape) {
    window.update_with_buffer(
        &buffer,
        image.width as usize,
        image.height as usize,
    )?;
}

A biblioteca da janela espera um buffer de u32. Para cada pixel, usamos seu byte como índice na paleta. to_u32 posiciona vermelho nos bits 16–23, verde nos bits 8–15 e azul nos bits 0–7. O collectmaterializa o iterador em um vetor.

O laço continua enquanto a janela estiver aberta e Escape não estiver pressionado.update_with_buffer entrega o mesmo quadro à janela. Nossa imagem é estática, então o buffer não muda; ainda precisamos processar eventos para a janela continuar responsiva.

Uma versão reduzida do exemplo: os índices apontam para quatro cores da paleta.

15 / O FORMATO DEPOIS DA VERSÃO 1

Compatibilidade não acontece sozinha. Ela precisa estar desenhada.

Nosso SLAIMG é deliberadamente simples: um byte por pixel e nenhuma compressão. Uma imagem 4.096 × 4.096 usa cerca de 16 MiB só em índices. A próxima versão poderia aplicar run-length encoding a sequências repetidas ou DEFLATE ao bloco de pixels. Uma flag indicaria compressão e o cabeçalho traria tamanho descomprimido.

COMPRESSÃO

Menos bytes

Comprimir pixels, limitar saída e rejeitar expansão desproporcional.

CHECKSUM

Mais escopo

CRC por bloco permite descobrir onde a corrupção ocorreu.

VERSÃO

Leitura previsível

Versões maiores devem ser recusadas até o decoder entender o contrato.

METADADOS

Contexto opcional

Autor, data e perfil de cor podem viver em chunks ignoráveis.

STREAMING

Uso gradual

Blocos por linha evitam manter toda a imagem descomprimida.

AUTENTICIDADE

Origem verificável

Assinatura criptográfica é diferente de CRC e exige chaves.

Se simplesmente mudarmos o significado de um campo e mantivermos versão 1, arquivos antigos poderão ser interpretados de maneira errada. Se criarmos versão 2, o decoder antigo responderá “não suportada”, que é muito melhor que mostrar pixels corrompidos. Compatibilidade segura inclui falhar com clareza.

Chunks seriam uma evolução mais flexível. Cada seção teria tipo e comprimento. Um leitor antigo poderia pular um chunk opcional desconhecido somando seu comprimento ao offset. Um chunk crítico desconhecido exigiria rejeição. Essa é a mesma decisão arquitetural vista em formatos maduros: declarar quais extensões podem ser ignoradas.

16 / O MESMO CONCEITO EM OUTROS LUGARES

Antivírus, navegadores, GitHub e ícones usam pistas semelhantes — com objetivos diferentes.

ANTIVÍRUS

Assinatura aqui significa outra coisa também

Antivírus podem procurar sequências conhecidas, estrutura suspeita, comportamento, empacotadores e reputação. Magic bytes classificam formato; uma assinatura de malware tenta reconhecer uma ameaça. Ambas observam bytes, mas respondem perguntas diferentes.

MALWARE

Mudar extensão explora pessoas e associações

Um executável chamado foto.jpg.exe continua executável. Quando extensões conhecidas são ocultadas, o nome pode parecer uma imagem. O sistema não foi enganado sobre o formato; a interface escondeu uma pista importante da pessoa.

NAVEGADORES

O servidor declara, o navegador decide quanto confiar

URLs nem precisam terminar em extensão. O servidor envia Content-Type. Navegadores aplicam regras de sniffing e segurança porque interpretar bytes ativos como imagem ou texto pode mudar completamente o risco. O cabeçalhoX-Content-Type-Options: nosniff pede interpretação estrita.

GITHUB

Linguagem não cabe em uma única extensão

Plataformas combinam nomes, extensões, shebangs, heurísticas, arquivos gerados e conteúdo. Um arquivo sem extensão pode começar com#!/usr/bin/env python3. Classificar linguagem é um problema parecido com classificar formato, mas com mais ambiguidade.

LINUX

Executar e abrir são decisões separadas

O bit de execução autoriza a tentativa. O kernel ainda precisa reconhecer o binário, como ELF, ou um shebang que indique o interpretador. Uma extensão.sh não é requisito do kernel.

WINDOWS

O ícone normalmente vem da associação

O Explorer pode obter ícone e descrição do tipo associado ou do próprio executável. Por isso mudar o programa padrão muda a aparência de muitos arquivos sem modificar nenhum deles.

Uploads: onde a aula vira segurança de produção

Um formulário pode enviar nome, MIME declarado e bytes. Não confie em apenas um. Gere um nome interno, limite o tamanho antes e durante a leitura, detecte o tipo esperado, decodifique com biblioteca atualizada, imponha limites de dimensões e armazene fora de diretórios executáveis. Para imagens, recodificar o conteúdo costuma remover estruturas não necessárias.

Também proteja contra ZIP bombs: arquivos minúsculos que expandem absurdamente; path traversal: entradas como ../../config; e discrepância de parser: dois componentes interpretando o mesmo arquivo de formas diferentes. A pergunta “que tipo é esse?” precisa vir acompanhada de “o que faremos com ele?”.

17 / PROJETO FINAL

Crie, inspecione, corrompa e abra seu primeiro .slaimg.

O projeto completo está no GitHub. Ele usa Rust 2024, a bibliotecacrc32fast para CRC-32 e minifb para uma janela simples. Com Rust e Git instalados, clone o repositório e entre na pasta:

Abrir o código-fonte do SLAIMG no GitHub
git clone https://github.com/sl4ureano/slaimg.git
cd slaimg

Agora execute os comandos do projeto:

cargo test
cargo run -- create example.slaimg
cargo run -- info example.slaimg
cargo run -- view example.slaimg

cargo test compila e executa os testes. O primeiro faz round trip: cria uma imagem em memória, codifica, decodifica e exige igualdade. O segundo altera o primeiro byte e exige erro. create gera a imagem de exemplo.info valida sem interface gráfica. view abre a janela.

EXPERIMENTO 01Troque .slaimg por .txt e use info.
EXPERIMENTO 02Mude SLAI no editor hexadecimal.
EXPERIMENTO 03Altere um pixel sem atualizar o CRC.
EXPERIMENTO 04Crie uma paleta com índice inválido.

No primeiro experimento, o comando funciona porque recebe explicitamente o caminho e valida os bytes — a extensão não participa. No segundo, o decoder acusa magic inválido. No terceiro, a estrutura ainda parece plausível, mas o checksum denuncia alteração. No quarto, o encoder deve recusar antes de gravar.

O ciclo completo que construímos

MODELOImage em memória
ENCODERvalores viram bytes
DECODERbytes viram Image
VIEWERcores viram pixels

E agora podemos responder à pergunta do título com precisão. O computador não possui um sentido secreto para arquivos. O sistema operacional usa convenções e preferências para escolher um programa. O programa usa uma especificação para transformar bytes em significado. Quando faltam certezas, ele combina pistas.

RESPOSTA FINAL

A extensão inicia a conversa. A estrutura do arquivo decide se ela fazia sentido.

18 / PERGUNTAS FREQUENTES

Dúvidas que aparecem quando começamos a olhar além da extensão.

A extensão define o formato de um arquivo?

Não. A extensão é parte do nome e funciona principalmente como pista para o sistema escolher um aplicativo. O formato está nos bytes e na estrutura interna.

Magic bytes identificam qualquer arquivo sem erro?

Não. Eles são uma evidência forte, mas curta. Formatos relacionados podem compartilhar assinaturas, arquivos podem ser poliglotas e alguns formatos não possuem uma assinatura única.

Por que renomear foto.png para foto.txt não converte a imagem?

Porque somente o nome mudou. Os bytes comprimidos, os chunks e o cabeçalho do PNG continuam exatamente iguais.

MIME type e extensão são a mesma coisa?

Não. A extensão pertence ao nome; o MIME type é um rótulo de mídia como image/png usado em protocolos, desktops e aplicações.

É seguro confiar no Content-Type enviado por um navegador?

Não sozinho. Em uploads, o servidor deve limitar tamanho, validar a estrutura real, decodificar com biblioteca segura e armazenar o arquivo fora de áreas executáveis.

O que é um parser binário?

É um programa que transforma uma sequência de bytes em valores com significado seguindo a especificação de um formato e rejeitando combinações inválidas.

Por que o projeto usa little endian?

Porque precisávamos escolher uma ordem estável para números com mais de um byte. Little endian é comum e Rust oferece conversões explícitas para ela.

Como adicionar compressão ao SLAIMG?

Reserve uma flag para indicar o algoritmo, armazene o tamanho descomprimido e aplique a compressão somente aos pixels. O decoder deve limitar a saída para evitar bombas de descompressão.

O que este artigo fez você pensar?

Dúvidas, experiências e contrapontos ajudam a próxima pessoa a enxergar o assunto por outro ângulo.

Todos passam por moderação. Ao enviar, você concorda com a política de privacidade.

Receba os próximos artigos.

Uma mensagem quando uma nova investigação estiver pronta. Só isso.