Skip to main content

rust_ethernet_ip_protocol/
cip.rs

1//! CIP request, response, and Common Packet Format codecs.
2
3use bytes::{Buf, BufMut, BytesMut};
4
5use crate::{Decode, Encode, ProtocolError, Result};
6
7/// CIP Read Tag service code.
8pub const READ_TAG: u8 = 0x4C;
9/// CIP Write Tag service code.
10pub const WRITE_TAG: u8 = 0x4D;
11#[allow(dead_code)]
12/// CIP Multiple Service Packet service code.
13pub const MULTIPLE_SERVICE_PACKET: u8 = 0x0A;
14
15/// An unconnected CIP service request.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct CipRequest {
18    /// CIP service code.
19    pub service: u8,
20    /// Word-aligned encoded request path.
21    pub path: Vec<u8>,
22    /// Service-specific request data.
23    pub data: Vec<u8>,
24}
25
26impl CipRequest {
27    /// Creates a request from an encoded path and service data.
28    pub fn new(service: u8, path: Vec<u8>, data: Vec<u8>) -> Self {
29        Self {
30            service,
31            path,
32            data,
33        }
34    }
35
36    /// Validates path presence, alignment, and the one-byte word-count limit.
37    pub fn validate(&self) -> Result<()> {
38        if self.path.is_empty() {
39            return Err(ProtocolError::new(format!(
40                "invalid CIP request path for service 0x{:02X}: path must not be empty",
41                self.service
42            )));
43        }
44
45        if !self.path.len().is_multiple_of(2) {
46            return Err(ProtocolError::new(format!(
47                "invalid CIP request path for service 0x{:02X}: path length {} is not word-aligned",
48                self.service,
49                self.path.len()
50            )));
51        }
52
53        let path_words = self.path.len() / 2;
54        if path_words > usize::from(u8::MAX) {
55            return Err(ProtocolError::new(format!(
56                "invalid CIP request path for service 0x{:02X}: path length {} bytes exceeds 510-byte CIP limit",
57                self.service,
58                self.path.len()
59            )));
60        }
61
62        Ok(())
63    }
64
65    /// Validates and appends the encoded request to `buf`.
66    pub fn encode(&self, buf: &mut BytesMut) -> Result<()> {
67        self.validate()?;
68        buf.put_u8(self.service);
69        let path_words =
70            u8::try_from(self.path.len() / 2).expect("validated path word count fits in u8");
71        buf.put_u8(path_words);
72        buf.put_slice(&self.path);
73        buf.put_slice(&self.data);
74        Ok(())
75    }
76}
77
78impl Decode for CipRequest {
79    fn decode(buf: &mut impl Buf) -> Result<Self> {
80        if buf.remaining() < 2 {
81            return Err(ProtocolError::new("CIP request too short".to_string()));
82        }
83
84        let service = buf.get_u8();
85        let path_size_words = buf.get_u8() as usize;
86        let path_len = path_size_words * 2;
87        if buf.remaining() < path_len {
88            return Err(ProtocolError::new("CIP request path truncated".to_string()));
89        }
90        let path = buf.copy_to_bytes(path_len).to_vec();
91        let data = buf.copy_to_bytes(buf.remaining()).to_vec();
92
93        Ok(Self {
94            service,
95            path,
96            data,
97        })
98    }
99}
100
101/// A decoded CIP service response.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct CipResponse {
104    /// Reply service code, normally the request service with bit 7 set.
105    pub service: u8,
106    /// CIP general status code.
107    pub status: u8,
108    /// Optional 16-bit extended status words.
109    pub additional_status: Vec<u16>,
110    /// Service-specific response data.
111    pub data: Vec<u8>,
112}
113
114impl Encode for CipResponse {
115    fn encode(&self, buf: &mut BytesMut) {
116        buf.put_u8(self.service);
117        buf.put_u8(0);
118        buf.put_u8(self.status);
119        buf.put_u8(self.additional_status.len() as u8);
120        for status in &self.additional_status {
121            buf.put_u16_le(*status);
122        }
123        buf.put_slice(&self.data);
124    }
125}
126
127impl Decode for CipResponse {
128    fn decode(buf: &mut impl Buf) -> Result<Self> {
129        if buf.remaining() < 4 {
130            return Err(ProtocolError::new("CIP response too short".to_string()));
131        }
132
133        let service = buf.get_u8();
134        let _reserved = buf.get_u8();
135        let status = buf.get_u8();
136        let additional_status_size = buf.get_u8() as usize;
137        if buf.remaining() < additional_status_size * 2 {
138            return Err(ProtocolError::new(
139                "CIP response additional status truncated".to_string(),
140            ));
141        }
142
143        let mut additional_status = Vec::with_capacity(additional_status_size);
144        for _ in 0..additional_status_size {
145            additional_status.push(buf.get_u16_le());
146        }
147        let data = buf.copy_to_bytes(buf.remaining()).to_vec();
148
149        Ok(Self {
150            service,
151            status,
152            additional_status,
153            data,
154        })
155    }
156}
157
158/// One Common Packet Format item.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct CpfItem {
161    /// CPF item type identifier.
162    pub type_id: u16,
163    /// Item payload.
164    pub data: Vec<u8>,
165}
166
167/// Payload carried by SendRRData or SendUnitData encapsulation commands.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct SendDataRequest {
170    /// Encapsulation interface handle; zero for CIP.
171    pub interface_handle: u32,
172    /// Encapsulation timeout in seconds.
173    pub timeout: u16,
174    /// Common Packet Format items.
175    pub items: Vec<CpfItem>,
176}
177
178impl SendDataRequest {
179    /// Wraps an unconnected CIP message in the standard null-address/data items.
180    pub fn unconnected(item_data: &[u8]) -> Self {
181        Self {
182            interface_handle: 0,
183            timeout: 5,
184            items: vec![
185                CpfItem {
186                    type_id: 0x0000,
187                    data: Vec::new(),
188                },
189                CpfItem {
190                    type_id: 0x00B2,
191                    data: item_data.to_vec(),
192                },
193            ],
194        }
195    }
196}
197
198impl Encode for SendDataRequest {
199    fn encode(&self, buf: &mut BytesMut) {
200        buf.put_u32_le(self.interface_handle);
201        buf.put_u16_le(self.timeout);
202        buf.put_u16_le(self.items.len() as u16);
203        for item in &self.items {
204            buf.put_u16_le(item.type_id);
205            buf.put_u16_le(item.data.len() as u16);
206            buf.put_slice(&item.data);
207        }
208    }
209}
210
211impl Decode for SendDataRequest {
212    fn decode(buf: &mut impl Buf) -> Result<Self> {
213        if buf.remaining() < 8 {
214            return Err(ProtocolError::new("CPF data too short"));
215        }
216
217        let interface_handle = buf.get_u32_le();
218        let timeout = buf.get_u16_le();
219        let item_count = buf.get_u16_le() as usize;
220        let mut items = Vec::with_capacity(item_count);
221        for _ in 0..item_count {
222            if buf.remaining() < 4 {
223                return Err(ProtocolError::new("Response truncated while parsing items"));
224            }
225            let type_id = buf.get_u16_le();
226            let item_length = buf.get_u16_le() as usize;
227            if buf.remaining() < item_length {
228                return Err(ProtocolError::new("Data item truncated"));
229            }
230            items.push(CpfItem {
231                type_id,
232                data: buf.copy_to_bytes(item_length).to_vec(),
233            });
234        }
235
236        Ok(Self {
237            interface_handle,
238            timeout,
239            items,
240        })
241    }
242}