openigtlink_rust/protocol/types/
command.rs

1//! COMMAND message type implementation
2//!
3//! The COMMAND message type is used to transfer command strings structured in XML.
4//! It provides command ID and name fields for referencing messages.
5
6use crate::error::{IgtlError, Result};
7use crate::protocol::message::Message;
8use bytes::{Buf, BufMut};
9
10/// Size of command name field
11const COMMAND_NAME_SIZE: usize = 20;
12
13/// COMMAND message containing command data with ID and name
14///
15/// # OpenIGTLink Specification
16/// - Message type: "COMMAND"
17/// - Body format: COMMAND_ID (uint32) + COMMAND_NAME (`char[20]`) + ENCODING (uint16) + LENGTH (uint32) + COMMAND (`uint8[LENGTH]`)
18/// - Character encoding: MIBenum value (default: 3 = US-ASCII)
19#[derive(Debug, Clone, PartialEq)]
20pub struct CommandMessage {
21    /// Unique ID of this command
22    pub command_id: u32,
23
24    /// Name of the command (max 20 chars)
25    pub command_name: String,
26
27    /// Character encoding as MIBenum value
28    /// Common values:
29    /// - 3: US-ASCII (default)
30    /// - 106: UTF-8
31    pub encoding: u16,
32
33    /// The command string (often XML)
34    pub command: String,
35}
36
37impl CommandMessage {
38    /// Create a new COMMAND message with US-ASCII encoding
39    pub fn new(
40        command_id: u32,
41        command_name: impl Into<String>,
42        command: impl Into<String>,
43    ) -> Self {
44        CommandMessage {
45            command_id,
46            command_name: command_name.into(),
47            encoding: 3, // US-ASCII
48            command: command.into(),
49        }
50    }
51
52    /// Create a COMMAND message with UTF-8 encoding
53    pub fn utf8(
54        command_id: u32,
55        command_name: impl Into<String>,
56        command: impl Into<String>,
57    ) -> Self {
58        CommandMessage {
59            command_id,
60            command_name: command_name.into(),
61            encoding: 106, // UTF-8
62            command: command.into(),
63        }
64    }
65
66    /// Create a COMMAND message with custom encoding
67    pub fn with_encoding(
68        command_id: u32,
69        command_name: impl Into<String>,
70        encoding: u16,
71        command: impl Into<String>,
72    ) -> Self {
73        CommandMessage {
74            command_id,
75            command_name: command_name.into(),
76            encoding,
77            command: command.into(),
78        }
79    }
80
81    /// Get the command string as a reference
82    pub fn as_str(&self) -> &str {
83        &self.command
84    }
85}
86
87impl Message for CommandMessage {
88    fn message_type() -> &'static str {
89        "COMMAND"
90    }
91
92    fn encode_content(&self) -> Result<Vec<u8>> {
93        let command_bytes = self.command.as_bytes();
94        let command_len = command_bytes.len();
95
96        let mut buf = Vec::with_capacity(4 + COMMAND_NAME_SIZE + 2 + 4 + command_len);
97
98        // Encode COMMAND_ID (uint32)
99        buf.put_u32(self.command_id);
100
101        // Encode COMMAND_NAME (`char[20]`)
102        let mut name_bytes = [0u8; COMMAND_NAME_SIZE];
103        let name_str = self.command_name.as_bytes();
104        let copy_len = name_str.len().min(COMMAND_NAME_SIZE - 1);
105        name_bytes[..copy_len].copy_from_slice(&name_str[..copy_len]);
106        buf.extend_from_slice(&name_bytes);
107
108        // Encode ENCODING (uint16)
109        buf.put_u16(self.encoding);
110
111        // Encode LENGTH (uint32)
112        buf.put_u32(command_len as u32);
113
114        // Encode COMMAND bytes
115        buf.extend_from_slice(command_bytes);
116
117        Ok(buf)
118    }
119
120    fn decode_content(mut data: &[u8]) -> Result<Self> {
121        if data.len() < 4 + COMMAND_NAME_SIZE + 2 + 4 {
122            return Err(IgtlError::InvalidSize {
123                expected: 4 + COMMAND_NAME_SIZE + 2 + 4,
124                actual: data.len(),
125            });
126        }
127
128        // Decode COMMAND_ID
129        let command_id = data.get_u32();
130
131        // Decode COMMAND_NAME (`char[20]`)
132        let name_bytes = &data[..COMMAND_NAME_SIZE];
133        data.advance(COMMAND_NAME_SIZE);
134
135        let name_len = name_bytes
136            .iter()
137            .position(|&b| b == 0)
138            .unwrap_or(COMMAND_NAME_SIZE);
139        let command_name = String::from_utf8(name_bytes[..name_len].to_vec())?;
140
141        // Decode ENCODING
142        let encoding = data.get_u16();
143
144        // Decode LENGTH
145        let length = data.get_u32() as usize;
146
147        // Check remaining data size
148        if data.len() < length {
149            return Err(IgtlError::InvalidSize {
150                expected: length,
151                actual: data.len(),
152            });
153        }
154
155        // Decode COMMAND
156        let command_bytes = &data[..length];
157        let command = String::from_utf8(command_bytes.to_vec())?;
158
159        Ok(CommandMessage {
160            command_id,
161            command_name,
162            encoding,
163            command,
164        })
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_message_type() {
174        assert_eq!(CommandMessage::message_type(), "COMMAND");
175    }
176
177    #[test]
178    fn test_new() {
179        let msg = CommandMessage::new(1, "START", "<cmd>start</cmd>");
180        assert_eq!(msg.command_id, 1);
181        assert_eq!(msg.command_name, "START");
182        assert_eq!(msg.encoding, 3);
183        assert_eq!(msg.command, "<cmd>start</cmd>");
184    }
185
186    #[test]
187    fn test_utf8() {
188        let msg = CommandMessage::utf8(2, "STOP", "<cmd>停止</cmd>");
189        assert_eq!(msg.encoding, 106);
190    }
191
192    #[test]
193    fn test_with_encoding() {
194        let msg = CommandMessage::with_encoding(3, "TEST", 42, "<test/>");
195        assert_eq!(msg.encoding, 42);
196    }
197
198    #[test]
199    fn test_as_str() {
200        let msg = CommandMessage::new(1, "CMD", "test");
201        assert_eq!(msg.as_str(), "test");
202    }
203
204    #[test]
205    fn test_encode_simple() {
206        let msg = CommandMessage::new(100, "START", "GO");
207        let encoded = msg.encode_content().unwrap();
208
209        // Check command ID (first 4 bytes)
210        assert_eq!(
211            u32::from_be_bytes([encoded[0], encoded[1], encoded[2], encoded[3]]),
212            100
213        );
214
215        // Check encoding field (at offset 4 + 20 = 24)
216        assert_eq!(u16::from_be_bytes([encoded[24], encoded[25]]), 3);
217
218        // Check length field (at offset 26)
219        assert_eq!(
220            u32::from_be_bytes([encoded[26], encoded[27], encoded[28], encoded[29]]),
221            2
222        );
223    }
224
225    #[test]
226    fn test_roundtrip_simple() {
227        let original = CommandMessage::new(42, "TEST", "Hello");
228        let encoded = original.encode_content().unwrap();
229        let decoded = CommandMessage::decode_content(&encoded).unwrap();
230
231        assert_eq!(decoded.command_id, original.command_id);
232        assert_eq!(decoded.command_name, original.command_name);
233        assert_eq!(decoded.encoding, original.encoding);
234        assert_eq!(decoded.command, original.command);
235    }
236
237    #[test]
238    fn test_roundtrip_xml() {
239        let xml =
240            r#"<?xml version="1.0" encoding="UTF-8"?><command><action>start</action></command>"#;
241        let original = CommandMessage::new(1, "XML_CMD", xml);
242        let encoded = original.encode_content().unwrap();
243        let decoded = CommandMessage::decode_content(&encoded).unwrap();
244
245        assert_eq!(decoded.command, xml);
246    }
247
248    #[test]
249    fn test_roundtrip_utf8() {
250        let original = CommandMessage::utf8(5, "日本語", "こんにちは世界");
251        let encoded = original.encode_content().unwrap();
252        let decoded = CommandMessage::decode_content(&encoded).unwrap();
253
254        assert_eq!(decoded.command_name, original.command_name);
255        assert_eq!(decoded.command, original.command);
256    }
257
258    #[test]
259    fn test_name_truncation() {
260        let long_name = "ThisIsAVeryLongCommandNameThatExceedsTwentyCharacters";
261        let msg = CommandMessage::new(1, long_name, "test");
262        let encoded = msg.encode_content().unwrap();
263        let decoded = CommandMessage::decode_content(&encoded).unwrap();
264
265        assert!(decoded.command_name.len() < 20);
266    }
267
268    #[test]
269    fn test_empty_command() {
270        let msg = CommandMessage::new(0, "EMPTY", "");
271        let encoded = msg.encode_content().unwrap();
272        let decoded = CommandMessage::decode_content(&encoded).unwrap();
273
274        assert_eq!(decoded.command, "");
275    }
276
277    #[test]
278    fn test_decode_invalid_size() {
279        let data = vec![0u8; 10]; // Too short
280        let result = CommandMessage::decode_content(&data);
281        assert!(result.is_err());
282    }
283
284    #[test]
285    fn test_decode_truncated_command() {
286        let mut data = vec![0u8; 30]; // Header only
287                                      // Set LENGTH to 10 at offset 26
288        data[26..30].copy_from_slice(&10u32.to_be_bytes());
289        // But don't provide the 10 bytes of command data
290
291        let result = CommandMessage::decode_content(&data);
292        assert!(result.is_err());
293    }
294}