firebird-wire 0.1.5

Pure-Rust sync driver for Firebird 5+ (wire protocol, SRP auth, batch, services, events)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# Notas de engenharia reversa do wire-protocol do Firebird 5

Capturado do `/opt/firebird/bin/isql` real (FB 5.0.3, protocolo **v19**)
via `strace -f -x -e trace=sendto,recvfrom -s 4096`. Estes são os layouts de
bytes verdadeiros (ground-truth) que este driver tem como alvo. Servidor de
teste: `127.0.0.1:3555`, `employee`, SYSDBA/masterkey, `WireCrypt=Disabled`.

## FEITO e validado (commitado)

- **Handshake**: op_connect → op_accept_data(94) → prova SRP no DPB de attach →
  op_response. Veja `connection.rs`. Auth = Srp256, prova = SHA1 para H(user),
  hash do plugin (SHA256) apenas para o M externo. Tags CNCT: specific_data=7,
  plugin_name=8, login=9, plugin_list=10, client_crypt=11.
- **Op codes** (corrigidos vs. memória antiga, deslocados em +2 a partir de
  op_trusted_auth):
  trusted_auth=90, cancel=91, cont_auth=92, ping=93, accept_data=94,
  crypt=96, cond_accept=98, batch_create=99..batch_cs=103, info_batch=111,
  fetch_scroll=112. response=9, attach=19, detach=21.
- **Transações**: op_transaction(29)/commit(30)/rollback(31) — funcionando.
- Sucesso do vetor de status = `[isc_arg_gds(1), 0, isc_arg_end(0)]`; **o código
  gds 0 é sucesso, não um erro**.

## VALIDADO AO VIVO — camada de instruções (statements)

O código está em `statement.rs` (+ `blr.rs`, `message.rs`, `value.rs`). **Todos
os 6 testes de `tests/integration.rs` passam contra um FB5 real** (protocolo v19,
`employee`): connect/ping, transações, prepare+describe, execute+fetch (104
linhas), query parametrizada (1 linha) e contagem de linhas afetadas (UPDATE de 5
linhas). Rodam com `FB_PASSWORD` definido. Fluxo: allocate → ler handle → prepare
(describe-info extraída dos dados do op_response) → execute → buscar linhas em
lote → free. A instrução é enviada **sequencialmente** (allocate, ler a resposta
para o handle real, depois prepare).

**Linhas afetadas — `op_info_sql` (70) + `isc_info_sql_records` (0x17).** Envia o
item 0x17; a resposta traz um bloco aninhado com os contadores `isc_info_req_*`
(select=13, insert=14, update=15, delete=16), cada um `tag(1)+len(2 LE)+valor`.
Um UPDATE de N linhas reporta select=N e update=N. Veja `Statement::rows_affected`.

**BLOBs (leitura e escrita) — validado ao vivo.** Veja `blob.rs`.

- **Correção de op codes:** os `*_blob2` estavam deslocados em 1 no `consts.rs`.
  A enum é sequencial: op_ddl=55, **op_open_blob2=56**, op_create_blob2=57,
  op_get_slice=58, op_put_slice=59, op_slice=60, **op_seek_blob=61**,
  op_allocate_statement=62. A faixa baixa (op_get_segment=36, op_close_blob=39)
  estava certa.
- **`op_open_blob2` (56):** `bpb(cstring) | transaction(i32) | blob_id(quad 8B)`  a BPB vem ANTES da transação (fall-through do op_open_blob no xdr). Resposta:
  op_response com `p_resp_object` = handle do blob.
- **`op_get_segment` (36):** `blob_handle | buffer_len(i32) | segment(cstring vazia)`.
  Resposta: op_response onde `p_resp_object` = status (0=ok/mais, 1=isc_segment
  parcial, 2=isc_segstr_eof) e `p_resp_data` = segmentos empacotados, cada um
  `comprimento(2 LE) + bytes`.
- **`op_close_blob` (39):** só o handle. Resposta op_response.
- **`op_create_blob2` (57):** mesmo layout do `op_open_blob2``bpb(cstring) |
  transaction(i32) | blob_id(quad 8B, ignorado — enviar 0)`. Resposta: op_response
  com `p_resp_object` = novo handle, `p_resp_blob_id` = blob_id atribuído.
- **`op_put_segment` (37):** `blob_handle(i32) | segment_len(i32) | data(cstring)`.
  O cstring contém os bytes brutos SEM prefixo de 2 bytes LE. `segment_len` == tamanho
  do cstring. **Atenção:** o cliente C da fbclient envolve os dados com um prefixo de
  2 bytes LE para suportar batching de segmentos num único op, mas o servidor armazena
  o conteúdo do cstring verbatim — portanto enviamos bytes puros.
- **`op_cancel_blob` (38):** só o handle. Resposta op_response. Descarta o blob.
- **Inline blobs (FB5):** com `inline_blob_size = 0xffff` no op_execute (o que o
  fbclient envia), o servidor EMBUTE blobs pequenos na resposta do fetch e o
  cliente nunca manda op_open_blob/op_get_segment — por isso uma captura strace
  do fbclient não mostra ops de blob. Nós enviamos `inline_blob_size = 0` para
  desativar o inline e ler pelo protocolo clássico (também serve para blobs
  grandes).

Três descobertas confirmadas por captura strace do fbclient/isql real:

1. **`isc_info_sql` owner=18, alias=19** (NÃO alias=18/owner=19). A tag 0x12
   carrega o owner da tabela, 0x13 carrega o alias da coluna. Corrigido em
   `consts.rs`.

2. **`op_execute` (63), layout da v19 — exatamente 9 palavras sem parâmetros:**
   ```
   op_execute, statement, transaction,
   in_blr(cstring), in_message_number, in_message_count,
   out_blr(cstring), out_message_number,
   inline_blob_size = 0x0000ffff    ← UM campo final (FB5, proto ≥18)
   ```
   NÃO há campo `timeout` aqui. Enviar 10 palavras (timeout + inline separados)
   faz o servidor **fechar a conexão**.

3. **A mensagem de parâmetros vem ENTRE `in_message_count` e `out_blr`** (não no
   fim do pacote), em formato compacto (bitmap de nulos + valores XDR):
   ```
   ... in_blr(len12+dados), in_message_number=0, in_message_count=1,
   00 00 00 00   ← bitmap de nulos (nada nulo)
   00 00 00 02   ← emp_no = 2 (SHORT como long big-endian de 4 bytes)
   00 00 00 00   ← out_blr (len 0)
   00 00 00 00   ← out_message_number
   00 00 ff ff   ← inline_blob_size
   ```

4. **op_fetch em lote:** ao pedir `op_fetch` (out_message_count=N), o servidor
   transmite vários `op_fetch_response` (status 0, count 1 + mensagem) e termina
   com um pacote `count=0` (status 100 = fim do cursor; status 0 = limite do lote
   atingido, há mais). É preciso drenar todos até o terminador — buscar 1 por vez
   dessincroniza o stream.

## A FAZER: statements — capturas de referência abaixo

### op_allocate_statement (62) + op_prepare_statement (68), em lote num único envio
```
00 00 00 3e                op_allocate_statement
00 00 00 00                db_handle
00 00 00 44                op_prepare_statement
00 00 00 02                transaction handle
ff ff ff ff                statement handle = -1 (diferido; use o resultado do allocate)
00 00 00 03                dialect = 3
00 00 00 36 <54 bytes>     texto SQL "SELECT emp_no, first_name FROM employee WHERE emp_no=2"
00 00 00 1a <26 bytes>     requisição de info-items, depois pad, depois buffer_len i32
```
Info-items solicitados (26 bytes): `15 1b 05 07 09 0b 0c 0d 0e 10 11 12 13 08  04 07 09 0b 0c 0d 0e 10 11 12 13 08`
= stmt_type(0x15), 0x1b(flags?), depois bloco BIND `05 07[describe_vars] {09 0b 0c 0d 0e 10 11 12 13} 08`, depois bloco SELECT `04 07 {…} 08`.
buffer_len que o isql usou ≈ 0xfb80. O allocate retorna o handle real da instrução (o ‑1 diferido funciona com envio lazy; nós limitamos em ptype_batch_send, então é só enviar o allocate, ler a resposta, pegar o handle e então fazer o prepare com ele).

### RESPOSTA do op_prepare (campo data do op_response, info de descrição)
Fluxo de info (comprimentos em little-endian): cada item = tag(1) + len(2 LE) + value.
```
15 04 00  01 00 00 00      isc_info_sql_stmt_type = 1 (select)
1b 04 00  03 00 00 00      item 0x1b = 3 (ignorar)
05                         isc_info_sql_bind  (bloco de parâmetros de entrada)
  07 04 00  00 00 00 00    describe_vars = 0  (sem parâmetros)
04                         isc_info_sql_select (bloco de saída)
  07 04 00  02 00 00 00    describe_vars = 2  (emp_no, first_name)
  09 .. (sqlda_seq) 0b ..(type) 0c..(subtype) 0d..(scale) 0e..(length)
  10..(field) 11..(relation) 12..(alias) 13..(owner) 08 (describe_end)  por var
```
Parsing: percorra os itens; para cada var colete type/subtype/scale/length/nomes.

### op_execute (63) — layout de campos da v19 (SELECT sem parâmetros), em lote com op_fetch
```
00 00 00 3f                op_execute
00 00 00 03                statement handle
00 00 00 01                transaction handle
00 00 00 00                in_blr   (cstring, len 0)
00 00 00 00                in_message_number
00 00 00 00                in_message_count
00 00 00 00                out_blr  (cstring, len 0)  [campos estilo execute2 presentes na v19]
00 00 00 00                out_message_number
00 00 00 00                timeout (FB4+)
00 00 ff ff                ??? final — reverificar o campo exato (cursor_flags / inline_blob_size). 4 palavras zero + "00 00 ff ff"; conte com precisão usando um decodificador antes de confiar.
```
NOTA: a resposta do op_execute é op_response (sucesso). Para SELECT as linhas vêm
do op_fetch.

### op_fetch (65)
```
00 00 00 41                op_fetch
00 00 00 03                statement handle
00 00 00 13 <19B> pad      out_blr (cstring, 19 bytes)
00 00 00 00                out_message_number
00 00 03 e8                out_message_count = 1000 (tamanho do lote)
```
out_blr (19 bytes) para [emp_no SMALLINT, first_name VARCHAR(15)]:
```
05            blr_version5
02            blr_begin
04 00         blr_message, message#0
04 00         contagem de campos = 4  (= 2 colunas × {dado + indicador-de-nulo})
07 00         blr_short scale 0      (emp_no)
07 00         blr_short scale 0      (indicador de nulo)
26 00 00 0f 00 blr_varying2 charset 0 length 15  (first_name)  [0x26=38]
07 00         blr_short scale 0      (indicador de nulo)
ff            blr_end
4c            blr_eoc
```
Códigos de tipo BLR vistos: blr_short=7, blr_varying2=38(0x26) [charset(2 LE)+len(2 LE)],
blr_version5=5, blr_begin=2, blr_message=4, blr_end=255, blr_eoc=76.

### op_fetch_response (66) + mensagem de linha  — ⚠️ LAYOUT DE NULOS NÃO RESOLVIDO
```
00 00 00 42   op_fetch_response
00 00 00 00   status = 0 (linha presente;  100 = fim do cursor)
00 00 00 01   count = 1 (mensagens neste pacote; 0 = nenhuma)
<os bytes da mensagem seguem, depois mais pacotes op_fetch_response até count=0>
```
Linha para emp_no=2, first_name="Robert" (ambos NOT NULL) = **20 bytes**:
```
00 00 00 00   <- palavra inicial = 0
00 00 00 02   <- emp_no = 2  (XDR: SMALLINT enviado como long big-endian de 4 bytes)
00 00 00 06   <- comprimento do varchar = 6
52 6f 62 65 72 74   "Robert"
00 00         <- 2 bytes finais
```
### RESOLVIDO — formato da mensagem de linha
Verificado com uma captura de NULL forçado (`SELECT emp_no, CAST(NULL AS VARCHAR(15)) … WHERE emp_no=2`)
→ mensagem de 8 bytes `02 00 00 00  00 00 00 02`. Comparando as duas linhas:

**Mensagem de linha = bitmap de nulos, depois os valores codificados em XDR apenas das colunas NÃO-NULAS.**
- Bitmap de nulos: `align4(ceil(ncols/8))` bytes (4 bytes para ≤32 colunas),
  **little-endian**, bit *i* ligado ⇒ coluna *i* É NULL.
- Depois, para cada coluna **em ordem, apenas se não for nula**, seu valor XDR:
  - SMALLINT/INTEGER → big-endian de 4 bytes (com extensão de sinal)
  - BIGINT/INT64 → big-endian de 8 bytes
  - FLOAT → 4 bytes, DOUBLE → 8 bytes
  - VARCHAR → comprimento(4 BE) + bytes + pad para 4
  - CHAR(n) → n bytes + pad para 4
  - DATE/TIME → 4 bytes; TIMESTAMP → 8 bytes (date long + time long)
  - BLOB → quad/blob-id (8 bytes)
  - Colunas NULAS contribuem com **zero** bytes para a seção de dados.

Exemplos:
- `[emp_no=2, "Robert"]``00000000`(máscara) `00000002`(emp_no) `00000006`+"Robert"+`0000` = 20 B
- `[emp_no=2, NULL]``02000000`(máscara, bit1) `00000002`(emp_no) = 8 B

(O out_blr que ENVIAMOS ainda declara 2 campos de dado + 2 null-short conforme a captura;
a camada XDR empacota os nulos no bitmap inicial no wire. Codifique os parâmetros da
mesma forma para INSERT: bitmap de nulos inicial + valores XDR não-nulos.)

### `op_exec_immediate` (64) — DDL/DML sem prepare
Confirmado via strace de `isc_start_transaction` + `isc_dsql_execute_immediate` (cliente C mínimo):
```
00 00 00 40   op_exec_immediate
00 00 00 01   tx_handle     ← CAMPO 1 é a transação (não o banco!)
00 00 00 00   db_handle     ← CAMPO 2 é o banco de dados
00 00 00 03   dialect = 3
<cstring: SQL text>
<cstring: items (vazio)>
00 00 00 00   buffer_length = 0
```
**Atenção:** a ordem é `tx_handle | db_handle` — oposta à expectativa baseada no nome `p_exnod_database`.
O servidor v19 NÃO tem campo de timeout extra (ao contrário de op_prepare/op_execute no v16+).
O handle de transação deve ser real (não 0); tx_handle=0 falha para DDL mesmo com db_handle correto.
O driver cria uma transação implícita e faz commit quando `tx=None` é passado para `exec_immediate`.

### DML em lote (`op_batch_*`, 99–103) — RESOLVIDO
Capturado de um cliente C++ usando a interface OO `IBatch` (ver `11.batch.cpp`),
sob `strace -f -x -e trace=sendto,recvfrom`. Fluxo: allocate+prepare (já
conhecidos), depois:

**`op_batch_create` (99):**
```
00 00 00 63   op
00 00 00 02   stmt handle
[CSTRING]     blr da mensagem: len(4) + BLR + pad   (igual ao in_blr de op_execute)
00 00 00 1e   p_batch_msglen = tamanho do buffer de mensagem do CLIENTE (não compactado)
[CSTRING]     pb: len(4) + parameter block + pad
```
- O `msglen` é o layout que o BLR descreve (campo alinhado + indicador de nulo
  SQL_SHORT cada), SEM arredondamento final. INTEGER+VARCHAR(20)=30. Ver
  `message::message_buffer_len`.
- O PB usa byte de versão (1) + clumplets com comprimento LE de 4 bytes:
  `01 02 04000000 01000000` = versão1 + TAG_RECORD_COUNTS(2) len=4 valor=1.
  Outras tags: MULTIERROR=1, BLOB_POLICY=4 (ver `batch_tag`).

**`op_batch_msg` (100):** `stmt | count(u32) | mensagens`. Cada mensagem está no
mesmo formato compacto de `op_execute` (bitmap de nulos LE + valores XDR das
não-nulas), já alinhado a 4; concatenadas sem moldura entre elas.

**`op_batch_exec` (101):** `stmt | transaction`. Responde com `op_batch_cs`.

**`op_batch_cs` (103)** — estado de conclusão (resposta do exec):
```
op | stmt | reccount | updates | vectors | errors |
updates × i32   (contagens por msg: >=0 linhas; -1=EXECUTE_FAILED; -2=SUCCESS_NO_INFO)
vectors × (pos u32 + status vector)   (erros detalhados por msg)
errors  × u32   (lista simples de posições com erro; vazia quando há detalhados)
```
Confirmado com erros forçados (PK duplicada + MULTIERROR): `updates=[1,1,-1,1,-1]`,
`vectors=2` com posições 2 e 4 (cada uma com seu status vector de violação de PK).

**`op_batch_rls` (102):** `stmt` (libera o batch). O cliente C ainda envia
`op_free_statement(67)` depois para soltar a instrução.

**`op_batch_sync` (110):** só o op code (sem handle). O cliente C agrupa
create+msg e usa o sync para drenar as respostas adiadas; o servidor responde a
cada op deferido com um `op_response` (3 respostas de 32 bytes coalescidas numa
recv de 96 bytes). Como nosso driver é síncrono (lê a resposta de cada op na
hora), NÃO precisamos de batch_sync. Ver `batch.rs`.

## Cursores roláveis (`op_fetch_scroll`, FB5)

Decodificado de um cliente C++ (`/tmp/fbscroll/scroll.cpp`) usando a OO API:
`openCursor(..., IStatement::CURSOR_TYPE_SCROLLABLE)` seguido de
`fetchAbsolute/Prior/Last/First/Relative/Next`, sob `strace -e sendto`.

**Abrir cursor rolável — `op_execute` (63):** o pacote é **idêntico** ao de um
cursor normal; a ÚNICA palavra que muda é `cursor_flags`, logo após `out_blr`:
```
op_execute | stmt | tx | in_blr(cstring) | in_msg_number | in_msg_count |
            out_blr(cstring) | cursor_flags | inline_blob_size(proto>=18)
```
`cursor_flags = 1` (CURSOR_TYPE_SCROLLABLE) abre rolável; `0` = normal. O
op_execute (≠ op_execute2) NÃO carrega `out_message_number` nessa posição — o que
o driver antes rotulava assim era de fato `cursor_flags` (e enviava 0, por sorte
correto para não-rolável). fbclient também envia `inline_blob_size = 0xffff`; nós
mandamos 0 (sem inline de blob).

**`op_fetch_scroll` (112):**
```
op | stmt | out_blr(cstring) | message_number | fetch_count | direction | offset
```
- `direction`: NEXT=0, PRIOR=1, FIRST=2, LAST=3, ABSOLUTE=4, RELATIVE=5
  (conferem com `scroll::*` em consts.rs).
- `offset`: posição absoluta (1-based) para ABSOLUTE; deslocamento com sinal para
  RELATIVE; 0 nas demais.
- `fetch_count`: o fbclient manda 1 nos saltos (ABSOLUTE/RELATIVE/FIRST/LAST) e
  faz prefetch (1000) só em PRIOR/NEXT sequenciais. Nosso driver manda sempre 1.

Resposta: `op_fetch_response (66)` igual ao fetch normal — `status | count` por
linha, terminador com `count=0`; `status=100` ⇒ posição fora do cursor (sem
linha). Ver `Statement::fetch_scroll` em `statement.rs`.

### BLOBs em batch (`op_batch_blob_stream`, 105) — RESOLVIDO

Política `BLOB_STREAM`: no `op_batch_create`, o PB ganha o clumplet
`TAG_BLOB_POLICY(4) = 3`. Crucial: o `message_blr` da instrução precisa declarar
a coluna BLOB com **`blr_blob2`(17)** = `17 | sub_type(2 LE) | charset(2 LE)`
(não `blr_quad`!); senão o servidor não vê coluna de blob e o
`op_batch_blob_stream` falha com `isc_batch_blobs` ("no blobs associated with
batch statement"). A linha referencia o blob pelo id (quad) no campo BLOB.

`op_batch_blob_stream` (105): `op | stmt | length(u32) | stream`.
- `stream` = concatenação CRUA (sem padding entre blobs) dos blobs, cada um
  `id(quad 8B BE) | size(4B BE) | bpb_size(4B BE) | bpb | dados`. Tudo big-endian.
- `length` ≠ bytes no wire: é o tamanho do BUFFER que o servidor aloca, a soma de
  `align4(16 + bpb + dados)` por blob, e **deve ser múltiplo de 4** (senão o
  servidor rejeita e fecha). O servidor (`xdr_blob_stream`) percorre o stream
  lendo cada blob com `xdr_quad`/`xdr_u_long`/`xdr_bytes` (que NÃO dão padding no
  wire) e avança o ponteiro do buffer com alinhamento de 4 SEM consumir bytes do
  wire; o laço para quando o que resta é < 16 (cabeçalho parcial) ou chega a 0.
  Por isso o wire carrega menos bytes que `length`. Captura: dados de 14 B →
  conteúdo wire 30 B, `length` 32; dados de 17 B → 33 B, `length` 36.
- Os blobs vão ANTES das mensagens (`op_batch_msg`). O `op_batch_msg` codifica o
  campo BLOB da linha como o id (quad 8B BE), igual a qualquer `Value::Blob`.
- A próxima op após o blob stream pode começar em offset NÃO múltiplo de 4 (o
  fbclient coalesce blob_stream + msg num envio só; nós enviamos separados e o
  servidor lê em sequência sem problema). Resposta: `op_response` normal.

Ver `Batch::add_blob` / `execute` em `batch.rs`. 1 teste ao vivo
(`batch_blob_stream`, 3 blobs de tamanhos diferentes, lidos de volta e conferidos).

## Wire-crypt ChaCha (`op_crypt`, 96 + `op_cont_auth`, 92) — VALIDADO AO VIVO

**Fluxo real (capturado do isql contra um servidor `WireCrypt=Required`):**
1. `op_connect``op_cond_accept` (98): o accept condicional vem com o buffer de
   chaves **vazio** (a auth precisa continuar). O driver antes parava aqui e
   embutia a prova SRP no DPB do attach — por isso NUNCA recebia o nonce e o
   wire-crypt jamais funcionava.
2. `op_cont_auth` (92): `data(prova hex, cstring) | name(plugin) | list(plugins) |
   keys(cstring vazia)`. → resposta `op_response` cujo `p_resp_data` é o buffer de
   chaves de cifra.
3. **Buffer de chaves** = clumplets `tag(1) | len(1) | dados`:
   - `tag 00` = tipo da chave (`"Symmetric"`).
   - `tag 01` = lista de plugins (`"ChaCha ChaCha64 Arc4"`, separada por espaço).
   - `tag 03` = dados específicos do plugin: `"<plugin>\0"` + IV. Para ChaCha:
     `"ChaCha\0"` + 16 bytes (os 12 primeiros = nonce, os 4 últimos = contador 0).
     Para ChaCha64: `"ChaCha64\0"` + 8 bytes.
   - `find_after(keys, b"ChaCha\0", 12)` já extrai o nonce direto desse buffer.
4. `op_crypt` (96): `plugin(cstring) | "Symmetric"(cstring)`. Logo após enviá-lo,
   habilita a cifra; a resposta do op_crypt já vem CRIPTOGRAFADA.
5. `op_attach` (criptografado) — sem prova no DPB (já autenticado por cont_auth).

Servidores `WireCrypt=Disabled` mandam `op_accept_data` (94, não cond): aí o driver
segue o atalho antigo (prova no DPB, sem crypt). O caminho cont_auth só dispara em
`op_cond_accept` com `wire_crypt != Disabled`. Ver `continue_auth`/`negotiate_crypt`
em `connection.rs`. Validar com instância privada: copiar `security5.fdb`, conf com
`WireCrypt=Required` + `RemoteServicePort=3556`, e rodar com `FB_CRYPT_DB`.

### (histórico) detalhes da cifra

Confirmado pela fonte do FB (`src/plugins/crypt/chacha/ChaCha.cpp`) + driver Go
(`nakagami/firebirdsql`):
- **Chave** = `SHA-256(K)` (32 bytes), onde `K` é a chave de sessão SRP. O Arc4,
  por contraste, usa `K` direto.
- **Nonce**: NÃO é trocado via callback. O servidor anuncia cada plugin no buffer
  `keys` do handshake (o mesmo `accept.keys` que já varremos por `"Arc4"`); para
  ChaCha o nome vem seguido de `\0` e do nonce: `"ChaCha\0"` + 12 bytes (IETF,
  contador de 32 bits) ou `"ChaCha64\0"` + 8 bytes (contador de 64 bits).
- **op_crypt** (96): `plugin(cstring) | "Symmetric"(cstring)` — idêntico ao Arc4,
  só muda o nome. Contador inicial 0; mesma chave+nonce nas DUAS direções.
- A cifra ChaCha20 está em `wirecrypt.rs` e passa o vetor da RFC 8439 §2.3.2.
- **Ressalva:** ponta a ponta não testado — o servidor de teste está
  `WireCrypt = Disabled` e não anuncia plugins (logo o Arc4 também nunca rodou ao
  vivo aqui). Validar com um servidor `WireCrypt = Required`.

### BLOBs segmentados em batch (`op_batch_set_bpb`, 106) — RESOLVIDO

`op_batch_set_bpb` (106): `op | stmt | bpb(cstring)`. O servidor lê o
`isc_bpb_type` do BPB e liga/desliga a flag `FLAG_DEFAULT_SEGMENTED` do batch.
O BPB do fbclient para segmentado = `01 03 04 00000000` = versão1, tag
`isc_bpb_type`(3), len=4, valor `isc_bpb_type_segmented`(0). Enviado antes do
stream de blobs.

Com a flag segmentada ligada, os blobs no `op_batch_blob_stream` têm os dados
enquadrados em segmentos. **No wire**, cada segmento é `u32` big-endian
(comprimento, ex.: `00 00 00 13` = 19) + os bytes, concatenados SEM padding
(confirmado na Parte 3 do `11.batch.cpp`: segmentos d1/\\n/d2/\\n/d3). **Mas** o
campo `size` do blob e o comprimento do stream seguem a contabilidade do *buffer*
do servidor (`xdr_blob_stream`), que usa cabeçalho de 2 bytes alinhado a
`BLOB_SEGHDR_ALIGN`=2: `size = bpb_len + Σ align2(seg_len + 2)`. O `xdr_u_short`
do header de segmento ocupa 4 bytes no wire mas só 2 na contabilidade do buffer —
por isso wire e `size`/`length` divergem (mesmo padrão do stream de blobs). Ver
`Batch::add_blob`/`set_segmented` em `batch.rs`. 1 teste ao vivo
(`batch_segmented_blob`).

Todos os op codes de batch (99–106) estão implementados e testados ao vivo:
create/msg/exec/rls/cs, blob_stream (105), regblob (104) e set_bpb (106).

## Eventos assíncronos (canal auxiliar) — VALIDADO AO VIVO

Decodificado de um cliente C (`isc_event_block` + `isc_wait_for_event`) sob
`strace -e trace=network,read`, com `POST_EVENT` disparado de um isql paralelo.

1. **`op_connect_request` (53):** `op | type(=1 async) | db_handle | partner(=0)`.
   Resposta `op_response` com `p_resp_data` = `sockaddr_in` de 16 bytes:
   `família(2, host-endian) | porta(2, BE de rede) | ip(4) | zeros(8)`. O cliente
   abre um SEGUNDO socket TCP para `(ip do servidor, porta)` = canal auxiliar.
2. **`op_que_events` (48):** `op | db_handle | epb(cstring) | ast(4=0) | arg(4=0) |
   event_id(4)`. EPB = `versão(1) | [namelen(1) | nome | count(4 LE)]…`. O
   `event_id` é escolhido pelo cliente. Resposta: `op_response`.
3. **`op_event` (52)** chega pelo canal auxiliar quando alguém faz `POST_EVENT` +
   commit: `op | db_handle | epb(cstring com counts atualizados, LE) | ast(4) |
   event_id(4)`. Comparando os counts com os anteriores sabe-se o que disparou.
4. **`op_cancel_events` (49):** `op | db_handle | event_id`.

Eventos são one-shot: re-registrar (`op_que_events`) após cada `op_event`. O
canal auxiliar é só-leitura no cliente (o servidor empurra `op_event`); não há
handshake nele. Ver `events.rs` (`Connection::listen_events`/`EventListener`).

## Gerenciador de serviços (`op_service_*`) — VALIDADO AO VIVO

Decodificado de um `strace -e trace=network` do `fbsvcmgr` contra o servidor de
exemplo (porta 3555). O **handshake é idêntico** ao de um attach de banco
(`op_connect` + accept + SRP + wire-crypt); só muda a operação anunciada no bloco
connect (`op_service_attach` = 82) e o "arquivo" (`service_mgr`). Está fatorado
em `connection::handshake`, reusado por `Connection` e `ServiceManager`.

1. **`op_service_attach` (82):** `op | obj(0) | "service_mgr"(cstring) | spb(cstring)`.
   O **SPB de attach** começa com DOIS bytes `isc_spb_version, isc_spb_current_version`
   (ambos `2`), seguidos de clumplets de comprimento de 1 byte (forma clássica):
   `isc_spb_user_name(28)` e a autenticação igual ao DPB —
   `isc_spb_auth_plugin_name(116)`, `isc_spb_auth_plugin_list(117)` e a prova SRP em
   `isc_spb_specific_auth_data(111)` (que divide o tag com `isc_spb_trusted_auth`).
   Resposta `op_response`; o `p_resp_object` é o handle do serviço (0 no exemplo).
   No caminho `WireCrypt=Required` a prova vai no `op_cont_auth` e o SPB não a leva
   (igual ao DPB: estados `Proof`/`Legacy`/`Done` em `AuthState`).
2. **`op_service_info` (84):** `op | handle | incarnation(0) | send_items(cstring) |
   recv_items(cstring) | buffer_length(i32)`. ATENÇÃO à ordem: os itens de
   **recepção vêm ANTES** do `buffer_length` (confirmado por strace). A resposta é
   um `op_response` cujo `p_resp_data` é um buffer de info: itens de **string** são
   `tag | len(2 LE) | dados`, terminados por `isc_info_end(1)`. Ex.: pedir
   `isc_info_svc_server_version(55)` devolve `37 1b00 "LI-V5.0.3…" 01`.
   (Itens **inteiros** como `isc_info_svc_version(54)` usam outra codificação — sem
   o prefixo de 2 bytes — e ainda não estão decodificados; ver TODO abaixo.)
3. **`op_service_start` (85):** `op | handle | incarnation(0) | spb(cstring)`. O
   primeiro byte do SPB é o código da ação (`isc_action_svc_*`). Ações sem
   argumentos (ex.: `isc_action_svc_get_fb_log(12)`) levam só esse byte. A saída
   textual é drenada DEPOIS, com chamadas repetidas de `op_service_info` pedindo
   `isc_info_svc_to_eof(63)` (ou `isc_info_svc_line(62)`) até vir vazio.
4. **`op_service_detach` (83):** `op | handle`.

Ver `service.rs` (`ServiceManager`). **TODO:** (a) decodificar itens de info
INTEIROS (`svc_version`, `svc_running`, `svr_db_info`); (b) a codificação do SPB de
**ação** com argumentos (strings com len de 2 bytes? inteiros sem len?) — precisa de
um strace de `op_service_start` com `gbak`/`gstat` para confirmar antes de expor
`backup`/`restore`/`statistics`.