Skip to main content

firebird_wire/
service.rs

1//! O **gerenciador de serviços** do Firebird (Service Manager): backup/restore
2//! (gbak), estatísticas (gstat), reparos (gfix), gestão de usuários, leitura do
3//! log do servidor, etc.
4//!
5//! Um anexo de serviço usa o MESMO handshake de uma conexão de banco (connect +
6//! SRP + wire-crypt), mas com a operação `op_service_attach` (82) e o "arquivo"
7//! especial `service_mgr`. Em vez de um DPB, o attach carrega um **SPB** (Service
8//! Parameter Buffer). O fluxo de wire (decodificado de um strace do `fbsvcmgr`):
9//!
10//! 1. `op_service_attach` (82): `op | obj(0) | "service_mgr"(cstring) | spb(cstring)`.
11//!    O SPB começa com dois bytes `isc_spb_version, isc_spb_current_version`
12//!    (ambos `2`) e então clumplets: `user_name(28)`, e a autenticação igual ao
13//!    DPB — `auth_plugin_name(116)`, `auth_plugin_list(117)`,
14//!    `specific_auth_data(111)` (a prova SRP). A resposta traz o handle do serviço.
15//! 2. `op_service_info` (84): `op | handle | incarnation(0) | send_items(cstring)
16//!    | recv_items(cstring) | buffer_length(i32)`. A resposta é um `op_response`
17//!    cujo `p_resp_data` é um buffer de info clássico (`tag | len(2 LE) | valor …`
18//!    terminado por `isc_info_end`). É usada tanto para consultas (versão do
19//!    servidor, ambiente) quanto para drenar a saída textual de uma ação.
20//! 3. `op_service_start` (85): `op | handle | incarnation(0) | spb(cstring)`. O
21//!    primeiro byte do SPB é o código da ação (`isc_action_svc_*`), seguido de
22//!    seus argumentos. Dispara a ação; a saída é lida depois via `op_service_info`.
23//! 4. `op_service_detach` (83): `op | handle`.
24//!
25//! **Codificação do SPB de uma ação** (decodificada de straces de
26//! `fbsvcmgr action_backup/action_db_stats`): difere tanto do SPB de attach
27//! quanto dos DPBs de banco. O primeiro byte é o código da ação; em seguida os
28//! argumentos, onde:
29//! - argumento **string** (ex.: `isc_spb_dbname` 106, `isc_spb_bkp_file` 5):
30//!   `tag(1) | comprimento(2, little-endian) | bytes` — note o prefixo de
31//!   **2 bytes**, não o de 1 byte dos clumplets de attach;
32//! - argumento **inteiro** (ex.: `isc_spb_bkp_factor` 6, `isc_spb_options` 108):
33//!   `tag(1) | valor(4, little-endian)`, **sem** prefixo de comprimento;
34//! - **flag** isolada (ex.: `isc_spb_verbose` 107): apenas `tag(1)`.
35//!
36//! As opções de backup/restore/estatísticas são um bitmask carregado em um único
37//! `isc_spb_options` (o servidor o lê como máscara de bits).
38
39use crate::config::ConnectConfig;
40use crate::connection::{AuthState, Handshake, handshake};
41use crate::error::{Error, Result};
42use crate::wire::consts::*;
43use crate::wire::response::read_response;
44use crate::wire::stream::{FbStream, info_payload, op_packet};
45use crate::wire::xdr::ParameterBuffer;
46
47/// Tamanho padrão do buffer de resposta para consultas/saída de serviço.
48const DEFAULT_INFO_BUF: i32 = 32768;
49
50/// Um anexo autenticado ao gerenciador de serviços de um servidor Firebird.
51pub struct ServiceManager {
52    stream: FbStream,
53    handle: i32,
54}
55
56impl ServiceManager {
57    /// Anexa ao gerenciador de serviços usando host/porta/credenciais de `config`
58    /// (o campo `database` é ignorado; o alvo é sempre `service_mgr`).
59    pub fn attach(config: &ConnectConfig) -> Result<ServiceManager> {
60        Self::attach_inner(config)
61    }
62
63    fn attach_inner(config: &ConnectConfig) -> Result<ServiceManager> {
64        let Handshake {
65            mut stream,
66            protocol_version: _,
67            auth,
68        } = handshake(config, op::SERVICE_ATTACH, "service_mgr")?;
69
70        let spb = build_attach_spb(config, &auth);
71        let mut w = op_packet(op::SERVICE_ATTACH);
72        w.put_i32(0); // id do objeto
73        w.put_str("service_mgr");
74        w.put_bytes(&spb);
75        stream.send(&w)?;
76        let resp = crate::connection::attach_response(&mut stream)?;
77
78        Ok(ServiceManager {
79            stream,
80            handle: resp.handle,
81        })
82    }
83
84    /// Se a comunicação (wire) com o serviço está criptografada.
85    pub fn is_encrypted(&self) -> bool {
86        self.stream.is_encrypted()
87    }
88
89    /// Desanexa do gerenciador de serviços (`op_service_detach`) e fecha o socket.
90    pub fn close(mut self) -> Result<()> {
91        let mut w = op_packet(op::SERVICE_DETACH);
92        w.put_i32(self.handle);
93        self.stream.send(&w)?;
94        let _ = read_response(&mut self.stream)?;
95        Ok(())
96    }
97
98    // -- API de baixo nível -------------------------------------------------
99
100    /// Envia `op_service_info` com os itens de requisição (`recv`) e devolve o
101    /// `p_resp_data` bruto (um buffer de info terminado por `isc_info_end`).
102    ///
103    /// `send` são itens de configuração para esta chamada (ex.:
104    /// `isc_info_svc_timeout`); normalmente vazio.
105    pub fn info(&mut self, send: &[u8], recv: &[u8], buf_len: i32) -> Result<Vec<u8>> {
106        let mut w = op_packet(op::SERVICE_INFO);
107        w.put_i32(self.handle);
108        w.put_i32(0); // incarnation
109        w.put_bytes(send); // itens de "envio"
110        w.put_bytes(recv); // itens de "recepção" (o que queremos)
111        w.put_i32(buf_len);
112        self.stream.send(&w)?;
113        let resp = read_response(&mut self.stream)?;
114        Ok(resp.data)
115    }
116
117    /// Dispara uma ação (`op_service_start`). O `spb` deve começar pelo código da
118    /// ação (`svc_action::*`). Use [`ServiceManager::run`] para também coletar a
119    /// saída textual da ação.
120    pub fn start(&mut self, spb: &[u8]) -> Result<()> {
121        let mut w = op_packet(op::SERVICE_START);
122        w.put_i32(self.handle);
123        w.put_i32(0); // incarnation
124        w.put_bytes(spb);
125        self.stream.send(&w)?;
126        read_response(&mut self.stream)?;
127        Ok(())
128    }
129
130    /// Dispara uma ação e coleta toda a sua saída textual, drenando o serviço com
131    /// chamadas sucessivas de `op_service_info`/`isc_info_svc_to_eof` até o fim.
132    pub fn run(&mut self, spb: &[u8]) -> Result<String> {
133        self.start(spb)?;
134        self.collect_output()
135    }
136
137    /// Lê a saída acumulada do serviço (após um [`start`](Self::start)) até o EOF.
138    pub fn collect_output(&mut self) -> Result<String> {
139        let mut out = String::new();
140        loop {
141            let data = self.info(&[], &[svc_info::TO_EOF], DEFAULT_INFO_BUF)?;
142            let chunk = parse_svc_string(&data, svc_info::TO_EOF)?;
143            if chunk.is_empty() {
144                break;
145            }
146            out.push_str(&String::from_utf8_lossy(&chunk));
147        }
148        Ok(out)
149    }
150
151    // -- consultas de info comuns -------------------------------------------
152
153    /// A versão do servidor Firebird (`isc_info_svc_server_version`).
154    pub fn server_version(&mut self) -> Result<String> {
155        self.query_string(svc_info::SERVER_VERSION)
156    }
157
158    /// A implementação do servidor (`isc_info_svc_implementation`).
159    pub fn implementation(&mut self) -> Result<String> {
160        self.query_string(svc_info::IMPLEMENTATION)
161    }
162
163    /// O caminho do banco de segurança em uso (`isc_info_svc_user_dbpath`).
164    pub fn security_database(&mut self) -> Result<String> {
165        self.query_string(svc_info::USER_DBPATH)
166    }
167
168    /// O valor de `$FIREBIRD` no servidor (`isc_info_svc_get_env`).
169    pub fn home_directory(&mut self) -> Result<String> {
170        self.query_string(svc_info::GET_ENV)
171    }
172
173    /// A versão do protocolo do Service Manager (`isc_info_svc_version`; um
174    /// inteiro, p.ex. `2` no Firebird 5).
175    pub fn manager_version(&mut self) -> Result<i32> {
176        self.query_int(svc_info::VERSION)
177    }
178
179    /// Indica se uma ação ainda está em execução nesta conexão de serviço
180    /// (`isc_info_svc_running`). Útil para sondar o andamento entre leituras de
181    /// saída de uma ação longa (backup, restore, etc.).
182    pub fn is_running(&mut self) -> Result<bool> {
183        Ok(self.query_int(svc_info::RUNNING)? != 0)
184    }
185
186    // -- ações de alto nível ------------------------------------------------
187
188    /// Lê o log do servidor (`firebird.log`) via `isc_action_svc_get_fb_log`.
189    /// (A ação não tem argumentos: o SPB é apenas o código da ação.)
190    pub fn get_fb_log(&mut self) -> Result<String> {
191        let mut spb = ParameterBuffer::raw();
192        spb.tag(svc_action::GET_FB_LOG);
193        self.run(&spb.into_vec())
194    }
195
196    /// Faz backup de `database` (alias ou caminho no servidor) para `backup_file`
197    /// (caminho **no servidor**) via `gbak`. `options` é um bitmask de `svc_bkp::*`
198    /// (use `0` para o padrão). Devolve a saída textual do gbak (modo verbose).
199    pub fn backup(&mut self, database: &str, backup_file: &str, options: u32) -> Result<String> {
200        let mut spb = ActionSpb::new(svc_action::BACKUP);
201        spb.string(spb::DBNAME, database);
202        spb.string(svc_bkp::FILE, backup_file);
203        if options != 0 {
204            spb.int(spb::OPTIONS, options);
205        }
206        spb.flag(spb::VERBOSE);
207        self.run(&spb.into_vec())
208    }
209
210    /// Restaura `backup_file` (caminho **no servidor**) para `database` via `gbak`.
211    /// `options` é um bitmask de `svc_res::*`; se nem `REPLACE` nem `CREATE`
212    /// estiverem presentes, assume `CREATE` (o padrão do gbak). Devolve a saída
213    /// textual do gbak (modo verbose).
214    pub fn restore(&mut self, backup_file: &str, database: &str, options: u32) -> Result<String> {
215        let mut options = options;
216        if options & (svc_res::REPLACE | svc_res::CREATE) == 0 {
217            options |= svc_res::CREATE;
218        }
219        let mut spb = ActionSpb::new(svc_action::RESTORE);
220        // Na restauração os papéis se invertem: bkp_file é a ORIGEM, dbname o DESTINO.
221        spb.string(svc_bkp::FILE, backup_file);
222        spb.string(spb::DBNAME, database);
223        spb.int(spb::OPTIONS, options);
224        spb.flag(spb::VERBOSE);
225        self.run(&spb.into_vec())
226    }
227
228    /// Coleta estatísticas de `database` via `gstat` (`isc_action_svc_db_stats`).
229    /// `options` é um bitmask de `svc_sts::*` (use `0` para o cabeçalho do banco).
230    pub fn statistics(&mut self, database: &str, options: u32) -> Result<String> {
231        let mut spb = ActionSpb::new(svc_action::DB_STATS);
232        spb.string(spb::DBNAME, database);
233        if options != 0 {
234            spb.int(spb::OPTIONS, options);
235        }
236        self.run(&spb.into_vec())
237    }
238
239    // -- nbackup (backup incremental) ---------------------------------------
240
241    /// Backup incremental (`nbackup`) de `database` para `backup_file` (caminho
242    /// **no servidor**) no `level` dado (0 = base de uma cadeia incremental).
243    /// `options` é um bitmask de `svc_nbk::*` (use `0` para o padrão). Devolve a
244    /// saída textual.
245    pub fn nbackup(
246        &mut self,
247        database: &str,
248        backup_file: &str,
249        level: u32,
250        options: u32,
251    ) -> Result<String> {
252        let mut spb = ActionSpb::new(svc_action::NBAK);
253        spb.string(spb::DBNAME, database);
254        spb.string(svc_nbk::FILE, backup_file);
255        spb.int(svc_nbk::LEVEL, level);
256        if options != 0 {
257            spb.int(spb::OPTIONS, options);
258        }
259        self.run(&spb.into_vec())
260    }
261
262    /// Restauração incremental (`nrestore`): reconstrói `database` (caminho **no
263    /// servidor**) aplicando `backup_files` em ordem (nível 0 primeiro).
264    pub fn nrestore(
265        &mut self,
266        database: &str,
267        backup_files: &[&str],
268        options: u32,
269    ) -> Result<String> {
270        let mut spb = ActionSpb::new(svc_action::NREST);
271        spb.string(spb::DBNAME, database);
272        for file in backup_files {
273            spb.string(svc_nbk::FILE, file);
274        }
275        if options != 0 {
276            spb.int(spb::OPTIONS, options);
277        }
278        self.run(&spb.into_vec())
279    }
280
281    // -- validação online ---------------------------------------------------
282
283    /// Validação ONLINE de `database` (`isc_action_svc_validate`). `tables` e
284    /// `indices` são expressões regulares opcionais (None = tudo). Devolve o
285    /// relatório textual.
286    pub fn validate(
287        &mut self,
288        database: &str,
289        tables: Option<&str>,
290        indices: Option<&str>,
291    ) -> Result<String> {
292        let mut spb = ActionSpb::new(svc_action::VALIDATE);
293        spb.string(spb::DBNAME, database);
294        if let Some(t) = tables {
295            spb.string(svc_val::TAB_INCL, t);
296        }
297        if let Some(i) = indices {
298            spb.string(svc_val::IDX_INCL, i);
299        }
300        self.run(&spb.into_vec())
301    }
302
303    // -- repair / gfix ------------------------------------------------------
304
305    /// Manutenção/reparo de `database` via `gfix` (`isc_action_svc_repair`).
306    /// `options` é um bitmask de `svc_rpr::*` (ex.: `MEND_DB | FULL`, ou
307    /// `VALIDATE_DB` para checagem). Devolve a saída textual.
308    pub fn repair(&mut self, database: &str, options: u32) -> Result<String> {
309        let mut spb = ActionSpb::new(svc_action::REPAIR);
310        spb.string(spb::DBNAME, database);
311        spb.int(spb::OPTIONS, options);
312        self.run(&spb.into_vec())
313    }
314
315    /// Atalho de [`repair`](Self::repair) que dispara um sweep manual.
316    pub fn sweep(&mut self, database: &str) -> Result<String> {
317        self.repair(database, svc_rpr::SWEEP_DB)
318    }
319
320    // -- propriedades (gfix) ------------------------------------------------
321
322    /// Define o intervalo de sweep automático em transações (`gfix -h`).
323    pub fn set_sweep_interval(&mut self, database: &str, interval: u32) -> Result<()> {
324        self.properties(database, |s| {
325            s.int(svc_prp::SWEEP_INTERVAL, interval);
326        })
327    }
328
329    /// Define o tamanho do cache do banco em páginas (`gfix -buffers`).
330    pub fn set_page_buffers(&mut self, database: &str, buffers: u32) -> Result<()> {
331        self.properties(database, |s| {
332            s.int(svc_prp::PAGE_BUFFERS, buffers);
333        })
334    }
335
336    /// Liga/desliga a escrita síncrona (forced writes) do banco (`gfix -write`).
337    pub fn set_forced_writes(&mut self, database: &str, sync: bool) -> Result<()> {
338        let mode = if sync {
339            svc_prp::WM_SYNC
340        } else {
341            svc_prp::WM_ASYNC
342        };
343        self.properties(database, |s| {
344            s.byte(svc_prp::WRITE_MODE, mode);
345        })
346    }
347
348    /// Define o modo de acesso do banco: somente leitura ou leitura/escrita
349    /// (`gfix -mode`).
350    pub fn set_read_only(&mut self, database: &str, read_only: bool) -> Result<()> {
351        let mode = if read_only {
352            svc_prp::AM_READONLY
353        } else {
354            svc_prp::AM_READWRITE
355        };
356        self.properties(database, |s| {
357            s.byte(svc_prp::ACCESS_MODE, mode);
358        })
359    }
360
361    /// Coloca o banco OFFLINE (shutdown) no `mode` dado (`svc_prp::SM_*`),
362    /// aguardando até `timeout` segundos pelo término das conexões ativas.
363    pub fn shutdown(&mut self, database: &str, mode: u8, timeout: u32) -> Result<()> {
364        self.properties(database, |s| {
365            s.byte(svc_prp::SHUTDOWN_MODE, mode);
366            s.int(svc_prp::ATTACHMENTS_SHUTDOWN, timeout);
367        })
368    }
369
370    /// Traz o banco de volta ONLINE no `mode` dado (`svc_prp::SM_*`).
371    pub fn bring_online(&mut self, database: &str, mode: u8) -> Result<()> {
372        self.properties(database, |s| {
373            s.byte(svc_prp::ONLINE_MODE, mode);
374        })
375    }
376
377    /// Helper interno: dispara `isc_action_svc_properties` sobre `database` com os
378    /// argumentos montados por `build`, descartando a saída.
379    fn properties<F>(&mut self, database: &str, build: F) -> Result<()>
380    where
381        F: FnOnce(&mut ActionSpb),
382    {
383        let mut spb = ActionSpb::new(svc_action::PROPERTIES);
384        spb.string(spb::DBNAME, database);
385        build(&mut spb);
386        self.run(&spb.into_vec())?;
387        Ok(())
388    }
389
390    // -- trace --------------------------------------------------------------
391
392    /// Inicia uma sessão de trace com o texto de configuração `config` (formato
393    /// `fbtrace.conf`); `name` rotula a sessão. Devolve a saída inicial (que
394    /// inclui a linha "Trace session ID N started"). A sessão continua no servidor
395    /// após o retorno — pare-a com [`trace_stop`](Self::trace_stop). Para drenar o
396    /// fluxo contínuo de eventos use uma conexão de serviço DEDICADA, pois esta
397    /// chamada lê apenas a saída já disponível.
398    pub fn trace_start(&mut self, name: &str, config: &str) -> Result<String> {
399        let mut spb = ActionSpb::new(svc_action::TRACE_START);
400        if !name.is_empty() {
401            spb.string(svc_trc::NAME, name);
402        }
403        spb.string(svc_trc::CFG, config);
404        self.run(&spb.into_vec())
405    }
406
407    /// Para a sessão de trace de id `session_id`.
408    pub fn trace_stop(&mut self, session_id: u32) -> Result<String> {
409        self.trace_action(svc_action::TRACE_STOP, session_id)
410    }
411
412    /// Suspende a sessão de trace de id `session_id`.
413    pub fn trace_suspend(&mut self, session_id: u32) -> Result<String> {
414        self.trace_action(svc_action::TRACE_SUSPEND, session_id)
415    }
416
417    /// Retoma a sessão de trace de id `session_id`.
418    pub fn trace_resume(&mut self, session_id: u32) -> Result<String> {
419        self.trace_action(svc_action::TRACE_RESUME, session_id)
420    }
421
422    /// Lista as sessões de trace ativas no servidor.
423    pub fn trace_list(&mut self) -> Result<String> {
424        let spb = ActionSpb::new(svc_action::TRACE_LIST);
425        self.run(&spb.into_vec())
426    }
427
428    fn trace_action(&mut self, action: u8, session_id: u32) -> Result<String> {
429        let mut spb = ActionSpb::new(action);
430        spb.int(svc_trc::ID, session_id);
431        self.run(&spb.into_vec())
432    }
433
434    // -- gestão de usuários (banco de segurança) ----------------------------
435
436    /// Cria um usuário no banco de segurança (`isc_action_svc_add_user`).
437    pub fn add_user(&mut self, user: &UserParams) -> Result<()> {
438        self.run(&build_user_spb(svc_action::ADD_USER, user))?;
439        Ok(())
440    }
441
442    /// Altera um usuário existente (`isc_action_svc_modify_user`). Só os campos
443    /// presentes em `user` são modificados.
444    pub fn modify_user(&mut self, user: &UserParams) -> Result<()> {
445        self.run(&build_user_spb(svc_action::MODIFY_USER, user))?;
446        Ok(())
447    }
448
449    /// Remove um usuário (`isc_action_svc_delete_user`).
450    pub fn delete_user(&mut self, username: &str) -> Result<()> {
451        let mut spb = ActionSpb::new(svc_action::DELETE_USER);
452        spb.string(svc_sec::USERNAME, username);
453        self.run(&spb.into_vec())?;
454        Ok(())
455    }
456
457    /// Lista todos os usuários do banco de segurança
458    /// (`isc_action_svc_display_user` + `isc_info_svc_get_users`).
459    pub fn display_users(&mut self) -> Result<Vec<UserInfo>> {
460        let spb = ActionSpb::new(svc_action::DISPLAY_USER);
461        self.fetch_users(spb.into_vec())
462    }
463
464    /// Consulta um único usuário pelo nome; devolve `None` se não existir.
465    pub fn display_user(&mut self, username: &str) -> Result<Option<UserInfo>> {
466        let mut spb = ActionSpb::new(svc_action::DISPLAY_USER);
467        spb.string(svc_sec::USERNAME, username);
468        Ok(self.fetch_users(spb.into_vec())?.into_iter().next())
469    }
470
471    /// Dispara um `display_user` e decodifica o buffer `isc_info_svc_get_users`.
472    /// `isc_info_svc_get_users` não tem continuação incremental: se a lista não
473    /// couber no buffer, o servidor marca `isc_info_truncated` e devemos repetir
474    /// com um buffer maior. Dobramos o tamanho até caber (ou atingir o teto).
475    fn fetch_users(&mut self, spb: Vec<u8>) -> Result<Vec<UserInfo>> {
476        const MAX_INFO_BUF: i32 = 16 * 1024 * 1024;
477        let mut buf_len = DEFAULT_INFO_BUF;
478        loop {
479            // Re-dispara a ação a cada tentativa para que o buffer maior receba a
480            // lista completa do zero (get_users não tem continuação incremental).
481            self.start(&spb)?;
482            let data = self.info(&[], &[svc_info::GET_USERS], buf_len)?;
483            if data.last() == Some(&INFO_TRUNCATED) && buf_len < MAX_INFO_BUF {
484                buf_len = buf_len.saturating_mul(2).min(MAX_INFO_BUF);
485                continue;
486            }
487            let payload = parse_svc_string(&data, svc_info::GET_USERS)?;
488            return parse_users(&payload);
489        }
490    }
491
492    // -- auxiliares ---------------------------------------------------------
493
494    /// Consulta um único item de info que devolve uma string.
495    fn query_string(&mut self, item: u8) -> Result<String> {
496        let data = self.info(&[], &[item], DEFAULT_INFO_BUF)?;
497        let value = parse_svc_string(&data, item)?;
498        Ok(String::from_utf8_lossy(&value).into_owned())
499    }
500
501    /// Consulta um único item de info que devolve um inteiro.
502    fn query_int(&mut self, item: u8) -> Result<i32> {
503        let data = self.info(&[], &[item], DEFAULT_INFO_BUF)?;
504        parse_svc_int(&data, item)
505    }
506}
507
508/// Constrói o SPB de `op_service_attach`. Cabeçalho `[version, current_version]`
509/// (ambos `2`) seguido do usuário e da autenticação, espelhando o DPB de banco.
510fn build_attach_spb(config: &ConnectConfig, auth: &AuthState) -> Vec<u8> {
511    let mut spb = ParameterBuffer::raw();
512    spb.tag(SPB_VERSION);
513    spb.tag(SPB_CURRENT_VERSION);
514
515    spb.string(spb::USER_NAME, &config.normalized_user());
516
517    match auth {
518        AuthState::Proof(a) => {
519            spb.string(spb::AUTH_PLUGIN_NAME, &a.plugin);
520            spb.string(spb::AUTH_PLUGIN_LIST, "Srp256,Srp");
521            spb.string(spb::SPECIFIC_AUTH_DATA, &a.proof_hex);
522        }
523        AuthState::Legacy => {
524            spb.string(spb::PASSWORD, &config.password);
525        }
526        AuthState::Done => {}
527    }
528
529    if let Some(role) = &config.role {
530        spb.string(spb::SQL_ROLE_NAME, role);
531    }
532    spb.into_vec()
533}
534
535/// Construtor do SPB de uma ação `op_service_start`. O primeiro byte é o código
536/// da ação; argumentos string usam prefixo de comprimento de **2 bytes (LE)**,
537/// argumentos inteiros são **4 bytes LE sem prefixo**, e flags são só o tag.
538/// (Distinto do [`ParameterBuffer`], que usa clumplets de 1 byte.)
539struct ActionSpb {
540    buf: Vec<u8>,
541}
542
543impl ActionSpb {
544    fn new(action: u8) -> Self {
545        Self { buf: vec![action] }
546    }
547
548    /// Argumento string: `tag | comprimento(2 LE) | bytes`.
549    fn string(&mut self, tag: u8, value: &str) -> &mut Self {
550        let bytes = value.as_bytes();
551        self.buf.push(tag);
552        self.buf
553            .extend_from_slice(&(bytes.len() as u16).to_le_bytes());
554        self.buf.extend_from_slice(bytes);
555        self
556    }
557
558    /// Argumento inteiro: `tag | valor(4 LE)`, sem prefixo de comprimento.
559    fn int(&mut self, tag: u8, value: u32) -> &mut Self {
560        self.buf.push(tag);
561        self.buf.extend_from_slice(&value.to_le_bytes());
562        self
563    }
564
565    /// Argumento de um único byte: `tag | valor(1)`. Usado pelos parâmetros de
566    /// modo de `isc_action_svc_properties` (write/access/shutdown mode).
567    fn byte(&mut self, tag: u8, value: u8) -> &mut Self {
568        self.buf.push(tag);
569        self.buf.push(value);
570        self
571    }
572
573    /// Flag isolada: apenas o tag.
574    fn flag(&mut self, tag: u8) -> &mut Self {
575        self.buf.push(tag);
576        self
577    }
578
579    fn into_vec(self) -> Vec<u8> {
580        self.buf
581    }
582}
583
584/// Parâmetros para criar (`add_user`) ou alterar (`modify_user`) um usuário.
585/// Construa com [`UserParams::new`] e os métodos encadeáveis; só os campos
586/// definidos entram no SPB (em `modify_user`, os ausentes ficam intactos).
587#[derive(Debug, Clone, Default)]
588pub struct UserParams {
589    username: String,
590    password: Option<String>,
591    first_name: Option<String>,
592    middle_name: Option<String>,
593    last_name: Option<String>,
594    user_id: Option<u32>,
595    group_id: Option<u32>,
596    admin: Option<bool>,
597}
598
599impl UserParams {
600    /// Inicia os parâmetros para o usuário de nome `username`.
601    pub fn new(username: impl Into<String>) -> Self {
602        Self {
603            username: username.into(),
604            ..Default::default()
605        }
606    }
607
608    /// Define a senha (`isc_spb_sec_password`).
609    pub fn password(mut self, v: impl Into<String>) -> Self {
610        self.password = Some(v.into());
611        self
612    }
613
614    /// Define o primeiro nome (`isc_spb_sec_firstname`).
615    pub fn first_name(mut self, v: impl Into<String>) -> Self {
616        self.first_name = Some(v.into());
617        self
618    }
619
620    /// Define o nome do meio (`isc_spb_sec_middlename`).
621    pub fn middle_name(mut self, v: impl Into<String>) -> Self {
622        self.middle_name = Some(v.into());
623        self
624    }
625
626    /// Define o sobrenome (`isc_spb_sec_lastname`).
627    pub fn last_name(mut self, v: impl Into<String>) -> Self {
628        self.last_name = Some(v.into());
629        self
630    }
631
632    /// Define o UID Unix (`isc_spb_sec_userid`).
633    pub fn user_id(mut self, v: u32) -> Self {
634        self.user_id = Some(v);
635        self
636    }
637
638    /// Define o GID Unix (`isc_spb_sec_groupid`).
639    pub fn group_id(mut self, v: u32) -> Self {
640        self.group_id = Some(v);
641        self
642    }
643
644    /// Concede ou revoga o papel de administrador (`isc_spb_sec_admin`).
645    pub fn admin(mut self, v: bool) -> Self {
646        self.admin = Some(v);
647        self
648    }
649}
650
651/// Um usuário do banco de segurança, devolvido por
652/// [`ServiceManager::display_users`]/[`display_user`](ServiceManager::display_user).
653#[derive(Debug, Clone, Default, PartialEq, Eq)]
654pub struct UserInfo {
655    /// Nome do usuário no banco de segurança.
656    pub username: String,
657    /// Primeiro nome cadastrado, ou string vazia quando ausente.
658    pub first_name: String,
659    /// Nome do meio cadastrado, ou string vazia quando ausente.
660    pub middle_name: String,
661    /// Sobrenome cadastrado, ou string vazia quando ausente.
662    pub last_name: String,
663    /// UID Unix associado ao usuário, quando usado pelo servidor.
664    pub user_id: u32,
665    /// GID Unix associado ao usuário, quando usado pelo servidor.
666    pub group_id: u32,
667}
668
669/// Monta o SPB de `add_user`/`modify_user` a partir de [`UserParams`]. Campos
670/// string usam `tag | len(2 LE) | bytes`; UID/GID/admin são inteiros de 4 bytes.
671fn build_user_spb(action: u8, p: &UserParams) -> Vec<u8> {
672    let mut spb = ActionSpb::new(action);
673    spb.string(svc_sec::USERNAME, &p.username);
674    if let Some(v) = &p.password {
675        spb.string(svc_sec::PASSWORD, v);
676    }
677    if let Some(v) = &p.first_name {
678        spb.string(svc_sec::FIRSTNAME, v);
679    }
680    if let Some(v) = &p.middle_name {
681        spb.string(svc_sec::MIDDLENAME, v);
682    }
683    if let Some(v) = &p.last_name {
684        spb.string(svc_sec::LASTNAME, v);
685    }
686    if let Some(v) = p.user_id {
687        spb.int(svc_sec::USERID, v);
688    }
689    if let Some(v) = p.group_id {
690        spb.int(svc_sec::GROUPID, v);
691    }
692    if let Some(v) = p.admin {
693        spb.int(svc_sec::ADMIN, v as u32);
694    }
695    spb.into_vec()
696}
697
698/// Decodifica o buffer interno de `isc_info_svc_get_users`: uma sequência plana
699/// de sub-itens, um registro de usuário começando a cada `isc_spb_sec_username`.
700/// Strings (username/firstname/middlename/lastname/groupname) usam `tag|len(2
701/// LE)|bytes`; UID/GID (`isc_spb_sec_userid`/`groupid`) são inteiros de 4 bytes
702/// LE sem prefixo de comprimento.
703fn parse_users(payload: &[u8]) -> Result<Vec<UserInfo>> {
704    let mut users = Vec::new();
705    let mut cur: Option<UserInfo> = None;
706    let mut p = 0usize;
707    while p < payload.len() {
708        let tag = payload[p];
709        p += 1;
710        match tag {
711            svc_sec::USERNAME
712            | svc_sec::GROUPNAME
713            | svc_sec::FIRSTNAME
714            | svc_sec::MIDDLENAME
715            | svc_sec::LASTNAME => {
716                let len = payload
717                    .get(p..p + 2)
718                    .ok_or_else(|| Error::protocol("get_users: comprimento truncado"))?;
719                let len = u16::from_le_bytes([len[0], len[1]]) as usize;
720                let value = payload
721                    .get(p + 2..p + 2 + len)
722                    .ok_or_else(|| Error::protocol("get_users: valor truncado"))?;
723                let s = String::from_utf8_lossy(value).into_owned();
724                p += 2 + len;
725                if tag == svc_sec::USERNAME {
726                    if let Some(u) = cur.take() {
727                        users.push(u);
728                    }
729                    cur = Some(UserInfo {
730                        username: s,
731                        ..Default::default()
732                    });
733                } else if let Some(u) = cur.as_mut() {
734                    match tag {
735                        svc_sec::FIRSTNAME => u.first_name = s,
736                        svc_sec::MIDDLENAME => u.middle_name = s,
737                        svc_sec::LASTNAME => u.last_name = s,
738                        _ => {} // groupname: ignorado
739                    }
740                }
741            }
742            svc_sec::USERID | svc_sec::GROUPID => {
743                let v = payload
744                    .get(p..p + 4)
745                    .ok_or_else(|| Error::protocol("get_users: inteiro truncado"))?;
746                let v = u32::from_le_bytes([v[0], v[1], v[2], v[3]]);
747                p += 4;
748                if let Some(u) = cur.as_mut() {
749                    if tag == svc_sec::USERID {
750                        u.user_id = v;
751                    } else {
752                        u.group_id = v;
753                    }
754                }
755            }
756            _ => break, // tag desconhecida (ou fim): encerra
757        }
758    }
759    if let Some(u) = cur.take() {
760        users.push(u);
761    }
762    Ok(users)
763}
764
765/// Lê o primeiro item `tag | len(2 LE) | valor` de um buffer de info de serviço.
766fn read_svc_item(payload: &[u8]) -> Result<(u8, &[u8])> {
767    if payload.len() < 3 {
768        return Err(Error::protocol("buffer de info de serviço curto demais"));
769    }
770    let tag = payload[0];
771    let len = u16::from_le_bytes([payload[1], payload[2]]) as usize;
772    let value = payload
773        .get(3..3 + len)
774        .ok_or_else(|| Error::protocol("item de info de serviço truncado"))?;
775    Ok((tag, value))
776}
777
778/// Extrai o valor (bytes) de um item de info de serviço de string, verificando o
779/// tag esperado. Trata o caso de buffer vazio (saída esgotada) devolvendo vazio.
780fn parse_svc_string(data: &[u8], expected: u8) -> Result<Vec<u8>> {
781    let payload = info_payload(data)?;
782    if payload.is_empty() {
783        return Ok(Vec::new());
784    }
785    let (tag, value) = read_svc_item(payload)?;
786    if tag != expected {
787        return Err(Error::protocol(format!(
788            "esperava item de serviço {expected}, veio {tag}"
789        )));
790    }
791    Ok(value.to_vec())
792}
793
794/// Extrai um item de info de serviço INTEIRO (`tag(1) | valor(4 LE)`, sem prefixo
795/// de comprimento — confirmado por strace de `fbsvcmgr info_version`: o item 54
796/// chega como `36 02 00 00 00 01`, valor 2 seguido de `isc_info_end`).
797fn parse_svc_int(data: &[u8], expected: u8) -> Result<i32> {
798    let payload = info_payload(data)?;
799    if payload.is_empty() {
800        return Err(Error::protocol("buffer de info de serviço vazio"));
801    }
802    if payload[0] != expected {
803        return Err(Error::protocol(format!(
804            "esperava item de serviço {expected}, veio {}",
805            payload[0]
806        )));
807    }
808    let v = payload
809        .get(1..5)
810        .ok_or_else(|| Error::protocol("item inteiro de info de serviço truncado"))?;
811    Ok(i32::from_le_bytes([v[0], v[1], v[2], v[3]]))
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817
818    #[test]
819    fn attach_spb_header_and_user() {
820        let cfg = ConnectConfig::new().user("sysdba");
821        let spb = build_attach_spb(&cfg, &AuthState::Legacy);
822        // Cabeçalho: version, current_version.
823        assert_eq!(spb[0], SPB_VERSION);
824        assert_eq!(spb[1], SPB_CURRENT_VERSION);
825        // user_name normalizado em maiúsculas.
826        assert!(spb.windows(6).any(|w| w == b"SYSDBA"));
827        // sem sessão SRP -> senha legada presente como clumplet.
828        assert!(spb.contains(&spb::PASSWORD));
829    }
830
831    #[test]
832    fn action_spb_byte_and_int_layout() {
833        // properties: dbname (string, len 2 LE) + write_mode (byte) + sweep (int 4 LE).
834        let mut spb = ActionSpb::new(svc_action::PROPERTIES);
835        spb.string(spb::DBNAME, "db");
836        spb.byte(svc_prp::WRITE_MODE, svc_prp::WM_SYNC);
837        spb.int(svc_prp::SWEEP_INTERVAL, 5000);
838        let v = spb.into_vec();
839        assert_eq!(
840            v,
841            vec![
842                svc_action::PROPERTIES,
843                spb::DBNAME,
844                2,
845                0,
846                b'd',
847                b'b',
848                svc_prp::WRITE_MODE,
849                svc_prp::WM_SYNC,
850                svc_prp::SWEEP_INTERVAL,
851                0x88,
852                0x13,
853                0,
854                0, // 5000 LE
855            ]
856        );
857    }
858
859    #[test]
860    fn nbackup_spb_has_level_as_int() {
861        let mut spb = ActionSpb::new(svc_action::NBAK);
862        spb.string(spb::DBNAME, "d");
863        spb.string(svc_nbk::FILE, "f");
864        spb.int(svc_nbk::LEVEL, 0);
865        let v = spb.into_vec();
866        assert_eq!(v[0], svc_action::NBAK);
867        // ...dbname "d"... então nbk_file "f"... então nbk_level 0 (4 LE).
868        assert!(v.windows(4).any(|w| w == [svc_nbk::FILE, 1, 0, b'f']));
869        assert!(v.ends_with(&[svc_nbk::LEVEL, 0, 0, 0, 0]));
870    }
871
872    #[test]
873    fn parse_int_item_from_strace() {
874        // isc_info_svc_version capturado ao vivo: tag 54, valor 2 (4 LE), isc_info_end.
875        let buf = [svc_info::VERSION, 2, 0, 0, 0, INFO_END];
876        assert_eq!(parse_svc_int(&buf, svc_info::VERSION).unwrap(), 2);
877        // Tag inesperado é erro.
878        assert!(parse_svc_int(&buf, svc_info::RUNNING).is_err());
879    }
880
881    #[test]
882    fn parse_string_item() {
883        // tag 55 (server_version), len 3 (LE), "abc", isc_info_end
884        let buf = [55u8, 3, 0, b'a', b'b', b'c', INFO_END];
885        let v = parse_svc_string(&buf, svc_info::SERVER_VERSION).unwrap();
886        assert_eq!(v, b"abc");
887    }
888
889    #[test]
890    fn parse_empty_output_is_empty() {
891        let buf = [INFO_END];
892        let v = parse_svc_string(&buf, svc_info::TO_EOF).unwrap();
893        assert!(v.is_empty());
894    }
895
896    #[test]
897    fn action_spb_string_uses_2byte_le_length() {
898        // Espelha o strace de `fbsvcmgr action_db_stats dbname employee`:
899        // 0b (db_stats) | 6a (dbname) | 0800 (len 8 LE) | "employee".
900        let mut spb = ActionSpb::new(svc_action::DB_STATS);
901        spb.string(spb::DBNAME, "employee");
902        assert_eq!(spb.into_vec(), b"\x0b\x6a\x08\x00employee".to_vec(),);
903    }
904
905    #[test]
906    fn user_spb_add_matches_strace() {
907        // strace de `action_add_user sec_username FDBTEST sec_password secret99
908        // sec_firstname Test sec_lastname User`:
909        // 04 | 07 0700 FDBTEST | 08 0800 secret99 | 0a 0400 Test | 0c 0400 User.
910        let p = UserParams::new("FDBTEST")
911            .password("secret99")
912            .first_name("Test")
913            .last_name("User");
914        let spb = build_user_spb(svc_action::ADD_USER, &p);
915        assert_eq!(
916            spb,
917            b"\x04\x07\x07\x00FDBTEST\x08\x08\x00secret99\x0a\x04\x00Test\x0c\x04\x00User".to_vec(),
918        );
919    }
920
921    #[test]
922    fn user_spb_delete_is_just_username() {
923        let mut spb = ActionSpb::new(svc_action::DELETE_USER);
924        spb.string(svc_sec::USERNAME, "FDBTEST");
925        assert_eq!(spb.into_vec(), b"\x05\x07\x07\x00FDBTEST".to_vec());
926    }
927
928    /// Decodifica uma string hex em bytes (auxiliar de teste).
929    fn hex(s: &str) -> Vec<u8> {
930        (0..s.len())
931            .step_by(2)
932            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
933            .collect()
934    }
935
936    #[test]
937    fn parse_users_from_strace_payload() {
938        // Payload interno de isc_info_svc_get_users capturado ao vivo (3 usuários),
939        // montado por campo para legibilidade.
940        let payload = hex(concat!(
941            "070600",
942            "535953444241", // username "SYSDBA"
943            "0a0300",
944            "53716c", // first "Sql"
945            "0b0600",
946            "536572766572", // middle "Server"
947            "0c0d00",
948            "41646d696e6973747261746f72", // last "Administrator"
949            "05",
950            "00000000", // userid 0
951            "06",
952            "00000000", // groupid 0
953            "070800",
954            "4653435343504938", // username "FSCSCPI8"
955            "0a0000",
956            "0b0000",
957            "0c0000", // first/middle/last vazios
958            "05",
959            "00000000",
960            "06",
961            "00000000",
962            "070600",
963            "465343534950", // username "FSCSIP"
964            "0a0000",
965            "0b0000",
966            "0c0000",
967            "05",
968            "00000000",
969            "06",
970            "00000000",
971        ));
972        let users = parse_users(&payload).unwrap();
973        assert_eq!(users.len(), 3);
974        assert_eq!(users[0].username, "SYSDBA");
975        assert_eq!(users[0].first_name, "Sql");
976        assert_eq!(users[0].middle_name, "Server");
977        assert_eq!(users[0].last_name, "Administrator");
978        assert_eq!(users[1].username, "FSCSCPI8");
979        assert_eq!(users[1].first_name, "");
980        assert_eq!(users[2].username, "FSCSIP");
981    }
982
983    #[test]
984    fn action_spb_backup_matches_strace() {
985        // `action_backup dbname employee bkp_file /tmp/x bkp_factor não usado`,
986        // com NO_GARBAGE_COLLECT|IGNORE_CHECKSUMS e verbose:
987        // 01 | dbname | bkp_file | 6c (options) | bitmask(4 LE) | 6b (verbose).
988        let mut spb = ActionSpb::new(svc_action::BACKUP);
989        spb.string(spb::DBNAME, "db");
990        spb.string(svc_bkp::FILE, "f");
991        spb.int(
992            spb::OPTIONS,
993            svc_bkp::NO_GARBAGE_COLLECT | svc_bkp::IGNORE_CHECKSUMS,
994        );
995        spb.flag(spb::VERBOSE);
996        assert_eq!(
997            spb.into_vec(),
998            vec![
999                svc_action::BACKUP,
1000                spb::DBNAME,
1001                2,
1002                0,
1003                b'd',
1004                b'b',
1005                svc_bkp::FILE,
1006                1,
1007                0,
1008                b'f',
1009                spb::OPTIONS,
1010                0x09,
1011                0,
1012                0,
1013                0, // 8 | 1 = 9, little-endian
1014                spb::VERBOSE,
1015            ],
1016        );
1017    }
1018}