oracledb-protocol 0.9.1

Sans-I/O Oracle TNS/TTC protocol core for the oracledb crate.
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
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
#![forbid(unsafe_code)]

use super::*;

/// Whether the connect data travels inline in the CONNECT packet. Longer
/// descriptors must be sent in a separate DATA packet right after the CONNECT
/// packet (reference messages/connect.pyx `ConnectMessage.send`); the caller
/// owns that follow-up send.
pub fn connect_data_fits_inline(connect_data: &str) -> bool {
    connect_data.len() <= TNS_MAX_CONNECT_DATA
}

pub fn build_connect_packet_payload(connect_data: &str, sdu: u16) -> Result<Vec<u8>> {
    let connect_bytes = connect_data.as_bytes();
    let connect_len =
        u16::try_from(connect_bytes.len()).map_err(|_| ProtocolError::PacketTooLarge {
            length: connect_bytes.len(),
        })?;

    let mut writer = TtcWriter::new();
    writer.write_u16be(TNS_VERSION_DESIRED);
    writer.write_u16be(TNS_VERSION_MIN);
    writer.write_u16be(TNS_GSO_DONT_CARE);
    writer.write_u16be(sdu);
    writer.write_u16be(sdu);
    writer.write_u16be(TNS_PROTOCOL_CHARACTERISTICS);
    writer.write_u16be(0);
    writer.write_u16be(1);
    writer.write_u16be(connect_len);
    writer.write_u16be(74);
    writer.write_u32be(0);
    let nsi_flags = TNS_NSI_SUPPORT_SECURITY_RENEG | TNS_NSI_DISABLE_NA;
    writer.write_u8(nsi_flags);
    writer.write_u8(nsi_flags);
    writer.write_u64be(0);
    writer.write_u64be(0);
    writer.write_u64be(0);
    writer.write_u32be(u32::from(sdu));
    writer.write_u32be(u32::from(sdu));
    writer.write_u32be(0);
    writer.write_u32be(0);
    // Connect data above TNS_MAX_CONNECT_DATA is carried in a separate DATA
    // packet; the header still advertises the full length either way.
    if connect_data_fits_inline(connect_data) {
        writer.write_raw(connect_bytes);
    }
    Ok(writer.into_bytes())
}

pub fn parse_accept_payload(payload: &[u8]) -> Result<AcceptInfo> {
    let mut reader = TtcReader::new(payload);
    let protocol_version = reader.read_u16be()?;
    // Refuse below-floor servers BEFORE touching the rest of the payload
    // (reference messages/connect.pyx: `if protocol_version <
    // TNS_VERSION_MIN_ACCEPTED: ERR_SERVER_VERSION_NOT_SUPPORTED`). Pre-12.1
    // servers use an older, shorter ACCEPT layout — Oracle 11g (version 314)
    // sends 24 payload bytes, so parsing on would die with a misleading
    // "truncated TTC payload" instead of naming the real problem.
    if protocol_version < TNS_VERSION_MIN_ACCEPTED {
        return Err(ProtocolError::UnsupportedVersion {
            version: protocol_version,
            minimum: TNS_VERSION_MIN_ACCEPTED,
        });
    }
    let protocol_options = reader.read_u16be()?;
    reader.skip(10)?;
    let flags1 = reader.read_u8()?;
    if has_u8_flag(flags1, TNS_NSI_NA_REQUIRED) {
        return Err(ProtocolError::UnsupportedFeature(
            "Native Network Encryption and Data Integrity",
        ));
    }
    reader.skip(9)?;
    let sdu = reader.read_u32be()?;
    let mut flags2 = 0;
    if protocol_version >= 318 {
        reader.skip(5)?;
        flags2 = reader.read_u32be()?;
    }

    Ok(AcceptInfo {
        protocol_version,
        protocol_options,
        sdu,
        supports_fast_auth: has_u32_flag(flags2, TNS_ACCEPT_FLAG_FAST_AUTH),
        supports_oob_check: has_u32_flag(flags2, TNS_ACCEPT_FLAG_CHECK_OOB),
        // Reference: Capabilities.supports_oob = protocol_options &
        // TNS_GSO_CAN_RECV_ATTENTION (capabilities.pyx:121).
        supports_oob: protocol_options & TNS_GSO_CAN_RECV_ATTENTION != 0,
        supports_end_of_response: protocol_version >= 319
            && has_u32_flag(flags2, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE),
    })
}

pub fn build_fast_auth_phase_one_payload(
    user: &str,
    program: &str,
    machine: &str,
    osuser: &str,
    terminal: &str,
    pid: u32,
) -> Result<Vec<u8>> {
    let mut out = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
    append_auth_phase_one(&mut out, user, program, machine, osuser, terminal, pid)?;
    Ok(out)
}

// Byte layout of FAST_AUTH_PREFIX_HEX (mirrors reference
// messages/fast_auth.pyx `FastAuthMessage._write_message`):
//   [0..4)    fast-auth envelope: msg type 34, version 1, char-conv flags
//   [4..23)   embedded protocol-negotiation message (msg type 1, version 6,
//             terminator, driver name string + NUL)
//   [23..29)  envelope glue: unused server charset/ncharset placeholders +
//             the pinned ttc field version byte
//   [29..)    embedded data-types message (msg type 2, UTF8 charsets,
//             encoding flags, compile/runtime caps, static type table)
// The classic (non-fast-auth) handshake sends the same two embedded messages
// as standalone round trips, so pre-23ai servers negotiate byte-identically
// to the reference implementation.
const FAST_AUTH_PROTOCOL_MSG_START: usize = 4;
const FAST_AUTH_PROTOCOL_MSG_END: usize = 23;
const FAST_AUTH_DATA_TYPES_MSG_START: usize = 29;

fn fast_auth_prefix_slice(start: usize, end: Option<usize>) -> Result<Vec<u8>> {
    let prefix = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
    let slice = match end {
        Some(end) => prefix.get(start..end),
        None => prefix.get(start..),
    };
    slice
        .map(<[u8]>::to_vec)
        .ok_or(ProtocolError::TtcDecode("fast-auth prefix too short"))
}

/// Standalone TTC protocol-negotiation message (msg type 1) for the classic
/// pre-23ai handshake. Byte-identical to the copy embedded in the fast-auth
/// bundle (reference messages/protocol.pyx `ProtocolMessage._write_message`).
pub fn build_protocol_negotiation_payload() -> Result<Vec<u8>> {
    let payload = fast_auth_prefix_slice(
        FAST_AUTH_PROTOCOL_MSG_START,
        Some(FAST_AUTH_PROTOCOL_MSG_END),
    )?;
    debug_assert_eq!(payload.first(), Some(&TNS_MSG_TYPE_PROTOCOL));
    Ok(payload)
}

/// Standalone TTC data-types message (msg type 2) for the classic pre-23ai
/// handshake. Byte-identical to the copy embedded in the fast-auth bundle
/// (reference messages/data_types.pyx `DataTypesMessage._write_message`).
pub fn build_data_types_payload() -> Result<Vec<u8>> {
    let payload = fast_auth_prefix_slice(FAST_AUTH_DATA_TYPES_MSG_START, None)?;
    debug_assert_eq!(payload.first(), Some(&TNS_MSG_TYPE_DATA_TYPES));
    Ok(payload)
}

/// Standalone auth phase-one function message for the classic pre-23ai
/// handshake — the same message [`build_fast_auth_phase_one_payload`] appends
/// after the fast-auth bundle, sent on its own round trip instead.
pub fn build_auth_phase_one_payload(
    user: &str,
    program: &str,
    machine: &str,
    osuser: &str,
    terminal: &str,
    pid: u32,
) -> Result<Vec<u8>> {
    let mut out = Vec::new();
    append_auth_phase_one(&mut out, user, program, machine, osuser, terminal, pid)?;
    Ok(out)
}

/// Fast-auth bundle for **token authentication**: the same static
/// protocol/data-types prefix as [`build_fast_auth_phase_one_payload`], but with
/// a phase-two `AUTH_TOKEN` message appended (no verifier round-trip). The caller
/// sends this once and reads a single auth response.
pub fn build_fast_auth_token_payload(
    user: &str,
    token: &str,
    driver_name: &str,
    version_num: u32,
    connect_string: &str,
    edition: Option<&str>,
) -> Result<Vec<u8>> {
    build_fast_auth_token_payload_with_pop(
        user,
        token,
        driver_name,
        version_num,
        connect_string,
        edition,
        None,
    )
}

/// [`build_fast_auth_token_payload`] with optional OCI IAM proof-of-possession.
///
/// This preserves the public 0.8.x API. For proxy token authentication, use
/// [`build_fast_auth_token_payload_with_pop_and_proxy`].
pub fn build_fast_auth_token_payload_with_pop(
    user: &str,
    token: &str,
    driver_name: &str,
    version_num: u32,
    connect_string: &str,
    edition: Option<&str>,
    pop: Option<TokenPop<'_>>,
) -> Result<Vec<u8>> {
    build_fast_auth_token_payload_with_pop_and_proxy(
        user,
        token,
        driver_name,
        version_num,
        connect_string,
        edition,
        pop,
        None,
    )
}

/// [`build_fast_auth_token_payload_with_pop`] with optional proxy user
/// (`PROXY_CLIENT_NAME`).
#[allow(clippy::too_many_arguments)] // public token-auth compatibility surface
pub fn build_fast_auth_token_payload_with_pop_and_proxy(
    user: &str,
    token: &str,
    driver_name: &str,
    version_num: u32,
    connect_string: &str,
    edition: Option<&str>,
    pop: Option<TokenPop<'_>>,
    proxy_user: Option<&str>,
) -> Result<Vec<u8>> {
    let mut out = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
    append_auth_phase_two_token_with_pop_and_proxy(
        &mut out,
        user,
        token,
        driver_name,
        version_num,
        connect_string,
        edition,
        pop,
        proxy_user,
    )?;
    Ok(out)
}

/// Builds the standalone classic-auth phase-two token message.
///
/// Unlike [`build_fast_auth_token_payload`], this does not prepend the
/// fast-auth envelope. Pre-23ai servers receive protocol negotiation and data
/// types as separate round trips, then this self-contained `AUTH_TOKEN`
/// phase-two message.
pub fn build_auth_phase_two_token_payload(
    user: &str,
    token: &str,
    driver_name: &str,
    version_num: u32,
    connect_string: &str,
    edition: Option<&str>,
) -> Result<Vec<u8>> {
    build_auth_phase_two_token_payload_with_pop(
        user,
        token,
        driver_name,
        version_num,
        connect_string,
        edition,
        None,
    )
}

/// [`build_auth_phase_two_token_payload`] with optional OCI IAM proof-of-possession.
///
/// This preserves the public 0.8.x API. For proxy token authentication, use
/// [`build_auth_phase_two_token_payload_with_pop_and_proxy`].
pub fn build_auth_phase_two_token_payload_with_pop(
    user: &str,
    token: &str,
    driver_name: &str,
    version_num: u32,
    connect_string: &str,
    edition: Option<&str>,
    pop: Option<TokenPop<'_>>,
) -> Result<Vec<u8>> {
    build_auth_phase_two_token_payload_with_pop_and_proxy(
        user,
        token,
        driver_name,
        version_num,
        connect_string,
        edition,
        pop,
        None,
    )
}

/// [`build_auth_phase_two_token_payload_with_pop`] with optional proxy user
/// (`PROXY_CLIENT_NAME`).
#[allow(clippy::too_many_arguments)] // public token-auth compatibility surface
pub fn build_auth_phase_two_token_payload_with_pop_and_proxy(
    user: &str,
    token: &str,
    driver_name: &str,
    version_num: u32,
    connect_string: &str,
    edition: Option<&str>,
    pop: Option<TokenPop<'_>>,
    proxy_user: Option<&str>,
) -> Result<Vec<u8>> {
    let mut out = Vec::new();
    append_auth_phase_two_token_with_pop_and_proxy(
        &mut out,
        user,
        token,
        driver_name,
        version_num,
        connect_string,
        edition,
        pop,
        proxy_user,
    )?;
    Ok(out)
}

pub fn build_function_payload(function_code: u8, ttc_field_version: u8) -> Vec<u8> {
    build_function_payload_with_seq(function_code, 1, ttc_field_version)
}

pub fn build_function_payload_with_seq(
    function_code: u8,
    seq_num: u8,
    ttc_field_version: u8,
) -> Vec<u8> {
    build_function_payload_with_seq_and_token(function_code, seq_num, 0, ttc_field_version)
}

/// Bare function message with an explicit pipeline token (messages/base.pyx
/// `_write_function_code` writes `ub8 token_num` for field version >= 23.1
/// ext 1; non-pipelined messages carry 0). On a pre-23.1-ext-1 connection the
/// token field does not exist on the wire at all; pipelining (nonzero tokens)
/// only happens on 23ai-negotiated connections, so no token is ever dropped.
pub fn build_function_payload_with_seq_and_token(
    function_code: u8,
    seq_num: u8,
    token_num: u64,
    ttc_field_version: u8,
) -> Vec<u8> {
    let mut writer = TtcWriter::new();
    writer.write_function_code_with_seq(function_code, seq_num);
    if version_gates::writes_pipeline_token(ttc_field_version) {
        writer.write_ub8(token_num);
    } else {
        debug_assert_eq!(
            token_num, 0,
            "pipeline tokens require a 23ai-negotiated connection"
        );
    }
    writer.into_bytes()
}

pub(crate) fn skip_protocol_message(
    reader: &mut TtcReader<'_>,
) -> Result<Option<ClientCapabilities>> {
    let _server_version = reader.read_u8()?;
    reader.skip(1)?;
    loop {
        if reader.read_u8()? == 0 {
            break;
        }
    }
    let charset_id = reader.read_u16le()?;
    let _server_flags = reader.read_u8()?;
    let num_elem = reader.read_u16le()?;
    reader.skip(usize::from(num_elem) * 5)?;
    let fdo_len = reader.read_u16be()?;
    reader.skip(usize::from(fdo_len))?;
    let compile_caps = reader.read_bytes()?;
    let runtime_caps = reader.read_bytes()?;
    let Some(compile_caps) = compile_caps else {
        return Ok(None);
    };
    let server_ttc_field_version = compile_caps
        .get(TNS_CCAP_FIELD_VERSION)
        .copied()
        .unwrap_or_else(|| ClientCapabilities::default().ttc_field_version);
    // The effective field version is the LOWER of what the server reports and
    // what this client supports (reference capabilities.pyx
    // `_adjust_for_server_compile_caps`: "if server < client: client =
    // server"). Taking the max would over-claim 23ai-era field formats against
    // pre-23ai servers.
    let ttc_field_version =
        server_ttc_field_version.min(ClientCapabilities::default().ttc_field_version);
    let max_string_size = if runtime_caps
        .as_deref()
        .and_then(|caps| caps.get(TNS_RCAP_TTC))
        .is_some_and(|flags| flags & TNS_RCAP_TTC_32K != 0)
    {
        32_767
    } else {
        4_000
    };
    Ok(Some(ClientCapabilities {
        ttc_field_version,
        max_string_size,
        charset_id,
    }))
}

pub(crate) fn skip_data_types_response(reader: &mut TtcReader<'_>) -> Result<()> {
    loop {
        let data_type = reader.read_u16be()?;
        if data_type == 0 {
            break;
        }
        let conv_data_type = reader.read_u16be()?;
        if conv_data_type != 0 {
            reader.skip(4)?;
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn classic_handshake_messages_slice_the_fast_auth_prefix() {
        let protocol = build_protocol_negotiation_payload().expect("protocol payload");
        assert_eq!(protocol[0], TNS_MSG_TYPE_PROTOCOL);
        assert_eq!(protocol[1], 6, "protocol version byte (8.1 and higher)");
        assert_eq!(protocol[2], 0, "array terminator");
        assert!(
            protocol.ends_with(b"python-oracledb\0"),
            "driver name string with NUL terminator"
        );

        let data_types = build_data_types_payload().expect("data types payload");
        assert_eq!(data_types[0], TNS_MSG_TYPE_DATA_TYPES);
        // UTF8 charset (873) little-endian for charset and ncharset.
        assert_eq!(&data_types[1..5], &[0x69, 0x03, 0x69, 0x03]);

        // The two standalone messages are exact slices of the fast-auth bundle,
        // so classic and fast-auth handshakes can never drift apart.
        let full = Vec::from_hex(FAST_AUTH_PREFIX_HEX).expect("prefix decodes");
        assert_eq!(full[0], TNS_MSG_TYPE_FAST_AUTH);
        assert_eq!(
            &full[FAST_AUTH_PROTOCOL_MSG_START..FAST_AUTH_PROTOCOL_MSG_END],
            &protocol[..]
        );
        assert_eq!(&full[FAST_AUTH_DATA_TYPES_MSG_START..], &data_types[..]);
    }

    #[test]
    fn classic_token_payload_is_the_fast_auth_phase_two_suffix() {
        let classic = build_auth_phase_two_token_payload(
            "scott",
            "token-secret",
            "rust-oracledb",
            4_000_000_000,
            "db.example.com/service",
            Some("MY_EDITION"),
        )
        .expect("classic token payload");
        let fast = build_fast_auth_token_payload(
            "scott",
            "token-secret",
            "rust-oracledb",
            4_000_000_000,
            "db.example.com/service",
            Some("MY_EDITION"),
        )
        .expect("fast token payload");
        let prefix = Vec::from_hex(FAST_AUTH_PREFIX_HEX).expect("prefix decodes");

        assert_eq!(&fast[..prefix.len()], prefix.as_slice());
        assert_eq!(&fast[prefix.len()..], classic.as_slice());
        assert_eq!(classic[0], TNS_MSG_TYPE_FUNCTION);
        assert_eq!(classic[1], TNS_FUNC_AUTH_PHASE_TWO);
        assert_eq!(
            classic[2], 1,
            "standalone phase two is the first TTC function"
        );
    }

    // ---- ACCEPT protocol-version gate boundary tests ----------------------
    //
    // parse_accept_payload mirrors three reference gates keyed on the ACCEPT's
    // protocol_version / protocol_options (references connect.pyx:65/75/111,
    // capabilities.pyx:126, protocol.pyx:262). The live matrix crosses these
    // (all live servers are >= 318, 23ai advertises end-of-response), but this
    // offline test pins each boundary exactly.

    /// A full (>= 318 layout) ACCEPT payload with a caller-controlled
    /// protocol_version field, so the same trailing flags2 bytes can be parsed
    /// on either side of the 318/319 gates.
    fn accept_bytes(version: u16, options: u16, flags2: u32) -> Vec<u8> {
        let mut w = TtcWriter::new();
        w.write_u16be(version); // protocol version
        w.write_u16be(options); // protocol options
        w.write_raw(&[0u8; 10]); // skip(10)
        w.write_u8(0); // flags1 (no NA_REQUIRED)
        w.write_raw(&[0u8; 9]); // skip(9)
        w.write_u32be(8192); // sdu
        w.write_raw(&[0u8; 5]); // skip(5) before flags2
        w.write_u32be(flags2); // flags2 (only read when version >= 318)
        w.into_bytes()
    }

    #[test]
    fn accept_parsing_gates_capabilities_on_protocol_version() {
        // protocol.pyx:262 — supports_oob is a plain flag on protocol_options,
        // independent of protocol_version.
        assert!(
            !parse_accept_payload(&accept_bytes(319, 0, 0))
                .unwrap()
                .supports_oob,
            "no CAN_RECV_ATTENTION bit => supports_oob false"
        );
        assert!(
            parse_accept_payload(&accept_bytes(319, TNS_GSO_CAN_RECV_ATTENTION, 0))
                .unwrap()
                .supports_oob,
            "CAN_RECV_ATTENTION bit => supports_oob true"
        );

        // connect.pyx:75 (MIN_OOB_CHECK, >= 318) — flags2 (and everything it
        // carries) is only read at/above 318. Same trailing bytes, version off
        // by one, must flip the derived capability.
        let flags2 = TNS_ACCEPT_FLAG_FAST_AUTH | TNS_ACCEPT_FLAG_CHECK_OOB;
        assert!(
            !parse_accept_payload(&accept_bytes(317, 0, flags2))
                .unwrap()
                .supports_fast_auth,
            "below 318 flags2 is not read"
        );
        let at_318 = parse_accept_payload(&accept_bytes(318, 0, flags2)).unwrap();
        assert!(at_318.supports_fast_auth, "at 318 flags2 is read");
        assert!(
            at_318.supports_oob_check,
            "at 318 the CHECK_OOB flag is read"
        );

        // capabilities.pyx:126 (MIN_END_OF_RESPONSE) — end-of-response requires
        // BOTH protocol_version >= 319 AND the flag. Prove the version gate and
        // the flag gate independently.
        assert!(
            !parse_accept_payload(&accept_bytes(318, 0, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE))
                .unwrap()
                .supports_end_of_response,
            "318 < 319: no end-of-response even with the flag"
        );
        assert!(
            parse_accept_payload(&accept_bytes(319, 0, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE))
                .unwrap()
                .supports_end_of_response,
            "319 + flag: end-of-response negotiated"
        );
        assert!(
            !parse_accept_payload(&accept_bytes(319, 0, 0))
                .unwrap()
                .supports_end_of_response,
            "319 without the flag: no end-of-response"
        );
    }

    /// Minimal protocol-info message with controllable compile/runtime caps.
    /// The leading fields are deliberately empty because this test targets the
    /// capability section consumed by `skip_protocol_message`.
    fn protocol_info_with_caps(field_version: u8, runtime_ttc: u8) -> Vec<u8> {
        let mut writer = TtcWriter::new();
        writer.write_u8(0); // server version (not used by the parser)
        writer.write_u8(0); // skipped server flags
        writer.write_u8(0); // NUL-terminated server version string
        writer.write_u16le(873); // AL32UTF8 charset
        writer.write_u8(0); // server flags
        writer.write_u16le(0); // no element descriptors
        writer.write_u16be(0); // no FDO bytes

        let mut compile_caps = vec![0; TNS_CCAP_FIELD_VERSION + 1];
        compile_caps[TNS_CCAP_FIELD_VERSION] = field_version;
        writer
            .write_bytes_with_length(&compile_caps)
            .expect("short compile caps");

        let mut runtime_caps = vec![0; TNS_RCAP_TTC + 1];
        runtime_caps[TNS_RCAP_TTC] = runtime_ttc;
        writer
            .write_bytes_with_length(&runtime_caps)
            .expect("short runtime caps");
        writer.into_bytes()
    }

    #[test]
    fn nineteen_c_caps_profile_derives_the_reference_19c_mask() {
        // constants.pxi:503 defines 19.1-ext1 as 13. A 19c-shaped protocol
        // message has no 32K TTC bit, so python-oracledb selects 4K strings
        // (capabilities.pyx:134-150); the Rust parser must make the same mask.
        let bytes = protocol_info_with_caps(TNS_CCAP_FIELD_VERSION_19_1_EXT_1, 0);
        let caps = skip_protocol_message(&mut TtcReader::new(&bytes))
            .expect("19c profile decodes")
            .expect("compile caps present");

        assert_eq!(
            caps.ttc_field_version, TNS_CCAP_FIELD_VERSION_19_1_EXT_1,
            "server field version caps the client at the 19c profile"
        );
        assert_eq!(caps.max_string_size, 4_000);
        assert_eq!(caps.charset_id, 873);
    }
}