Skip to main content

oracledb_protocol/thin/
connect.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4
5/// Whether the connect data travels inline in the CONNECT packet. Longer
6/// descriptors must be sent in a separate DATA packet right after the CONNECT
7/// packet (reference messages/connect.pyx `ConnectMessage.send`); the caller
8/// owns that follow-up send.
9pub fn connect_data_fits_inline(connect_data: &str) -> bool {
10    connect_data.len() <= TNS_MAX_CONNECT_DATA
11}
12
13pub fn build_connect_packet_payload(connect_data: &str, sdu: u16) -> Result<Vec<u8>> {
14    let connect_bytes = connect_data.as_bytes();
15    let connect_len =
16        u16::try_from(connect_bytes.len()).map_err(|_| ProtocolError::PacketTooLarge {
17            length: connect_bytes.len(),
18        })?;
19
20    let mut writer = TtcWriter::new();
21    writer.write_u16be(TNS_VERSION_DESIRED);
22    writer.write_u16be(TNS_VERSION_MIN);
23    writer.write_u16be(TNS_GSO_DONT_CARE);
24    writer.write_u16be(sdu);
25    writer.write_u16be(sdu);
26    writer.write_u16be(TNS_PROTOCOL_CHARACTERISTICS);
27    writer.write_u16be(0);
28    writer.write_u16be(1);
29    writer.write_u16be(connect_len);
30    writer.write_u16be(74);
31    writer.write_u32be(0);
32    let nsi_flags = TNS_NSI_SUPPORT_SECURITY_RENEG | TNS_NSI_DISABLE_NA;
33    writer.write_u8(nsi_flags);
34    writer.write_u8(nsi_flags);
35    writer.write_u64be(0);
36    writer.write_u64be(0);
37    writer.write_u64be(0);
38    writer.write_u32be(u32::from(sdu));
39    writer.write_u32be(u32::from(sdu));
40    writer.write_u32be(0);
41    writer.write_u32be(0);
42    // Connect data above TNS_MAX_CONNECT_DATA is carried in a separate DATA
43    // packet; the header still advertises the full length either way.
44    if connect_data_fits_inline(connect_data) {
45        writer.write_raw(connect_bytes);
46    }
47    Ok(writer.into_bytes())
48}
49
50pub fn parse_accept_payload(payload: &[u8]) -> Result<AcceptInfo> {
51    let mut reader = TtcReader::new(payload);
52    let protocol_version = reader.read_u16be()?;
53    // Refuse below-floor servers BEFORE touching the rest of the payload
54    // (reference messages/connect.pyx: `if protocol_version <
55    // TNS_VERSION_MIN_ACCEPTED: ERR_SERVER_VERSION_NOT_SUPPORTED`). Pre-12.1
56    // servers use an older, shorter ACCEPT layout — Oracle 11g (version 314)
57    // sends 24 payload bytes, so parsing on would die with a misleading
58    // "truncated TTC payload" instead of naming the real problem.
59    if protocol_version < TNS_VERSION_MIN_ACCEPTED {
60        return Err(ProtocolError::UnsupportedVersion {
61            version: protocol_version,
62            minimum: TNS_VERSION_MIN_ACCEPTED,
63        });
64    }
65    let protocol_options = reader.read_u16be()?;
66    reader.skip(10)?;
67    let flags1 = reader.read_u8()?;
68    if has_u8_flag(flags1, TNS_NSI_NA_REQUIRED) {
69        return Err(ProtocolError::UnsupportedFeature(
70            "Native Network Encryption and Data Integrity",
71        ));
72    }
73    reader.skip(9)?;
74    let sdu = reader.read_u32be()?;
75    let mut flags2 = 0;
76    if protocol_version >= 318 {
77        reader.skip(5)?;
78        flags2 = reader.read_u32be()?;
79    }
80
81    Ok(AcceptInfo {
82        protocol_version,
83        protocol_options,
84        sdu,
85        supports_fast_auth: has_u32_flag(flags2, TNS_ACCEPT_FLAG_FAST_AUTH),
86        supports_oob_check: has_u32_flag(flags2, TNS_ACCEPT_FLAG_CHECK_OOB),
87        // Reference: Capabilities.supports_oob = protocol_options &
88        // TNS_GSO_CAN_RECV_ATTENTION (capabilities.pyx:121).
89        supports_oob: protocol_options & TNS_GSO_CAN_RECV_ATTENTION != 0,
90        supports_end_of_response: protocol_version >= 319
91            && has_u32_flag(flags2, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE),
92    })
93}
94
95pub fn build_fast_auth_phase_one_payload(
96    user: &str,
97    program: &str,
98    machine: &str,
99    osuser: &str,
100    terminal: &str,
101    pid: u32,
102) -> Result<Vec<u8>> {
103    let mut out = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
104        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
105    append_auth_phase_one(&mut out, user, program, machine, osuser, terminal, pid)?;
106    Ok(out)
107}
108
109// Byte layout of FAST_AUTH_PREFIX_HEX (mirrors reference
110// messages/fast_auth.pyx `FastAuthMessage._write_message`):
111//   [0..4)    fast-auth envelope: msg type 34, version 1, char-conv flags
112//   [4..23)   embedded protocol-negotiation message (msg type 1, version 6,
113//             terminator, driver name string + NUL)
114//   [23..29)  envelope glue: unused server charset/ncharset placeholders +
115//             the pinned ttc field version byte
116//   [29..)    embedded data-types message (msg type 2, UTF8 charsets,
117//             encoding flags, compile/runtime caps, static type table)
118// The classic (non-fast-auth) handshake sends the same two embedded messages
119// as standalone round trips, so pre-23ai servers negotiate byte-identically
120// to the reference implementation.
121const FAST_AUTH_PROTOCOL_MSG_START: usize = 4;
122const FAST_AUTH_PROTOCOL_MSG_END: usize = 23;
123const FAST_AUTH_DATA_TYPES_MSG_START: usize = 29;
124
125fn fast_auth_prefix_slice(start: usize, end: Option<usize>) -> Result<Vec<u8>> {
126    let prefix = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
127        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
128    let slice = match end {
129        Some(end) => prefix.get(start..end),
130        None => prefix.get(start..),
131    };
132    slice
133        .map(<[u8]>::to_vec)
134        .ok_or(ProtocolError::TtcDecode("fast-auth prefix too short"))
135}
136
137/// Standalone TTC protocol-negotiation message (msg type 1) for the classic
138/// pre-23ai handshake. Byte-identical to the copy embedded in the fast-auth
139/// bundle (reference messages/protocol.pyx `ProtocolMessage._write_message`).
140pub fn build_protocol_negotiation_payload() -> Result<Vec<u8>> {
141    let payload = fast_auth_prefix_slice(
142        FAST_AUTH_PROTOCOL_MSG_START,
143        Some(FAST_AUTH_PROTOCOL_MSG_END),
144    )?;
145    debug_assert_eq!(payload.first(), Some(&TNS_MSG_TYPE_PROTOCOL));
146    Ok(payload)
147}
148
149/// Standalone TTC data-types message (msg type 2) for the classic pre-23ai
150/// handshake. Byte-identical to the copy embedded in the fast-auth bundle
151/// (reference messages/data_types.pyx `DataTypesMessage._write_message`).
152pub fn build_data_types_payload() -> Result<Vec<u8>> {
153    let payload = fast_auth_prefix_slice(FAST_AUTH_DATA_TYPES_MSG_START, None)?;
154    debug_assert_eq!(payload.first(), Some(&TNS_MSG_TYPE_DATA_TYPES));
155    Ok(payload)
156}
157
158/// Standalone auth phase-one function message for the classic pre-23ai
159/// handshake — the same message [`build_fast_auth_phase_one_payload`] appends
160/// after the fast-auth bundle, sent on its own round trip instead.
161pub fn build_auth_phase_one_payload(
162    user: &str,
163    program: &str,
164    machine: &str,
165    osuser: &str,
166    terminal: &str,
167    pid: u32,
168) -> Result<Vec<u8>> {
169    let mut out = Vec::new();
170    append_auth_phase_one(&mut out, user, program, machine, osuser, terminal, pid)?;
171    Ok(out)
172}
173
174/// Fast-auth bundle for **token authentication**: the same static
175/// protocol/data-types prefix as [`build_fast_auth_phase_one_payload`], but with
176/// a phase-two `AUTH_TOKEN` message appended (no verifier round-trip). The caller
177/// sends this once and reads a single auth response.
178pub fn build_fast_auth_token_payload(
179    user: &str,
180    token: &str,
181    driver_name: &str,
182    version_num: u32,
183    connect_string: &str,
184    edition: Option<&str>,
185) -> Result<Vec<u8>> {
186    build_fast_auth_token_payload_with_pop(
187        user,
188        token,
189        driver_name,
190        version_num,
191        connect_string,
192        edition,
193        None,
194    )
195}
196
197/// [`build_fast_auth_token_payload`] with optional OCI IAM proof-of-possession.
198///
199/// This preserves the public 0.8.x API. For proxy token authentication, use
200/// [`build_fast_auth_token_payload_with_pop_and_proxy`].
201pub fn build_fast_auth_token_payload_with_pop(
202    user: &str,
203    token: &str,
204    driver_name: &str,
205    version_num: u32,
206    connect_string: &str,
207    edition: Option<&str>,
208    pop: Option<TokenPop<'_>>,
209) -> Result<Vec<u8>> {
210    build_fast_auth_token_payload_with_pop_and_proxy(
211        user,
212        token,
213        driver_name,
214        version_num,
215        connect_string,
216        edition,
217        pop,
218        None,
219    )
220}
221
222/// [`build_fast_auth_token_payload_with_pop`] with optional proxy user
223/// (`PROXY_CLIENT_NAME`).
224#[allow(clippy::too_many_arguments)] // public token-auth compatibility surface
225pub fn build_fast_auth_token_payload_with_pop_and_proxy(
226    user: &str,
227    token: &str,
228    driver_name: &str,
229    version_num: u32,
230    connect_string: &str,
231    edition: Option<&str>,
232    pop: Option<TokenPop<'_>>,
233    proxy_user: Option<&str>,
234) -> Result<Vec<u8>> {
235    let mut out = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
236        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
237    append_auth_phase_two_token_with_pop_and_proxy(
238        &mut out,
239        user,
240        token,
241        driver_name,
242        version_num,
243        connect_string,
244        edition,
245        pop,
246        proxy_user,
247    )?;
248    Ok(out)
249}
250
251/// Builds the standalone classic-auth phase-two token message.
252///
253/// Unlike [`build_fast_auth_token_payload`], this does not prepend the
254/// fast-auth envelope. Pre-23ai servers receive protocol negotiation and data
255/// types as separate round trips, then this self-contained `AUTH_TOKEN`
256/// phase-two message.
257pub fn build_auth_phase_two_token_payload(
258    user: &str,
259    token: &str,
260    driver_name: &str,
261    version_num: u32,
262    connect_string: &str,
263    edition: Option<&str>,
264) -> Result<Vec<u8>> {
265    build_auth_phase_two_token_payload_with_pop(
266        user,
267        token,
268        driver_name,
269        version_num,
270        connect_string,
271        edition,
272        None,
273    )
274}
275
276/// [`build_auth_phase_two_token_payload`] with optional OCI IAM proof-of-possession.
277///
278/// This preserves the public 0.8.x API. For proxy token authentication, use
279/// [`build_auth_phase_two_token_payload_with_pop_and_proxy`].
280pub fn build_auth_phase_two_token_payload_with_pop(
281    user: &str,
282    token: &str,
283    driver_name: &str,
284    version_num: u32,
285    connect_string: &str,
286    edition: Option<&str>,
287    pop: Option<TokenPop<'_>>,
288) -> Result<Vec<u8>> {
289    build_auth_phase_two_token_payload_with_pop_and_proxy(
290        user,
291        token,
292        driver_name,
293        version_num,
294        connect_string,
295        edition,
296        pop,
297        None,
298    )
299}
300
301/// [`build_auth_phase_two_token_payload_with_pop`] with optional proxy user
302/// (`PROXY_CLIENT_NAME`).
303#[allow(clippy::too_many_arguments)] // public token-auth compatibility surface
304pub fn build_auth_phase_two_token_payload_with_pop_and_proxy(
305    user: &str,
306    token: &str,
307    driver_name: &str,
308    version_num: u32,
309    connect_string: &str,
310    edition: Option<&str>,
311    pop: Option<TokenPop<'_>>,
312    proxy_user: Option<&str>,
313) -> Result<Vec<u8>> {
314    let mut out = Vec::new();
315    append_auth_phase_two_token_with_pop_and_proxy(
316        &mut out,
317        user,
318        token,
319        driver_name,
320        version_num,
321        connect_string,
322        edition,
323        pop,
324        proxy_user,
325    )?;
326    Ok(out)
327}
328
329pub fn build_function_payload(function_code: u8, ttc_field_version: u8) -> Vec<u8> {
330    build_function_payload_with_seq(function_code, 1, ttc_field_version)
331}
332
333pub fn build_function_payload_with_seq(
334    function_code: u8,
335    seq_num: u8,
336    ttc_field_version: u8,
337) -> Vec<u8> {
338    build_function_payload_with_seq_and_token(function_code, seq_num, 0, ttc_field_version)
339}
340
341/// Bare function message with an explicit pipeline token (messages/base.pyx
342/// `_write_function_code` writes `ub8 token_num` for field version >= 23.1
343/// ext 1; non-pipelined messages carry 0). On a pre-23.1-ext-1 connection the
344/// token field does not exist on the wire at all; pipelining (nonzero tokens)
345/// only happens on 23ai-negotiated connections, so no token is ever dropped.
346pub fn build_function_payload_with_seq_and_token(
347    function_code: u8,
348    seq_num: u8,
349    token_num: u64,
350    ttc_field_version: u8,
351) -> Vec<u8> {
352    let mut writer = TtcWriter::new();
353    writer.write_function_code_with_seq(function_code, seq_num);
354    if version_gates::writes_pipeline_token(ttc_field_version) {
355        writer.write_ub8(token_num);
356    } else {
357        debug_assert_eq!(
358            token_num, 0,
359            "pipeline tokens require a 23ai-negotiated connection"
360        );
361    }
362    writer.into_bytes()
363}
364
365pub(crate) fn skip_protocol_message(
366    reader: &mut TtcReader<'_>,
367) -> Result<Option<ClientCapabilities>> {
368    let _server_version = reader.read_u8()?;
369    reader.skip(1)?;
370    loop {
371        if reader.read_u8()? == 0 {
372            break;
373        }
374    }
375    let charset_id = reader.read_u16le()?;
376    let _server_flags = reader.read_u8()?;
377    let num_elem = reader.read_u16le()?;
378    reader.skip(usize::from(num_elem) * 5)?;
379    let fdo_len = reader.read_u16be()?;
380    reader.skip(usize::from(fdo_len))?;
381    let compile_caps = reader.read_bytes()?;
382    let runtime_caps = reader.read_bytes()?;
383    let Some(compile_caps) = compile_caps else {
384        return Ok(None);
385    };
386    let server_ttc_field_version = compile_caps
387        .get(TNS_CCAP_FIELD_VERSION)
388        .copied()
389        .unwrap_or_else(|| ClientCapabilities::default().ttc_field_version);
390    // The effective field version is the LOWER of what the server reports and
391    // what this client supports (reference capabilities.pyx
392    // `_adjust_for_server_compile_caps`: "if server < client: client =
393    // server"). Taking the max would over-claim 23ai-era field formats against
394    // pre-23ai servers.
395    let ttc_field_version =
396        server_ttc_field_version.min(ClientCapabilities::default().ttc_field_version);
397    let max_string_size = if runtime_caps
398        .as_deref()
399        .and_then(|caps| caps.get(TNS_RCAP_TTC))
400        .is_some_and(|flags| flags & TNS_RCAP_TTC_32K != 0)
401    {
402        32_767
403    } else {
404        4_000
405    };
406    Ok(Some(ClientCapabilities {
407        ttc_field_version,
408        max_string_size,
409        charset_id,
410    }))
411}
412
413pub(crate) fn skip_data_types_response(reader: &mut TtcReader<'_>) -> Result<()> {
414    loop {
415        let data_type = reader.read_u16be()?;
416        if data_type == 0 {
417            break;
418        }
419        let conv_data_type = reader.read_u16be()?;
420        if conv_data_type != 0 {
421            reader.skip(4)?;
422        }
423    }
424    Ok(())
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn classic_handshake_messages_slice_the_fast_auth_prefix() {
433        let protocol = build_protocol_negotiation_payload().expect("protocol payload");
434        assert_eq!(protocol[0], TNS_MSG_TYPE_PROTOCOL);
435        assert_eq!(protocol[1], 6, "protocol version byte (8.1 and higher)");
436        assert_eq!(protocol[2], 0, "array terminator");
437        assert!(
438            protocol.ends_with(b"python-oracledb\0"),
439            "driver name string with NUL terminator"
440        );
441
442        let data_types = build_data_types_payload().expect("data types payload");
443        assert_eq!(data_types[0], TNS_MSG_TYPE_DATA_TYPES);
444        // UTF8 charset (873) little-endian for charset and ncharset.
445        assert_eq!(&data_types[1..5], &[0x69, 0x03, 0x69, 0x03]);
446
447        // The two standalone messages are exact slices of the fast-auth bundle,
448        // so classic and fast-auth handshakes can never drift apart.
449        let full = Vec::from_hex(FAST_AUTH_PREFIX_HEX).expect("prefix decodes");
450        assert_eq!(full[0], TNS_MSG_TYPE_FAST_AUTH);
451        assert_eq!(
452            &full[FAST_AUTH_PROTOCOL_MSG_START..FAST_AUTH_PROTOCOL_MSG_END],
453            &protocol[..]
454        );
455        assert_eq!(&full[FAST_AUTH_DATA_TYPES_MSG_START..], &data_types[..]);
456    }
457
458    #[test]
459    fn classic_token_payload_is_the_fast_auth_phase_two_suffix() {
460        let classic = build_auth_phase_two_token_payload(
461            "scott",
462            "token-secret",
463            "rust-oracledb",
464            4_000_000_000,
465            "db.example.com/service",
466            Some("MY_EDITION"),
467        )
468        .expect("classic token payload");
469        let fast = build_fast_auth_token_payload(
470            "scott",
471            "token-secret",
472            "rust-oracledb",
473            4_000_000_000,
474            "db.example.com/service",
475            Some("MY_EDITION"),
476        )
477        .expect("fast token payload");
478        let prefix = Vec::from_hex(FAST_AUTH_PREFIX_HEX).expect("prefix decodes");
479
480        assert_eq!(&fast[..prefix.len()], prefix.as_slice());
481        assert_eq!(&fast[prefix.len()..], classic.as_slice());
482        assert_eq!(classic[0], TNS_MSG_TYPE_FUNCTION);
483        assert_eq!(classic[1], TNS_FUNC_AUTH_PHASE_TWO);
484        assert_eq!(
485            classic[2], 1,
486            "standalone phase two is the first TTC function"
487        );
488    }
489
490    // ---- ACCEPT protocol-version gate boundary tests ----------------------
491    //
492    // parse_accept_payload mirrors three reference gates keyed on the ACCEPT's
493    // protocol_version / protocol_options (references connect.pyx:65/75/111,
494    // capabilities.pyx:126, protocol.pyx:262). The live matrix crosses these
495    // (all live servers are >= 318, 23ai advertises end-of-response), but this
496    // offline test pins each boundary exactly.
497
498    /// A full (>= 318 layout) ACCEPT payload with a caller-controlled
499    /// protocol_version field, so the same trailing flags2 bytes can be parsed
500    /// on either side of the 318/319 gates.
501    fn accept_bytes(version: u16, options: u16, flags2: u32) -> Vec<u8> {
502        let mut w = TtcWriter::new();
503        w.write_u16be(version); // protocol version
504        w.write_u16be(options); // protocol options
505        w.write_raw(&[0u8; 10]); // skip(10)
506        w.write_u8(0); // flags1 (no NA_REQUIRED)
507        w.write_raw(&[0u8; 9]); // skip(9)
508        w.write_u32be(8192); // sdu
509        w.write_raw(&[0u8; 5]); // skip(5) before flags2
510        w.write_u32be(flags2); // flags2 (only read when version >= 318)
511        w.into_bytes()
512    }
513
514    #[test]
515    fn accept_parsing_gates_capabilities_on_protocol_version() {
516        // protocol.pyx:262 — supports_oob is a plain flag on protocol_options,
517        // independent of protocol_version.
518        assert!(
519            !parse_accept_payload(&accept_bytes(319, 0, 0))
520                .unwrap()
521                .supports_oob,
522            "no CAN_RECV_ATTENTION bit => supports_oob false"
523        );
524        assert!(
525            parse_accept_payload(&accept_bytes(319, TNS_GSO_CAN_RECV_ATTENTION, 0))
526                .unwrap()
527                .supports_oob,
528            "CAN_RECV_ATTENTION bit => supports_oob true"
529        );
530
531        // connect.pyx:75 (MIN_OOB_CHECK, >= 318) — flags2 (and everything it
532        // carries) is only read at/above 318. Same trailing bytes, version off
533        // by one, must flip the derived capability.
534        let flags2 = TNS_ACCEPT_FLAG_FAST_AUTH | TNS_ACCEPT_FLAG_CHECK_OOB;
535        assert!(
536            !parse_accept_payload(&accept_bytes(317, 0, flags2))
537                .unwrap()
538                .supports_fast_auth,
539            "below 318 flags2 is not read"
540        );
541        let at_318 = parse_accept_payload(&accept_bytes(318, 0, flags2)).unwrap();
542        assert!(at_318.supports_fast_auth, "at 318 flags2 is read");
543        assert!(
544            at_318.supports_oob_check,
545            "at 318 the CHECK_OOB flag is read"
546        );
547
548        // capabilities.pyx:126 (MIN_END_OF_RESPONSE) — end-of-response requires
549        // BOTH protocol_version >= 319 AND the flag. Prove the version gate and
550        // the flag gate independently.
551        assert!(
552            !parse_accept_payload(&accept_bytes(318, 0, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE))
553                .unwrap()
554                .supports_end_of_response,
555            "318 < 319: no end-of-response even with the flag"
556        );
557        assert!(
558            parse_accept_payload(&accept_bytes(319, 0, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE))
559                .unwrap()
560                .supports_end_of_response,
561            "319 + flag: end-of-response negotiated"
562        );
563        assert!(
564            !parse_accept_payload(&accept_bytes(319, 0, 0))
565                .unwrap()
566                .supports_end_of_response,
567            "319 without the flag: no end-of-response"
568        );
569    }
570
571    /// Minimal protocol-info message with controllable compile/runtime caps.
572    /// The leading fields are deliberately empty because this test targets the
573    /// capability section consumed by `skip_protocol_message`.
574    fn protocol_info_with_caps(field_version: u8, runtime_ttc: u8) -> Vec<u8> {
575        let mut writer = TtcWriter::new();
576        writer.write_u8(0); // server version (not used by the parser)
577        writer.write_u8(0); // skipped server flags
578        writer.write_u8(0); // NUL-terminated server version string
579        writer.write_u16le(873); // AL32UTF8 charset
580        writer.write_u8(0); // server flags
581        writer.write_u16le(0); // no element descriptors
582        writer.write_u16be(0); // no FDO bytes
583
584        let mut compile_caps = vec![0; TNS_CCAP_FIELD_VERSION + 1];
585        compile_caps[TNS_CCAP_FIELD_VERSION] = field_version;
586        writer
587            .write_bytes_with_length(&compile_caps)
588            .expect("short compile caps");
589
590        let mut runtime_caps = vec![0; TNS_RCAP_TTC + 1];
591        runtime_caps[TNS_RCAP_TTC] = runtime_ttc;
592        writer
593            .write_bytes_with_length(&runtime_caps)
594            .expect("short runtime caps");
595        writer.into_bytes()
596    }
597
598    #[test]
599    fn nineteen_c_caps_profile_derives_the_reference_19c_mask() {
600        // constants.pxi:503 defines 19.1-ext1 as 13. A 19c-shaped protocol
601        // message has no 32K TTC bit, so python-oracledb selects 4K strings
602        // (capabilities.pyx:134-150); the Rust parser must make the same mask.
603        let bytes = protocol_info_with_caps(TNS_CCAP_FIELD_VERSION_19_1_EXT_1, 0);
604        let caps = skip_protocol_message(&mut TtcReader::new(&bytes))
605            .expect("19c profile decodes")
606            .expect("compile caps present");
607
608        assert_eq!(
609            caps.ttc_field_version, TNS_CCAP_FIELD_VERSION_19_1_EXT_1,
610            "server field version caps the client at the 19c profile"
611        );
612        assert_eq!(caps.max_string_size, 4_000);
613        assert_eq!(caps.charset_id, 873);
614    }
615}