feat: MD5 no runtime Rust e rl.nextLine na lane C (fecha o 13º vermelho) - #17
Merged
Conversation
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ringnã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 respondiaundefined, o que deixava o casocrypto-shimsda 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 emscr_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 porquecreateHmac("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
ifno meio do caminho. As duas tabelas de algoritmo decrypto.rsviraram enums —CryptoDigesteCryptoHmac, com um braçoMd5e um braçoRing. Um único lugar decide qual implementação roda, então a cadeia estática fundida, o one-shotcrypto.hashe 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çoNonedo lookup continua existindo — a ilha pergunta com uma string de runtime, e um nome que nenhum runtime carrega (sha3-256,blake2b512) ainda tem de RESPONDERundefinedem vez de lançar, porque é o shim que levanta o "Digest method not supported" do Node.O frontend destranca
md5junto (LOWERED_DIGEST_ALGORITHMS), e isso não custou nada do lado C:scr_crypto_digest_rawjá 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.tsambiente e manifesto de superfície acompanham; nenhuma entrada mudou de classe, só as três notas.2.
rl.nextLinena lane Cfor 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.cO slot de callback pendente de
questiontinha quase a forma certa, mas faltava o modo "próxima linha OU fim":scr_rl_settle_closeDESCARTAVA um callback pendente (aquestiondo Node de fato nunca responde), enquantonextLineprecisa resolverundefinedno fim e resolver de novo em toda chamada seguinte, que é o que termina o laço.O que o plano não previa:
nextLinenão é "question que também responde undefined". É o único consumidor que SOBREVIVE às suas linhas. Ofor awaitdo Node é um iteradorEventEmitter.on('line')— ou seja, um LISTENER: uma linha parseada enquanto o corpo do laço roda (entre um await settlar e onextLineseguinte) fica bufferizada, não se perde. Umaquestionnão tem essa fila — entre duas perguntas nada escuta, que é exatamente por que uma linha não reclamada continua caindo numa interface só dequestion. A interface ganhou então uma fila de linhas, ligada na primeira vez que um programa pedenextLine; 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, ofor awaitentrega"b"e umaquestionpendente não recebe nada. O motivo está nolib/internal/readline/interface.js: oonendemite'line'DIRETAMENTE, enquanto o caminho por chunk passa por[kOnLine]— e só[kOnLine]responde umaquestion. Então a linha parcial final é do iterador e não da pergunta. O\rretido continua sendo a exceção, e agora por um motivo escrito: ele é um terminador de verdade cuja decisão\r\nexpirou, veio pelo caminho comum, e aquestionresponde.(b) o adaptador de promise no emissor C
Segue o precedente
raceAdapterFor, mas usando a máquina deresolveque já existe em vez de inventar uma. O call site criascr_promise_new()e entrega ao runtime uma closure de resolve comum (scr_make_resolve_fn), cujocaps[0]segura a promise +1 e cujoscr_resolve_ref_impla libera — a mesma máquina denew Promise, com uma resposta de readline onde estaria oresolvedo executor. O adaptador internado por union monta o braçostringda linha respondida, ou o braçoundefined(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 deemit-async.ts/emitter.ts: os dois estão no teto de dívida congelado docheck-rust-file-lines, e engordá-los é justamente o que aquele gate proíbe. O teto deemit-exprs.tsdesce de 7.964 para 7.963 pela extração.Provas
2870-crypto-md5-digests.ts(novo), lane C2870-crypto-md5-digests.ts, lane rust2794-readline-async-iterator.mjs, lane C2794-readline-async-iterator.mjs, lane LLVMemit-c-readline-next-line.test.ts(novo)cargo test(runtime-rust)cargo clippy -- -D warningspnpm lint(line caps + island bootstrap + eslint)pnpm buildpnpm manifestO 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,
\rretido no fim, a ordem do evento'close', e umclose()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