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