From 8bad1f1b5679a28918d28367ba5d7230a157f5d5 Mon Sep 17 00:00:00 2001 From: jsxs Date: Sat, 15 Feb 2025 23:46:42 +0900 Subject: [PATCH 1/9] feat(rust): Add initial S7 protocol implementation - Add async S7 protocol parsing with Tokio - Implement fuzz testing infrastructure - Add comprehensive documentation - Set up CI/CD pipeline for Rust --- .github/workflows/ci.yml | 58 ++++++ plc4x-rust/CONTRIBUTING.md | 43 +++++ plc4x-rust/Cargo.toml | 22 +++ plc4x-rust/README.md | 54 ++++++ plc4x-rust/fuzz/Cargo.toml | 18 ++ plc4x-rust/fuzz/fuzz_targets/header_parser.rs | 7 + plc4x-rust/src/error.rs | 13 ++ plc4x-rust/src/lib.rs | 9 + plc4x-rust/src/s7.rs | 175 ++++++++++++++++++ plc4x-rust/src/types.rs | 37 ++++ 10 files changed, 436 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 plc4x-rust/CONTRIBUTING.md create mode 100644 plc4x-rust/Cargo.toml create mode 100644 plc4x-rust/README.md create mode 100644 plc4x-rust/fuzz/Cargo.toml create mode 100644 plc4x-rust/fuzz/fuzz_targets/header_parser.rs create mode 100644 plc4x-rust/src/error.rs create mode 100644 plc4x-rust/src/lib.rs create mode 100644 plc4x-rust/src/s7.rs create mode 100644 plc4x-rust/src/types.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..a8d2df1663a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: Industrial CI +on: [push, pull_request] + +jobs: + industrial_validation: + name: Protocol Implementation Validation + runs-on: ubuntu-latest + strategy: + matrix: + rust: [stable, 1.75.0] + platform: [x86_64, aarch64] + + steps: + - uses: actions/checkout@v4 + + - name: Rust Toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + target: ${{ matrix.platform }}-unknown-linux-gnu + override: true + + - name: Cache Dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Security Audit + uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Protocol Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --target ${{ matrix.platform }}-unknown-linux-gnu + + industrial_linting: + name: Industrial-Grade Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-targets --all-features -- -D warnings + + - uses: actions-rs/format-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all -- --check diff --git a/plc4x-rust/CONTRIBUTING.md b/plc4x-rust/CONTRIBUTING.md new file mode 100644 index 00000000000..c1efdae09da --- /dev/null +++ b/plc4x-rust/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to PLC4X Rust Implementation + +## Development Setup + +1. Install Rust (stable channel) +2. Clone the repository +3. Install development tools: + ```bash + cargo install cargo-fuzz + ``` + +## Code Style +- Follow Rust standard formatting (use `rustfmt`) +- Add documentation for public APIs +- Include tests for new functionality + +## Testing + +### Before Submitting a PR +Run all checks: +```bash +# Format code +cargo fmt -- --check + +# Run linter +cargo clippy + +# Run unit tests +cargo test + +# Run fuzzer +cargo fuzz run header_parser +``` + +## Pull Request Process +1. Create a feature branch +2. Add tests for new functionality +3. Update documentation +4. Ensure all tests pass +5. Submit PR with clear description + +## Questions? +Feel free to reach out to the maintainers or open an issue. diff --git a/plc4x-rust/Cargo.toml b/plc4x-rust/Cargo.toml new file mode 100644 index 00000000000..cff6b697060 --- /dev/null +++ b/plc4x-rust/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "plc4x-rust" +version = "0.1.0" +edition = "2021" +description = "Rust bindings for Apache PLC4X" +license = "Apache-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nom = "7.1.3" +thiserror = "1.0" +bytes = "1.4" +tokio = { version = "1.28", features = ["full"] } +tracing = "0.1" + +[dev-dependencies] +tokio-test = "0.4" +tokio = { version = "1.28", features = ["full", "test-util"] } + +[profile.release] +debug = true diff --git a/plc4x-rust/README.md b/plc4x-rust/README.md new file mode 100644 index 00000000000..3385f80118b --- /dev/null +++ b/plc4x-rust/README.md @@ -0,0 +1,54 @@ +# PLC4X Rust Implementation + +This project implements Rust bindings for Apache PLC4X, focusing on the Siemens S7 protocol. The implementation aims to provide: + +- Memory-safe, zero-copy protocol parsing using nom +- Async/await support for modern Rust applications +- High-performance industrial communication +- Type-safe protocol implementation + +## Current Status + +- [x] Initial project structure +- [x] Basic S7 protocol types +- [ ] Protocol parsing implementation +- [ ] Connection handling +- [ ] Async support +- [ ] Testing infrastructure + +## Getting Started + +### Prerequisites + +- Rust (stable channel) +- Cargo + +### Installation + +```bash +git clone https://github.com/apache/plc4x +cd plc4x-rust +cargo build +``` + +## Testing + +### Unit Tests + +```bash +cargo test +``` + +### Fuzz Testing + +```bash +# Install cargo-fuzz (only needed once) +cargo install cargo-fuzz + +# Run the fuzzer +cargo fuzz run header_parser +``` + +## Contributing + +Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and development process. diff --git a/plc4x-rust/fuzz/Cargo.toml b/plc4x-rust/fuzz/Cargo.toml new file mode 100644 index 00000000000..dbd6ad44a03 --- /dev/null +++ b/plc4x-rust/fuzz/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "plc4x-rust-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +plc4x-rust = { path = ".." } + +[[bin]] +name = "header_parser" +path = "fuzz_targets/header_parser.rs" +test = false +doc = false diff --git a/plc4x-rust/fuzz/fuzz_targets/header_parser.rs b/plc4x-rust/fuzz/fuzz_targets/header_parser.rs new file mode 100644 index 00000000000..92364eccdb2 --- /dev/null +++ b/plc4x-rust/fuzz/fuzz_targets/header_parser.rs @@ -0,0 +1,7 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use plc4x_rust::s7::S7Header; + +fuzz_target!(|data: &[u8]| { + let _ = S7Header::parse(data); +}); diff --git a/plc4x-rust/src/error.rs b/plc4x-rust/src/error.rs new file mode 100644 index 00000000000..40b5375c6f3 --- /dev/null +++ b/plc4x-rust/src/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Protocol error: {0}")] + Protocol(String), + + #[error("Connection error: {0}")] + Connection(String), + + #[error("Parse error: {0}")] + Parse(String), +} diff --git a/plc4x-rust/src/lib.rs b/plc4x-rust/src/lib.rs new file mode 100644 index 00000000000..6279fdeda87 --- /dev/null +++ b/plc4x-rust/src/lib.rs @@ -0,0 +1,9 @@ +//! PLC4X Rust implementation for S7 protocol + +mod error; +mod s7; +mod types; + +pub use error::Error; +pub use s7::{S7Connector, S7Header, MessageType}; +pub use types::{CommunicationType, FunctionCode, AreaCode}; diff --git a/plc4x-rust/src/s7.rs b/plc4x-rust/src/s7.rs new file mode 100644 index 00000000000..401d1b0d8a6 --- /dev/null +++ b/plc4x-rust/src/s7.rs @@ -0,0 +1,175 @@ +//! S7 protocol implementation for PLC4X +//! +//! This module provides the core functionality for communicating with +//! Siemens S7 PLCs using a memory-safe, zero-copy implementation. + +use bytes::BytesMut; +use nom::IResult; +use nom::number::complete::{be_u8, be_u16}; +use nom::sequence::tuple; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use std::io; + +/// S7 protocol header structure +/// +/// Represents the header of an S7 protocol packet, containing +/// protocol identification, message type, and length information. +#[derive(Debug, Clone)] +pub struct S7Header { + protocol_id: u8, + message_type: MessageType, + reserved: u16, + pdu_reference: u16, + parameter_length: u16, + data_length: u16, +} + +/// Message types supported by the S7 protocol +/// +/// Represents the different types of messages that can be +/// exchanged in the S7 protocol. +#[derive(Debug, Clone, Copy)] +pub enum MessageType { + /// Job request message + Job = 0x01, + /// Acknowledgment without data + Ack = 0x02, + /// Acknowledgment with data + AckData = 0x03, +} + +impl S7Header { + pub fn new(message_type: MessageType, pdu_reference: u16) -> Self { + Self { + protocol_id: 0x32, // Standard S7 protocol ID + message_type, + reserved: 0, + pdu_reference, + parameter_length: 0, + data_length: 0, + } + } + + pub fn parse(input: &[u8]) -> IResult<&[u8], S7Header> { + let (input, (protocol_id, message_type_raw, reserved, pdu_ref, param_len, data_len)) = + tuple(( + be_u8, + be_u8, + be_u16, + be_u16, + be_u16, + be_u16, + ))(input)?; + + let message_type = match message_type_raw { + 0x01 => MessageType::Job, + 0x02 => MessageType::Ack, + 0x03 => MessageType::AckData, + _ => return Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag + ))), + }; + + Ok((input, S7Header { + protocol_id, + message_type, + reserved, + pdu_reference: pdu_ref, + parameter_length: param_len, + data_length: data_len, + })) + } +} + +pub struct S7Connector { + // Connection details will go here +} + +impl S7Connector { + pub fn new() -> Self { + Self {} + } + + pub async fn negotiate(&self, stream: &mut T) -> io::Result<()> + where + T: AsyncReadExt + AsyncWriteExt + Unpin, + { + // Read the handshake header + let mut header_buf = [0u8; 10]; + stream.read_exact(&mut header_buf).await?; + + // Parse the header + if let Ok((_, header)) = S7Header::parse(&header_buf) { + match header.message_type { + MessageType::AckData => Ok(()), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Unexpected message type in handshake" + )), + } + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "Failed to parse handshake header" + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::duplex; + + #[test] + fn test_s7_header_creation() { + let header = S7Header::new(MessageType::Job, 1); + assert_eq!(header.protocol_id, 0x32); + assert_eq!(header.pdu_reference, 1); + } + + #[test] + fn test_message_type_values() { + assert_eq!(MessageType::Job as u8, 0x01); + assert_eq!(MessageType::Ack as u8, 0x02); + assert_eq!(MessageType::AckData as u8, 0x03); + } + + #[test] + fn test_s7_header_parsing() { + let test_data = &[ + 0x32, // protocol_id + 0x01, // message_type (Job) + 0x00, 0x00, // reserved + 0x00, 0x01, // pdu_reference + 0x00, 0x00, // parameter_length + 0x00, 0x00, // data_length + ]; + + let (remaining, header) = S7Header::parse(test_data).unwrap(); + assert!(remaining.is_empty()); + assert_eq!(header.protocol_id, 0x32); + assert_eq!(header.pdu_reference, 1); + matches!(header.message_type, MessageType::Job); + } + + #[tokio::test] + async fn test_async_connection_handshake() { + let connector = S7Connector::new(); + let (mut tx, mut rx) = duplex(1024); + + tokio::spawn(async move { + let mut buf = [0u8; 12]; + tx.write_all(&[ + 0x32, 0x03, // Valid header + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00 + ]).await.unwrap(); + tx.read_exact(&mut buf).await.unwrap(); + }); + + let result = connector.negotiate(&mut rx).await; + assert!(result.is_ok()); + } +} diff --git a/plc4x-rust/src/types.rs b/plc4x-rust/src/types.rs new file mode 100644 index 00000000000..4906b9e16ff --- /dev/null +++ b/plc4x-rust/src/types.rs @@ -0,0 +1,37 @@ +//! S7 protocol type definitions + +/// S7 Communication Type +#[derive(Debug, Clone, Copy)] +pub enum CommunicationType { + PG = 0x01, + OP = 0x02, + Step7Basic = 0x03, +} + +/// S7 Function Code +#[derive(Debug, Clone, Copy)] +pub enum FunctionCode { + ReadVar = 0x04, + WriteVar = 0x05, + RequestDownload = 0x1A, + Download = 0x1B, + DownloadEnded = 0x1C, + // Add more as needed +} + +/// S7 Area Code +#[derive(Debug, Clone, Copy)] +pub enum AreaCode { + SysInfo = 0x03, + SysFlags = 0x05, + AnaInput = 0x06, + AnaOutput = 0x07, + P = 0x80, + Inputs = 0x81, + Outputs = 0x82, + Flags = 0x83, + DB = 0x84, + DI = 0x85, + Local = 0x86, + V = 0x87, +} From b0c913b3f088797b145318fcba010a92d70bb88e Mon Sep 17 00:00:00 2001 From: jsxs Date: Wed, 19 Feb 2025 11:01:16 +0900 Subject: [PATCH 2/9] feat(s7): Implement core S7 protocol structures and parsing --- plc4rs/src/protocols/s7/mod.rs | 407 +++++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 plc4rs/src/protocols/s7/mod.rs diff --git a/plc4rs/src/protocols/s7/mod.rs b/plc4rs/src/protocols/s7/mod.rs new file mode 100644 index 00000000000..aca54b88642 --- /dev/null +++ b/plc4rs/src/protocols/s7/mod.rs @@ -0,0 +1,407 @@ +use nom::{ + bytes::complete::take, + number::complete::{be_u8, be_u16, be_u32}, + IResult, +}; + +#[derive(Debug, Clone)] +pub struct TPKTPacket { + protocol_id: u8, // Always 0x03 + reserved: u8, // Always 0x00 + length: u16, // Total length including header + payload: COTPPacket, +} + +impl TPKTPacket { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, protocol_id) = be_u8(input)?; + let (input, reserved) = be_u8(input)?; + let (input, length) = be_u16(input)?; + let (input, payload) = COTPPacket::parse(input)?; + + Ok((input, TPKTPacket { + protocol_id, + reserved, + length, + payload, + })) + } +} + +#[derive(Debug, Clone)] +pub enum COTPPacket { + Data(COTPDataPacket), + ConnectionRequest(COTPConnectionRequest), + ConnectionResponse(COTPConnectionResponse), +} + +impl COTPPacket { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, header_length) = be_u8(input)?; + let (input, tpdu_code) = be_u8(input)?; + + match tpdu_code { + 0xF0 => { + let (input, packet) = COTPDataPacket::parse(input)?; + Ok((input, COTPPacket::Data(packet))) + }, + 0xE0 => { + let (input, packet) = COTPConnectionRequest::parse(input)?; + Ok((input, COTPPacket::ConnectionRequest(packet))) + }, + 0xD0 => { + let (input, packet) = COTPConnectionResponse::parse(input)?; + Ok((input, COTPPacket::ConnectionResponse(packet))) + }, + _ => Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag + ))), + } + } +} + +#[derive(Debug, Clone)] +pub struct COTPDataPacket { + eot: bool, // End of transmission + tpdu_ref: u8, // Reference number + data: S7Message, // S7 protocol data +} + +impl COTPDataPacket { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, flags) = be_u8(input)?; + let (input, tpdu_ref) = be_u8(input)?; + let (input, data) = S7Message::parse(input)?; + + Ok((input, COTPDataPacket { + eot: (flags & 0x80) != 0, + tpdu_ref, + data, + })) + } +} + +#[derive(Debug, Clone)] +pub struct S7Message { + header: S7Header, + parameters: Option, + payload: Option, +} + +impl S7Message { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, header) = S7Header::parse(input)?; + let (input, parameters) = if header.parameter_length > 0 { + let (input, params) = S7Parameters::parse(input, header.parameter_length)?; + (input, Some(params)) + } else { + (input, None) + }; + let (input, payload) = if header.data_length > 0 { + let (input, data) = S7Payload::parse(input, header.data_length)?; + (input, Some(data)) + } else { + (input, None) + }; + + Ok((input, S7Message { + header, + parameters, + payload, + })) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum MessageType { + JobRequest = 0x01, + Ack = 0x02, + AckData = 0x03, + UserData = 0x07, +} + +impl TryFrom for MessageType { + type Error = S7Error; + + fn try_from(value: u8) -> Result { + match value { + 0x01 => Ok(MessageType::JobRequest), + 0x02 => Ok(MessageType::Ack), + 0x03 => Ok(MessageType::AckData), + 0x07 => Ok(MessageType::UserData), + _ => Err(S7Error::InvalidMessageType(value)), + } + } +} + +#[derive(Debug, Clone)] +pub struct S7Header { + protocol_id: u8, // Always 0x32 + message_type: MessageType, + reserved: u16, // Always 0x0000 + pdu_reference: u16, + parameter_length: u16, + data_length: u16, +} + +impl S7Header { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, protocol_id) = be_u8(input)?; + let (input, msg_type) = be_u8(input)?; + let (input, reserved) = be_u16(input)?; + let (input, pdu_reference) = be_u16(input)?; + let (input, parameter_length) = be_u16(input)?; + let (input, data_length) = be_u16(input)?; + + let message_type = MessageType::try_from(msg_type) + .map_err(|_| nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag + )))?; + + Ok((input, S7Header { + protocol_id, + message_type, + reserved, + pdu_reference, + parameter_length, + data_length, + })) + } +} + +#[derive(Debug, Clone)] +pub struct S7Parameters { + parameter_type: ParameterType, + items: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub enum ParameterType { + Setup = 0xF0, + ReadVar = 0x04, + WriteVar = 0x05, + StartUpload = 0x1D, + Upload = 0x1E, + EndUpload = 0x1F, + // Add more as needed +} + +impl TryFrom for ParameterType { + type Error = S7Error; + + fn try_from(value: u8) -> Result { + match value { + 0xF0 => Ok(ParameterType::Setup), + 0x04 => Ok(ParameterType::ReadVar), + 0x05 => Ok(ParameterType::WriteVar), + 0x1D => Ok(ParameterType::StartUpload), + 0x1E => Ok(ParameterType::Upload), + 0x1F => Ok(ParameterType::EndUpload), + _ => Err(S7Error::InvalidParameterType(value)), + } + } +} + +impl S7Parameters { + pub fn parse(input: &[u8], length: u16) -> IResult<&[u8], Self> { + let (input, param_type) = be_u8(input)?; + let parameter_type = ParameterType::try_from(param_type) + .map_err(|_| nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag + )))?; + + let (input, items) = match parameter_type { + ParameterType::ReadVar | ParameterType::WriteVar => { + let (input, item_count) = be_u8(input)?; + let mut items = Vec::with_capacity(item_count as usize); + let mut remaining = input; + + for _ in 0..item_count { + let (input, item) = ParameterItem::parse(remaining)?; + items.push(item); + remaining = input; + } + (remaining, items) + }, + _ => (input, Vec::new()), + }; + + Ok((input, S7Parameters { + parameter_type, + items, + })) + } +} + +#[derive(Debug, Clone)] +pub struct ParameterItem { + var_spec: VarSpec, + addr_length: u8, + syntax_id: SyntaxId, + area: Area, + db_number: u16, + start_addr: u32, + length: u16, +} + +#[derive(Debug, Clone, Copy)] +pub enum VarSpec { + Bit = 0x01, + Byte = 0x02, + Word = 0x04, + DWord = 0x06, + // Add more as needed +} + +impl TryFrom for VarSpec { + type Error = S7Error; + + fn try_from(value: u8) -> Result { + match value { + 0x01 => Ok(VarSpec::Bit), + 0x02 => Ok(VarSpec::Byte), + 0x04 => Ok(VarSpec::Word), + 0x06 => Ok(VarSpec::DWord), + _ => Err(S7Error::InvalidVarSpec(value)), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum SyntaxId { + S7Any = 0x10, + // Add more as needed +} + +impl TryFrom for SyntaxId { + type Error = S7Error; + + fn try_from(value: u8) -> Result { + match value { + 0x10 => Ok(SyntaxId::S7Any), + _ => Err(S7Error::InvalidSyntaxId(value)), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Area { + Inputs = 0x81, + Outputs = 0x82, + Flags = 0x83, + DataBlocks = 0x84, + // Add more as needed +} + +impl TryFrom for Area { + type Error = S7Error; + + fn try_from(value: u8) -> Result { + match value { + 0x81 => Ok(Area::Inputs), + 0x82 => Ok(Area::Outputs), + 0x83 => Ok(Area::Flags), + 0x84 => Ok(Area::DataBlocks), + _ => Err(S7Error::InvalidArea(value)), + } + } +} + +impl ParameterItem { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, var_spec) = be_u8(input)?; + let (input, length) = be_u8(input)?; + let (input, syntax_id) = be_u8(input)?; + let (input, transport_size) = be_u8(input)?; + let (input, db_number) = be_u16(input)?; + let (input, area) = be_u8(input)?; + let (input, start_addr) = be_u32(input)?; + + // ... implement conversion and validation ... + + Ok((input, ParameterItem { + var_spec: VarSpec::Byte, // TODO: proper conversion + addr_length: length, + syntax_id: SyntaxId::S7Any, // TODO: proper conversion + area: Area::DataBlocks, // TODO: proper conversion + db_number, + start_addr, + length: 0, // TODO: calculate from transport_size + })) + } +} + +// ... more implementations following s7.mspec + +#[derive(Debug, Clone)] +pub struct S7Payload { + items: Vec, +} + +#[derive(Debug, Clone)] +pub struct PayloadItem { + return_code: ReturnCode, + transport_size: TransportSize, + data: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub enum ReturnCode { + Success = 0x00, + HardwareError = 0x01, + AccessError = 0x03, + OutOfRange = 0x05, + NotSupported = 0x06, + // Add more as needed +} + +#[derive(Debug, Clone, Copy)] +pub enum TransportSize { + Bit = 0x01, + Byte = 0x02, + Word = 0x04, + DWord = 0x06, + // Add more as needed +} + +impl S7Payload { + pub fn parse(input: &[u8], length: u16) -> IResult<&[u8], Self> { + let mut items = Vec::new(); + let mut remaining = input; + let end_pos = length as usize; + + while remaining.len() > 0 && (input.len() - remaining.len()) < end_pos { + let (input, item) = PayloadItem::parse(remaining)?; + items.push(item); + remaining = input; + } + + Ok((remaining, S7Payload { items })) + } +} + +impl PayloadItem { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, return_code) = be_u8(input)?; + let (input, transport_size) = be_u8(input)?; + let (input, length) = be_u16(input)?; + let (input, data) = take(length as usize)(input)?; + + Ok((input, PayloadItem { + return_code: ReturnCode::try_from(return_code) + .map_err(|_| nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag + )))?, + transport_size: TransportSize::try_from(transport_size) + .map_err(|_| nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag + )))?, + data: data.to_vec(), + })) + } +} From bec8671986fba71ced02c431cddfc2db92a5ede2 Mon Sep 17 00:00:00 2001 From: jsxs Date: Wed, 19 Feb 2025 11:01:30 +0900 Subject: [PATCH 3/9] feat(s7): Add error handling for S7 protocol --- plc4rs/src/protocols/s7/error.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 plc4rs/src/protocols/s7/error.rs diff --git a/plc4rs/src/protocols/s7/error.rs b/plc4rs/src/protocols/s7/error.rs new file mode 100644 index 00000000000..afc86029ac8 --- /dev/null +++ b/plc4rs/src/protocols/s7/error.rs @@ -0,0 +1,25 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum S7Error { + #[error("Invalid message type: {0:#04x}")] + InvalidMessageType(u8), + + #[error("Invalid function code: {0:#04x}")] + InvalidFunctionCode(u8), + + #[error("Invalid parameter type: {0:#04x}")] + InvalidParameterType(u8), + + #[error("Invalid protocol ID: expected 0x32, got {0:#04x}")] + InvalidProtocolId(u8), + + #[error("Invalid TPKT version: expected 0x03, got {0:#04x}")] + InvalidTpktVersion(u8), + + #[error("Invalid length: {0}")] + InvalidLength(String), + + #[error("Parse error: {0}")] + ParseError(String), +} From 5b433e01dce8e68a4a7e5d05fa5cd34e458dea38 Mon Sep 17 00:00:00 2001 From: jsxs Date: Wed, 19 Feb 2025 11:01:42 +0900 Subject: [PATCH 4/9] feat(s7): Add connection management structures --- plc4rs/src/protocols/s7/connection.rs | 77 +++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 plc4rs/src/protocols/s7/connection.rs diff --git a/plc4rs/src/protocols/s7/connection.rs b/plc4rs/src/protocols/s7/connection.rs new file mode 100644 index 00000000000..fc2d8b24257 --- /dev/null +++ b/plc4rs/src/protocols/s7/connection.rs @@ -0,0 +1,77 @@ +#[derive(Debug, Clone)] +pub struct COTPConnectionRequest { + dst_ref: u16, + src_ref: u16, + class: u8, + parameters: Vec, +} + +#[derive(Debug, Clone)] +pub struct COTPConnectionResponse { + dst_ref: u16, + src_ref: u16, + class: u8, + parameters: Vec, +} + +#[derive(Debug, Clone)] +pub enum ConnectionParameter { + TpduSize(u8), + SrcTsap(u16), + DstTsap(u16), +} + +impl COTPConnectionRequest { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, dst_ref) = be_u16(input)?; + let (input, src_ref) = be_u16(input)?; + let (input, class) = be_u8(input)?; + let (input, parameters) = ConnectionParameter::parse_all(input)?; + + Ok((input, COTPConnectionRequest { + dst_ref, + src_ref, + class, + parameters, + })) + } +} + +impl ConnectionParameter { + fn parse_all(input: &[u8]) -> IResult<&[u8], Vec> { + let mut parameters = Vec::new(); + let mut remaining = input; + + while !remaining.is_empty() { + let (input, param) = Self::parse(remaining)?; + parameters.push(param); + remaining = input; + } + + Ok((remaining, parameters)) + } + + fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, param_code) = be_u8(input)?; + let (input, param_length) = be_u8(input)?; + + match param_code { + 0xC0 => { + let (input, size) = be_u8(input)?; + Ok((input, ConnectionParameter::TpduSize(size))) + }, + 0xC1 => { + let (input, tsap) = be_u16(input)?; + Ok((input, ConnectionParameter::SrcTsap(tsap))) + }, + 0xC2 => { + let (input, tsap) = be_u16(input)?; + Ok((input, ConnectionParameter::DstTsap(tsap))) + }, + _ => Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag + ))), + } + } +} From cd046bef8856ba0145fa1856fd613f94cc720168 Mon Sep 17 00:00:00 2001 From: jsxs Date: Wed, 19 Feb 2025 11:01:52 +0900 Subject: [PATCH 5/9] test(s7): Add comprehensive test suite --- plc4rs/src/protocols/s7/tests.rs | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 plc4rs/src/protocols/s7/tests.rs diff --git a/plc4rs/src/protocols/s7/tests.rs b/plc4rs/src/protocols/s7/tests.rs new file mode 100644 index 00000000000..8679db1da39 --- /dev/null +++ b/plc4rs/src/protocols/s7/tests.rs @@ -0,0 +1,76 @@ +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tpkt_packet_parse() { + let input = &[ + 0x03, 0x00, 0x00, 0x1A, // TPKT header + 0x02, 0xF0, 0x80, // COTP header + 0x32, 0x01, 0x00, 0x00, // S7 header start + 0x00, 0x01, 0x00, 0x08, + 0x00, 0x00, // S7 header end + // ... payload ... + ]; + + let (remaining, packet) = TPKTPacket::parse(input).unwrap(); + assert_eq!(packet.protocol_id, 0x03); + assert_eq!(packet.length, 26); + assert!(remaining.is_empty()); + } + + #[test] + fn test_s7_message_parse() { + // Add S7 message parsing test + } + + #[test] + fn test_connection_request_parse() { + let input = &[ + 0x00, 0x0C, // Destination Reference + 0x00, 0x10, // Source Reference + 0x00, // Class + 0xC1, 0x02, 0x01, 0x00, // Source TSAP + 0xC2, 0x02, 0x01, 0x02 // Destination TSAP + ]; + + let (remaining, request) = COTPConnectionRequest::parse(input).unwrap(); + assert_eq!(request.dst_ref, 12); + assert_eq!(request.src_ref, 16); + assert_eq!(request.class, 0); + assert!(remaining.is_empty()); + } + + #[test] + fn test_s7_payload_parse() { + let input = &[ + 0x00, // Return code (Success) + 0x04, // Transport size (Word) + 0x00, 0x02, // Length + 0x12, 0x34 // Data + ]; + + let (remaining, payload) = S7Payload::parse(input, 6).unwrap(); + assert_eq!(payload.items.len(), 1); + assert_eq!(payload.items[0].data, vec![0x12, 0x34]); + assert!(remaining.is_empty()); + } + + #[test] + fn test_parameter_item_parse() { + let input = &[ + 0x02, // VarSpec (Byte) + 0x0A, // Length + 0x10, // Syntax ID (S7Any) + 0x02, // Transport size + 0x00, 0x01, // DB number + 0x84, // Area (DataBlocks) + 0x00, 0x00, 0x00, 0x00 // Start address + ]; + + let (remaining, item) = ParameterItem::parse(input).unwrap(); + assert_eq!(item.addr_length, 10); + assert_eq!(item.db_number, 1); + assert!(remaining.is_empty()); + } +} From 629e0247fe512e6f9737ec4826f1a51c8a19d0d4 Mon Sep 17 00:00:00 2001 From: jsxs Date: Wed, 19 Feb 2025 11:02:01 +0900 Subject: [PATCH 6/9] chore: Add initial dependencies --- plc4x-rust/Cargo.lock | 469 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 plc4x-rust/Cargo.lock diff --git a/plc4x-rust/Cargo.lock b/plc4x-rust/Cargo.lock new file mode 100644 index 00000000000..cf0a75cb3ee --- /dev/null +++ b/plc4x-rust/Cargo.lock @@ -0,0 +1,469 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "plc4x-rust" +version = "0.1.0" +dependencies = [ + "bytes", + "nom", + "thiserror", + "tokio", + "tokio-test", + "tracing", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" From c6bf64f5f89e9a2ab93e7e44ae8c763afc123c34 Mon Sep 17 00:00:00 2001 From: jsxs Date: Thu, 20 Feb 2025 13:30:08 +0900 Subject: [PATCH 7/9] feat(plc4rs): Add Transport trait with TCP impl. Add initial SPI layer for PLC4X Rust with: - Transport trait with retry and logging support - TCP transport implementation with configuration - Basic error handling using thiserror - Example showing TCP transport usage This provides the foundation for protocol-specific implementations. --- plc4rs/examples/transport_usage.rs | 34 +++++++ plc4rs/src/spi/config.rs | 27 ++++++ plc4rs/src/spi/error.rs | 17 ++++ plc4rs/src/spi/mod.rs | 141 +++++++++++++++++++++++++++++ plc4rs/src/spi/tcp.rs | 89 ++++++++++++++++++ 5 files changed, 308 insertions(+) create mode 100644 plc4rs/examples/transport_usage.rs create mode 100644 plc4rs/src/spi/config.rs create mode 100644 plc4rs/src/spi/error.rs create mode 100644 plc4rs/src/spi/mod.rs create mode 100644 plc4rs/src/spi/tcp.rs diff --git a/plc4rs/examples/transport_usage.rs b/plc4rs/examples/transport_usage.rs new file mode 100644 index 00000000000..5937c72389f --- /dev/null +++ b/plc4rs/examples/transport_usage.rs @@ -0,0 +1,34 @@ +use plc4rs::spi::{ + TcpTransport, + config::{TransportConfig, TcpConfig}, +}; +use std::time::Duration; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // TCP Example + let tcp_config = TcpConfig { + base: TransportConfig { + connect_timeout: Duration::from_secs(10), + read_timeout: Duration::from_secs(2), + write_timeout: Duration::from_secs(2), + buffer_size: 1024, + }, + no_delay: true, + keep_alive: true, + }; + + let mut tcp = TcpTransport::new_with_config("192.168.1.1".into(), 102, tcp_config); + tcp.connect().await?; + + let data = b"Hello PLC"; + tcp.write(data).await?; + + let mut buffer = vec![0u8; 1024]; + let len = tcp.read(&mut buffer).await?; + println!("Received: {:?}", &buffer[..len]); + + tcp.close().await?; + + Ok(()) +} diff --git a/plc4rs/src/spi/config.rs b/plc4rs/src/spi/config.rs new file mode 100644 index 00000000000..6cff36368e9 --- /dev/null +++ b/plc4rs/src/spi/config.rs @@ -0,0 +1,27 @@ +use std::time::Duration; + +#[derive(Debug, Clone)] +pub struct TransportConfig { + pub connect_timeout: Duration, + pub read_timeout: Duration, + pub write_timeout: Duration, + pub buffer_size: usize, +} + +impl Default for TransportConfig { + fn default() -> Self { + TransportConfig { + connect_timeout: Duration::from_secs(5), + read_timeout: Duration::from_secs(1), + write_timeout: Duration::from_secs(1), + buffer_size: 8192, + } + } +} + +#[derive(Debug, Clone)] +pub struct TcpConfig { + pub base: TransportConfig, + pub no_delay: bool, + pub keep_alive: bool, +} diff --git a/plc4rs/src/spi/error.rs b/plc4rs/src/spi/error.rs new file mode 100644 index 00000000000..83598e7bf6f --- /dev/null +++ b/plc4rs/src/spi/error.rs @@ -0,0 +1,17 @@ +use thiserror::Error; +use std::io; + +#[derive(Error, Debug)] +pub enum TransportError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + + #[error("Connection error: {0}")] + Connection(String), + + #[error("Not connected")] + NotConnected, + + #[error("Already connected")] + AlreadyConnected, +} diff --git a/plc4rs/src/spi/mod.rs b/plc4rs/src/spi/mod.rs new file mode 100644 index 00000000000..90e8aeed7a8 --- /dev/null +++ b/plc4rs/src/spi/mod.rs @@ -0,0 +1,141 @@ +//! Service Provider Interface (SPI) for PLC4X +//! +//! This module provides the core abstractions for implementing different transport +//! mechanisms in PLC4X. The main trait is `Transport` which defines the basic +//! operations that any transport implementation must provide. +//! +//! # Transport Types +//! +//! Currently implemented: +//! - TCP: For TCP/IP based protocols +//! +//! # Example +//! ```rust +//! use plc4rs::spi::{Transport, TcpTransport}; +//! +//! async fn example() { +//! let mut transport = TcpTransport::new("192.168.1.1".to_string(), 102); +//! transport.connect().await.unwrap(); +//! // ... use transport ... +//! transport.close().await.unwrap(); +//! } +//! ``` + +use std::fmt::Debug; +use tracing::{debug, error, info, warn}; + +/// Retry configuration for transport operations +#[derive(Debug, Clone)] +pub struct RetryConfig { + /// Maximum number of retry attempts + pub max_attempts: u32, + /// Delay between retry attempts + pub retry_delay: std::time::Duration, + /// Whether to use exponential backoff + pub use_backoff: bool, +} + +impl Default for RetryConfig { + fn default() -> Self { + Self { + max_attempts: 3, + retry_delay: std::time::Duration::from_millis(100), + use_backoff: true, + } + } +} + +/// Core trait for implementing transport mechanisms +#[async_trait::async_trait] +pub trait Transport: Send + Sync { + /// Establishes a connection to the target device + async fn connect(&mut self) -> Result<(), TransportError> { + self.connect_with_retry(RetryConfig::default()).await + } + + /// Connects with retry logic + async fn connect_with_retry(&mut self, retry_config: RetryConfig) -> Result<(), TransportError> { + let mut attempt = 0; + let mut delay = retry_config.retry_delay; + + loop { + attempt += 1; + match self.connect_internal().await { + Ok(()) => { + info!("Connection established on attempt {}", attempt); + return Ok(()); + } + Err(e) => { + if attempt >= retry_config.max_attempts { + error!("Connection failed after {} attempts: {}", attempt, e); + return Err(e); + } + warn!("Connection attempt {} failed: {}", attempt, e); + tokio::time::sleep(delay).await; + if retry_config.use_backoff { + delay *= 2; + } + } + } + } + } + + /// Internal connect implementation + #[doc(hidden)] + async fn connect_internal(&mut self) -> Result<(), TransportError>; + + /// Reads data with logging + async fn read(&mut self, buffer: &mut [u8]) -> Result { + debug!("Attempting to read {} bytes", buffer.len()); + match self.read_internal(buffer).await { + Ok(n) => { + debug!("Successfully read {} bytes", n); + Ok(n) + } + Err(e) => { + error!("Read error: {}", e); + Err(e) + } + } + } + + /// Internal read implementation + #[doc(hidden)] + async fn read_internal(&mut self, buffer: &mut [u8]) -> Result; + + /// Writes data with logging + async fn write(&mut self, data: &[u8]) -> Result { + debug!("Attempting to write {} bytes", data.len()); + match self.write_internal(data).await { + Ok(n) => { + debug!("Successfully wrote {} bytes", n); + Ok(n) + } + Err(e) => { + error!("Write error: {}", e); + Err(e) + } + } + } + + /// Internal write implementation + #[doc(hidden)] + async fn write_internal(&mut self, data: &[u8]) -> Result; + + /// Closes the connection with logging + async fn close(&mut self) -> Result<(), TransportError> { + info!("Closing connection"); + self.close_internal().await + } + + /// Internal close implementation + #[doc(hidden)] + async fn close_internal(&mut self) -> Result<(), TransportError>; +} + +// Implement transport types +pub mod tcp; +pub mod error; + +pub use error::TransportError; +pub use tcp::TcpTransport; diff --git a/plc4rs/src/spi/tcp.rs b/plc4rs/src/spi/tcp.rs new file mode 100644 index 00000000000..4274b63ba2d --- /dev/null +++ b/plc4rs/src/spi/tcp.rs @@ -0,0 +1,89 @@ +use tokio::net::TcpStream; +use crate::spi::{Transport, TransportError}; +use crate::spi::config::TcpConfig; +use std::io; + +pub struct TcpTransport { + stream: Option, + address: String, + port: u16, + config: TcpConfig, +} + +impl TcpTransport { + pub fn new(address: String, port: u16) -> Self { + Self::new_with_config(address, port, TcpConfig { + base: Default::default(), + no_delay: true, + keep_alive: true, + }) + } + + pub fn new_with_config(address: String, port: u16, config: TcpConfig) -> Self { + TcpTransport { + stream: None, + address, + port, + config, + } + } +} + +impl Transport for TcpTransport { + async fn connect_internal(&mut self) -> Result<(), TransportError> { + if self.stream.is_some() { + return Err(TransportError::AlreadyConnected); + } + + let addr = format!("{}:{}", self.address, self.port); + let stream = TcpStream::connect(addr).await?; + + // Apply TCP-specific settings + stream.set_nodelay(self.config.no_delay)?; + stream.set_keepalive(self.config.keep_alive.then_some(self.config.base.connect_timeout))?; + + self.stream = Some(stream); + Ok(()) + } + + async fn read(&mut self, buffer: &mut [u8]) -> Result { + let stream = self.stream.as_mut() + .ok_or(TransportError::NotConnected)?; + + use tokio::io::AsyncReadExt; + Ok(stream.read(buffer).await?) + } + + async fn write(&mut self, data: &[u8]) -> Result { + let stream = self.stream.as_mut() + .ok_or(TransportError::NotConnected)?; + + use tokio::io::AsyncWriteExt; + Ok(stream.write(data).await?) + } + + async fn close(&mut self) -> Result<(), TransportError> { + if let Some(stream) = self.stream.take() { + use tokio::io::AsyncWriteExt; + stream.shutdown().await?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio_test::block_on; + + #[test] + fn test_tcp_transport() { + let mut transport = TcpTransport::new("127.0.0.1".to_string(), 102); + + // Test connection + block_on(async { + assert!(transport.connect().await.is_err()); // Should fail as no server is running + assert!(transport.stream.is_none()); + }); + } +} From 8a0ef33b104e7cf5090fa0224bc92ee0df8d5578 Mon Sep 17 00:00:00 2001 From: jsxs Date: Fri, 21 Feb 2025 11:01:01 +0900 Subject: [PATCH 8/9] feat(plc4rs): Add UDP transport implementation - Implement UDP transport following TCP pattern - Add configuration support for UDP - Update example to show both TCP and UDP usage - Add basic UDP transport tests This builds on the TCP transport to provide UDP support for the PLC4X Rust implementation. --- plc4rs/examples/transport_usage.rs | 21 ++++++- plc4rs/src/spi/mod.rs | 2 + plc4rs/src/spi/udp.rs | 91 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 plc4rs/src/spi/udp.rs diff --git a/plc4rs/examples/transport_usage.rs b/plc4rs/examples/transport_usage.rs index 5937c72389f..fc8b69ad805 100644 --- a/plc4rs/examples/transport_usage.rs +++ b/plc4rs/examples/transport_usage.rs @@ -1,5 +1,5 @@ use plc4rs::spi::{ - TcpTransport, + TcpTransport, UdpTransport, config::{TransportConfig, TcpConfig}, }; use std::time::Duration; @@ -26,9 +26,26 @@ async fn main() -> Result<(), Box> { let mut buffer = vec![0u8; 1024]; let len = tcp.read(&mut buffer).await?; - println!("Received: {:?}", &buffer[..len]); + println!("TCP Received: {:?}", &buffer[..len]); tcp.close().await?; + + // UDP Example + let udp_config = TransportConfig { + connect_timeout: Duration::from_secs(5), + read_timeout: Duration::from_secs(1), + write_timeout: Duration::from_secs(1), + buffer_size: 1024, + }; + + let mut udp = UdpTransport::new_with_config("192.168.1.1".into(), 102, udp_config); + udp.connect().await?; + + udp.write(data).await?; + let len = udp.read(&mut buffer).await?; + println!("UDP Received: {:?}", &buffer[..len]); + + udp.close().await?; Ok(()) } diff --git a/plc4rs/src/spi/mod.rs b/plc4rs/src/spi/mod.rs index 90e8aeed7a8..1b7288112b8 100644 --- a/plc4rs/src/spi/mod.rs +++ b/plc4rs/src/spi/mod.rs @@ -135,7 +135,9 @@ pub trait Transport: Send + Sync { // Implement transport types pub mod tcp; +pub mod udp; pub mod error; pub use error::TransportError; pub use tcp::TcpTransport; +pub use udp::UdpTransport; diff --git a/plc4rs/src/spi/udp.rs b/plc4rs/src/spi/udp.rs new file mode 100644 index 00000000000..08bf97e97be --- /dev/null +++ b/plc4rs/src/spi/udp.rs @@ -0,0 +1,91 @@ +use tokio::net::UdpSocket; +use crate::spi::{Transport, TransportError}; +use crate::spi::config::TransportConfig; +use std::io; + +pub struct UdpTransport { + socket: Option, + address: String, + port: u16, + config: TransportConfig, + buffer: Vec, +} + +impl UdpTransport { + pub fn new(address: String, port: u16) -> Self { + Self::new_with_config(address, port, TransportConfig::default()) + } + + pub fn new_with_config(address: String, port: u16, config: TransportConfig) -> Self { + UdpTransport { + socket: None, + address, + port, + config, + buffer: vec![0; config.buffer_size], + } + } +} + +#[async_trait::async_trait] +impl Transport for UdpTransport { + async fn connect_internal(&mut self) -> Result<(), TransportError> { + if self.socket.is_some() { + return Err(TransportError::AlreadyConnected); + } + + let socket = UdpSocket::bind("0.0.0.0:0").await?; + let addr = format!("{}:{}", self.address, self.port); + socket.connect(&addr).await?; + + self.socket = Some(socket); + Ok(()) + } + + async fn read_internal(&mut self, buffer: &mut [u8]) -> Result { + let socket = self.socket.as_ref() + .ok_or(TransportError::NotConnected)?; + + let len = socket.recv(buffer).await?; + Ok(len) + } + + async fn write_internal(&mut self, data: &[u8]) -> Result { + let socket = self.socket.as_ref() + .ok_or(TransportError::NotConnected)?; + + let len = socket.send(data).await?; + Ok(len) + } + + async fn close_internal(&mut self) -> Result<(), TransportError> { + self.socket = None; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio_test::block_on; + + #[test] + fn test_udp_lifecycle() { + let mut transport = UdpTransport::new("127.0.0.1".to_string(), 1234); + + block_on(async { + // Test connection + assert!(transport.connect().await.is_ok()); + assert!(transport.socket.is_some()); + + // Test write/read + let data = b"test data"; + let result = transport.write(data).await; + assert!(result.is_ok() || result.is_err()); // May fail as no server + + // Test close + assert!(transport.close().await.is_ok()); + assert!(transport.socket.is_none()); + }); + } +} From 4f8e5ee4fa7da79346a7f18d0547ed8f86589db9 Mon Sep 17 00:00:00 2001 From: jsxs Date: Fri, 7 Mar 2025 15:07:21 +0900 Subject: [PATCH 9/9] fix: Implement PR feedback for Rust module - Rename directory to plc4rs - Add Apache license headers to all files - Fix code issues and Maven integration - Remove plc4x-rust directory --- .github/workflows/ci.yml | 16 + plc4rs/CONTRIBUTING.md | 18 + {plc4x-rust => plc4rs}/Cargo.lock | 153 +++++- plc4rs/Cargo.toml | 40 ++ plc4rs/README.md | 52 ++ plc4rs/examples/transport_usage.rs | 19 + plc4rs/fuzz/Cargo.toml | 16 + plc4rs/fuzz/fuzz_targets/header_parser.rs | 19 + plc4rs/pom.xml | 59 +++ plc4rs/src/lib.rs | 27 ++ plc4rs/src/license_header.txt | 18 + plc4rs/src/protocols/mod.rs | 20 + plc4rs/src/protocols/s7/connection.rs | 88 +++- plc4rs/src/protocols/s7/error.rs | 48 +- plc4rs/src/protocols/s7/mod.rs | 445 ++++++++++++------ plc4rs/src/protocols/s7/tests.rs | 51 +- plc4rs/src/s7.rs | 40 ++ plc4rs/src/spi/config.rs | 176 ++++++- plc4rs/src/spi/error.rs | 29 +- plc4rs/src/spi/mod.rs | 173 ++----- plc4rs/src/spi/tcp.rs | 172 +++++-- plc4rs/src/spi/udp.rs | 151 ++++-- plc4rs/src/types.rs | 29 ++ plc4x-rust/CONTRIBUTING.md | 43 -- plc4x-rust/Cargo.toml | 22 - plc4x-rust/README.md | 54 --- plc4x-rust/fuzz/Cargo.toml | 18 - plc4x-rust/fuzz/fuzz_targets/header_parser.rs | 7 - plc4x-rust/src/error.rs | 13 - plc4x-rust/src/lib.rs | 9 - plc4x-rust/src/s7.rs | 175 ------- plc4x-rust/src/types.rs | 37 -- pom.xml | 7 + 33 files changed, 1415 insertions(+), 829 deletions(-) create mode 100644 plc4rs/CONTRIBUTING.md rename {plc4x-rust => plc4rs}/Cargo.lock (74%) create mode 100644 plc4rs/Cargo.toml create mode 100644 plc4rs/README.md create mode 100644 plc4rs/fuzz/Cargo.toml create mode 100644 plc4rs/fuzz/fuzz_targets/header_parser.rs create mode 100644 plc4rs/pom.xml create mode 100644 plc4rs/src/lib.rs create mode 100644 plc4rs/src/license_header.txt create mode 100644 plc4rs/src/protocols/mod.rs create mode 100644 plc4rs/src/s7.rs create mode 100644 plc4rs/src/types.rs delete mode 100644 plc4x-rust/CONTRIBUTING.md delete mode 100644 plc4x-rust/Cargo.toml delete mode 100644 plc4x-rust/README.md delete mode 100644 plc4x-rust/fuzz/Cargo.toml delete mode 100644 plc4x-rust/fuzz/fuzz_targets/header_parser.rs delete mode 100644 plc4x-rust/src/error.rs delete mode 100644 plc4x-rust/src/lib.rs delete mode 100644 plc4x-rust/src/s7.rs delete mode 100644 plc4x-rust/src/types.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8d2df1663a..2a76e3d1987 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. name: Industrial CI on: [push, pull_request] diff --git a/plc4rs/CONTRIBUTING.md b/plc4rs/CONTRIBUTING.md new file mode 100644 index 00000000000..e8329c297ed --- /dev/null +++ b/plc4rs/CONTRIBUTING.md @@ -0,0 +1,18 @@ + diff --git a/plc4x-rust/Cargo.lock b/plc4rs/Cargo.lock similarity index 74% rename from plc4x-rust/Cargo.lock rename to plc4rs/Cargo.lock index cf0a75cb3ee..f668bc1bff9 100644 --- a/plc4x-rust/Cargo.lock +++ b/plc4rs/Cargo.lock @@ -39,6 +39,17 @@ dependencies = [ "syn", ] +[[package]] +name = "async-trait" +version = "0.1.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -62,15 +73,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cfg-if" @@ -90,11 +101,17 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" -version = "0.2.169" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "lock_api" @@ -106,6 +123,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + [[package]] name = "memchr" version = "2.7.4" @@ -120,9 +143,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] @@ -148,6 +171,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "object" version = "0.36.7" @@ -163,6 +196,12 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.3" @@ -193,40 +232,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] -name = "plc4x-rust" +name = "plc4rs" version = "0.1.0" dependencies = [ + "async-trait", "bytes", "nom", + "socket2", "thiserror", "tokio", "tokio-test", "tracing", + "tracing-subscriber", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ "bitflags", ] @@ -243,6 +285,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -270,9 +321,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" dependencies = [ "proc-macro2", "quote", @@ -299,6 +350,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tokio" version = "1.43.0" @@ -381,13 +442,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "valuable" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "wasi" @@ -395,6 +488,28 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/plc4rs/Cargo.toml b/plc4rs/Cargo.toml new file mode 100644 index 00000000000..7a6ac4c2957 --- /dev/null +++ b/plc4rs/Cargo.toml @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[package] +name = "plc4rs" +version = "0.1.0" +edition = "2021" +authors = ["Apache PLC4X Team "] +description = "Rust implementation of the Apache PLC4X project" +license = "Apache-2.0" +repository = "https://github.com/apache/plc4x" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.43.0", features = ["full"] } +thiserror = "1.0.69" +tracing = "0.1.41" +bytes = "1.10.0" +nom = "7.1.3" +async-trait = "0.1.77" +socket2 = "0.5.8" + +[dev-dependencies] +tokio-test = "0.4.3" +tracing-subscriber = "0.3.18" diff --git a/plc4rs/README.md b/plc4rs/README.md new file mode 100644 index 00000000000..73f709fac3f --- /dev/null +++ b/plc4rs/README.md @@ -0,0 +1,52 @@ +# PLC4RS - Apache PLC4X for Rust + +This is the Rust implementation of the Apache PLC4X project, providing a library for industrial communication with PLCs. + +## Features + +- Transport layer with TCP and UDP implementations +- S7 protocol implementation +- Async I/O with Tokio +- Memory-safe implementation with no unsafe code + +## Usage + +See the examples directory for usage examples. + +## Building + +### Prerequisites + +- Rust 1.75.0 or newer +- Cargo + +### Building with Cargo + +```bash +cargo build +``` + +### Building with Maven + +```bash +mvn -Pwith-rust clean install +``` + +## License + +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. diff --git a/plc4rs/examples/transport_usage.rs b/plc4rs/examples/transport_usage.rs index fc8b69ad805..253348483c5 100644 --- a/plc4rs/examples/transport_usage.rs +++ b/plc4rs/examples/transport_usage.rs @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + use plc4rs::spi::{ TcpTransport, UdpTransport, config::{TransportConfig, TcpConfig}, diff --git a/plc4rs/fuzz/Cargo.toml b/plc4rs/fuzz/Cargo.toml new file mode 100644 index 00000000000..8f596af951a --- /dev/null +++ b/plc4rs/fuzz/Cargo.toml @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/plc4rs/fuzz/fuzz_targets/header_parser.rs b/plc4rs/fuzz/fuzz_targets/header_parser.rs new file mode 100644 index 00000000000..bd244d07ab4 --- /dev/null +++ b/plc4rs/fuzz/fuzz_targets/header_parser.rs @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + diff --git a/plc4rs/pom.xml b/plc4rs/pom.xml new file mode 100644 index 00000000000..65139e06b7e --- /dev/null +++ b/plc4rs/pom.xml @@ -0,0 +1,59 @@ + + + + + 4.0.0 + + + org.apache.plc4x + plc4x-parent + 0.13.0-SNAPSHOT + + + plc4rs + pom + + PLC4Rust + Implementation of the protocol adapters for usage as Rust module. + + + + + + org.questdb + rust-maven-plugin + 1.2.0 + + + rust-build-id + + build + + + ${project.basedir} + true + + + + + + + + diff --git a/plc4rs/src/lib.rs b/plc4rs/src/lib.rs new file mode 100644 index 00000000000..21f6a974254 --- /dev/null +++ b/plc4rs/src/lib.rs @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Export modules +pub mod protocols; +pub mod s7; +pub mod spi; +pub mod types; + +// Re-export commonly used types +pub use crate::spi::Transport; diff --git a/plc4rs/src/license_header.txt b/plc4rs/src/license_header.txt new file mode 100644 index 00000000000..7149be379a7 --- /dev/null +++ b/plc4rs/src/license_header.txt @@ -0,0 +1,18 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ diff --git a/plc4rs/src/protocols/mod.rs b/plc4rs/src/protocols/mod.rs new file mode 100644 index 00000000000..93ef908f511 --- /dev/null +++ b/plc4rs/src/protocols/mod.rs @@ -0,0 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +pub mod s7; diff --git a/plc4rs/src/protocols/s7/connection.rs b/plc4rs/src/protocols/s7/connection.rs index fc2d8b24257..5d8d8f31c9a 100644 --- a/plc4rs/src/protocols/s7/connection.rs +++ b/plc4rs/src/protocols/s7/connection.rs @@ -1,3 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use nom::number::complete::{be_u16, be_u8}; +use nom::IResult; + #[derive(Debug, Clone)] pub struct COTPConnectionRequest { dst_ref: u16, @@ -6,6 +28,13 @@ pub struct COTPConnectionRequest { parameters: Vec, } +#[derive(Debug, Clone)] +pub enum ConnectionParameter { + TpduSize(u8), + SrcTsap(u16), + DstTsap(u16), +} + #[derive(Debug, Clone)] pub struct COTPConnectionResponse { dst_ref: u16, @@ -14,26 +43,41 @@ pub struct COTPConnectionResponse { parameters: Vec, } -#[derive(Debug, Clone)] -pub enum ConnectionParameter { - TpduSize(u8), - SrcTsap(u16), - DstTsap(u16), +impl COTPConnectionRequest { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, dst_ref) = be_u16(input)?; + let (input, src_ref) = be_u16(input)?; + let (input, class) = be_u8(input)?; + let (input, parameters) = ConnectionParameter::parse_all(input)?; + + Ok(( + input, + COTPConnectionRequest { + dst_ref, + src_ref, + class, + parameters, + }, + )) + } } -impl COTPConnectionRequest { +impl COTPConnectionResponse { pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { let (input, dst_ref) = be_u16(input)?; let (input, src_ref) = be_u16(input)?; let (input, class) = be_u8(input)?; let (input, parameters) = ConnectionParameter::parse_all(input)?; - - Ok((input, COTPConnectionRequest { - dst_ref, - src_ref, - class, - parameters, - })) + + Ok(( + input, + COTPConnectionResponse { + dst_ref, + src_ref, + class, + parameters, + }, + )) } } @@ -41,37 +85,37 @@ impl ConnectionParameter { fn parse_all(input: &[u8]) -> IResult<&[u8], Vec> { let mut parameters = Vec::new(); let mut remaining = input; - + while !remaining.is_empty() { let (input, param) = Self::parse(remaining)?; parameters.push(param); remaining = input; } - + Ok((remaining, parameters)) } - + fn parse(input: &[u8]) -> IResult<&[u8], Self> { let (input, param_code) = be_u8(input)?; let (input, param_length) = be_u8(input)?; - + match param_code { 0xC0 => { let (input, size) = be_u8(input)?; Ok((input, ConnectionParameter::TpduSize(size))) - }, + } 0xC1 => { let (input, tsap) = be_u16(input)?; Ok((input, ConnectionParameter::SrcTsap(tsap))) - }, + } 0xC2 => { let (input, tsap) = be_u16(input)?; Ok((input, ConnectionParameter::DstTsap(tsap))) - }, + } _ => Err(nom::Err::Error(nom::error::Error::new( input, - nom::error::ErrorKind::Tag + nom::error::ErrorKind::Tag, ))), } } -} +} diff --git a/plc4rs/src/protocols/s7/error.rs b/plc4rs/src/protocols/s7/error.rs index afc86029ac8..5770c698d40 100644 --- a/plc4rs/src/protocols/s7/error.rs +++ b/plc4rs/src/protocols/s7/error.rs @@ -1,25 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + use thiserror::Error; #[derive(Error, Debug)] pub enum S7Error { #[error("Invalid message type: {0:#04x}")] InvalidMessageType(u8), - + #[error("Invalid function code: {0:#04x}")] InvalidFunctionCode(u8), - + #[error("Invalid parameter type: {0:#04x}")] InvalidParameterType(u8), - + #[error("Invalid protocol ID: expected 0x32, got {0:#04x}")] InvalidProtocolId(u8), - + #[error("Invalid TPKT version: expected 0x03, got {0:#04x}")] InvalidTpktVersion(u8), - + #[error("Invalid length: {0}")] InvalidLength(String), - + #[error("Parse error: {0}")] ParseError(String), -} + + #[error("Invalid variable specification: {0:#04x}")] + InvalidVarSpec(u8), + + #[error("Invalid syntax ID: {0:#04x}")] + InvalidSyntaxId(u8), + + #[error("Invalid area: {0:#04x}")] + InvalidArea(u8), + + #[error("Invalid return code: {0:#04x}")] + InvalidReturnCode(u8), + + #[error("Invalid transport size: {0:#04x}")] + InvalidTransportSize(u8), +} diff --git a/plc4rs/src/protocols/s7/mod.rs b/plc4rs/src/protocols/s7/mod.rs index aca54b88642..42561337a9c 100644 --- a/plc4rs/src/protocols/s7/mod.rs +++ b/plc4rs/src/protocols/s7/mod.rs @@ -1,14 +1,178 @@ -use nom::{ - bytes::complete::take, - number::complete::{be_u8, be_u16, be_u32}, - IResult, -}; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +pub mod connection; +pub mod error; + +use crate::protocols::s7::connection::{COTPConnectionRequest, COTPConnectionResponse}; +use crate::protocols::s7::error::S7Error; +use nom::bytes::complete::take; +use nom::number::complete::{be_u16, be_u32, be_u8}; +use nom::IResult; +use std::convert::TryFrom; + +/// S7 protocol constants +pub const PROTOCOL_ID: u8 = 0x32; +pub const DEFAULT_PDU_SIZE: u16 = 1024; +pub const DEFAULT_MAX_AMQS_CONS: u16 = 8; +pub const DEFAULT_MAX_AMQS_CALLING: u16 = 8; + +/// Message types for S7 protocol +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MessageType { + JobRequest = 0x01, + Ack = 0x02, + AckData = 0x03, + UserData = 0x07, +} + +impl TryFrom for MessageType { + type Error = S7Error; + + fn try_from(value: u8) -> Result { + match value { + 0x01 => Ok(MessageType::JobRequest), + 0x02 => Ok(MessageType::Ack), + 0x03 => Ok(MessageType::AckData), + 0x07 => Ok(MessageType::UserData), + _ => Err(S7Error::InvalidMessageType(value)), + } + } +} + +impl MessageType { + /// Parse a message type from a byte + pub fn from_byte(byte: u8) -> Result { + match byte { + 0x01 => Ok(MessageType::JobRequest), + 0x02 => Ok(MessageType::Ack), + 0x03 => Ok(MessageType::AckData), + 0x07 => Ok(MessageType::UserData), + _ => Err(error::S7Error::InvalidMessageType(byte)), + } + } + + /// Parse a message type from a byte slice + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, byte) = be_u8(input)?; + match Self::from_byte(byte) { + Ok(message_type) => Ok((input, message_type)), + Err(_) => Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag, + ))), + } + } +} + +/// Function codes for S7 protocol +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FunctionCode { + Setup = 0xF0, + ReadVar = 0x04, + WriteVar = 0x05, + RequestDownload = 0x1A, + DownloadBlock = 0x1B, + DownloadEnded = 0x1C, + StartUpload = 0x1D, + Upload = 0x1E, + EndUpload = 0x1F, + PlcControl = 0x28, + PlcStop = 0x29, +} + +impl FunctionCode { + /// Parse a function code from a byte + pub fn from_byte(byte: u8) -> Result { + match byte { + 0xF0 => Ok(FunctionCode::Setup), + 0x04 => Ok(FunctionCode::ReadVar), + 0x05 => Ok(FunctionCode::WriteVar), + 0x1A => Ok(FunctionCode::RequestDownload), + 0x1B => Ok(FunctionCode::DownloadBlock), + 0x1C => Ok(FunctionCode::DownloadEnded), + 0x1D => Ok(FunctionCode::StartUpload), + 0x1E => Ok(FunctionCode::Upload), + 0x1F => Ok(FunctionCode::EndUpload), + 0x28 => Ok(FunctionCode::PlcControl), + 0x29 => Ok(FunctionCode::PlcStop), + _ => Err(error::S7Error::InvalidFunctionCode(byte)), + } + } + + /// Parse a function code from a byte slice + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, byte) = be_u8(input)?; + match Self::from_byte(byte) { + Ok(function_code) => Ok((input, function_code)), + Err(_) => Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag, + ))), + } + } +} + +/// S7 message header +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct S7Header { + protocol_id: u8, + message_type: MessageType, + reserved: u16, + pdu_reference: u16, + parameter_length: u16, + data_length: u16, +} + +impl S7Header { + pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { + let (input, protocol_id) = be_u8(input)?; + let (input, msg_type) = be_u8(input)?; + let (input, reserved) = be_u16(input)?; + let (input, pdu_reference) = be_u16(input)?; + let (input, parameter_length) = be_u16(input)?; + let (input, data_length) = be_u16(input)?; + + let message_type = MessageType::try_from(msg_type).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)) + })?; + + Ok(( + input, + S7Header { + protocol_id, + message_type, + reserved, + pdu_reference, + parameter_length, + data_length, + }, + )) + } +} #[derive(Debug, Clone)] pub struct TPKTPacket { - protocol_id: u8, // Always 0x03 - reserved: u8, // Always 0x00 - length: u16, // Total length including header + protocol_id: u8, // Always 0x03 + reserved: u8, // Always 0x00 + length: u16, // Total length including header payload: COTPPacket, } @@ -19,12 +183,15 @@ impl TPKTPacket { let (input, length) = be_u16(input)?; let (input, payload) = COTPPacket::parse(input)?; - Ok((input, TPKTPacket { - protocol_id, - reserved, - length, - payload, - })) + Ok(( + input, + TPKTPacket { + protocol_id, + reserved, + length, + payload, + }, + )) } } @@ -39,23 +206,23 @@ impl COTPPacket { pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { let (input, header_length) = be_u8(input)?; let (input, tpdu_code) = be_u8(input)?; - + match tpdu_code { 0xF0 => { let (input, packet) = COTPDataPacket::parse(input)?; Ok((input, COTPPacket::Data(packet))) - }, + } 0xE0 => { let (input, packet) = COTPConnectionRequest::parse(input)?; - Ok((input, COTPPacket::ConnectionRequest(packet))) - }, + Ok((input, COTPPacket::ConnectionRequest(packet))) + } 0xD0 => { let (input, packet) = COTPConnectionResponse::parse(input)?; Ok((input, COTPPacket::ConnectionResponse(packet))) - }, + } _ => Err(nom::Err::Error(nom::error::Error::new( input, - nom::error::ErrorKind::Tag + nom::error::ErrorKind::Tag, ))), } } @@ -63,8 +230,8 @@ impl COTPPacket { #[derive(Debug, Clone)] pub struct COTPDataPacket { - eot: bool, // End of transmission - tpdu_ref: u8, // Reference number + eot: bool, // End of transmission + tpdu_ref: u8, // Reference number data: S7Message, // S7 protocol data } @@ -74,11 +241,14 @@ impl COTPDataPacket { let (input, tpdu_ref) = be_u8(input)?; let (input, data) = S7Message::parse(input)?; - Ok((input, COTPDataPacket { - eot: (flags & 0x80) != 0, - tpdu_ref, - data, - })) + Ok(( + input, + COTPDataPacket { + eot: (flags & 0x80) != 0, + tpdu_ref, + data, + }, + )) } } @@ -105,78 +275,17 @@ impl S7Message { (input, None) }; - Ok((input, S7Message { - header, - parameters, - payload, - })) - } -} - -#[derive(Debug, Clone, Copy)] -pub enum MessageType { - JobRequest = 0x01, - Ack = 0x02, - AckData = 0x03, - UserData = 0x07, -} - -impl TryFrom for MessageType { - type Error = S7Error; - - fn try_from(value: u8) -> Result { - match value { - 0x01 => Ok(MessageType::JobRequest), - 0x02 => Ok(MessageType::Ack), - 0x03 => Ok(MessageType::AckData), - 0x07 => Ok(MessageType::UserData), - _ => Err(S7Error::InvalidMessageType(value)), - } - } -} - -#[derive(Debug, Clone)] -pub struct S7Header { - protocol_id: u8, // Always 0x32 - message_type: MessageType, - reserved: u16, // Always 0x0000 - pdu_reference: u16, - parameter_length: u16, - data_length: u16, -} - -impl S7Header { - pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { - let (input, protocol_id) = be_u8(input)?; - let (input, msg_type) = be_u8(input)?; - let (input, reserved) = be_u16(input)?; - let (input, pdu_reference) = be_u16(input)?; - let (input, parameter_length) = be_u16(input)?; - let (input, data_length) = be_u16(input)?; - - let message_type = MessageType::try_from(msg_type) - .map_err(|_| nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Tag - )))?; - - Ok((input, S7Header { - protocol_id, - message_type, - reserved, - pdu_reference, - parameter_length, - data_length, - })) + Ok(( + input, + S7Message { + header, + parameters, + payload, + }, + )) } } -#[derive(Debug, Clone)] -pub struct S7Parameters { - parameter_type: ParameterType, - items: Vec, -} - #[derive(Debug, Clone, Copy)] pub enum ParameterType { Setup = 0xF0, @@ -190,7 +299,7 @@ pub enum ParameterType { impl TryFrom for ParameterType { type Error = S7Error; - + fn try_from(value: u8) -> Result { match value { 0xF0 => Ok(ParameterType::Setup), @@ -204,35 +313,42 @@ impl TryFrom for ParameterType { } } +#[derive(Debug, Clone)] +pub struct S7Parameters { + parameter_type: ParameterType, + items: Vec, +} + impl S7Parameters { pub fn parse(input: &[u8], length: u16) -> IResult<&[u8], Self> { let (input, param_type) = be_u8(input)?; - let parameter_type = ParameterType::try_from(param_type) - .map_err(|_| nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Tag - )))?; + let parameter_type = ParameterType::try_from(param_type).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)) + })?; let (input, items) = match parameter_type { ParameterType::ReadVar | ParameterType::WriteVar => { let (input, item_count) = be_u8(input)?; let mut items = Vec::with_capacity(item_count as usize); let mut remaining = input; - + for _ in 0..item_count { let (input, item) = ParameterItem::parse(remaining)?; items.push(item); remaining = input; } (remaining, items) - }, + } _ => (input, Vec::new()), }; - Ok((input, S7Parameters { - parameter_type, - items, - })) + Ok(( + input, + S7Parameters { + parameter_type, + items, + }, + )) } } @@ -258,7 +374,7 @@ pub enum VarSpec { impl TryFrom for VarSpec { type Error = S7Error; - + fn try_from(value: u8) -> Result { match value { 0x01 => Ok(VarSpec::Bit), @@ -278,7 +394,7 @@ pub enum SyntaxId { impl TryFrom for SyntaxId { type Error = S7Error; - + fn try_from(value: u8) -> Result { match value { 0x10 => Ok(SyntaxId::S7Any), @@ -298,7 +414,7 @@ pub enum Area { impl TryFrom for Area { type Error = S7Error; - + fn try_from(value: u8) -> Result { match value { 0x81 => Ok(Area::Inputs), @@ -312,29 +428,42 @@ impl TryFrom for Area { impl ParameterItem { pub fn parse(input: &[u8]) -> IResult<&[u8], Self> { - let (input, var_spec) = be_u8(input)?; + let (input, var_spec_byte) = be_u8(input)?; let (input, length) = be_u8(input)?; - let (input, syntax_id) = be_u8(input)?; + let (input, syntax_id_byte) = be_u8(input)?; let (input, transport_size) = be_u8(input)?; let (input, db_number) = be_u16(input)?; - let (input, area) = be_u8(input)?; + let (input, area_byte) = be_u8(input)?; let (input, start_addr) = be_u32(input)?; - // ... implement conversion and validation ... - - Ok((input, ParameterItem { - var_spec: VarSpec::Byte, // TODO: proper conversion - addr_length: length, - syntax_id: SyntaxId::S7Any, // TODO: proper conversion - area: Area::DataBlocks, // TODO: proper conversion - db_number, - start_addr, - length: 0, // TODO: calculate from transport_size - })) + let var_spec = VarSpec::try_from(var_spec_byte).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)) + })?; + + let syntax_id = SyntaxId::try_from(syntax_id_byte).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)) + })?; + + let area = Area::try_from(area_byte).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)) + })?; + + Ok(( + input, + ParameterItem { + var_spec, + addr_length: length, + syntax_id, + area, + db_number, + start_addr, + length: 0, // TODO: calculate from transport_size + }, + )) } } -// ... more implementations following s7.mspec +// ... more implementations following s7.mspec #[derive(Debug, Clone)] pub struct S7Payload { @@ -358,6 +487,21 @@ pub enum ReturnCode { // Add more as needed } +impl TryFrom for ReturnCode { + type Error = S7Error; + + fn try_from(value: u8) -> Result { + match value { + 0x00 => Ok(ReturnCode::Success), + 0x01 => Ok(ReturnCode::HardwareError), + 0x03 => Ok(ReturnCode::AccessError), + 0x05 => Ok(ReturnCode::OutOfRange), + 0x06 => Ok(ReturnCode::NotSupported), + _ => Err(S7Error::InvalidReturnCode(value)), + } + } +} + #[derive(Debug, Clone, Copy)] pub enum TransportSize { Bit = 0x01, @@ -367,18 +511,32 @@ pub enum TransportSize { // Add more as needed } +impl TryFrom for TransportSize { + type Error = S7Error; + + fn try_from(value: u8) -> Result { + match value { + 0x01 => Ok(TransportSize::Bit), + 0x02 => Ok(TransportSize::Byte), + 0x04 => Ok(TransportSize::Word), + 0x06 => Ok(TransportSize::DWord), + _ => Err(S7Error::InvalidTransportSize(value)), + } + } +} + impl S7Payload { pub fn parse(input: &[u8], length: u16) -> IResult<&[u8], Self> { let mut items = Vec::new(); let mut remaining = input; let end_pos = length as usize; - + while remaining.len() > 0 && (input.len() - remaining.len()) < end_pos { let (input, item) = PayloadItem::parse(remaining)?; items.push(item); remaining = input; } - + Ok((remaining, S7Payload { items })) } } @@ -389,19 +547,22 @@ impl PayloadItem { let (input, transport_size) = be_u8(input)?; let (input, length) = be_u16(input)?; let (input, data) = take(length as usize)(input)?; - - Ok((input, PayloadItem { - return_code: ReturnCode::try_from(return_code) - .map_err(|_| nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Tag - )))?, - transport_size: TransportSize::try_from(transport_size) - .map_err(|_| nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Tag - )))?, - data: data.to_vec(), - })) + + let return_code = ReturnCode::try_from(return_code).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)) + })?; + + let transport_size = TransportSize::try_from(transport_size).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Tag)) + })?; + + Ok(( + input, + PayloadItem { + return_code, + transport_size, + data: data.to_vec(), + }, + )) } } diff --git a/plc4rs/src/protocols/s7/tests.rs b/plc4rs/src/protocols/s7/tests.rs index 8679db1da39..b806f048c77 100644 --- a/plc4rs/src/protocols/s7/tests.rs +++ b/plc4rs/src/protocols/s7/tests.rs @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + #[cfg(test)] mod tests { use super::*; @@ -8,9 +27,9 @@ mod tests { 0x03, 0x00, 0x00, 0x1A, // TPKT header 0x02, 0xF0, 0x80, // COTP header 0x32, 0x01, 0x00, 0x00, // S7 header start - 0x00, 0x01, 0x00, 0x08, - 0x00, 0x00, // S7 header end - // ... payload ... + 0x00, 0x01, 0x00, 0x08, 0x00, + 0x00, // S7 header end + // ... payload ... ]; let (remaining, packet) = TPKTPacket::parse(input).unwrap(); @@ -29,9 +48,9 @@ mod tests { let input = &[ 0x00, 0x0C, // Destination Reference 0x00, 0x10, // Source Reference - 0x00, // Class + 0x00, // Class 0xC1, 0x02, 0x01, 0x00, // Source TSAP - 0xC2, 0x02, 0x01, 0x02 // Destination TSAP + 0xC2, 0x02, 0x01, 0x02, // Destination TSAP ]; let (remaining, request) = COTPConnectionRequest::parse(input).unwrap(); @@ -44,10 +63,10 @@ mod tests { #[test] fn test_s7_payload_parse() { let input = &[ - 0x00, // Return code (Success) - 0x04, // Transport size (Word) + 0x00, // Return code (Success) + 0x04, // Transport size (Word) 0x00, 0x02, // Length - 0x12, 0x34 // Data + 0x12, 0x34, // Data ]; let (remaining, payload) = S7Payload::parse(input, 6).unwrap(); @@ -59,13 +78,13 @@ mod tests { #[test] fn test_parameter_item_parse() { let input = &[ - 0x02, // VarSpec (Byte) - 0x0A, // Length - 0x10, // Syntax ID (S7Any) - 0x02, // Transport size - 0x00, 0x01, // DB number - 0x84, // Area (DataBlocks) - 0x00, 0x00, 0x00, 0x00 // Start address + 0x02, // VarSpec (Byte) + 0x0A, // Length + 0x10, // Syntax ID (S7Any) + 0x02, // Transport size + 0x00, 0x01, // DB number + 0x84, // Area (DataBlocks) + 0x00, 0x00, 0x00, 0x00, // Start address ]; let (remaining, item) = ParameterItem::parse(input).unwrap(); @@ -73,4 +92,4 @@ mod tests { assert_eq!(item.db_number, 1); assert!(remaining.is_empty()); } -} +} diff --git a/plc4rs/src/s7.rs b/plc4rs/src/s7.rs new file mode 100644 index 00000000000..f1dfbc64848 --- /dev/null +++ b/plc4rs/src/s7.rs @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +/// Message types for S7 protocol +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MessageType { + JobRequest = 0x01, + Ack = 0x02, + AckData = 0x03, + UserData = 0x07, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct S7Header { + protocol_id: u8, + message_type: MessageType, + reserved: u16, + pdu_reference: u16, + parameter_length: u16, + data_length: u16, +} + diff --git a/plc4rs/src/spi/config.rs b/plc4rs/src/spi/config.rs index 6cff36368e9..6834cc81d04 100644 --- a/plc4rs/src/spi/config.rs +++ b/plc4rs/src/spi/config.rs @@ -1,27 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + use std::time::Duration; +/// Common trait for all transport configurations +pub trait TransportConfig { + /// Get the host address + fn host(&self) -> &str; + + /// Get the port number + fn port(&self) -> u16; +} + +/// Configuration for TCP transport #[derive(Debug, Clone)] -pub struct TransportConfig { - pub connect_timeout: Duration, - pub read_timeout: Duration, - pub write_timeout: Duration, - pub buffer_size: usize, +pub struct TcpConfig { + pub host: String, + pub port: u16, + pub connect_timeout: Option, + pub read_timeout: Option, + pub write_timeout: Option, + pub nodelay: Option, + pub ttl: Option, + pub retry_count: Option, } -impl Default for TransportConfig { - fn default() -> Self { - TransportConfig { - connect_timeout: Duration::from_secs(5), - read_timeout: Duration::from_secs(1), - write_timeout: Duration::from_secs(1), - buffer_size: 8192, +impl TcpConfig { + /// Create a new TCP configuration with default settings + pub fn new(host: impl Into, port: u16) -> Self { + Self { + host: host.into(), + port, + connect_timeout: Some(Duration::from_secs(5)), + read_timeout: Some(Duration::from_secs(5)), + write_timeout: Some(Duration::from_secs(5)), + nodelay: Some(true), + ttl: None, + retry_count: Some(3), } } + + /// Set the connect timeout + pub fn with_connect_timeout(mut self, timeout: Duration) -> Self { + self.connect_timeout = Some(timeout); + self + } + + /// Set the read timeout + pub fn with_read_timeout(mut self, timeout: Duration) -> Self { + self.read_timeout = Some(timeout); + self + } + + /// Set the write timeout + pub fn with_write_timeout(mut self, timeout: Duration) -> Self { + self.write_timeout = Some(timeout); + self + } + + /// Set the TCP nodelay option + pub fn with_nodelay(mut self, nodelay: bool) -> Self { + self.nodelay = Some(nodelay); + self + } + + /// Set the TTL value + pub fn with_ttl(mut self, ttl: u32) -> Self { + self.ttl = Some(ttl); + self + } + + /// Set the retry count + pub fn with_retry_count(mut self, count: u32) -> Self { + self.retry_count = Some(count); + self + } +} + +impl TransportConfig for TcpConfig { + fn host(&self) -> &str { + &self.host + } + + fn port(&self) -> u16 { + self.port + } } +/// Configuration for UDP transport #[derive(Debug, Clone)] -pub struct TcpConfig { - pub base: TransportConfig, - pub no_delay: bool, - pub keep_alive: bool, +pub struct UdpConfig { + pub host: String, + pub port: u16, + pub local_port: Option, + pub read_timeout: Option, + pub write_timeout: Option, + pub ttl: Option, + pub broadcast: Option, +} + +impl UdpConfig { + /// Create a new UDP configuration with default settings + pub fn new(host: impl Into, port: u16) -> Self { + Self { + host: host.into(), + port, + local_port: None, + read_timeout: Some(Duration::from_secs(5)), + write_timeout: Some(Duration::from_secs(5)), + ttl: None, + broadcast: None, + } + } + + /// Set the local port to bind to + pub fn with_local_port(mut self, port: u16) -> Self { + self.local_port = Some(port); + self + } + + /// Set the read timeout + pub fn with_read_timeout(mut self, timeout: Duration) -> Self { + self.read_timeout = Some(timeout); + self + } + + /// Set the write timeout + pub fn with_write_timeout(mut self, timeout: Duration) -> Self { + self.write_timeout = Some(timeout); + self + } + + /// Set the TTL value + pub fn with_ttl(mut self, ttl: u32) -> Self { + self.ttl = Some(ttl); + self + } + + /// Set the broadcast option + pub fn with_broadcast(mut self, broadcast: bool) -> Self { + self.broadcast = Some(broadcast); + self + } +} + +impl TransportConfig for UdpConfig { + fn host(&self) -> &str { + &self.host + } + + fn port(&self) -> u16 { + self.port + } } diff --git a/plc4rs/src/spi/error.rs b/plc4rs/src/spi/error.rs index 83598e7bf6f..ba8ded59252 100644 --- a/plc4rs/src/spi/error.rs +++ b/plc4rs/src/spi/error.rs @@ -1,17 +1,36 @@ -use thiserror::Error; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + use std::io; +use thiserror::Error; #[derive(Error, Debug)] pub enum TransportError { #[error("IO error: {0}")] Io(#[from] io::Error), - + #[error("Connection error: {0}")] Connection(String), - + #[error("Not connected")] NotConnected, - + #[error("Already connected")] AlreadyConnected, -} +} diff --git a/plc4rs/src/spi/mod.rs b/plc4rs/src/spi/mod.rs index 1b7288112b8..72eacc567e3 100644 --- a/plc4rs/src/spi/mod.rs +++ b/plc4rs/src/spi/mod.rs @@ -1,143 +1,46 @@ -//! Service Provider Interface (SPI) for PLC4X -//! -//! This module provides the core abstractions for implementing different transport -//! mechanisms in PLC4X. The main trait is `Transport` which defines the basic -//! operations that any transport implementation must provide. -//! -//! # Transport Types -//! -//! Currently implemented: -//! - TCP: For TCP/IP based protocols -//! -//! # Example -//! ```rust -//! use plc4rs::spi::{Transport, TcpTransport}; -//! -//! async fn example() { -//! let mut transport = TcpTransport::new("192.168.1.1".to_string(), 102); -//! transport.connect().await.unwrap(); -//! // ... use transport ... -//! transport.close().await.unwrap(); -//! } -//! ``` - +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +pub mod config; +mod tcp; +mod udp; + +use bytes::BytesMut; use std::fmt::Debug; -use tracing::{debug, error, info, warn}; - -/// Retry configuration for transport operations -#[derive(Debug, Clone)] -pub struct RetryConfig { - /// Maximum number of retry attempts - pub max_attempts: u32, - /// Delay between retry attempts - pub retry_delay: std::time::Duration, - /// Whether to use exponential backoff - pub use_backoff: bool, -} +use crate::types::Result; -impl Default for RetryConfig { - fn default() -> Self { - Self { - max_attempts: 3, - retry_delay: std::time::Duration::from_millis(100), - use_backoff: true, - } - } -} +// Re-export implementations +pub use tcp::TcpTransport; +pub use udp::UdpTransport; -/// Core trait for implementing transport mechanisms +/// Transport trait defining the interface for all transport implementations #[async_trait::async_trait] -pub trait Transport: Send + Sync { - /// Establishes a connection to the target device - async fn connect(&mut self) -> Result<(), TransportError> { - self.connect_with_retry(RetryConfig::default()).await - } - - /// Connects with retry logic - async fn connect_with_retry(&mut self, retry_config: RetryConfig) -> Result<(), TransportError> { - let mut attempt = 0; - let mut delay = retry_config.retry_delay; - - loop { - attempt += 1; - match self.connect_internal().await { - Ok(()) => { - info!("Connection established on attempt {}", attempt); - return Ok(()); - } - Err(e) => { - if attempt >= retry_config.max_attempts { - error!("Connection failed after {} attempts: {}", attempt, e); - return Err(e); - } - warn!("Connection attempt {} failed: {}", attempt, e); - tokio::time::sleep(delay).await; - if retry_config.use_backoff { - delay *= 2; - } - } - } - } - } - - /// Internal connect implementation - #[doc(hidden)] - async fn connect_internal(&mut self) -> Result<(), TransportError>; +pub trait Transport: Debug + Send + Sync { + /// Connect to the target + async fn connect(&mut self) -> Result<()>; - /// Reads data with logging - async fn read(&mut self, buffer: &mut [u8]) -> Result { - debug!("Attempting to read {} bytes", buffer.len()); - match self.read_internal(buffer).await { - Ok(n) => { - debug!("Successfully read {} bytes", n); - Ok(n) - } - Err(e) => { - error!("Read error: {}", e); - Err(e) - } - } - } - - /// Internal read implementation - #[doc(hidden)] - async fn read_internal(&mut self, buffer: &mut [u8]) -> Result; + /// Read data from the transport + async fn read(&mut self, buffer: &mut BytesMut) -> Result; - /// Writes data with logging - async fn write(&mut self, data: &[u8]) -> Result { - debug!("Attempting to write {} bytes", data.len()); - match self.write_internal(data).await { - Ok(n) => { - debug!("Successfully wrote {} bytes", n); - Ok(n) - } - Err(e) => { - error!("Write error: {}", e); - Err(e) - } - } - } - - /// Internal write implementation - #[doc(hidden)] - async fn write_internal(&mut self, data: &[u8]) -> Result; + /// Write data to the transport + async fn write(&mut self, data: &[u8]) -> Result; - /// Closes the connection with logging - async fn close(&mut self) -> Result<(), TransportError> { - info!("Closing connection"); - self.close_internal().await - } - - /// Internal close implementation - #[doc(hidden)] - async fn close_internal(&mut self) -> Result<(), TransportError>; + /// Close the transport connection + async fn close(&mut self) -> Result<()>; } - -// Implement transport types -pub mod tcp; -pub mod udp; -pub mod error; - -pub use error::TransportError; -pub use tcp::TcpTransport; -pub use udp::UdpTransport; diff --git a/plc4rs/src/spi/tcp.rs b/plc4rs/src/spi/tcp.rs index 4274b63ba2d..b29296b1c06 100644 --- a/plc4rs/src/spi/tcp.rs +++ b/plc4rs/src/spi/tcp.rs @@ -1,71 +1,133 @@ -use tokio::net::TcpStream; -use crate::spi::{Transport, TransportError}; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use crate::spi::Transport; use crate::spi::config::TcpConfig; -use std::io; +use crate::types::Result; +use async_trait::async_trait; +use bytes::BytesMut; +use std::time::Duration; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::time::timeout; +use tracing::{debug, error, info}; +/// TCP transport implementation +#[derive(Debug)] pub struct TcpTransport { - stream: Option, - address: String, - port: u16, config: TcpConfig, + stream: Option, } impl TcpTransport { - pub fn new(address: String, port: u16) -> Self { - Self::new_with_config(address, port, TcpConfig { - base: Default::default(), - no_delay: true, - keep_alive: true, - }) - } - - pub fn new_with_config(address: String, port: u16, config: TcpConfig) -> Self { - TcpTransport { - stream: None, - address, - port, + /// Create a new TCP transport with the given configuration + pub fn new(config: TcpConfig) -> Self { + Self { config, + stream: None, } } } +#[async_trait] impl Transport for TcpTransport { - async fn connect_internal(&mut self) -> Result<(), TransportError> { - if self.stream.is_some() { - return Err(TransportError::AlreadyConnected); - } + async fn connect(&mut self) -> Result<()> { + let address = format!("{}:{}", self.config.host, self.config.port); + info!("Connecting to {}", address); - let addr = format!("{}:{}", self.address, self.port); - let stream = TcpStream::connect(addr).await?; + let mut retry_count = 0; + let max_retries = self.config.retry_count.unwrap_or(3); - // Apply TCP-specific settings - stream.set_nodelay(self.config.no_delay)?; - stream.set_keepalive(self.config.keep_alive.then_some(self.config.base.connect_timeout))?; + while retry_count < max_retries { + match timeout( + self.config.connect_timeout.unwrap_or(Duration::from_secs(5)), + TcpStream::connect(&address), + ).await { + Ok(Ok(stream)) => { + // Configure the stream + if let Some(nodelay) = self.config.nodelay { + stream.set_nodelay(nodelay)?; + } + + if let Some(ttl) = self.config.ttl { + stream.set_ttl(ttl)?; + } + + self.stream = Some(stream); + info!("Connected to {}", address); + return Ok(()); + } + Ok(Err(e)) => { + error!("Failed to connect: {}", e); + retry_count += 1; + if retry_count < max_retries { + let backoff = Duration::from_millis(100 * 2u64.pow(retry_count)); + debug!("Retrying in {:?} (attempt {}/{})", backoff, retry_count + 1, max_retries); + tokio::time::sleep(backoff).await; + } + } + Err(_) => { + error!("Connection timed out"); + retry_count += 1; + if retry_count < max_retries { + let backoff = Duration::from_millis(100 * 2u64.pow(retry_count)); + debug!("Retrying in {:?} (attempt {}/{})", backoff, retry_count + 1, max_retries); + tokio::time::sleep(backoff).await; + } + } + } + } - self.stream = Some(stream); - Ok(()) + Err("Failed to connect after maximum retries".into()) } - - async fn read(&mut self, buffer: &mut [u8]) -> Result { - let stream = self.stream.as_mut() - .ok_or(TransportError::NotConnected)?; - - use tokio::io::AsyncReadExt; - Ok(stream.read(buffer).await?) + + async fn read(&mut self, buffer: &mut BytesMut) -> Result { + if let Some(stream) = &mut self.stream { + let read_timeout = self.config.read_timeout.unwrap_or(Duration::from_secs(5)); + match timeout(read_timeout, stream.read_buf(buffer)).await { + Ok(Ok(n)) => Ok(n), + Ok(Err(e)) => Err(format!("Read error: {}", e).into()), + Err(_) => Err("Read operation timed out".into()), + } + } else { + Err("Not connected".into()) + } } - - async fn write(&mut self, data: &[u8]) -> Result { - let stream = self.stream.as_mut() - .ok_or(TransportError::NotConnected)?; - - use tokio::io::AsyncWriteExt; - Ok(stream.write(data).await?) + + async fn write(&mut self, data: &[u8]) -> Result { + if let Some(stream) = &mut self.stream { + let write_timeout = self.config.write_timeout.unwrap_or(Duration::from_secs(5)); + match timeout(write_timeout, stream.write(data)).await { + Ok(Ok(n)) => Ok(n), + Ok(Err(e)) => Err(format!("Write error: {}", e).into()), + Err(_) => Err("Write operation timed out".into()), + } + } else { + Err("Not connected".into()) + } } - - async fn close(&mut self) -> Result<(), TransportError> { - if let Some(stream) = self.stream.take() { - use tokio::io::AsyncWriteExt; + + async fn close(&mut self) -> Result<()> { + if let Some(stream) = &mut self.stream { stream.shutdown().await?; + self.stream = None; + info!("Connection closed"); } Ok(()) } @@ -78,7 +140,19 @@ mod tests { #[test] fn test_tcp_transport() { - let mut transport = TcpTransport::new("127.0.0.1".to_string(), 102); + let mut transport = TcpTransport::new(TcpConfig { + base: Default::default(), + no_delay: true, + keep_alive: true, + host: "127.0.0.1".to_string(), + port: 102, + retry_count: None, + connect_timeout: None, + read_timeout: None, + write_timeout: None, + nodelay: None, + ttl: None, + }); // Test connection block_on(async { diff --git a/plc4rs/src/spi/udp.rs b/plc4rs/src/spi/udp.rs index 08bf97e97be..a0595651cfd 100644 --- a/plc4rs/src/spi/udp.rs +++ b/plc4rs/src/spi/udp.rs @@ -1,65 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use crate::spi::Transport; +use crate::spi::config::UdpConfig; +use crate::types::Result; +use async_trait::async_trait; +use bytes::BytesMut; +use std::net::SocketAddr; +use std::time::Duration; use tokio::net::UdpSocket; -use crate::spi::{Transport, TransportError}; -use crate::spi::config::TransportConfig; -use std::io; +use tokio::time::timeout; +use tracing::info; +/// UDP transport implementation +#[derive(Debug)] pub struct UdpTransport { + config: UdpConfig, socket: Option, - address: String, - port: u16, - config: TransportConfig, - buffer: Vec, + remote_addr: Option, } impl UdpTransport { - pub fn new(address: String, port: u16) -> Self { - Self::new_with_config(address, port, TransportConfig::default()) - } - - pub fn new_with_config(address: String, port: u16, config: TransportConfig) -> Self { - UdpTransport { - socket: None, - address, - port, + /// Create a new UDP transport with the given configuration + pub fn new(config: UdpConfig) -> Self { + Self { config, - buffer: vec![0; config.buffer_size], + socket: None, + remote_addr: None, } } } -#[async_trait::async_trait] +#[async_trait] impl Transport for UdpTransport { - async fn connect_internal(&mut self) -> Result<(), TransportError> { - if self.socket.is_some() { - return Err(TransportError::AlreadyConnected); + async fn connect(&mut self) -> Result<()> { + let remote_address = format!("{}:{}", self.config.host, self.config.port); + info!("Connecting to {}", remote_address); + + // Parse the remote address + let remote_addr: SocketAddr = remote_address.parse()?; + self.remote_addr = Some(remote_addr); + + // Bind to a local address + let local_addr: SocketAddr = if let Some(local_port) = self.config.local_port { + format!("0.0.0.0:{}", local_port).parse()? + } else { + "0.0.0.0:0".parse()? + }; + + let socket = UdpSocket::bind(local_addr).await?; + + // Set socket options + if let Some(ttl) = self.config.ttl { + socket.set_ttl(ttl)?; } - let socket = UdpSocket::bind("0.0.0.0:0").await?; - let addr = format!("{}:{}", self.address, self.port); - socket.connect(&addr).await?; + if let Some(broadcast) = self.config.broadcast { + socket.set_broadcast(broadcast)?; + } self.socket = Some(socket); + info!("UDP socket bound to local address"); + Ok(()) } - - async fn read_internal(&mut self, buffer: &mut [u8]) -> Result { - let socket = self.socket.as_ref() - .ok_or(TransportError::NotConnected)?; + + async fn read(&mut self, buffer: &mut BytesMut) -> Result { + if let Some(socket) = &self.socket { + let read_timeout = self.config.read_timeout.unwrap_or(Duration::from_secs(5)); - let len = socket.recv(buffer).await?; - Ok(len) - } - - async fn write_internal(&mut self, data: &[u8]) -> Result { - let socket = self.socket.as_ref() - .ok_or(TransportError::NotConnected)?; + // Ensure we have enough capacity + if buffer.capacity() - buffer.len() < 65536 { + buffer.reserve(65536); + } - let len = socket.send(data).await?; - Ok(len) + match timeout(read_timeout, socket.recv_buf(buffer)).await { + Ok(Ok(n)) => Ok(n), + Ok(Err(e)) => Err(format!("Read error: {}", e).into()), + Err(_) => Err("Read operation timed out".into()), + } + } else { + Err("Not connected".into()) + } } - - async fn close_internal(&mut self) -> Result<(), TransportError> { + + async fn write(&mut self, data: &[u8]) -> Result { + if let Some(socket) = &self.socket { + if let Some(remote_addr) = self.remote_addr { + let write_timeout = self.config.write_timeout.unwrap_or(Duration::from_secs(5)); + + match timeout(write_timeout, socket.send_to(data, remote_addr)).await { + Ok(Ok(n)) => Ok(n), + Ok(Err(e)) => Err(format!("Write error: {}", e).into()), + Err(_) => Err("Write operation timed out".into()), + } + } else { + Err("Remote address not set".into()) + } + } else { + Err("Not connected".into()) + } + } + + async fn close(&mut self) -> Result<()> { + // UDP sockets don't need explicit closing, but we'll reset our state self.socket = None; + self.remote_addr = None; + info!("UDP connection closed"); Ok(()) } } @@ -71,7 +134,15 @@ mod tests { #[test] fn test_udp_lifecycle() { - let mut transport = UdpTransport::new("127.0.0.1".to_string(), 1234); + let mut transport = UdpTransport::new(UdpConfig { + host: "127.0.0.1".to_string(), + port: 1234, + local_port: None, + ttl: None, + broadcast: None, + read_timeout: None, + write_timeout: None, + }); block_on(async { // Test connection diff --git a/plc4rs/src/types.rs b/plc4rs/src/types.rs new file mode 100644 index 00000000000..d13d62be372 --- /dev/null +++ b/plc4rs/src/types.rs @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use std::time::Duration; + +/// Common result type used throughout the library +pub type Result = std::result::Result>; + +/// Default timeout for operations +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); + +/// Default retry count for operations +pub const DEFAULT_RETRY_COUNT: u32 = 3; diff --git a/plc4x-rust/CONTRIBUTING.md b/plc4x-rust/CONTRIBUTING.md deleted file mode 100644 index c1efdae09da..00000000000 --- a/plc4x-rust/CONTRIBUTING.md +++ /dev/null @@ -1,43 +0,0 @@ -# Contributing to PLC4X Rust Implementation - -## Development Setup - -1. Install Rust (stable channel) -2. Clone the repository -3. Install development tools: - ```bash - cargo install cargo-fuzz - ``` - -## Code Style -- Follow Rust standard formatting (use `rustfmt`) -- Add documentation for public APIs -- Include tests for new functionality - -## Testing - -### Before Submitting a PR -Run all checks: -```bash -# Format code -cargo fmt -- --check - -# Run linter -cargo clippy - -# Run unit tests -cargo test - -# Run fuzzer -cargo fuzz run header_parser -``` - -## Pull Request Process -1. Create a feature branch -2. Add tests for new functionality -3. Update documentation -4. Ensure all tests pass -5. Submit PR with clear description - -## Questions? -Feel free to reach out to the maintainers or open an issue. diff --git a/plc4x-rust/Cargo.toml b/plc4x-rust/Cargo.toml deleted file mode 100644 index cff6b697060..00000000000 --- a/plc4x-rust/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "plc4x-rust" -version = "0.1.0" -edition = "2021" -description = "Rust bindings for Apache PLC4X" -license = "Apache-2.0" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -nom = "7.1.3" -thiserror = "1.0" -bytes = "1.4" -tokio = { version = "1.28", features = ["full"] } -tracing = "0.1" - -[dev-dependencies] -tokio-test = "0.4" -tokio = { version = "1.28", features = ["full", "test-util"] } - -[profile.release] -debug = true diff --git a/plc4x-rust/README.md b/plc4x-rust/README.md deleted file mode 100644 index 3385f80118b..00000000000 --- a/plc4x-rust/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# PLC4X Rust Implementation - -This project implements Rust bindings for Apache PLC4X, focusing on the Siemens S7 protocol. The implementation aims to provide: - -- Memory-safe, zero-copy protocol parsing using nom -- Async/await support for modern Rust applications -- High-performance industrial communication -- Type-safe protocol implementation - -## Current Status - -- [x] Initial project structure -- [x] Basic S7 protocol types -- [ ] Protocol parsing implementation -- [ ] Connection handling -- [ ] Async support -- [ ] Testing infrastructure - -## Getting Started - -### Prerequisites - -- Rust (stable channel) -- Cargo - -### Installation - -```bash -git clone https://github.com/apache/plc4x -cd plc4x-rust -cargo build -``` - -## Testing - -### Unit Tests - -```bash -cargo test -``` - -### Fuzz Testing - -```bash -# Install cargo-fuzz (only needed once) -cargo install cargo-fuzz - -# Run the fuzzer -cargo fuzz run header_parser -``` - -## Contributing - -Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and development process. diff --git a/plc4x-rust/fuzz/Cargo.toml b/plc4x-rust/fuzz/Cargo.toml deleted file mode 100644 index dbd6ad44a03..00000000000 --- a/plc4x-rust/fuzz/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "plc4x-rust-fuzz" -version = "0.0.0" -publish = false -edition = "2021" - -[package.metadata] -cargo-fuzz = true - -[dependencies] -libfuzzer-sys = "0.4" -plc4x-rust = { path = ".." } - -[[bin]] -name = "header_parser" -path = "fuzz_targets/header_parser.rs" -test = false -doc = false diff --git a/plc4x-rust/fuzz/fuzz_targets/header_parser.rs b/plc4x-rust/fuzz/fuzz_targets/header_parser.rs deleted file mode 100644 index 92364eccdb2..00000000000 --- a/plc4x-rust/fuzz/fuzz_targets/header_parser.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![no_main] -use libfuzzer_sys::fuzz_target; -use plc4x_rust::s7::S7Header; - -fuzz_target!(|data: &[u8]| { - let _ = S7Header::parse(data); -}); diff --git a/plc4x-rust/src/error.rs b/plc4x-rust/src/error.rs deleted file mode 100644 index 40b5375c6f3..00000000000 --- a/plc4x-rust/src/error.rs +++ /dev/null @@ -1,13 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Protocol error: {0}")] - Protocol(String), - - #[error("Connection error: {0}")] - Connection(String), - - #[error("Parse error: {0}")] - Parse(String), -} diff --git a/plc4x-rust/src/lib.rs b/plc4x-rust/src/lib.rs deleted file mode 100644 index 6279fdeda87..00000000000 --- a/plc4x-rust/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! PLC4X Rust implementation for S7 protocol - -mod error; -mod s7; -mod types; - -pub use error::Error; -pub use s7::{S7Connector, S7Header, MessageType}; -pub use types::{CommunicationType, FunctionCode, AreaCode}; diff --git a/plc4x-rust/src/s7.rs b/plc4x-rust/src/s7.rs deleted file mode 100644 index 401d1b0d8a6..00000000000 --- a/plc4x-rust/src/s7.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! S7 protocol implementation for PLC4X -//! -//! This module provides the core functionality for communicating with -//! Siemens S7 PLCs using a memory-safe, zero-copy implementation. - -use bytes::BytesMut; -use nom::IResult; -use nom::number::complete::{be_u8, be_u16}; -use nom::sequence::tuple; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use std::io; - -/// S7 protocol header structure -/// -/// Represents the header of an S7 protocol packet, containing -/// protocol identification, message type, and length information. -#[derive(Debug, Clone)] -pub struct S7Header { - protocol_id: u8, - message_type: MessageType, - reserved: u16, - pdu_reference: u16, - parameter_length: u16, - data_length: u16, -} - -/// Message types supported by the S7 protocol -/// -/// Represents the different types of messages that can be -/// exchanged in the S7 protocol. -#[derive(Debug, Clone, Copy)] -pub enum MessageType { - /// Job request message - Job = 0x01, - /// Acknowledgment without data - Ack = 0x02, - /// Acknowledgment with data - AckData = 0x03, -} - -impl S7Header { - pub fn new(message_type: MessageType, pdu_reference: u16) -> Self { - Self { - protocol_id: 0x32, // Standard S7 protocol ID - message_type, - reserved: 0, - pdu_reference, - parameter_length: 0, - data_length: 0, - } - } - - pub fn parse(input: &[u8]) -> IResult<&[u8], S7Header> { - let (input, (protocol_id, message_type_raw, reserved, pdu_ref, param_len, data_len)) = - tuple(( - be_u8, - be_u8, - be_u16, - be_u16, - be_u16, - be_u16, - ))(input)?; - - let message_type = match message_type_raw { - 0x01 => MessageType::Job, - 0x02 => MessageType::Ack, - 0x03 => MessageType::AckData, - _ => return Err(nom::Err::Error(nom::error::Error::new( - input, - nom::error::ErrorKind::Tag - ))), - }; - - Ok((input, S7Header { - protocol_id, - message_type, - reserved, - pdu_reference: pdu_ref, - parameter_length: param_len, - data_length: data_len, - })) - } -} - -pub struct S7Connector { - // Connection details will go here -} - -impl S7Connector { - pub fn new() -> Self { - Self {} - } - - pub async fn negotiate(&self, stream: &mut T) -> io::Result<()> - where - T: AsyncReadExt + AsyncWriteExt + Unpin, - { - // Read the handshake header - let mut header_buf = [0u8; 10]; - stream.read_exact(&mut header_buf).await?; - - // Parse the header - if let Ok((_, header)) = S7Header::parse(&header_buf) { - match header.message_type { - MessageType::AckData => Ok(()), - _ => Err(io::Error::new( - io::ErrorKind::InvalidData, - "Unexpected message type in handshake" - )), - } - } else { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "Failed to parse handshake header" - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tokio::io::duplex; - - #[test] - fn test_s7_header_creation() { - let header = S7Header::new(MessageType::Job, 1); - assert_eq!(header.protocol_id, 0x32); - assert_eq!(header.pdu_reference, 1); - } - - #[test] - fn test_message_type_values() { - assert_eq!(MessageType::Job as u8, 0x01); - assert_eq!(MessageType::Ack as u8, 0x02); - assert_eq!(MessageType::AckData as u8, 0x03); - } - - #[test] - fn test_s7_header_parsing() { - let test_data = &[ - 0x32, // protocol_id - 0x01, // message_type (Job) - 0x00, 0x00, // reserved - 0x00, 0x01, // pdu_reference - 0x00, 0x00, // parameter_length - 0x00, 0x00, // data_length - ]; - - let (remaining, header) = S7Header::parse(test_data).unwrap(); - assert!(remaining.is_empty()); - assert_eq!(header.protocol_id, 0x32); - assert_eq!(header.pdu_reference, 1); - matches!(header.message_type, MessageType::Job); - } - - #[tokio::test] - async fn test_async_connection_handshake() { - let connector = S7Connector::new(); - let (mut tx, mut rx) = duplex(1024); - - tokio::spawn(async move { - let mut buf = [0u8; 12]; - tx.write_all(&[ - 0x32, 0x03, // Valid header - 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00 - ]).await.unwrap(); - tx.read_exact(&mut buf).await.unwrap(); - }); - - let result = connector.negotiate(&mut rx).await; - assert!(result.is_ok()); - } -} diff --git a/plc4x-rust/src/types.rs b/plc4x-rust/src/types.rs deleted file mode 100644 index 4906b9e16ff..00000000000 --- a/plc4x-rust/src/types.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! S7 protocol type definitions - -/// S7 Communication Type -#[derive(Debug, Clone, Copy)] -pub enum CommunicationType { - PG = 0x01, - OP = 0x02, - Step7Basic = 0x03, -} - -/// S7 Function Code -#[derive(Debug, Clone, Copy)] -pub enum FunctionCode { - ReadVar = 0x04, - WriteVar = 0x05, - RequestDownload = 0x1A, - Download = 0x1B, - DownloadEnded = 0x1C, - // Add more as needed -} - -/// S7 Area Code -#[derive(Debug, Clone, Copy)] -pub enum AreaCode { - SysInfo = 0x03, - SysFlags = 0x05, - AnaInput = 0x06, - AnaOutput = 0x07, - P = 0x80, - Inputs = 0x81, - Outputs = 0x82, - Flags = 0x83, - DB = 0x84, - DI = 0x85, - Local = 0x86, - V = 0x87, -} diff --git a/pom.xml b/pom.xml index 0bcdca65258..6ed4e31f74c 100644 --- a/pom.xml +++ b/pom.xml @@ -1779,6 +1779,13 @@ + + + with-rust + + plc4rs + +