Skip to content

feat: MD5 no runtime Rust e rl.nextLine na lane C (fecha o 13º vermelho) - #17

Merged
filipeforattini merged 2 commits into
mainfrom
md5-and-readline-nextline
Sep 2, 2026
Merged

feat: MD5 no runtime Rust e rl.nextLine na lane C (fecha o 13º vermelho)#17
filipeforattini merged 2 commits into
mainfrom
md5-and-readline-nextline

Conversation

@filipeforattini

Copy link
Copy Markdown
Member

Dois buracos honestos declarados por frentes anteriores, um commit cada. Nenhuma dependência nova entra, e o 13º vermelho do differential fecha.

1. MD5 no runtime Rust — sem dependência nova

ring não tem MD5 e não vai ter: é uma biblioteca de primitivas modernas e MD5 está quebrado para todo uso de segurança. Node carrega MD5 mesmo assim, e o ecossistema publicado depende disso — ETag, chave de cache, fingerprint de conteúdo. Com o digest ausente, createHash("md5") cercava no frontend e o bridge da ilha respondia undefined, o que deixava o caso crypto-shims da suíte npm vermelho na lane rust.

MD5 (RFC 1321) escrito à mão em packages/runtime-rust/src/md5.rs — 140 linhas, sob o #![forbid(unsafe_code)] do crate. O runtime C já soletra o mesmo digest em scr_lib.c; esse é o arquivo de referência semântica contra o qual este foi escrito, então as duas lanes concordam byte a byte. HMAC-MD5 (RFC 2104, bloco 64) vem junto porque createHmac("md5", …) existe no Node, não porque HMAC-MD5 seja aconselhável — e o arquivo diz isso em voz alta: nada ali é constant time, é um checksum, nunca um autenticador.

A ligação não é um if no meio do caminho. As duas tabelas de algoritmo de crypto.rs viraram enums — CryptoDigest e CryptoHmac, com um braço Md5 e um braço Ring. Um único lugar decide qual implementação roda, então a cadeia estática fundida, o one-shot crypto.hash e o bridge da ilha concordam sobre a tabela por construção, em vez de por três listas que precisam ser mantidas em sincronia à mão. O braço None do lookup continua existindo — a ilha pergunta com uma string de runtime, e um nome que nenhum runtime carrega (sha3-256, blake2b512) ainda tem de RESPONDER undefined em vez de lançar, porque é o shim que levanta o "Digest method not supported" do Node.

O frontend destranca md5 junto (LOWERED_DIGEST_ALGORITHMS), e isso não custou nada do lado C: scr_crypto_digest_raw já respondia md5 e o HMAC já escolhia o bloco 64 pelo digest — a cerca do frontend era a única coisa que faltava lá. Hints de recusa, .d.ts ambiente e manifesto de superfície acompanham; nenhuma entrada mudou de classe, só as três notas.

2. rl.nextLine na lane C

for await (const line of rl) só existia no backend Rust; o emissor C recusava de propósito, o que deixava a fixture 2794 vermelha na lane C e — por queda no fallback C — também na LLVM. O plano de duas partes está no corpo do PR #14; as duas partes entram aqui, e as sondas contra o Node mudaram o desenho de uma delas.

(a) o modo waiter em scr_readline.c

O slot de callback pendente de question tinha quase a forma certa, mas faltava o modo "próxima linha OU fim": scr_rl_settle_close DESCARTAVA um callback pendente (a question do Node de fato nunca responde), enquanto nextLine precisa resolver undefined no fim e resolver de novo em toda chamada seguinte, que é o que termina o laço.

O que o plano não previa: nextLine não é "question que também responde undefined". É o único consumidor que SOBREVIVE às suas linhas. O for await do Node é um iterador EventEmitter.on('line') — ou seja, um LISTENER: uma linha parseada enquanto o corpo do laço roda (entre um await settlar e o nextLine seguinte) fica bufferizada, não se perde. Uma question não tem essa fila — entre duas perguntas nada escuta, que é exatamente por que uma linha não reclamada continua caindo numa interface só de question. A interface ganhou então uma fila de linhas, ligada na primeira vez que um programa pede nextLine; sem ela, uma linha que chegasse no intervalo entre duas iterações sumiria.

A segunda descoberta é o EOF, e ela explica uma assimetria que parecia bug nosso. Com printf 'a\nb' | node, o for await entrega "b" e uma question pendente não recebe nada. O motivo está no lib/internal/readline/interface.js: o onend emite 'line' DIRETAMENTE, enquanto o caminho por chunk passa por [kOnLine] — e só [kOnLine] responde uma question. Então a linha parcial final é do iterador e não da pergunta. O \r retido continua sendo a exceção, e agora por um motivo escrito: ele é um terminador de verdade cuja decisão \r\n expirou, veio pelo caminho comum, e a question responde.

(b) o adaptador de promise no emissor C

Segue o precedente raceAdapterFor, mas usando a máquina de resolve que já existe em vez de inventar uma. O call site cria scr_promise_new() e entrega ao runtime uma closure de resolve comum (scr_make_resolve_fn), cujo caps[0] segura a promise +1 e cujo scr_resolve_ref_impl a libera — a mesma máquina de new Promise, com uma resposta de readline onde estaria o resolve do executor. O adaptador internado por union monta o braço string da linha respondida, ou o braço undefined (a instância unitária imortal internada) no fim, e cumpre. Uma tag de arm é dado do programa, então a variabilidade inteira é uma thunk por forma de union.

Isso vive em emit-readline.ts, arquivo novo, e não dentro de emit-async.ts/emitter.ts: os dois estão no teto de dívida congelado do check-rust-file-lines, e engordá-los é justamente o que aquele gate proíbe. O teto de emit-exprs.ts desce de 7.964 para 7.963 pela extração.

Provas

gate resultado
2870-crypto-md5-digests.ts (novo), lane C verde
2870-crypto-md5-digests.ts, lane rust verde
2794-readline-async-iterator.mjs, lane C verde (era o 13º vermelho)
2794-readline-async-iterator.mjs, lane LLVM verde (via fallback C)
emit-c-readline-next-line.test.ts (novo) 9/9
cargo test (runtime-rust) 141 passam
cargo clippy -- -D warnings limpo
pnpm lint (line caps + island bootstrap + eslint) 0 erros
pnpm build limpo
pnpm manifest regenera; só as 3 notas mudam, nenhuma classe

O fixture do corpus cobre md5 sobre string, Buffer e bytes crus, digests hex e base64, o one-shot crypto.hash, e HMAC-MD5 com chave curta, chave ASCII e chave de 80 bytes — mais longa que o bloco, então substituída pelo próprio digest. Node é o oráculo, como em toda a lane.

Os testes unitários Rust usam o apêndice A.5 do RFC 1321, que fixa o digest E o padding (o vetor alfanumérico de 62 bytes é o que precisa do segundo bloco), mais as três fronteiras de padding em torno de um bloco (55, 56 e 64 bytes) e os casos 1, 2 e 6 do RFC 2202 para o HMAC.

O teste novo de readline existe porque as lanes do corpus fecham o stdin na hora: a única forma que elas conseguem fixar é "entrada vazia, o laço termina". As metades interessantes de um iterador assíncrono precisam de bytes reais no fd 0, e este teste fornece — linhas inteiras, linha final parcial (a que só o iterador vê), uma linha sem terminador nenhum, entrada vazia, só terminadores, CRLF, \r retido no fim, a ordem do evento 'close', e um close() chamado de dentro do corpo do laço. Node é a expectativa em todos os nove.

O que NÃO foi tocado

Os 13 vermelhos herdados do main que o PR #14 documentou continuam como estavam — este PR fecha o 13º item daquela lista (2794), não os 13 vermelhos de regressão anteriores a ela. E a decisão de semântica do read() (o follow-up do #14) continua aberta.

🤖 Generated with Claude Code

`ring` não tem MD5 e nunca vai ter — é uma biblioteca de primitivas
modernas e MD5 está quebrado para todo uso de segurança. Node carrega
MD5 mesmo assim, e o ecossistema publicado depende disso: ETag, chave de
cache, fingerprint de conteúdo. Com o digest ausente, `createHash("md5")`
cercava no frontend e a ilha respondia `undefined` no bridge, o que
deixava o caso `crypto-shims` da suíte npm vermelho na lane rust.

MD5 (RFC 1321) escrito à mão em `packages/runtime-rust/src/md5.rs` (140
linhas, sob o `#![forbid(unsafe_code)]` do crate): nenhuma dependência
nova entra por causa de um checksum legado. O runtime C já soletra o
mesmo digest em `scr_lib.c` — esse é o arquivo de referência semântica
contra o qual este foi escrito, então as duas lanes concordam byte a
byte. HMAC-MD5 (RFC 2104, bloco 64) vem junto porque
`createHmac("md5", …)` existe no Node, não porque seja aconselhável.

A ligação em `crypto.rs` não é um `if` no meio do caminho: as duas
tabelas de algoritmo viraram enums (`CryptoDigest`, `CryptoHmac`) com um
braço `Md5` e um braço `Ring`. Um único lugar decide qual implementação
roda, então a cadeia estática fundida, o one-shot `crypto.hash` e o
bridge da ilha concordam sobre a tabela por construção, em vez de por
três listas que precisam ser mantidas em sincronia.

O frontend destranca `md5` junto (`LOWERED_DIGEST_ALGORITHMS`), porque o
runtime C já respondia md5 em `scr_crypto_digest_raw` — a cerca era a
única coisa que faltava lá. Os hints de recusa, o `.d.ts` ambiente e o
manifesto de superfície acompanham; nenhuma entrada mudou de classe, só
as três notas.

Provas:

- `tests/corpus/2870-crypto-md5-digests.ts` (novo): md5 sobre string,
  Buffer e bytes crus, digests hex e base64, o one-shot, e HMAC-MD5 com
  chave curta, chave ASCII e chave de 80 bytes (mais longa que o bloco,
  então substituída pelo próprio digest). Node é o oráculo. Verde na
  lane C e na lane rust.
- `cargo test`: os vetores do apêndice A.5 do RFC 1321 fixam o digest E
  o padding (o vetor alfanumérico de 62 bytes é o que precisa do segundo
  bloco), mais as três fronteiras de padding em torno de um bloco (55,
  56, 64 bytes) e os casos 1, 2 e 6 do RFC 2202 para o HMAC. 141 testes
  passam.
- `cargo clippy -- -D warnings`: limpo (o digest usa `as_chunks`, não
  `chunks_exact`, que o clippy 1.98 recusa com tamanho constante).
`for await (const line of rl)` só existia no backend Rust; o emissor C
recusava de propósito ("the native async-iterator slice currently belongs
to the Rust runtime"), o que deixava a fixture 2794 vermelha na lane C e,
por queda no fallback C, também na LLVM. Uma implementação em C resolve
as duas. O plano de duas partes está no corpo do PR #14; as duas partes
entram aqui.

## (a) o modo waiter em scr_readline.c

O slot de callback pendente de `question` tinha quase a forma certa, mas
faltava o modo "próxima linha OU fim": `scr_rl_settle_close` DESCARTAVA
um callback pendente (a `question` do Node de fato nunca responde),
enquanto `nextLine` precisa resolver `undefined` no fim, e resolver de
novo em toda chamada seguinte para o laço terminar.

O que o plano não previa, e as sondas contra o Node mostraram, é que
`nextLine` não é só "question que também responde undefined" — é o único
consumidor que SOBREVIVE às suas linhas. O `for await` do Node é um
iterador `EventEmitter.on('line')`, ou seja, um LISTENER: uma linha
parseada enquanto o corpo do laço roda (entre um await settlar e o
`nextLine` seguinte) fica bufferizada, não se perde. Uma `question` não
tem essa fila — entre duas perguntas nada escuta, que é exatamente por
que uma linha não reclamada continua caindo numa interface só de
`question`. Então a interface ganhou uma fila de linhas, ligada na
primeira vez que um programa pede `nextLine`.

A segunda descoberta é o EOF, e ela explica uma assimetria que parecia
bug do nosso lado. Com `printf 'a\nb' | node`, o `for await` entrega
`"b"` e uma `question` pendente NÃO recebe nada. O motivo está no
`lib/internal/readline/interface.js`: o `onend` emite `'line'`
DIRETAMENTE, enquanto o caminho por chunk passa por `[kOnLine]` — e só
`[kOnLine]` responde uma `question`. Ou seja, a linha parcial final é do
iterador e não da pergunta. O `\r` retido continua sendo a exceção: ele
é um terminador de verdade cuja decisão `\r\n` expirou, veio pelo caminho
comum, e a `question` responde.

## (b) o adaptador de promise no emissor C

Seguindo o precedente `raceAdapterFor`, mas com a máquina de `resolve`
que já existe: o call site cria `scr_promise_new()` e entrega ao runtime
uma closure de resolve comum (`scr_make_resolve_fn`), cujo caps[0] segura
a promise +1 e cujo `scr_resolve_ref_impl` a libera. O adaptador
internado por union monta o braço `string` da linha respondida, ou o
braço `undefined` (a instância unitária imortal internada) no fim, e
cumpre. Uma tag de arm é dado do programa, então a variabilidade inteira
é uma thunk por forma de union.

Isso vive em `emit-readline.ts`, arquivo novo, e não dentro de
`emit-async.ts`/`emitter.ts`: os dois estão no teto de dívida congelado
do `check-rust-file-lines`, e engordá-los é justamente o que aquele gate
proíbe. O teto de `emit-exprs.ts` desce de 7.964 para 7.963 pela extração.

## Provas

- 2794-readline-async-iterator: **verde na lane C** e **verde na LLVM**
  (que cai no fallback C). Era o 13º vermelho do differential.
- `packages/compiler/test/emit-c-readline-next-line.test.ts` (novo): as
  lanes do corpus fecham o stdin na hora, então a única forma que elas
  fixam é "entrada vazia, o laço termina". As metades interessantes de um
  iterador assíncrono precisam de bytes reais no fd 0, e este teste
  fornece: linhas inteiras, linha final parcial (a que só o iterador vê),
  uma linha sem terminador nenhum, entrada vazia, só terminadores, CRLF,
  `\r` retido no fim, a ordem do evento 'close', e um `close()` chamado
  de dentro do corpo do laço. Node é a expectativa em todos. 9/9 passam.
- `pnpm lint` (line caps + island bootstrap + eslint): 0 erros.
@filipeforattini
filipeforattini merged commit ca41252 into main Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant