packet_parser 1.5.0

A powerful and modular Rust crate for network packet parsing.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
// Copyright (c) 2025 Cyprien Avico avicocyprien@yahoo.com
//
// Licensed under the MIT License <LICENSE-MIT or http://opensource.org/licenses/MIT>.
// This file may not be copied, modified, or distributed except according to those terms.

//! S7Comm protocol parser implementation.
//!
//! This module provides functionality to parse and handle S7Comm protocol packets,
//! which is a communication protocol used by Siemens S7 PLCs. The implementation
//! supports parsing of TPKT, COTP, and S7 protocol layers.
//!
//! # Example
//! ```no_run
//! use packet_parser::parse::application::protocols::s7comm::S7CommPacket;
//!
//! // Example S7Comm packet (simplified for demonstration)
//! let raw_packet = [
//!     0x03, 0x00, 0x00, 0x16, 0x11, 0xE0, 0x00, 0x00,
//!     0x00, 0x01, 0x00, 0xC0, 0x01, 0x0A, 0xC1, 0x02,
//!     0x01, 0x00, 0xC2, 0x02, 0x01, 0x02
//! ];
//!
//! match S7CommPacket::try_from(&raw_packet[..]) {
//!     Ok(packet) => println!("Successfully parsed S7Comm packet: {:?}", packet),
//!     Err(e) => eprintln!("Failed to parse S7Comm packet: {}", e),
//! }
//! ```

use std::fmt;

use crate::{
    checks::application::s7comm::{
        validate_cotp_header_length, validate_data_length, validate_min_size,
        validate_parameter_data_not_empty, validate_parameter_item_header,
        validate_parameter_item_length, validate_parameter_length, validate_s7_header_length,
        validate_s7any_length, validate_tpkt_version,
    },
    errors::application::s7comm::S7CommParseError,
};

#[cfg_attr(doc, aquamarine::aquamarine)]
/// S7Comm Protocol Packet
///
/// ```mermaid
/// ---
/// title: S7CommPacket
/// ---
/// packet-beta
/// %% TPKT Header
/// 0-7: "TPKT Version u8"
/// 8-15: "TPKT Reserved u8"
/// 16-31: "TPKT Length u16"
///
/// %% COTP Header
/// 32-39: "COTP Length u8"
/// 40-47: "COTP PDU Type u8"
/// 48-63: "COTP Dest Ref u16"
/// 64-79: "COTP Src Ref u16"
/// 80-87: "COTP Last Data Unit"
///
/// %% S7 Header
/// 88-95: "S7 Protocol ID u8"
/// 96-103: "S7 ROSCTR u8"
/// 104-119: "S7 Reserved u16"
/// 120-135: "S7 PDU Ref u16"
/// 136-151: "S7 Param Len u16"
/// 152-167: "S7 Data Len u16"
/// 168-175: "S7 Error Class"
/// 176-183: "S7 Error Code"
///
/// %% S7 Parameter
/// 184-191: "Param Function u8"
/// ```
#[derive(Debug)]
pub struct S7CommPacket<'a> {
    /// TPKT Header (RFC 1006)
    pub tpkt: TpktHeader,

    /// COTP Header (ISO 8073 / X.224)
    pub cotp: CotpHeader,

    /// S7 Communication Header (S7Comm)
    pub s7_header: S7Header,

    /// S7 Parameter section containing function code and items
    pub parameter: S7Parameter<'a>,

    /// Optional payload data
    pub payload: Option<&'a [u8]>,
}

/// TPKT (Transport Protocol Data Unit) Header (4 bytes)
///
/// Defined in RFC 1006, this is the outermost protocol layer.
#[derive(Debug)]
pub struct TpktHeader {
    /// Protocol version (should be 0x03)
    pub version: u8,

    /// Reserved field (should be 0x00)
    pub reserved: u8,

    /// Total length of the TPKT packet (including header)
    pub length: u16,
}

/// COTP (Connection-Oriented Transport Protocol) Header
///
/// Defined in ISO 8073/X.224, this layer provides connection-oriented services.
#[derive(Debug)]
pub struct CotpHeader {
    /// Length of the COTP header
    pub length: u8,

    /// PDU type (0xF0 = Data TPDU)
    pub pdu_type: u8,

    /// Destination reference number
    pub destination_reference: u16,

    /// Source reference number
    pub source_reference: u16,

    /// Indicates if this is the last data unit
    pub last_data_unit: bool,
}

/// S7 Communication Protocol Header
///
/// This is the S7-specific protocol header that follows the COTP header.
#[derive(Debug)]
pub struct S7Header {
    /// Protocol ID (should be 0x32 for S7Comm)
    pub protocol_id: u8,

    /// Message type (0x01 = Job, 0x02 = Ack, 0x03 = Ack-Data, 0x07 = Userdata)
    pub rosctr: u8,

    /// Reserved field (should be 0x0000)
    pub reserved: u16,

    /// PDU reference number
    pub pduref: u16,

    /// Length of the parameter section
    pub parameter_length: u16,

    /// Length of the data section
    pub data_length: u16,

    /// Error class (only present in ACK/Error messages)
    pub error_class: Option<u8>,

    /// Error code (only present in ACK/Error messages)
    pub error_code: Option<u8>,
}

/// S7 Parameter section containing function code and items
///
/// This structure represents the parameter section of an S7Comm packet,
/// which contains the function code and associated parameter items.
#[derive(Debug)]
pub struct S7Parameter<'a> {
    /// Function code (e.g., 0x04 = Read Var, 0x05 = Write Var)
    pub function: u8,

    /// List of parameter items
    pub items: Vec<S7ParameterItem<'a>>,
}

/// Represents a single item in the S7 parameter section
///
/// This structure contains the addressing information for a single data item
/// being read from or written to the PLC.
#[derive(Debug)]
pub struct S7ParameterItem<'a> {
    /// Specification type (0x12 = Variable Specification)
    pub spec_type: u8,

    /// Length of the specification
    pub length: u8,

    /// Syntax ID (0x10 = S7ANY)
    pub syntax_id: u8,

    /// Transport size (0x02 = BYTE, 0x04 = WORD, etc.)
    pub transport_size: u8,

    /// DB number (0 for non-DB areas)
    pub db_number: u16,

    /// Memory area (0x81 = Input, 0x82 = Output, 0x83 = DB, etc.)
    pub area: u8,

    /// Memory address (3-byte address in big-endian format)
    pub address: u32,

    /// Raw bytes of the parameter item (if needed for debugging)
    pub raw: Option<&'a [u8]>,
}

impl<'a> S7CommPacket<'a> {
    /// Minimum required size for an S7Comm packet (TPKT + COTP + S7 Header)
    pub const MIN_SIZES: usize = 4 + 3 + 10;

    /// Attempts to parse a byte slice into an `S7CommPacket`.
    ///
    /// # Arguments
    /// * `packet` - A byte slice containing the raw S7Comm packet
    ///
    /// # Returns
    /// * `Ok(S7CommPacket)` if parsing was successful
    /// * `Err(S7CommParseError)` if the packet is malformed or incomplete
    ///
    /// # Example
    /// ```no_run
    /// # use packet_parser::parse::application::protocols::s7comm::S7CommPacket;
    /// let packet_data = [/* raw packet data */];
    /// match S7CommPacket::try_from(&packet_data[..]) {
    ///     Ok(packet) => println!("Successfully parsed packet: {:?}", packet),
    ///     Err(e) => eprintln!("Failed to parse packet: {}", e),
    /// }
    /// ```
    fn parse(packet: &'a [u8]) -> Result<Self, S7CommParseError> {
        // println!("S7CommPacket::try_from: packet len: {:?}", packet);
        validate_min_size(packet.len(), Self::MIN_SIZES)?;

        // Parse TPKT Header (4 bytes)
        validate_tpkt_version(packet[0])?;

        let tpkt = TpktHeader {
            version: packet[0],
            reserved: packet[1],
            length: u16::from_be_bytes([packet[2], packet[3]]),
        };

        // Parse COTP Header (starts at offset 4)
        let cotp_len = packet[4] as usize;
        validate_cotp_header_length(4 + cotp_len + 1, packet.len())?;

        let cotp = CotpHeader {
            length: packet[4],
            pdu_type: packet[5],
            destination_reference: u16::from_be_bytes([packet[6], packet[7]]),
            source_reference: u16::from_be_bytes([packet[8], packet[9]]),
            last_data_unit: (packet[10] & 0x80) != 0,
        };

        // S7 Header starts after TPKT + COTP
        let s7_start = 4 + cotp.length as usize + 1; // +1 for the length byte itself
        // println!("S7 header start: {}", s7_start);

        if s7_start + 10 > packet.len() {
            // println!(
            //     "Packet too short for S7 header: need {} bytes, have {}",
            //     s7_start + 10,
            //     packet.len()
            // );
            validate_s7_header_length(s7_start + 10, packet.len())?;
        }

        // First create the header without error fields
        let mut s7_header = S7Header {
            protocol_id: packet[s7_start],
            rosctr: packet[s7_start + 1],
            reserved: u16::from_be_bytes([packet[s7_start + 2], packet[s7_start + 3]]),
            pduref: u16::from_be_bytes([packet[s7_start + 4], packet[s7_start + 5]]),
            parameter_length: u16::from_be_bytes([packet[s7_start + 6], packet[s7_start + 7]]),
            data_length: u16::from_be_bytes([packet[s7_start + 8], packet[s7_start + 9]]),
            error_class: None,
            error_code: None,
        };

        // Update error fields if this is an ACK/Error message
        if s7_header.rosctr == 0x03 && s7_start + 11 < packet.len() {
            s7_header.error_class = Some(packet[s7_start + 10]);
            s7_header.error_code = Some(packet[s7_start + 11]);
        }

        // println!("S7 Header: {:?}", s7_header);

        // Print packet structure for debugging - only print up to the packet length
        // println!("Packet structure:");
        // println!(
        //     "  TPKT: {:02x} {:02x} {:02x}{:02x}",
        //     packet[0], packet[1], packet[2], packet[3]
        // );
        // println!(
        //     "  COTP: {:02x} {:02x} {:02x}",
        //     packet[4], packet[5], packet[6]
        // );

        // Only print S7 header bytes that exist in the packet
        let s7_header_end = std::cmp::min(s7_start + 12, packet.len());
        // print!("  S7 Header: ");
        for _byte in packet.iter().take(s7_header_end).skip(s7_start) {
            // print!("{:02x} ", byte);
        }
        // println!();

        // The parameter section starts right after the S7 header (10 bytes for header + 2 for error class/code if present)
        let s7_header_length = if s7_header.rosctr == 0x03 { 12 } else { 10 };
        let param_start = s7_start + s7_header_length;

        // println!("Parameter section start: {}", param_start);

        // If there's no parameter data, return an empty parameter section
        let parameter = if s7_header.parameter_length > 0 {
            let param_end = param_start + s7_header.parameter_length as usize;

            if param_end > packet.len() {
                // println!(
                //     "Invalid parameter length: param_end={}, packet_len={}",
                //     param_end,
                //     packet.len()
                // );
                // println!("  TPKT length: {}", tpkt.length);
                // println!("  COTP length: {}", cotp.length);
                // println!("  S7 parameter_length: {}", s7_header.parameter_length);
                // println!("  S7 data_length: {}", s7_header.data_length);
                validate_parameter_length(param_end, packet.len())?;
            }

            Self::parse_parameter(&packet[param_start..param_end])?
        } else {
            // Return empty parameter section
            S7Parameter {
                function: 0,
                items: Vec::new(),
            }
        };

        // Parse payload if present
        let payload = if s7_header.data_length > 0 {
            let data_start = param_start + s7_header.parameter_length as usize;
            let data_end = data_start + s7_header.data_length as usize;
            validate_data_length(data_end, packet.len())?;
            Some(&packet[data_start..data_end])
        } else {
            None
        };

        Ok(S7CommPacket {
            tpkt,
            cotp,
            s7_header,
            parameter,
            payload,
        })
    }

    /// Parses the S7 parameter section of the packet.
    ///
    /// This is a helper function used internally by `try_from` to parse
    /// the parameter section of an S7Comm packet.
    ///
    /// # Arguments
    /// * `data` - The parameter section bytes to parse
    ///
    /// # Returns
    /// * `Ok(S7Parameter)` if parsing was successful
    /// * `Err(S7CommParseError)` if the parameter data is invalid
    fn parse_parameter(data: &'a [u8]) -> Result<S7Parameter<'a>, S7CommParseError> {
        validate_parameter_data_not_empty(data)?;

        // Cas "fonction seule" (ex: parameter_length = 1)
        if data.len() == 1 {
            return Ok(S7Parameter {
                function: data[0],
                items: Vec::new(),
            });
        }

        let function = data[0];
        let item_count = data[1] as usize;
        let mut items = Vec::with_capacity(item_count);
        let mut offset = 2;

        for _ in 0..item_count {
            validate_parameter_item_header(offset, data.len())?;

            let spec_type = data[offset];
            let length = data[offset + 1] as usize;

            validate_parameter_item_length(offset, length, data.len())?;

            if spec_type == 0x12 && length >= 0x0A {
                validate_s7any_length(offset, data.len())?;

                let syntax_id = data[offset + 2];
                let transport_size = data[offset + 3];
                let db_number = u16::from_be_bytes([data[offset + 5], data[offset + 6]]);
                let area = data[offset + 7];
                let address = ((data[offset + 8] as u32) << 16)
                    | ((data[offset + 9] as u32) << 8)
                    | (data[offset + 10] as u32);

                items.push(S7ParameterItem {
                    spec_type,
                    length: length as u8,
                    syntax_id,
                    transport_size,
                    db_number,
                    area,
                    address,
                    raw: Some(&data[offset..offset + 2 + length]),
                });
            } else {
                items.push(S7ParameterItem {
                    spec_type,
                    length: length as u8,
                    syntax_id: 0,
                    transport_size: 0,
                    db_number: 0,
                    area: 0,
                    address: 0,
                    raw: Some(&data[offset..offset + 2 + length]),
                });
            }

            offset += 2 + length;
        }

        // Important : certains paquets ont item_count=0 => OK.
        Ok(S7Parameter { function, items })
    }
}

impl<'a> TryFrom<&'a [u8]> for S7CommPacket<'a> {
    type Error = S7CommParseError;

    fn try_from(packet: &'a [u8]) -> Result<Self, Self::Error> {
        Self::parse(packet)
    }
}

impl<'a> fmt::Display for S7CommPacket<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "S7Comm Protocol ")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use hex;

    #[test]
    fn test_s7comm_try_from() {
        // The provided hex string
        let hex_str = "0300001f02f080320100000013000e00000401120a10020001000083000000";

        // Convert hex string to bytes
        let bytes = hex::decode(hex_str).expect("Failed to decode hex string");

        // Try to parse as S7Comm packet
        let result = S7CommPacket::try_from(&bytes[..]);

        // Check if parsing succeeded
        assert!(
            result.is_ok(),
            "Failed to parse S7Comm packet: {:?}",
            result.err().unwrap()
        );

        // Add more assertions based on the expected values from your packet
    }

    #[test]
    fn test_s7comm_parse() {
        let hex_str = "0300003102f080320100000e00002000001a00010000000000095f30413030303031500d31303030353030303030343030";
        let bytes = hex::decode(hex_str).expect("Failed to decode hex string");
        let result = S7CommPacket::try_from(&bytes[..]);
        assert!(
            result.is_ok(),
            "Failed to parse S7Comm packet: {:?}",
            result.err().unwrap()
        );
    }

    #[test]
    fn test_parameter_request_download() {
        let hex_str = "0300001402f080320300000e000001000000001a";
        let bytes = hex::decode(hex_str).expect("Failed to decode hex string");
        let result = S7CommPacket::try_from(&bytes[..]);
        assert!(
            result.is_ok(),
            "Failed to parse S7Comm packet: {:?}",
            result.err().unwrap()
        );
    }
}