rust-ethernet-ip-protocol 1.0.0

EtherNet/IP wire protocol codecs used by rust-ethernet-ip
Documentation
use bytes::{Buf, BufMut, BytesMut};

use crate::{Decode, Encode, ProtocolError, Result};

pub const READ_TAG: u8 = 0x4C;
pub const WRITE_TAG: u8 = 0x4D;
#[allow(dead_code)]
pub const MULTIPLE_SERVICE_PACKET: u8 = 0x0A;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CipRequest {
    pub service: u8,
    pub path: Vec<u8>,
    pub data: Vec<u8>,
}

impl CipRequest {
    pub fn new(service: u8, path: Vec<u8>, data: Vec<u8>) -> Self {
        Self {
            service,
            path,
            data,
        }
    }

    pub fn validate(&self) -> Result<()> {
        if self.path.is_empty() {
            return Err(ProtocolError::new(format!(
                "invalid CIP request path for service 0x{:02X}: path must not be empty",
                self.service
            )));
        }

        if !self.path.len().is_multiple_of(2) {
            return Err(ProtocolError::new(format!(
                "invalid CIP request path for service 0x{:02X}: path length {} is not word-aligned",
                self.service,
                self.path.len()
            )));
        }

        let path_words = self.path.len() / 2;
        if path_words > usize::from(u8::MAX) {
            return Err(ProtocolError::new(format!(
                "invalid CIP request path for service 0x{:02X}: path length {} bytes exceeds 510-byte CIP limit",
                self.service,
                self.path.len()
            )));
        }

        Ok(())
    }

    pub fn encode(&self, buf: &mut BytesMut) -> Result<()> {
        self.validate()?;
        buf.put_u8(self.service);
        let path_words =
            u8::try_from(self.path.len() / 2).expect("validated path word count fits in u8");
        buf.put_u8(path_words);
        buf.put_slice(&self.path);
        buf.put_slice(&self.data);
        Ok(())
    }
}

impl Decode for CipRequest {
    fn decode(buf: &mut impl Buf) -> Result<Self> {
        if buf.remaining() < 2 {
            return Err(ProtocolError::new("CIP request too short".to_string()));
        }

        let service = buf.get_u8();
        let path_size_words = buf.get_u8() as usize;
        let path_len = path_size_words * 2;
        if buf.remaining() < path_len {
            return Err(ProtocolError::new("CIP request path truncated".to_string()));
        }
        let path = buf.copy_to_bytes(path_len).to_vec();
        let data = buf.copy_to_bytes(buf.remaining()).to_vec();

        Ok(Self {
            service,
            path,
            data,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CipResponse {
    pub service: u8,
    pub status: u8,
    pub additional_status: Vec<u16>,
    pub data: Vec<u8>,
}

impl Encode for CipResponse {
    fn encode(&self, buf: &mut BytesMut) {
        buf.put_u8(self.service);
        buf.put_u8(0);
        buf.put_u8(self.status);
        buf.put_u8(self.additional_status.len() as u8);
        for status in &self.additional_status {
            buf.put_u16_le(*status);
        }
        buf.put_slice(&self.data);
    }
}

impl Decode for CipResponse {
    fn decode(buf: &mut impl Buf) -> Result<Self> {
        if buf.remaining() < 4 {
            return Err(ProtocolError::new("CIP response too short".to_string()));
        }

        let service = buf.get_u8();
        let _reserved = buf.get_u8();
        let status = buf.get_u8();
        let additional_status_size = buf.get_u8() as usize;
        if buf.remaining() < additional_status_size * 2 {
            return Err(ProtocolError::new(
                "CIP response additional status truncated".to_string(),
            ));
        }

        let mut additional_status = Vec::with_capacity(additional_status_size);
        for _ in 0..additional_status_size {
            additional_status.push(buf.get_u16_le());
        }
        let data = buf.copy_to_bytes(buf.remaining()).to_vec();

        Ok(Self {
            service,
            status,
            additional_status,
            data,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CpfItem {
    pub type_id: u16,
    pub data: Vec<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SendDataRequest {
    pub interface_handle: u32,
    pub timeout: u16,
    pub items: Vec<CpfItem>,
}

impl SendDataRequest {
    pub fn unconnected(item_data: &[u8]) -> Self {
        Self {
            interface_handle: 0,
            timeout: 5,
            items: vec![
                CpfItem {
                    type_id: 0x0000,
                    data: Vec::new(),
                },
                CpfItem {
                    type_id: 0x00B2,
                    data: item_data.to_vec(),
                },
            ],
        }
    }
}

impl Encode for SendDataRequest {
    fn encode(&self, buf: &mut BytesMut) {
        buf.put_u32_le(self.interface_handle);
        buf.put_u16_le(self.timeout);
        buf.put_u16_le(self.items.len() as u16);
        for item in &self.items {
            buf.put_u16_le(item.type_id);
            buf.put_u16_le(item.data.len() as u16);
            buf.put_slice(&item.data);
        }
    }
}

impl Decode for SendDataRequest {
    fn decode(buf: &mut impl Buf) -> Result<Self> {
        if buf.remaining() < 8 {
            return Err(ProtocolError::new("CPF data too short"));
        }

        let interface_handle = buf.get_u32_le();
        let timeout = buf.get_u16_le();
        let item_count = buf.get_u16_le() as usize;
        let mut items = Vec::with_capacity(item_count);
        for _ in 0..item_count {
            if buf.remaining() < 4 {
                return Err(ProtocolError::new("Response truncated while parsing items"));
            }
            let type_id = buf.get_u16_le();
            let item_length = buf.get_u16_le() as usize;
            if buf.remaining() < item_length {
                return Err(ProtocolError::new("Data item truncated"));
            }
            items.push(CpfItem {
                type_id,
                data: buf.copy_to_bytes(item_length).to_vec(),
            });
        }

        Ok(Self {
            interface_handle,
            timeout,
            items,
        })
    }
}