Skip to main content

rust_ethernet_ip_protocol/
cip.rs

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