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}