Skip to main content

oracledb_protocol/thin/
sessionless.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4use crate::wire::ProtocolLimits;
5
6/// Body of the transaction-switch message (reference impl/thin/messages/
7/// tpc_switch.pyx `_write_message`), shared by the direct function call and the
8/// piggyback forms. `xid` is the (format_id, global_txn_id) of a sessionless
9/// transaction being started; `None` for a suspend/detach which carries no XID.
10pub(crate) fn write_tpc_txn_switch_body(
11    writer: &mut TtcWriter,
12    operation: u32,
13    flags: u32,
14    timeout: u32,
15    xid: Option<&[u8]>,
16) {
17    writer.write_ub4(operation);
18    writer.write_u8(0); // pointer (transaction context)
19    writer.write_ub4(0); // transaction context length
20    if let Some(global_txn_id) = xid {
21        // sessionless transactions send only a global transaction id; the
22        // branch qualifier is empty and the combined value is right-padded
23        // with zero bytes to 128 bytes (tpc_switch.pyx:80-81).
24        let mut xid_bytes = global_txn_id.to_vec();
25        xid_bytes.resize(128, 0);
26        writer.write_ub4(SESSIONLESS_FORMAT_ID);
27        writer.write_ub4(u32::try_from(global_txn_id.len()).unwrap_or(0)); // global txn id len
28        writer.write_ub4(0); // branch qualifier length
29        writer.write_u8(1); // pointer (XID)
30        writer.write_ub4(u32::try_from(xid_bytes.len()).unwrap_or(0));
31        writer.write_ub4(flags);
32        writer.write_ub4(timeout);
33        writer.write_u8(1); // pointer (application value)
34        writer.write_u8(1); // pointer (return context)
35        writer.write_u8(1); // pointer (return context length)
36        writer.write_u8(0); // pointer (internal name)
37        writer.write_ub4(0); // length of internal name
38        writer.write_u8(0); // pointer (external name)
39        writer.write_ub4(0); // length of external name
40        writer.write_raw(&xid_bytes);
41        writer.write_ub4(0); // application value
42    } else {
43        writer.write_ub4(0); // format id
44        writer.write_ub4(0); // global transaction id length
45        writer.write_ub4(0); // branch qualifier length
46        writer.write_u8(0); // pointer (XID)
47        writer.write_ub4(0); // XID length
48        writer.write_ub4(flags);
49        writer.write_ub4(timeout);
50        writer.write_u8(1); // pointer (application value)
51        writer.write_u8(1); // pointer (return context)
52        writer.write_u8(1); // pointer (return context length)
53        writer.write_u8(0); // pointer (internal name)
54        writer.write_ub4(0); // length of internal name
55        writer.write_u8(0); // pointer (external name)
56        writer.write_ub4(0); // length of external name
57        writer.write_ub4(0); // application value
58    }
59}
60
61/// Direct (non-deferred) transaction-switch function call used to begin/resume
62/// (`TNS_TPC_TXN_START` + new/resume flag, with `xid`) or suspend
63/// (`TNS_TPC_TXN_DETACH`, no `xid`) a sessionless transaction. Reference
64/// impl/thin/connection.pyx `begin/resume/suspend_sessionless_transaction`.
65pub fn build_tpc_txn_switch_payload_with_seq(
66    seq_num: u8,
67    token_num: u64,
68    operation: u32,
69    flags: u32,
70    timeout: u32,
71    xid: Option<&[u8]>,
72) -> Vec<u8> {
73    let mut writer = TtcWriter::new();
74    writer.write_function_code_with_seq(TNS_FUNC_TPC_TXN_SWITCH, seq_num);
75    writer.write_ub8(token_num);
76    write_tpc_txn_switch_body(&mut writer, operation, flags, timeout, xid);
77    writer.into_bytes()
78}
79
80/// Sessionless transaction-switch piggyback, prepended to the next execute
81/// message's payload (reference messages/base.pyx `_write_sessionless_piggyback`
82/// — the same message body written with a `TNS_MSG_TYPE_PIGGYBACK` header). Used
83/// for a deferred begin/resume (`defer_round_trip=True`) and for the
84/// `suspend_on_success` post-detach. `operation` already encodes whether a
85/// post-detach is folded in (`TNS_TPC_TXN_START | TNS_TPC_TXN_POST_DETACH`).
86pub fn build_sessionless_piggyback(
87    seq_num: u8,
88    token_num: u64,
89    operation: u32,
90    flags: u32,
91    timeout: u32,
92    xid: Option<&[u8]>,
93) -> Vec<u8> {
94    let mut writer = TtcWriter::new();
95    writer.write_u8(TNS_MSG_TYPE_PIGGYBACK);
96    writer.write_u8(TNS_FUNC_TPC_TXN_SWITCH);
97    writer.write_u8(seq_num);
98    writer.write_ub8(token_num);
99    write_tpc_txn_switch_body(&mut writer, operation, flags, timeout, xid);
100    writer.into_bytes()
101}
102
103/// Decode the sessionless state bits packed in the transaction-id key/value
104/// binary payload (reference `_update_sessionless_txn_state`). The last two
105/// bytes are the state mask and the sync version; the leading bytes are the
106/// transaction id itself.
107pub fn decode_sessionless_txn_state(binary: &[u8]) -> Result<Option<SessionlessTxnState>> {
108    if binary.len() < 2 {
109        return Err(ProtocolError::TtcDecode("short sessionless txn state"));
110    }
111    let state = binary[binary.len() - 2];
112    let sync_version = binary[binary.len() - 1];
113    if sync_version != 1 {
114        return Err(ProtocolError::TtcDecode("unknown transaction sync version"));
115    }
116    if state & TNS_TPC_TXNID_SYNC_UNSET != 0 {
117        Ok(Some(SessionlessTxnState::Unset))
118    } else if state & TNS_TPC_TXNID_SYNC_SET != 0 {
119        Ok(Some(SessionlessTxnState::Set {
120            started_on_server: state & TNS_TPC_TXNID_SYNC_SERVER != 0,
121        }))
122    } else {
123        Ok(None)
124    }
125}
126
127/// Parse a transaction-switch response (reference tpc_switch.pyx
128/// `_process_return_parameters` plus base.pyx message loop). Returns any
129/// sessionless state update carried by a transaction-id key/value pair; server
130/// errors (e.g. ORA-25351 / ORA-26217) are surfaced as `ProtocolError`.
131pub fn parse_tpc_txn_switch_response(
132    payload: &[u8],
133    capabilities: ClientCapabilities,
134) -> Result<Option<SessionlessTxnState>> {
135    parse_tpc_txn_switch_response_with_limits(payload, capabilities, ProtocolLimits::DEFAULT)
136}
137
138pub fn parse_tpc_txn_switch_response_with_limits(
139    payload: &[u8],
140    capabilities: ClientCapabilities,
141    limits: ProtocolLimits,
142) -> Result<Option<SessionlessTxnState>> {
143    let mut reader = TtcReader::with_limits(payload, limits)?;
144    let mut state = None;
145    while reader.remaining() > 0 {
146        let message_type = reader.read_u8()?;
147        match message_type {
148            0 => {}
149            TNS_MSG_TYPE_STATUS => {
150                let _call_status = reader.read_ub4()?;
151                let _seq = reader.read_ub2()?;
152            }
153            TNS_MSG_TYPE_PARAMETER => {
154                // tpc_switch.pyx `_process_return_parameters`: application value
155                // (ub4) then the return transaction context (ub2 length + bytes).
156                let _application_value = reader.read_ub4()?;
157                let context_len = reader.read_ub2()?;
158                if context_len > 0 {
159                    reader.skip(usize::from(context_len))?;
160                }
161            }
162            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
163                if let Some(update) = skip_server_side_piggyback(&mut reader)? {
164                    state = Some(update);
165                }
166            }
167            TNS_MSG_TYPE_END_OF_RESPONSE => break,
168            TNS_MSG_TYPE_ERROR => {
169                let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
170                if info.number != 0 {
171                    return Err(ProtocolError::ServerErrorInfo(Box::new(
172                        info.into_details(),
173                    )));
174                }
175            }
176            _ => break,
177        }
178    }
179    Ok(state)
180}
181
182/// Begin-pipeline piggyback (messages/base.pyx `_write_begin_pipeline_piggyback`
183/// and `_write_piggyback_code`): prepended to the first pipelined message's
184/// payload. The packet carrying it must set [`TNS_DATA_FLAGS_BEGIN_PIPELINE`].
185///
186/// `token_num` is the token of the message the piggyback rides on (1 for the
187/// first pipeline operation); `pipeline_mode` is one of
188/// [`TNS_PIPELINE_MODE_CONTINUE_ON_ERROR`] / [`TNS_PIPELINE_MODE_ABORT_ON_ERROR`].
189pub fn build_begin_pipeline_piggyback(seq_num: u8, token_num: u64, pipeline_mode: u8) -> Vec<u8> {
190    let mut writer = TtcWriter::new();
191    writer.write_u8(TNS_MSG_TYPE_PIGGYBACK);
192    writer.write_u8(TNS_FUNC_PIPELINE_BEGIN);
193    writer.write_u8(seq_num);
194    writer.write_ub8(token_num);
195    writer.write_ub2(0); // error set ID
196    writer.write_u8(0); // error set mode
197    writer.write_u8(pipeline_mode);
198    writer.into_bytes()
199}
200
201/// End-pipeline message (messages/end_pipeline.pyx): function 200 plus an
202/// unused ub4 identifier. Sent after every pipelined operation message; its
203/// packet carries no END_OF_REQUEST flag and its response is the final
204/// (N+1th) boundary-delimited response of the pipeline.
205pub fn build_end_pipeline_payload_with_seq(seq_num: u8) -> Vec<u8> {
206    let mut writer = TtcWriter::new();
207    writer.write_function_code_with_seq(TNS_FUNC_PIPELINE_END, seq_num);
208    writer.write_ub8(0); // token (the end-pipeline message itself has none)
209    writer.write_ub4(0); // error set ID (unused)
210    writer.into_bytes()
211}
212
213/// A two-phase-commit transaction id (reference `Xid` namedtuple). The
214/// `global_transaction_id` and `branch_qualifier` are the raw (already
215/// UTF-8 encoded) byte values; the shim coerces `str` members before calling.
216#[derive(Clone, Debug)]
217pub struct TpcXid<'a> {
218    pub format_id: u32,
219    pub global_transaction_id: &'a [u8],
220    pub branch_qualifier: &'a [u8],
221}
222
223/// Writes the XID descriptor + the 128-byte zero-padded XID block, shared by
224/// the full-XA switch (func 103) and change-state (func 104) messages. The
225/// descriptor (`format_id`, gtid length, bqual length, pointer, block length)
226/// is written at the caller-specified position; the 128-byte block itself is
227/// written by [`write_xid_block_bytes`] later in the message body, after the
228/// context bytes (reference tpc_switch.pyx / tpc_change_state.pyx).
229fn write_xid_descriptor(writer: &mut TtcWriter, xid: Option<&TpcXid<'_>>) {
230    match xid {
231        Some(xid) => {
232            writer.write_ub4(xid.format_id);
233            writer.write_ub4(u32::try_from(xid.global_transaction_id.len()).unwrap_or(0));
234            writer.write_ub4(u32::try_from(xid.branch_qualifier.len()).unwrap_or(0));
235            writer.write_u8(1); // pointer (XID)
236            writer.write_ub4(128); // length of the XID block
237        }
238        None => {
239            writer.write_ub4(0); // format id
240            writer.write_ub4(0); // global transaction id length
241            writer.write_ub4(0); // branch qualifier length
242            writer.write_u8(0); // pointer (XID)
243            writer.write_ub4(0); // XID length
244        }
245    }
246}
247
248/// The 128-byte XID block: `global_transaction_id + branch_qualifier`,
249/// right-zero-padded to exactly 128 bytes (reference tpc_switch.pyx:80-81).
250fn write_xid_block_bytes(writer: &mut TtcWriter, xid: &TpcXid<'_>) {
251    let mut xid_bytes = Vec::with_capacity(128);
252    xid_bytes.extend_from_slice(xid.global_transaction_id);
253    xid_bytes.extend_from_slice(xid.branch_qualifier);
254    xid_bytes.resize(128, 0);
255    writer.write_raw(&xid_bytes);
256}
257
258/// Full-XA transaction-switch payload (func 103), used by `tpc_begin`
259/// (`operation = TNS_TPC_TXN_START`) and `tpc_end` (`operation =
260/// TNS_TPC_TXN_DETACH`). Unlike [`build_tpc_txn_switch_payload_with_seq`] (the
261/// sessionless special case) this carries a real `format_id`, a non-empty
262/// branch qualifier, and the captured transaction `context` to echo back.
263/// Reference messages/tpc_switch.pyx `_write_message`.
264pub fn build_tpc_switch_payload_with_seq(
265    seq_num: u8,
266    operation: u32,
267    flags: u32,
268    timeout: u32,
269    xid: Option<&TpcXid<'_>>,
270    context: Option<&[u8]>,
271) -> Vec<u8> {
272    // Retained for API stability (this crate's public surface is semver-frozen).
273    // The original builder always emitted the ub8 TTC token, i.e. the >= 23.1
274    // (`TNS_CCAP_FIELD_VERSION_23_1_EXT_1`) framing; delegate with that version so
275    // existing callers see byte-identical output. New callers must pass the
276    // *negotiated* version via `..._and_version` so a pre-23ai server is not sent
277    // a stray token (ORA-03120). Bead rust-oracledb-hkwd.
278    build_tpc_switch_payload_with_seq_and_version(
279        seq_num,
280        operation,
281        flags,
282        timeout,
283        xid,
284        context,
285        TNS_CCAP_FIELD_VERSION_23_1_EXT_1,
286    )
287}
288
289/// Version-aware [`build_tpc_switch_payload_with_seq`]: the ub8 TTC token is
290/// emitted only when `ttc_field_version >= TNS_CCAP_FIELD_VERSION_23_1_EXT_1`
291/// (reference messages/base.pyx `_write_function_code`). Pre-23ai servers never
292/// negotiate that version and misparse a stray token as message content, failing
293/// the call with ORA-03120 (observed live on Oracle XE 18c/21c). Bead
294/// rust-oracledb-hkwd.
295#[allow(clippy::too_many_arguments)]
296pub fn build_tpc_switch_payload_with_seq_and_version(
297    seq_num: u8,
298    operation: u32,
299    flags: u32,
300    timeout: u32,
301    xid: Option<&TpcXid<'_>>,
302    context: Option<&[u8]>,
303    ttc_field_version: u8,
304) -> Vec<u8> {
305    let mut writer = TtcWriter::new();
306    writer.write_function_header(TNS_FUNC_TPC_TXN_SWITCH, seq_num, ttc_field_version);
307    writer.write_ub4(operation);
308    match context {
309        Some(context) => {
310            writer.write_u8(1); // pointer (transaction context)
311            writer.write_ub4(u32::try_from(context.len()).unwrap_or(0));
312        }
313        None => {
314            writer.write_u8(0); // pointer (transaction context)
315            writer.write_ub4(0); // transaction context length
316        }
317    }
318    write_xid_descriptor(&mut writer, xid);
319    writer.write_ub4(flags);
320    writer.write_ub4(timeout);
321    writer.write_u8(1); // pointer (application value)
322    writer.write_u8(1); // pointer (return context)
323    writer.write_u8(1); // pointer (return context length)
324    writer.write_u8(0); // pointer (internal name)
325    writer.write_ub4(0); // length of internal name
326    writer.write_u8(0); // pointer (external name)
327    writer.write_ub4(0); // length of external name
328    if let Some(context) = context {
329        writer.write_raw(context);
330    }
331    if let Some(xid) = xid {
332        write_xid_block_bytes(&mut writer, xid);
333    }
334    writer.write_ub4(0); // application value
335    writer.into_bytes()
336}
337
338/// TPC transaction change-state payload (func 104), used by `tpc_prepare`
339/// (`operation = TNS_TPC_TXN_PREPARE`), `tpc_commit` (`TNS_TPC_TXN_COMMIT`) and
340/// `tpc_rollback` (`TNS_TPC_TXN_ABORT`). `requested_state` is the desired state
341/// (0 for prepare; READ_ONLY/COMMITTED for commit; ABORTED for rollback).
342/// Reference messages/tpc_change_state.pyx `_write_message`.
343pub fn build_tpc_change_state_payload_with_seq(
344    seq_num: u8,
345    operation: u32,
346    requested_state: u32,
347    flags: u32,
348    xid: Option<&TpcXid<'_>>,
349    context: Option<&[u8]>,
350) -> Vec<u8> {
351    // Retained for API stability; delegates with the token-present (>= 23.1)
352    // framing this builder always emitted. New callers pass the negotiated
353    // version via `..._and_version` (bead rust-oracledb-hkwd).
354    build_tpc_change_state_payload_with_seq_and_version(
355        seq_num,
356        operation,
357        requested_state,
358        flags,
359        xid,
360        context,
361        TNS_CCAP_FIELD_VERSION_23_1_EXT_1,
362    )
363}
364
365/// Version-aware [`build_tpc_change_state_payload_with_seq`]: gates the ub8 TTC
366/// token on `ttc_field_version >= TNS_CCAP_FIELD_VERSION_23_1_EXT_1` so pre-23ai
367/// servers are not sent a stray token (ORA-03120). Bead rust-oracledb-hkwd.
368#[allow(clippy::too_many_arguments)]
369pub fn build_tpc_change_state_payload_with_seq_and_version(
370    seq_num: u8,
371    operation: u32,
372    requested_state: u32,
373    flags: u32,
374    xid: Option<&TpcXid<'_>>,
375    context: Option<&[u8]>,
376    ttc_field_version: u8,
377) -> Vec<u8> {
378    let mut writer = TtcWriter::new();
379    writer.write_function_header(TNS_FUNC_TPC_TXN_CHANGE_STATE, seq_num, ttc_field_version);
380    writer.write_ub4(operation);
381    match context {
382        Some(context) => {
383            writer.write_u8(1); // pointer (context)
384            writer.write_ub4(u32::try_from(context.len()).unwrap_or(0));
385        }
386        None => {
387            writer.write_u8(0); // pointer (context)
388            writer.write_ub4(0); // context length
389        }
390    }
391    write_xid_descriptor(&mut writer, xid);
392    writer.write_ub4(0); // timeout (always 0)
393    writer.write_ub4(requested_state);
394    writer.write_u8(1); // pointer (out state)
395    writer.write_ub4(flags);
396    if let Some(context) = context {
397        writer.write_raw(context);
398    }
399    if let Some(xid) = xid {
400        write_xid_block_bytes(&mut writer, xid);
401    }
402    writer.into_bytes()
403}
404
405/// Parse a full-XA transaction-switch response (reference tpc_switch.pyx
406/// `_process_return_parameters` plus the base.pyx message loop). Captures the
407/// returned transaction context (PARAMETER message) and the txn-in-progress bit
408/// (last call status). Server errors are surfaced as `ProtocolError`.
409pub fn parse_tpc_switch_response(
410    payload: &[u8],
411    capabilities: ClientCapabilities,
412) -> Result<TpcSwitchResponse> {
413    parse_tpc_switch_response_with_limits(payload, capabilities, ProtocolLimits::DEFAULT)
414}
415
416pub fn parse_tpc_switch_response_with_limits(
417    payload: &[u8],
418    capabilities: ClientCapabilities,
419    limits: ProtocolLimits,
420) -> Result<TpcSwitchResponse> {
421    let mut reader = TtcReader::with_limits(payload, limits)?;
422    let mut response = TpcSwitchResponse::default();
423    while reader.remaining() > 0 {
424        let message_type = reader.read_u8()?;
425        match message_type {
426            0 => {}
427            TNS_MSG_TYPE_STATUS => {
428                let call_status = reader.read_ub4()?;
429                let _seq = reader.read_ub2()?;
430                response.txn_in_progress = call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0;
431            }
432            TNS_MSG_TYPE_PARAMETER => {
433                // tpc_switch.pyx `_process_return_parameters`: application value
434                // (ub4) then the return transaction context (ub2 length + bytes).
435                let _application_value = reader.read_ub4()?;
436                let context_len = reader.read_ub2()?;
437                let context = reader.read_raw(usize::from(context_len))?;
438                response.context = context.to_vec();
439            }
440            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
441                if let Some(update) = skip_server_side_piggyback(&mut reader)? {
442                    response.sessionless_state = Some(update);
443                }
444            }
445            TNS_MSG_TYPE_END_OF_RESPONSE => break,
446            TNS_MSG_TYPE_ERROR => {
447                let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
448                if info.number != 0 {
449                    // On a server error the reference raises before
450                    // `_process_call_status` runs, so `_txn_in_progress` keeps
451                    // its prior value; we surface the error without touching the
452                    // flag.
453                    return Err(ProtocolError::ServerErrorInfo(Box::new(
454                        info.into_details(),
455                    )));
456                }
457                // The end-of-call ERROR (number 0 on success) carries the
458                // end-of-call status; sample the transaction-in-progress bit.
459                response.txn_in_progress = info.call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0;
460            }
461            _ => break,
462        }
463    }
464    Ok(response)
465}
466
467/// Parse a TPC change-state response (reference tpc_change_state.pyx
468/// `_process_return_parameters` plus the base.pyx message loop). Reads the out
469/// state from the PARAMETER message and the txn-in-progress bit from the last
470/// call status. Server errors are surfaced as `ProtocolError`.
471pub fn parse_tpc_change_state_response(
472    payload: &[u8],
473    capabilities: ClientCapabilities,
474) -> Result<TpcChangeStateResponse> {
475    parse_tpc_change_state_response_with_limits(payload, capabilities, ProtocolLimits::DEFAULT)
476}
477
478pub fn parse_tpc_change_state_response_with_limits(
479    payload: &[u8],
480    capabilities: ClientCapabilities,
481    limits: ProtocolLimits,
482) -> Result<TpcChangeStateResponse> {
483    let mut reader = TtcReader::with_limits(payload, limits)?;
484    let mut response = TpcChangeStateResponse::default();
485    while reader.remaining() > 0 {
486        let message_type = reader.read_u8()?;
487        match message_type {
488            0 => {}
489            TNS_MSG_TYPE_STATUS => {
490                let call_status = reader.read_ub4()?;
491                let _seq = reader.read_ub2()?;
492                response.txn_in_progress = call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0;
493            }
494            TNS_MSG_TYPE_PARAMETER => {
495                // tpc_change_state.pyx `_process_return_parameters` reads the
496                // out state (ub4).
497                response.state = reader.read_ub4()?;
498            }
499            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
500                skip_server_side_piggyback(&mut reader)?;
501            }
502            TNS_MSG_TYPE_END_OF_RESPONSE => break,
503            TNS_MSG_TYPE_ERROR => {
504                let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
505                if info.number != 0 {
506                    // On a server error the reference raises before
507                    // `_process_call_status` runs, so `_txn_in_progress` keeps
508                    // its prior value; we surface the error without touching the
509                    // flag.
510                    return Err(ProtocolError::ServerErrorInfo(Box::new(
511                        info.into_details(),
512                    )));
513                }
514                // The end-of-call ERROR (number 0 on success) carries the
515                // end-of-call status; sample the transaction-in-progress bit.
516                response.txn_in_progress = info.call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0;
517            }
518            _ => break,
519        }
520    }
521    Ok(response)
522}
523
524pub(crate) fn skip_keyword_value_pairs(reader: &mut TtcReader<'_>, num_pairs: u16) -> Result<()> {
525    read_keyword_value_pairs_for_txn_state(reader, num_pairs).map(|_| ())
526}
527
528/// Like [`skip_keyword_value_pairs`] but extracts the sessionless transaction
529/// state carried by the `TRANSACTION_ID` keyword (201). Reference
530/// `_process_keyword_value_pairs` calls `_update_sessionless_txn_state` on the
531/// binary value of that keyword.
532pub(crate) fn read_keyword_value_pairs_for_txn_state(
533    reader: &mut TtcReader<'_>,
534    num_pairs: u16,
535) -> Result<Option<SessionlessTxnState>> {
536    let mut state = None;
537    for _ in 0..num_pairs {
538        if reader.read_ub2()? > 0 {
539            let _text_value = reader.read_bytes()?;
540        }
541        let mut binary_value = None;
542        if reader.read_ub2()? > 0 {
543            binary_value = reader.read_bytes()?;
544        }
545        let keyword_num = reader.read_ub2()?;
546        if keyword_num == TNS_KEYWORD_NUM_TRANSACTION_ID {
547            if let Some(binary) = binary_value.as_deref() {
548                if let Some(update) = decode_sessionless_txn_state(binary)? {
549                    state = Some(update);
550                }
551            }
552        }
553    }
554    Ok(state)
555}
556
557#[cfg(test)]
558mod tpc_tests {
559    use super::*;
560
561    fn xid() -> ([u8; 7], [u8; 8]) {
562        (*b"txn4400", *b"branchId")
563    }
564
565    #[test]
566    fn tpc_begin_payload_encodes_format_branch_and_128_byte_xid() {
567        let (gtid, bqual) = xid();
568        let tpc_xid = TpcXid {
569            format_id: 4400,
570            global_transaction_id: &gtid,
571            branch_qualifier: &bqual,
572        };
573        let payload = build_tpc_switch_payload_with_seq(
574            4,
575            TNS_TPC_TXN_START,
576            TPC_TXN_FLAGS_NEW,
577            0,
578            Some(&tpc_xid),
579            None,
580        );
581        // [msg_type=3][func=0x67=103][seq=4] + token ub8(0) = 1 byte
582        assert_eq!(&payload[..3], &[3, TNS_FUNC_TPC_TXN_SWITCH, 4]);
583        let body = &payload[4..];
584        // operation ub4(START=1) = [1,1]; context ptr u8(0) = [0]; len ub4(0) = [0]
585        assert_eq!(&body[..4], &[1, 1, 0, 0]);
586        // format id ub4(4400=0x1130) = len2 + value (golden: 02 11 30)
587        assert_eq!(&body[4..7], &[2, 0x11, 0x30]);
588        // gtid len ub4(7)=[1,7], bqual len ub4(8)=[1,8], xid ptr u8(1)=[1],
589        // block len ub4(128)=[1,0x80]
590        assert_eq!(&body[7..14], &[1, 7, 1, 8, 1, 1, 0x80]);
591        // the 128-byte xid block must contain gtid+bqual zero-padded; it is the
592        // last 128 bytes before the trailing application value ub4(0) = [0].
593        let block_start = payload.len() - 128 - 1;
594        let block = &payload[block_start..block_start + 128];
595        assert_eq!(&block[..7], b"txn4400");
596        assert_eq!(&block[7..15], b"branchId");
597        assert!(block[15..].iter().all(|&byte| byte == 0));
598    }
599
600    #[test]
601    fn tpc_end_payload_echoes_context() {
602        let context = vec![0xAAu8; 168];
603        let payload =
604            build_tpc_switch_payload_with_seq(7, TNS_TPC_TXN_DETACH, 0, 0, None, Some(&context));
605        let body = &payload[4..];
606        // operation ub4(DETACH=2)=[1,2]; context ptr u8(1)=[1]; len ub4(168)=[1,0xA8]
607        assert_eq!(&body[..5], &[1, 2, 1, 1, 0xA8]);
608        // context bytes are echoed verbatim somewhere in the payload tail
609        assert!(payload
610            .windows(context.len())
611            .any(|window| window == context.as_slice()));
612    }
613
614    #[test]
615    fn change_state_prepare_payload_shape() {
616        let (gtid, bqual) = xid();
617        let tpc_xid = TpcXid {
618            format_id: 4400,
619            global_transaction_id: &gtid,
620            branch_qualifier: &bqual,
621        };
622        let payload = build_tpc_change_state_payload_with_seq(
623            8,
624            TNS_TPC_TXN_PREPARE,
625            TNS_TPC_TXN_STATE_PREPARE,
626            0,
627            Some(&tpc_xid),
628            None,
629        );
630        assert_eq!(&payload[..3], &[3, TNS_FUNC_TPC_TXN_CHANGE_STATE, 8]);
631        let body = &payload[4..];
632        // operation ub4(PREPARE=3)=[1,3]; context ptr u8(0)=[0]; len ub4(0)=[0]
633        assert_eq!(&body[..4], &[1, 3, 0, 0]);
634    }
635
636    #[test]
637    fn switch_response_captures_context_and_txn_bit() {
638        // PARAMETER(8): app_value ub4(0) + context_len ub2(4) + 4 context bytes;
639        // STATUS(9): call_status ub4 = 3 (TXN bit set) + seq ub2(0); EOR(29).
640        let mut payload = Vec::new();
641        payload.push(TNS_MSG_TYPE_PARAMETER);
642        payload.push(0); // app value ub4(0)
643        payload.extend_from_slice(&[2, 0, 4]); // context_len ub2 = 4
644        payload.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
645        payload.push(TNS_MSG_TYPE_STATUS);
646        payload.extend_from_slice(&[1, 3]); // call_status ub4 = 3
647        payload.extend_from_slice(&[0]); // seq ub2 = 0
648        payload.push(TNS_MSG_TYPE_END_OF_RESPONSE);
649
650        let response =
651            parse_tpc_switch_response(&payload, ClientCapabilities::default()).expect("decode");
652        assert_eq!(response.context, vec![0xDE, 0xAD, 0xBE, 0xEF]);
653        assert!(response.txn_in_progress);
654    }
655
656    #[test]
657    fn switch_response_end_status_clears_txn_bit() {
658        // STATUS call_status = 1 (TXN bit clear) -> txn_in_progress == false.
659        let mut payload = Vec::new();
660        payload.push(TNS_MSG_TYPE_STATUS);
661        payload.extend_from_slice(&[1, 1]); // call_status ub4 = 1
662        payload.extend_from_slice(&[0]); // seq ub2 = 0
663        payload.push(TNS_MSG_TYPE_END_OF_RESPONSE);
664
665        let response =
666            parse_tpc_switch_response(&payload, ClientCapabilities::default()).expect("decode");
667        assert!(!response.txn_in_progress);
668    }
669
670    #[test]
671    fn change_state_response_reads_out_state() {
672        // PARAMETER out state ub4 = 1 (REQUIRES_COMMIT); STATUS txn bit clear.
673        let mut payload = Vec::new();
674        payload.push(TNS_MSG_TYPE_PARAMETER);
675        payload.extend_from_slice(&[1, 1]); // state ub4 = 1
676        payload.push(TNS_MSG_TYPE_STATUS);
677        payload.extend_from_slice(&[1, 1]); // call_status ub4 = 1
678        payload.extend_from_slice(&[0]); // seq ub2 = 0
679        payload.push(TNS_MSG_TYPE_END_OF_RESPONSE);
680
681        let response = parse_tpc_change_state_response(&payload, ClientCapabilities::default())
682            .expect("decode");
683        assert_eq!(response.state, TNS_TPC_TXN_STATE_REQUIRES_COMMIT);
684        assert!(!response.txn_in_progress);
685    }
686}