Skip to main content

netget/server/db2/
drda.rs

1//! DRDA / DDM wire codec for the Db2 server.
2//!
3//! DRDA (Distributed Relational Database Architecture) frames every message in a
4//! **DSS** (Data Stream Structure) envelope carrying one or more **DDM**
5//! (Distributed Data Management) objects. There is essentially no maintained Rust
6//! crate for the server side of DRDA, so this is hand-rolled against the public
7//! DRDA specification (the same code points Apache Derby's `org.apache.derby.impl.drda`
8//! and IBM's Db2 documentation use).
9//!
10//! ## DSS envelope (6-byte header)
11//!
12//! ```text
13//! +--------+--------+--------+--------+--------+--------+
14//! | length (u16 BE) | 0xD0   | format | correlator (u16)|
15//! +--------+--------+--------+--------+--------+--------+
16//! ```
17//! `length` counts the whole DSS including the header. `0xD0` is the DDM magic.
18//! `format` low nibble is the DSS type (1=RQSDSS request, 2=RPYDSS reply,
19//! 3=OBJDSS object); the high nibble carries chaining flags. `correlator` ties a
20//! reply to its request.
21//!
22//! ## DDM object (4-byte header)
23//!
24//! ```text
25//! +--------+--------+--------+--------+ ...
26//! | length (u16 BE) | code point (u16) | data (length-4 bytes)
27//! +--------+--------+--------+--------+ ...
28//! ```
29//! The `data` region may itself be a sequence of nested code-pointed objects
30//! (parameters).
31//!
32//! ## What this codec covers
33//!
34//! Enough of the object layer for the connection handshake
35//! (EXCSAT/ACCSEC/SECCHK/ACCRDB) and the `EXCSQLIMM` → `SQLCARD` basic-query path.
36//! DDM character fields (user id, RDB name) are IBM037 EBCDIC, decoded best-effort
37//! here; SQL text is FD:OCA data in the requester's declared CCSID (`decode_sqlstt`).
38//! See `mod.rs`/`AGENTS.md` for what is deliberately not implemented (SELECT
39//! result-set retrieval: OPNQRY/QRYDSC/QRYDTA).
40
41/// DDM magic byte that opens every DSS.
42pub const DSS_MAGIC: u8 = 0xD0;
43
44/// DSS format low-nibble types.
45pub const DSSFMT_RQSDSS: u8 = 0x01;
46pub const DSSFMT_RPYDSS: u8 = 0x02;
47pub const DSSFMT_OBJDSS: u8 = 0x03;
48
49/// DRDA/DDM code points used by the handshake and basic query path.
50pub mod cp {
51    // Commands (RQSDSS)
52    pub const EXCSAT: u16 = 0x1041; // Exchange Server Attributes
53    pub const ACCSEC: u16 = 0x106D; // Access Security
54    pub const SECCHK: u16 = 0x106E; // Security Check
55    pub const ACCRDB: u16 = 0x2001; // Access RDB
56    pub const EXCSQLIMM: u16 = 0x200A; // Execute Immediate SQL
57    pub const EXCSQLSTT: u16 = 0x200B; // Execute SQL Statement
58    pub const EXCSQLSET: u16 = 0x2014; // Set SQL environment (special registers)
59    pub const RDBCMM: u16 = 0x200E; // Commit unit of work
60    pub const RDBRLLBCK: u16 = 0x200F; // Roll back unit of work
61
62    // Reply objects / messages (RPYDSS / OBJDSS)
63    pub const EXCSATRD: u16 = 0x1443; // EXCSAT reply data
64    pub const ACCSECRD: u16 = 0x14AC; // Access Security reply data
65    pub const SECCHKRM: u16 = 0x1219; // Security Check reply message
66    pub const ACCRDBRM: u16 = 0x2201; // Access RDB reply message
67    pub const SQLCARD: u16 = 0x2408; // SQL Communications Area Reply Data
68    pub const CMDNSPRM: u16 = 0x1250; // Command Not Supported reply message
69    pub const ENDUOWRM: u16 = 0x220C; // End unit of work reply message
70    pub const RDBATHRM: u16 = 0x22CB; // Not authorized to RDB reply message
71
72    // Scalar parameters
73    pub const EXTNAM: u16 = 0x115E; // External name
74    pub const SRVNAM: u16 = 0x116D; // Server name
75    pub const SRVRLSLV: u16 = 0x115A; // Server product release level
76    pub const SRVCLSNM: u16 = 0x1147; // Server class name
77    pub const MGRLVLLS: u16 = 0x1404; // Manager-level list
78    pub const SECMEC: u16 = 0x11A2; // Security mechanism
79    pub const SECCHKCD: u16 = 0x11A4; // Security-check code
80    pub const SVRCOD: u16 = 0x1149; // Severity code
81    pub const RDBNAM: u16 = 0x2110; // Relational database name
82    pub const USRID: u16 = 0x11A0; // User id
83    pub const PASSWORD: u16 = 0x11A1; // Password
84    pub const PRDID: u16 = 0x112E; // Product-specific identifier
85    pub const TYPDEFNAM: u16 = 0x002F; // Data-type-definition name
86    pub const TYPDEFOVR: u16 = 0x0035; // TYPDEF overrides
87    pub const SQLSTT: u16 = 0x2414; // SQL statement
88    pub const SQLSTTGRP: u16 = 0x2214; // SQL statement group (mixed/single byte)
89    pub const UOWDSP: u16 = 0x2115; // Unit of work disposition
90    pub const CCSIDSBC: u16 = 0x119C; // CCSID of single-byte characters
91    pub const CCSIDMBC: u16 = 0x119E; // CCSID of mixed-byte characters
92}
93
94/// SVRCOD severity levels.
95pub mod svrcod {
96    pub const INFO: u16 = 0;
97    pub const WARNING: u16 = 4;
98    pub const ERROR: u16 = 8;
99    pub const SEVERE: u16 = 16;
100}
101
102/// SECCHKCD security-check outcome codes (subset).
103pub mod secchkcd {
104    pub const SUCCESS: u8 = 0x00; // authentication succeeded
105    pub const PASSWORD_INVALID: u8 = 0x0F; // userid known, password invalid
106    pub const USERID_MISSING: u8 = 0x12; // userid missing
107    pub const USERID_UNKNOWN: u8 = 0x10; // userid not known
108}
109
110/// Encode a DDM object: `length(u16) | codepoint(u16) | data`.
111pub fn encode_object(codepoint: u16, data: &[u8]) -> Vec<u8> {
112    let len = (data.len() + 4) as u16;
113    let mut out = Vec::with_capacity(data.len() + 4);
114    out.extend_from_slice(&len.to_be_bytes());
115    out.extend_from_slice(&codepoint.to_be_bytes());
116    out.extend_from_slice(data);
117    out
118}
119
120/// Encode a scalar parameter (same wire shape as an object).
121pub fn encode_scalar(codepoint: u16, data: &[u8]) -> Vec<u8> {
122    encode_object(codepoint, data)
123}
124
125/// Encode a scalar parameter whose value is EBCDIC-encoded ASCII text.
126pub fn encode_scalar_str(codepoint: u16, text: &str) -> Vec<u8> {
127    encode_object(codepoint, &ascii_to_ebcdic(text))
128}
129
130/// Wrap a DDM object (or concatenation of them) in a DSS envelope.
131///
132/// * `dss_type` — one of `DSSFMT_RQSDSS` / `DSSFMT_RPYDSS` / `DSSFMT_OBJDSS`.
133/// * `chained` — set the "chained, same correlator" flags (0x40 | 0x10) so the
134///   peer expects another DSS with the same correlator to follow.
135pub fn encode_dss(dss_type: u8, chained: bool, correlator: u16, ddm: &[u8]) -> Vec<u8> {
136    let total = (ddm.len() + 6) as u16;
137    let format = if chained {
138        // 0x40 = DSSCHAIN (chained to next), 0x10 = next DSS has same correlator.
139        0x40 | 0x10 | (dss_type & 0x0F)
140    } else {
141        dss_type & 0x0F
142    };
143    let mut out = Vec::with_capacity(ddm.len() + 6);
144    out.extend_from_slice(&total.to_be_bytes());
145    out.push(DSS_MAGIC);
146    out.push(format);
147    out.extend_from_slice(&correlator.to_be_bytes());
148    out.extend_from_slice(ddm);
149    out
150}
151
152/// A parsed DSS carrying its top-level DDM command/object.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct ParsedDss {
155    /// Raw format byte (chaining flags + DSS type).
156    pub format: u8,
157    /// Low nibble of `format`: the DSS type.
158    pub dss_type: u8,
159    /// Request/reply correlator.
160    pub correlator: u16,
161    /// Top-level DDM code point (the command, e.g. EXCSAT).
162    pub codepoint: u16,
163    /// The top-level object's data region (nested parameters live here).
164    pub body: Vec<u8>,
165}
166
167/// Errors the DRDA parser can produce. All are recoverable at the connection
168/// level (the server logs and drops the connection) — none panic.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub enum DrdaError {
171    /// Fewer bytes than a DSS/DDM header requires.
172    Truncated,
173    /// The DDM magic byte (0xD0) was not where a DSS header requires it.
174    BadMagic(u8),
175    /// A declared length field was impossible (smaller than its own header).
176    BadLength(u16),
177}
178
179impl std::fmt::Display for DrdaError {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        match self {
182            DrdaError::Truncated => write!(f, "truncated DRDA frame"),
183            DrdaError::BadMagic(b) => write!(f, "bad DDM magic 0x{:02X} (expected 0xD0)", b),
184            DrdaError::BadLength(l) => write!(f, "impossible DRDA length {}", l),
185        }
186    }
187}
188
189impl std::error::Error for DrdaError {}
190
191/// The declared total length of the DSS at the front of `buf`, if the 2-byte
192/// length prefix is present. Used by the read loop to know how many bytes make a
193/// full frame before parsing it.
194pub fn dss_declared_len(buf: &[u8]) -> Option<usize> {
195    if buf.len() < 2 {
196        return None;
197    }
198    Some(u16::from_be_bytes([buf[0], buf[1]]) as usize)
199}
200
201/// Parse a single DSS from the front of `buf`, returning the parsed command and
202/// the number of bytes consumed.
203pub fn parse_dss(buf: &[u8]) -> Result<(ParsedDss, usize), DrdaError> {
204    if buf.len() < 6 {
205        return Err(DrdaError::Truncated);
206    }
207    let total = u16::from_be_bytes([buf[0], buf[1]]) as usize;
208    if total < 6 {
209        return Err(DrdaError::BadLength(total as u16));
210    }
211    if buf[2] != DSS_MAGIC {
212        return Err(DrdaError::BadMagic(buf[2]));
213    }
214    let format = buf[3];
215    let correlator = u16::from_be_bytes([buf[4], buf[5]]);
216    if buf.len() < total {
217        return Err(DrdaError::Truncated);
218    }
219    let ddm = &buf[6..total];
220    // Top-level DDM object header.
221    if ddm.len() < 4 {
222        return Err(DrdaError::Truncated);
223    }
224    let obj_len = u16::from_be_bytes([ddm[0], ddm[1]]) as usize;
225    if obj_len < 4 {
226        return Err(DrdaError::BadLength(obj_len as u16));
227    }
228    let codepoint = u16::from_be_bytes([ddm[2], ddm[3]]);
229    let end = obj_len.min(ddm.len());
230    let body = ddm[4..end].to_vec();
231    Ok((
232        ParsedDss {
233            format,
234            dss_type: format & 0x0F,
235            correlator,
236            codepoint,
237            body,
238        },
239        total,
240    ))
241}
242
243/// Parse a flat list of code-pointed parameters from an object body.
244///
245/// Stops at the first malformed length rather than looping forever. Nested groups
246/// are returned as raw bytes (the caller descends into the ones it cares about).
247pub fn parse_params(body: &[u8]) -> Vec<(u16, Vec<u8>)> {
248    let mut out = Vec::new();
249    let mut i = 0;
250    while i + 4 <= body.len() {
251        let len = u16::from_be_bytes([body[i], body[i + 1]]) as usize;
252        let cp = u16::from_be_bytes([body[i + 2], body[i + 3]]);
253        if len < 4 || i + len > body.len() {
254            break;
255        }
256        out.push((cp, body[i + 4..i + len].to_vec()));
257        i += len;
258    }
259    out
260}
261
262/// Find the first parameter with `codepoint` in an object body.
263pub fn find_param(body: &[u8], codepoint: u16) -> Option<Vec<u8>> {
264    parse_params(body)
265        .into_iter()
266        .find(|(cp, _)| *cp == codepoint)
267        .map(|(_, v)| v)
268}
269
270// ============================================================================
271// SQLCARD (SQL Communications Area Reply Data)
272// ============================================================================
273
274/// A success SQLCARD: an SQLCAGRP with the FDOCA NULL indicator (0xFF), which the
275/// driver reads as SQLCODE 0. This is the complete reply a real Db2 server sends
276/// after a successful `EXCSQLIMM` of a non-query statement.
277pub fn sqlcard_success() -> Vec<u8> {
278    encode_object(cp::SQLCARD, &[0xFF])
279}
280
281/// An error/warning SQLCARD: SQLCODE and SQLSTATE in a complete SQLAM 7 SQLCA.
282pub fn sqlcard_error(sqlcode: i32, sqlstate: &str) -> Vec<u8> {
283    sqlcard(sqlcode, sqlstate, 0)
284}
285
286/// A complete SQLAM 7 SQLCARD: the SQLCAGRP with its SQLCAXGRP present (so SQLERRD3 can
287/// carry the rows a statement affected) and its SQLDIAGGRP null. Character fields are in
288/// the single-byte CCSID ACCRDBRM declares (UTF-8, so plain ASCII here). Byte layout:
289///
290/// ```text
291/// 0x00                       SQLCAGRP present
292/// SQLCODE    i32 BE          4 bytes
293/// SQLSTATE   5 chars         padded with blanks
294/// SQLERRPROC 8 chars         "NETGET  "
295/// 0x00                       SQLCAXGRP present
296///   SQLERRD1..6  6 x i32     SQLERRD3 = rows affected, the rest 0
297///   SQLWARN0..A  11 chars    blanks
298///   SQLRDBNAME   u16 0       empty VCS
299///   SQLERRMSG_m  u16 0       empty VCM
300///   SQLERRMSG_s  u16 0       empty VCS
301/// 0xFF                       SQLDIAGGRP null
302/// ```
303///
304/// SQLDIAGGRP's null indicator is not optional at SQLAM 7: a driver reads it after the
305/// SQLCAXGRP whether or not anything is in it.
306pub fn sqlcard(sqlcode: i32, sqlstate: &str, rows: i32) -> Vec<u8> {
307    let mut data = Vec::with_capacity(64);
308    data.push(0x00);
309    data.extend_from_slice(&sqlcode.to_be_bytes());
310    let mut state = [b' '; 5];
311    for (i, b) in sqlstate
312        .bytes()
313        .filter(u8::is_ascii_alphanumeric)
314        .take(5)
315        .enumerate()
316    {
317        state[i] = b;
318    }
319    data.extend_from_slice(&state);
320    data.extend_from_slice(b"NETGET  ");
321    data.push(0x00);
322    for errd in [0, 0, rows, 0, 0, 0] {
323        data.extend_from_slice(&errd.to_be_bytes());
324    }
325    data.extend_from_slice(&[b' '; 11]);
326    data.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
327    data.push(0xFF);
328    encode_object(cp::SQLCARD, &data)
329}
330
331/// Set the chaining flags of a reply to one request DSS.
332///
333/// DRDA replies to a request chain with a reply chain: every reply DSS but the very last one
334/// of the chain says "chained", and says "same correlator" when the next reply DSS carries the
335/// same correlator. `reply` holds the DSSes answering one request; `request_chained` is that
336/// request's own chained flag, which says whether more replies follow after these. A driver
337/// that sends EXCSAT chained to ACCSEC rejects an unchained EXCSATRD as a syntax error.
338pub fn chain_reply(mut reply: Vec<u8>, request_chained: bool) -> Vec<u8> {
339    let mut starts = Vec::new();
340    let mut pos = 0;
341    while pos + 6 <= reply.len() {
342        let len = u16::from_be_bytes([reply[pos], reply[pos + 1]]) as usize;
343        if len < 6 || len & 0x8000 != 0 || pos + len > reply.len() {
344            return reply;
345        }
346        starts.push(pos);
347        pos += len;
348    }
349    for (i, &at) in starts.iter().enumerate() {
350        let next = starts.get(i + 1);
351        let chained = next.is_some() || request_chained;
352        let same = next.is_some_and(|&n| reply[n + 4..n + 6] == reply[at + 4..at + 6]);
353        let mut format = reply[at + 3] & 0x0F;
354        if chained {
355            format |= 0x40;
356            if same {
357                format |= 0x10;
358            }
359        }
360        reply[at + 3] = format;
361    }
362    reply
363}
364
365/// The SQL text in an SQLSTT body.
366///
367/// DRDA carries it as FD:OCA data: a nullable mixed-character string with a 4-byte length
368/// (`0x00 | len | bytes`), then a nullable single-byte one, of which one is null. The text is
369/// in the CCSID the requester declared, which every driver here declares as UTF-8. A body
370/// that is not shaped like that is read as bare EBCDIC, the encoding this server's own
371/// byte-literal tests use.
372pub fn decode_sqlstt(body: &[u8]) -> String {
373    let field = |b: &[u8]| -> Option<(Option<Vec<u8>>, usize)> {
374        match b.first()? {
375            0xFF => Some((None, 1)),
376            0x00 if b.len() >= 5 => {
377                let n = u32::from_be_bytes([b[1], b[2], b[3], b[4]]) as usize;
378                let end = 5usize.checked_add(n)?;
379                (end <= b.len()).then(|| (Some(b[5..end].to_vec()), end))
380            }
381            _ => None,
382        }
383    };
384    if let Some((mixed, used)) = field(body) {
385        let single = field(&body[used..]);
386        if let Some((single, used2)) = single {
387            if used + used2 == body.len() {
388                if let Some(text) = mixed.or(single) {
389                    return String::from_utf8_lossy(&text).trim_end().to_string();
390                }
391            }
392        }
393    }
394    ebcdic_to_ascii(body)
395}
396
397// ============================================================================
398// IBM037 (CP037) EBCDIC <-> ASCII, range-based for the printable set.
399// ============================================================================
400
401/// Convert one ASCII byte to its IBM037 EBCDIC equivalent. Characters outside the
402/// mapped printable set become 0x6F (EBCDIC '?').
403pub fn ascii_byte_to_ebcdic(c: u8) -> u8 {
404    match c {
405        b'A'..=b'I' => 0xC1 + (c - b'A'),
406        b'J'..=b'R' => 0xD1 + (c - b'J'),
407        b'S'..=b'Z' => 0xE2 + (c - b'S'),
408        b'a'..=b'i' => 0x81 + (c - b'a'),
409        b'j'..=b'r' => 0x91 + (c - b'j'),
410        b's'..=b'z' => 0xA2 + (c - b's'),
411        b'0'..=b'9' => 0xF0 + (c - b'0'),
412        b' ' => 0x40,
413        b'.' => 0x4B,
414        b'<' => 0x4C,
415        b'(' => 0x4D,
416        b'+' => 0x4E,
417        b'|' => 0x4F,
418        b'&' => 0x50,
419        b'!' => 0x5A,
420        b'$' => 0x5B,
421        b'*' => 0x5C,
422        b')' => 0x5D,
423        b';' => 0x5E,
424        b'-' => 0x60,
425        b'/' => 0x61,
426        b',' => 0x6B,
427        b'%' => 0x6C,
428        b'_' => 0x6D,
429        b'>' => 0x6E,
430        b'?' => 0x6F,
431        b'`' => 0x79,
432        b':' => 0x7A,
433        b'#' => 0x7B,
434        b'@' => 0x7C,
435        b'\'' => 0x7D,
436        b'=' => 0x7E,
437        b'"' => 0x7F,
438        b'~' => 0xA1,
439        b'[' => 0xBA,
440        b']' => 0xBB,
441        b'{' => 0xC0,
442        b'}' => 0xD0,
443        b'\\' => 0xE0,
444        b'^' => 0xB0,
445        _ => 0x6F,
446    }
447}
448
449/// Convert one IBM037 EBCDIC byte to ASCII. Unmapped bytes become b'?'.
450pub fn ebcdic_byte_to_ascii(e: u8) -> u8 {
451    match e {
452        0xC1..=0xC9 => b'A' + (e - 0xC1),
453        0xD1..=0xD9 => b'J' + (e - 0xD1),
454        0xE2..=0xE9 => b'S' + (e - 0xE2),
455        0x81..=0x89 => b'a' + (e - 0x81),
456        0x91..=0x99 => b'j' + (e - 0x91),
457        0xA2..=0xA9 => b's' + (e - 0xA2),
458        0xF0..=0xF9 => b'0' + (e - 0xF0),
459        0x40 => b' ',
460        0x4B => b'.',
461        0x4C => b'<',
462        0x4D => b'(',
463        0x4E => b'+',
464        0x4F => b'|',
465        0x50 => b'&',
466        0x5A => b'!',
467        0x5B => b'$',
468        0x5C => b'*',
469        0x5D => b')',
470        0x5E => b';',
471        0x60 => b'-',
472        0x61 => b'/',
473        0x6B => b',',
474        0x6C => b'%',
475        0x6D => b'_',
476        0x6E => b'>',
477        0x6F => b'?',
478        0x79 => b'`',
479        0x7A => b':',
480        0x7B => b'#',
481        0x7C => b'@',
482        0x7D => b'\'',
483        0x7E => b'=',
484        0x7F => b'"',
485        0xA1 => b'~',
486        0xBA => b'[',
487        0xBB => b']',
488        0xC0 => b'{',
489        0xD0 => b'}',
490        0xE0 => b'\\',
491        0xB0 => b'^',
492        _ => b'?',
493    }
494}
495
496/// EBCDIC-encode an ASCII string.
497pub fn ascii_to_ebcdic(s: &str) -> Vec<u8> {
498    s.bytes().map(ascii_byte_to_ebcdic).collect()
499}
500
501/// EBCDIC-encode a byte slice already known to be ASCII.
502pub fn ascii_to_ebcdic_bytes(b: &[u8]) -> Vec<u8> {
503    b.iter().map(|&c| ascii_byte_to_ebcdic(c)).collect()
504}
505
506/// Decode EBCDIC bytes to an ASCII string, trimming trailing spaces.
507pub fn ebcdic_to_ascii(bytes: &[u8]) -> String {
508    let s: String = bytes
509        .iter()
510        .map(|&e| ebcdic_byte_to_ascii(e) as char)
511        .collect();
512    s.trim_end().to_string()
513}