Skip to main content

firebird_wire/
config.rs

1//! Configuração da conexão.
2
3use std::time::Duration;
4
5use crate::error::{Error, Result};
6
7/// Postura desejada de criptografia da conexão (wire-encryption).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum WireCrypt {
10    /// Nunca criptografa. Falha contra um servidor configurado com `WireCrypt = Required`.
11    Disabled,
12    /// Criptografa quando o servidor oferece uma cifra suportada; caso contrário
13    /// continua em texto claro.
14    #[default]
15    Enabled,
16    /// Exige criptografia; falha o handshake se ela não puder ser negociada.
17    Required,
18}
19
20/// Tudo o que é necessário para abrir uma conexão com um servidor Firebird.
21#[derive(Debug, Clone)]
22pub struct ConnectConfig {
23    /// Nome ou IP do servidor Firebird.
24    pub host: String,
25    /// Porta TCP do servidor Firebird. O padrão do Firebird é `3050`.
26    pub port: u16,
27    /// Caminho ou alias do banco de dados no servidor (ex.: `employee` ou `/data/db.fdb`).
28    pub database: String,
29    /// Nome do usuário Firebird.
30    pub user: String,
31    /// Senha do usuário Firebird.
32    pub password: String,
33    /// Papel SQL opcional usado na conexão (`ROLE`).
34    pub role: Option<String>,
35    /// Charset da conexão (padrão `UTF8`).
36    pub charset: String,
37    /// Dialeto SQL (padrão `3`).
38    pub dialect: i32,
39    /// Política de criptografia da comunicação.
40    pub wire_crypt: WireCrypt,
41    /// Timeout de TCP + handshake.
42    pub connect_timeout: Option<Duration>,
43    /// Tamanho da página para [`crate::Connection::create_database`].
44    pub page_size: Option<i32>,
45    /// Fuso horário da sessão (FB4+), ex.: `America/Sao_Paulo`.
46    pub timezone: Option<String>,
47    /// Número de workers paralelos para a conexão (FB5).
48    pub parallel_workers: Option<i32>,
49    /// Quando `true`, emite `SET BIND OF INT128/DECFLOAT/TIME ZONE TO NATIVE`
50    /// logo após o attach (FB4+). Útil quando o servidor está com
51    /// `DataTypeCompatibility` ligado e coage esses tipos para os legados —
52    /// assim eles voltam como `Int128`/`DecFloat`/`TimeTz` em vez de aproximações.
53    pub native_data_types: bool,
54}
55
56impl Default for ConnectConfig {
57    fn default() -> Self {
58        ConnectConfig {
59            host: "localhost".to_string(),
60            port: 3050,
61            database: String::new(),
62            user: "SYSDBA".to_string(),
63            password: String::new(),
64            role: None,
65            charset: "UTF8".to_string(),
66            dialect: 3,
67            wire_crypt: WireCrypt::default(),
68            connect_timeout: Some(Duration::from_secs(15)),
69            page_size: None,
70            timezone: None,
71            parallel_workers: None,
72            native_data_types: false,
73        }
74    }
75}
76
77impl ConnectConfig {
78    /// Começa a partir dos padrões (`localhost:3050`, usuário `SYSDBA`, UTF8, dialeto 3).
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Define o host ou IP do servidor Firebird.
84    pub fn host(mut self, host: impl Into<String>) -> Self {
85        self.host = host.into();
86        self
87    }
88    /// Define a porta TCP do servidor Firebird.
89    pub fn port(mut self, port: u16) -> Self {
90        self.port = port;
91        self
92    }
93    /// Define o banco por alias ou caminho no servidor.
94    pub fn database(mut self, db: impl Into<String>) -> Self {
95        self.database = db.into();
96        self
97    }
98    /// Define o usuário Firebird.
99    pub fn user(mut self, user: impl Into<String>) -> Self {
100        self.user = user.into();
101        self
102    }
103    /// Define a senha do usuário Firebird.
104    pub fn password(mut self, password: impl Into<String>) -> Self {
105        self.password = password.into();
106        self
107    }
108    /// Define o papel SQL (`ROLE`) usado após o login.
109    pub fn role(mut self, role: impl Into<String>) -> Self {
110        self.role = Some(role.into());
111        self
112    }
113    /// Define o charset da conexão, como `UTF8`, `WIN1252` ou `ISO8859_1`.
114    pub fn charset(mut self, charset: impl Into<String>) -> Self {
115        self.charset = charset.into();
116        self
117    }
118    /// Define o dialeto SQL. Use `3` para bancos modernos.
119    pub fn dialect(mut self, dialect: i32) -> Self {
120        self.dialect = dialect;
121        self
122    }
123    /// Define se a comunicação deve ser criptografada, quando disponível ou obrigatoriamente.
124    pub fn wire_crypt(mut self, wc: WireCrypt) -> Self {
125        self.wire_crypt = wc;
126        self
127    }
128    /// Define o tempo máximo para abrir o socket TCP e completar o handshake.
129    pub fn connect_timeout(mut self, t: Duration) -> Self {
130        self.connect_timeout = Some(t);
131        self
132    }
133    /// Define o tamanho de página ao criar um banco novo com [`crate::Connection::create_database`].
134    pub fn page_size(mut self, size: i32) -> Self {
135        self.page_size = Some(size);
136        self
137    }
138    /// Define o fuso horário da sessão, por exemplo `America/Sao_Paulo`.
139    pub fn timezone(mut self, tz: impl Into<String>) -> Self {
140        self.timezone = Some(tz.into());
141        self
142    }
143    /// Define o número de workers paralelos solicitado ao servidor Firebird 5.
144    pub fn parallel_workers(mut self, n: i32) -> Self {
145        self.parallel_workers = Some(n);
146        self
147    }
148    /// Pede os tipos nativos (INT128/DECFLOAT/WITH TIME ZONE) após o attach.
149    /// Veja [`ConnectConfig::native_data_types`].
150    pub fn native_data_types(mut self, on: bool) -> Self {
151        self.native_data_types = on;
152        self
153    }
154
155    /// O nome de usuário normalizado da forma que o plugin SRP espera
156    /// (identificadores sem aspas são convertidos para maiúsculas).
157    pub(crate) fn normalized_user(&self) -> String {
158        self.user.to_uppercase()
159    }
160
161    /// Valida campos que vão para clumplets de 1 byte de comprimento (DPB/cnct),
162    /// onde um valor acima de 255 bytes corromperia silenciosamente o buffer.
163    /// Chamado no início do handshake. O `database` não entra aqui: vai num campo
164    /// XDR de comprimento de 4 bytes, sem esse limite.
165    pub(crate) fn validate(&self) -> Result<()> {
166        // O usuário é enviado em maiúsculas (pode mudar o nº de bytes em multibyte).
167        check_clumplet_len("user", &self.normalized_user())?;
168        check_clumplet_len("password", &self.password)?;
169        check_clumplet_len("charset", &self.charset)?;
170        if let Some(role) = &self.role {
171            check_clumplet_len("role", role)?;
172        }
173        if let Some(tz) = &self.timezone {
174            check_clumplet_len("timezone", tz)?;
175        }
176        Ok(())
177    }
178}
179
180/// Garante que um valor cabe num clumplet de comprimento de 1 byte (≤ 255 bytes).
181fn check_clumplet_len(field: &str, value: &str) -> Result<()> {
182    if value.len() > u8::MAX as usize {
183        return Err(Error::conversion(format!(
184            "{field} excede 255 bytes ({}); não cabe num parâmetro de conexão",
185            value.len()
186        )));
187    }
188    Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn default_config_is_valid() {
197        assert!(ConnectConfig::new().user("SYSDBA").validate().is_ok());
198    }
199
200    #[test]
201    fn over_long_password_is_rejected() {
202        let cfg = ConnectConfig::new().password("x".repeat(256));
203        let err = cfg.validate().unwrap_err();
204        assert!(
205            matches!(err, Error::Conversion(_)),
206            "esperava erro de conversão, veio {err:?}"
207        );
208    }
209
210    #[test]
211    fn max_length_fields_pass() {
212        let cfg = ConnectConfig::new()
213            .role("r".repeat(255))
214            .charset("c".repeat(255));
215        assert!(cfg.validate().is_ok());
216    }
217}