Skip to main content

zerodds_corba_giop/
code_set.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! `CONV_FRAME::CodeSetContext` — Codeset-Negotiation-ServiceContext (§13.10.2.5).
5//!
6//! ```text
7//! module CONV_FRAME {
8//!     typedef unsigned long CodeSetId;
9//!     struct CodeSetContext {
10//!         CodeSetId char_data;
11//!         CodeSetId wchar_data;
12//!     };
13//! };
14//! ```
15//!
16//! On the first request the client attaches a `ServiceContext` with
17//! `context_id = IOP::CodeSets (1)`; `context_data` is a CDR
18//! encapsulation (byte-order octet + `char_data` + `wchar_data`). The
19//! `CodeSetId` values are determined from the target IOR via
20//! [`CodeSetComponentInfo`] negotiation (see `zerodds_corba_ior`). The defaults
21//! are UTF-8 for `char` and UTF-16 for `wchar` — the latter keeps the WString BOM
22//! (§15.3.1.6) interoperable with omniORB/TAO/JacORB.
23
24use alloc::vec::Vec;
25
26use zerodds_cdr::{BufferReader, BufferWriter, Endianness};
27
28use crate::error::{GiopError, GiopResult};
29use crate::service_context::{ServiceContext, ServiceContextList, ServiceContextTag};
30
31/// Well-known `CodeSetId` values (OSF registry, spec §13.10.5.1).
32pub mod well_known {
33    /// `ISO 8859-1:1987` (Latin-1) — classic native `char` codeset.
34    pub const ISO_8859_1: u32 = 0x0001_0001;
35    /// `X/Open UTF-8` — `char` transmission codeset (ZeroDDS default, since
36    /// IDL `string` is UTF-8 internally).
37    pub const UTF_8: u32 = 0x0501_0001;
38    /// `Unicode UTF-16` — default `wchar` transmission codeset.
39    pub const UTF_16: u32 = 0x0001_0109;
40    /// `Unicode UCS-2 Level 1` — `wchar` alternative.
41    pub const UCS_2: u32 = 0x0001_0100;
42}
43
44/// `CONV_FRAME::CodeSetContext` — selected transmission codesets.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct CodeSetContext {
47    /// `char_data` — transmission codeset for `char`/`string` (TCSC).
48    pub char_data: u32,
49    /// `wchar_data` — transmission codeset for `wchar`/`wstring` (TCSW).
50    pub wchar_data: u32,
51}
52
53impl Default for CodeSetContext {
54    fn default() -> Self {
55        Self::default_pair()
56    }
57}
58
59impl CodeSetContext {
60    /// Constructor.
61    #[must_use]
62    pub const fn new(char_data: u32, wchar_data: u32) -> Self {
63        Self {
64            char_data,
65            wchar_data,
66        }
67    }
68
69    /// ZeroDDS default pair: UTF-8 (`char`) + UTF-16 (`wchar`).
70    #[must_use]
71    pub const fn default_pair() -> Self {
72        Self {
73            char_data: well_known::UTF_8,
74            wchar_data: well_known::UTF_16,
75        }
76    }
77
78    /// Encodes the body as a CDR encapsulation (byte-order octet + two
79    /// `unsigned long`). With standard CDR alignment relative to the start of
80    /// the encapsulation, `char_data` sits at offset 4 and `wchar_data` at offset 8.
81    ///
82    /// # Errors
83    /// Buffer write error.
84    pub fn encode_encapsulation(&self, endianness: Endianness) -> GiopResult<Vec<u8>> {
85        // Byte-order octet + body in ONE buffer → natural CDR alignment
86        // from offset 0 (the uint32 lands automatically at offset 4).
87        let mut w = BufferWriter::new(endianness);
88        w.write_u8(match endianness {
89            Endianness::Big => 0,
90            Endianness::Little => 1,
91        })?;
92        w.write_u32(self.char_data)?;
93        w.write_u32(self.wchar_data)?;
94        Ok(w.into_bytes())
95    }
96
97    /// Decodes a `CodeSetContext` encapsulation (byte-order octet + body).
98    ///
99    /// # Errors
100    /// Truncated/invalid endianness or buffer read error.
101    pub fn decode_encapsulation(encap: &[u8]) -> GiopResult<Self> {
102        if encap.is_empty() {
103            return Err(GiopError::Malformed(
104                "empty CodeSetContext encapsulation".into(),
105            ));
106        }
107        let endianness = match encap[0] {
108            0 => Endianness::Big,
109            1 => Endianness::Little,
110            other => {
111                return Err(GiopError::Malformed(alloc::format!(
112                    "invalid CodeSetContext byte-order octet: {other}"
113                )));
114            }
115        };
116        // Reader over the WHOLE encapsulation (origin = byte-order octet), so
117        // the uint32 alignment at offset 4 is correct; the byte-order octet
118        // itself is skipped.
119        let mut r = BufferReader::new(encap, endianness);
120        let _bo = r.read_u8()?;
121        let char_data = r.read_u32()?;
122        let wchar_data = r.read_u32()?;
123        Ok(Self {
124            char_data,
125            wchar_data,
126        })
127    }
128
129    /// Builds the GIOP `ServiceContext` (context_id = `IOP::CodeSets` = 1).
130    ///
131    /// # Errors
132    /// Encapsulation write error.
133    pub fn to_service_context(&self, endianness: Endianness) -> GiopResult<ServiceContext> {
134        let data = self.encode_encapsulation(endianness)?;
135        Ok(ServiceContext::new(
136            ServiceContextTag::CodeSets.as_u32(),
137            data,
138        ))
139    }
140
141    /// Looks up the `CodeSets` entry in a [`ServiceContextList`] and decodes
142    /// it. Returns `Ok(None)` if no codeset context is present.
143    ///
144    /// # Errors
145    /// Decode error if the entry exists but is corrupt.
146    pub fn from_service_context_list(list: &ServiceContextList) -> GiopResult<Option<Self>> {
147        let tag = ServiceContextTag::CodeSets.as_u32();
148        match list.0.iter().find(|c| c.context_id == tag) {
149            Some(ctx) => Ok(Some(Self::decode_encapsulation(&ctx.context_data)?)),
150            None => Ok(None),
151        }
152    }
153}
154
155#[cfg(test)]
156#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn well_known_ids_match_osf_registry() {
162        // Spec §13.10.5.1 — OSF codeset registry values.
163        assert_eq!(well_known::ISO_8859_1, 0x0001_0001);
164        assert_eq!(well_known::UTF_8, 0x0501_0001);
165        assert_eq!(well_known::UTF_16, 0x0001_0109);
166        assert_eq!(well_known::UCS_2, 0x0001_0100);
167    }
168
169    #[test]
170    fn encapsulation_wire_layout_be() {
171        let ctx = CodeSetContext::new(well_known::UTF_8, well_known::UTF_16);
172        let encap = ctx.encode_encapsulation(Endianness::Big).unwrap();
173        // [bo=0][pad pad pad][char_data BE][wchar_data BE] = 12 bytes.
174        assert_eq!(encap.len(), 12);
175        assert_eq!(encap[0], 0); // big-endian octet
176        assert_eq!(&encap[4..8], &0x0501_0001u32.to_be_bytes());
177        assert_eq!(&encap[8..12], &0x0001_0109u32.to_be_bytes());
178    }
179
180    #[test]
181    fn encapsulation_roundtrip_both_orders() {
182        for e in [Endianness::Big, Endianness::Little] {
183            let ctx = CodeSetContext::new(well_known::ISO_8859_1, well_known::UCS_2);
184            let encap = ctx.encode_encapsulation(e).unwrap();
185            assert_eq!(CodeSetContext::decode_encapsulation(&encap).unwrap(), ctx);
186        }
187    }
188
189    #[test]
190    fn service_context_roundtrip_via_list() {
191        let ctx = CodeSetContext::default_pair();
192        let sc = ctx.to_service_context(Endianness::Little).unwrap();
193        assert_eq!(sc.context_id, 1);
194        let list = ServiceContextList(alloc::vec![sc]);
195        let found = CodeSetContext::from_service_context_list(&list)
196            .unwrap()
197            .expect("CodeSets context present");
198        assert_eq!(found, ctx);
199    }
200
201    #[test]
202    fn absent_context_is_none() {
203        let list = ServiceContextList(alloc::vec![ServiceContext::new(42, alloc::vec![1, 2, 3])]);
204        assert_eq!(
205            CodeSetContext::from_service_context_list(&list).unwrap(),
206            None
207        );
208    }
209
210    #[test]
211    fn invalid_byte_order_octet_rejected() {
212        let bad = alloc::vec![0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
213        assert!(CodeSetContext::decode_encapsulation(&bad).is_err());
214    }
215}