rust_ethernet_ip_protocol/
cip.rs1use 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}