Capítulo 3 — Fundamentos de programação
Mapa da aula
Aula 03 publicada. Mapa, exemplo, EX007.
flowchart TD
N0["3.1 Entrada não confiável"]
N1["3.2 Camadas de validação"]
N2["3.3 Rejeitar, quarentenar, aceitar"]
N3["3.4 Disciplina do código pequeno"]
N4["3.6 Atividade prática: validador e casos de teste"]
N5["Síntese do capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5
N5 --> EX["Exemplo resolvido"]
EX --> Q["Exercício EX007"]
Desenho lógico
flowchart LR
N0["3.1 Entrada não confiável"]
N1["3.2 Camadas de validação"]
N2["3.3 Rejeitar, quarentenar, aceitar"]
N3["3.4 Disciplina do código pequeno"]
N4["3.6 Atividade prática: validador e casos de teste"]
N5["Síntese do capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX007"]
[3.1 Entrada não confiável]
|
v
[3.2 Camadas de validação]
|
v
[3.3 Rejeitar, quarentenar, aceitar]
|
v
[3.4 Disciplina do código pequeno]
|
v
[3.6 Atividade prática: validador e casos de teste]
|
v
[Síntese do capítulo]
|
v
[Exemplo resolvido] --> [Exercício EX007]
Objetivos de aprendizagem e conexão
Depois deste capítulo, você deve ser capaz de:
- escrever uma pequena ferramenta cujo comportamento diante de entradas ruins é decidido de antemão, e não descoberto em produção;
- validar entradas não confiáveis por allowlisting de estrutura, tipo, faixa e ordem;
- separar os desfechos rejeitar, quarentenar e aceitar, e explicar por que um sistema em operação precisa dos três;
- testar uma ferramenta com casos deliberadamente ruins e registrar os resultados como evidência.
Os implementadores em serviços críticos escrevem muito código pequeno: parsers, cola entre sistemas, verificações, geradores de relatórios. A maior parte nunca é revisada. Este capítulo trata de escrever esse código de modo que ele falhe com segurança, porque, no LANTERN, o código que lê as mensagens das estações decide no que o historiador — e, portanto, os operadores — acreditam sobre a rede de água.
3.1 Entrada não confiável
Entrada não confiável é qualquer dado cujo conteúdo você não controla: mensagens de dispositivos de campo, arquivos de fornecedores, campos digitados por usuários, registros do sistema de outra organização. Mesmo um dispositivo que é seu se torna não confiável quando você lembra que ele pode estar mal configurado, com defeito, ter sido substituído ou estar sendo personificado. A regra é simples: confira a entrada contra o que você espera antes de agir sobre ela, e decida de antemão o que acontece quando ela falha na verificação. A orientação do projeto OWASP sobre validação de entradas recomenda o allowlisting (aceitar apenas formas sabidamente boas) em vez do denylisting (bloquear formas sabidamente ruins), validar tanto a sintaxe quanto o significado, e fazê-lo o mais cedo possível (OWASP Input Validation Cheat Sheet).
3.2 Camadas de validação
Para uma mensagem de telemetria no formato STATION|TAG|VALUE|UNIX_TIME|SEQ, a validação tem camadas, e a ordem importa:
- Estrutura — exatamente cinco campos. Um número errado de campos significa que a mensagem não pode ser interpretada de forma alguma.
- Identidade e vocabulário — o identificador da estação corresponde exatamente ao padrão permitido (
PS-e dois dígitos) e a tag é uma que o sistema conhece. Tags desconhecidas são rejeitadas, e não guardadas "por via das dúvidas". - Tipo — o valor, o tempo e a sequência são interpretados como números. Valores que são interpretados, mas não são finitos (
nan,inf), são rejeitados; muitas linguagens os interpretam sem reclamar. - Significado físico — o valor fica dentro do que o processo físico pode produzir (um nível de reservatório entre 0 e 12 metros; cloro entre 0 e 5 mg/L neste projeto sintético). Um valor fora dessa faixa não é necessariamente malicioso — sensores falham —, então ele é quarentenado para revisão por uma pessoa, em vez de ser descartado ou guardado em silêncio.
- Tempo — o carimbo de tempo da mensagem está próximo do relógio do receptor. Uma diferença grande significa um problema de relógio ou uma mensagem antiga; quarentene-a.
- Ordem — o número de sequência é maior que o último aceito para aquela estação. Um número repetido ou menor é uma reprodução ou uma reordenação; rejeite-o.
3.3 Rejeitar, quarentenar, aceitar
Uma função de validação que só retorna "válido" ou "inválido" força uma má escolha para os dados limítrofes: ou jogar fora uma leitura de que um operador pode precisar, ou guardar uma que pode enganar. Os sistemas em operação precisam de um terceiro desfecho. Rejeitar significa que a entrada não pode ser confiada nem interpretada, e é registrada em log e descartada. Quarentenar significa que a entrada é interpretável, mas implausível; ela é mantida à parte, sinalizada e revisada. Aceitar significa que ela passou em todas as verificações e pode servir de base para ação. Cada desfecho é contado: um aumento súbito de rejeições é, por si só, um sinal de monitoramento (Capítulo 19).
3.4 Disciplina do código pequeno
Quatro hábitos tornam as pequenas ferramentas confiáveis. Mantenha uma função por decisão, para que ela possa ser testada sozinha. Torne o estado explícito — a última sequência vista por estação é passada como argumento, e não escondida numa global. Falhe fechado — qualquer caminho que você não previu termina em rejeição, e não em aceitação. E teste primeiro com casos ruins: para cada verificação, escreva pelo menos uma entrada que deveria falhar nela, e execute todas depois de cada mudança.
O Laboratório 3 segue os quatro em pequena escala: todas as decisões ficam em uma única função curta, validate(), que pode ser testada sozinha. A verificação de sequência recebe como argumento o último número aceito por estação, e é por isso que o 102 repetido pode ser testado sozinho; e a regra de plausibilidade do cloro é uma etapa separada, guiada pela tabela LIMITS depois que o valor é convertido, e é por isso que 9,80 mg/L pode ser quarentenado em vez de rejeitado sem mexer no parser.
3.5 Exemplo resolvido: o validador de mensagens das estações (sintético)
O Laboratório 3 implementa as seis camadas. Executá-lo sobre onze mensagens sintéticas produz três aceitações, seis rejeições e duas quarentenas. Olhe para os casos que um parser ingênuo erraria: nan é interpretado como float e seria guardado; 210.5;DROP só é pego porque o valor é interpretado como número, em vez de repassado como texto; PS-7 falha porque o padrão é casado por inteiro; um número de sequência 102 repetido é rejeitado como reprodução, embora a mensagem seja perfeita em todo o resto; e um valor de cloro de 9,80 mg/L é quarentenado, e não rejeitado, porque uma pessoa precisa saber se o analisador da PS-07 falhou.
3.6 Atividade prática: validador e casos de teste
Laboratório 3 (salve como lab03.py, execute com python3 lab03.py):
# Lab 3 - validating telemetry messages before they reach the historian (offline, standard library only)
# Synthetic message format from Fallowmere pumping stations: STATION|TAG|VALUE|UNIX_TIME|SEQ
import re
LIMITS = {"LEVEL_M": (0.0, 12.0), "FLOW_LS": (0.0, 450.0), "CL2_MGL": (0.0, 5.0)}
STATION = re.compile(r"PS-\d{2}")
def validate(line, now, last_seq):
parts = line.strip().split("|")
if len(parts) != 5:
return "reject: field count"
st, tag, val, ts, seq = parts
if not STATION.fullmatch(st):
return "reject: station id"
if tag not in LIMITS:
return "reject: unknown tag"
try:
v, t, q = float(val), int(ts), int(seq)
except ValueError:
return "reject: not numeric"
if v != v or v in (float("inf"), float("-inf")):
return "reject: not finite"
lo, hi = LIMITS[tag]
if not lo <= v <= hi:
return "quarantine: out of physical range"
if abs(now - t) > 300:
return "quarantine: clock skew >300 s"
if q <= last_seq.get(st, -1):
return "reject: replay or reorder"
last_seq[st] = q
return "accept"
now = 1_790_000_000
msgs = ["PS-04|LEVEL_M|3.42|1789999990|101", "PS-04|LEVEL_M|3.44|1789999995|102",
"PS-04|LEVEL_M|3.44|1789999995|102", "PS-07|CL2_MGL|0.61|1789999801|55",
"PS-07|CL2_MGL|9.80|1789999990|56", "PS-07|FLOW_LS|nan|1789999990|57",
"PS-7|FLOW_LS|120|1789999990|58", "PS-09|PRESS_BAR|2.1|1789999990|9",
"PS-09|FLOW_LS|210.5|1789990000|10", "PS-09|FLOW_LS|210.5;DROP|1789999990|11",
"PS-04|LEVEL_M|3.47|1789999999"]
state = {}
counts = {}
for m in msgs:
r = validate(m, now, state)
counts[r.split(":")[0]] = counts.get(r.split(":")[0], 0) + 1
print(f"{m:<38} {r}")
print(counts)
Saída esperada (Python 3.13.5):
PS-04|LEVEL_M|3.42|1789999990|101 accept
PS-04|LEVEL_M|3.44|1789999995|102 accept
PS-04|LEVEL_M|3.44|1789999995|102 reject: replay or reorder
PS-07|CL2_MGL|0.61|1789999801|55 accept
PS-07|CL2_MGL|9.80|1789999990|56 quarantine: out of physical range
PS-07|FLOW_LS|nan|1789999990|57 reject: not finite
PS-7|FLOW_LS|120|1789999990|58 reject: station id
PS-09|PRESS_BAR|2.1|1789999990|9 reject: unknown tag
PS-09|FLOW_LS|210.5|1789990000|10 quarantine: clock skew >300 s
PS-09|FLOW_LS|210.5;DROP|1789999990|11 reject: not numeric
PS-04|LEVEL_M|3.47|1789999999 reject: field count
{'accept': 3, 'reject': 6, 'quarantine': 2}
Antes de mudar qualquer coisa, anote o que você espera que cada linha imprima. Depois execute. Toda linha em que a sua expectativa estava errada é um ponto em que o seu modelo mental do código estava errado — e esse é o objetivo do exercício.
Três linhas costumam surpreender os leitores: nan ser rejeitado em vez de guardado, PS-7 falhar no padrão de identificador embora pareça inofensivo, e a leitura de cloro de 9,80 mg/L terminar em quarentena em vez de rejeição. Se você previu errado alguma delas, anote por quê; depois acrescente à lista de mensagens um caso ruim seu, como um número de sequência menor que 102 ou um identificador com um espaço no final, e confirme que o validador o rejeita antes de confiar na mudança.
3.7 Erros comuns e limites
- Validar com uma denylist ("rejeitar se contiver ponto e vírgula") em vez de uma allowlist do que é aceitável.
- Usar um casamento parcial para identificadores, de modo que
PS-7,XPS-04ouPS-04-testpassem. - Esquecer
nane o infinito. - Descartar em silêncio valores fora da faixa, de modo que um sensor com defeito pareça uma estação quieta.
- Atualizar o estado de reprodução antes que todas as outras verificações tenham passado — uma mensagem autenticada, mas malformada ou implausível, pode então bloquear mensagens legítimas que vêm depois (você verá isso no Capítulo 29).
- Escrever testes só para o caminho feliz.
A validação protege o receptor de dados malformados e implausíveis. Ela não prova que os dados são autênticos: uma mensagem perfeitamente formatada e plausível ainda pode ser forjada. A autenticação é acrescentada no Capítulo 15.
Exercícios — capítulo 3
EX007 — Preveja, depois execute. Objetivo: testar o seu modelo da ordem de validação. Entradas: Laboratório 3. Restrições: escreva as previsões antes de executar. Etapas: (1) acrescente três mensagens: PS-04|LEVEL_M|inf|1789999999|103, PS-04|LEVEL_M|12.0|1789999999|103, PS-04|LEVEL_M|-0.0|1789999999|104; (2) preveja cada desfecho; (3) execute e explique qualquer surpresa. Evidência esperada: previsões, saída, explicação. Rubrica: 3 = inf rejeitado por não ser finito, 12.0 aceito (limite inclusivo), -0.0 aceito porque −0,0 é igual a 0,0 e a sequência avançou, com um comentário sobre se −0,0 deveria ser normalizado; 2 = desfechos certos, explicação rasa; 1 = não executado; 0 = nada.
EX008 — Projeto da quarentena. Objetivo: projetar o caminho da quarentena. Entradas: Capítulo 3 ("Rejeitar, quarentenar, aceitar") e o caso do cloro da PS-07. Restrições: nenhum código é exigido. Etapas: (1) diga onde as mensagens quarentenadas são guardadas e por quanto tempo; (2) diga quem as revisa, com que rapidez, e o que dispara o escalonamento para a operação de Água; (3) diga o que a tela do operador deve mostrar enquanto um valor está em quarentena. Evidência esperada: projeto de meia página. Rubrica: 3 = armazenamento separado com retenção, revisor nomeado e tempo de resposta, regra de escalonamento ligada ao processo físico (por exemplo, duas leituras consecutivas de cloro implausíveis → ligar para a sala de controle), tela mostrando "valor retido — em revisão" em vez do último valor bom como se fosse atual; 2 = falta um elemento; 1 = "um administrador confere"; 0 = nada.
EX009 — Falhe fechado. Objetivo: encontrar um caminho que falha aberto. Entradas: este fragmento: try: v = float(val) except: pass; if v > hi: return "quarantine"; return "accept". Restrições: identifique, explique, corrija. Etapas: (1) mostre uma entrada para a qual ele retorna accept indevidamente; (2) explique; (3) reescreva-o. Evidência esperada: entrada, explicação, código corrigido. Rubrica: 3 = mostra que um valor não numérico deixa v sem definição ou obsoleto (um NameError na primeira mensagem, ou o valor da mensagem anterior reaproveitado depois), aponta a falta do limite inferior e o except sem tipo, e reescreve para retornar reject em qualquer falha de interpretação, com os dois limites e uma verificação de finitude; 2 = encontra um problema; 1 = vago; 0 = nada.
Síntese do capítulo
O código pequeno decide o que os operadores veem. Trate toda mensagem como não confiável, valide-a em camadas — estrutura, identidade, tipo, significado físico, tempo, ordem — e dê aos dados limítrofes um caminho de quarentena, para que as falhas não sejam nem escondidas nem guardadas como verdade. Mantenha as decisões em funções pequenas e testáveis, torne o estado explícito, falhe fechado e teste primeiro os casos ruins. Validação não é autenticação; essa vem depois.
Continue estudando — capítulo 03 de 30
← Capítulo 02 · Índice de capítulos do curso · Capítulo 04 →
Siga para a próxima lição escrita.