pub type Result<T> = std::result::Result<T, TagPathError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagPathError {
message: String,
}
impl TagPathError {
pub fn protocol(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for TagPathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for TagPathError {}
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TagPath {
Controller { tag_name: String },
Program {
program_name: String,
tag_name: String,
},
Array {
base_path: Box<TagPath>,
indices: Vec<u32>,
},
Bit {
base_path: Box<TagPath>,
bit_index: u8,
},
Member {
base_path: Box<TagPath>,
member_name: String,
},
StringLength { base_path: Box<TagPath> },
StringData { base_path: Box<TagPath>, index: u32 },
}
impl TagPath {
pub fn parse(path_str: &str) -> Result<Self> {
let parser = TagPathParser::new(path_str);
parser.parse()
}
pub fn as_string(&self) -> String {
match self {
TagPath::Controller { tag_name } => tag_name.clone(),
TagPath::Program {
program_name,
tag_name,
} => {
format!("Program:{program_name}.{tag_name}")
}
TagPath::Array { base_path, indices } => {
let base = base_path.as_string();
let indices_str = indices
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(",");
format!("{base}[{indices_str}]")
}
TagPath::Bit {
base_path,
bit_index,
} => {
format!("{base_path}.{bit_index}")
}
TagPath::Member {
base_path,
member_name,
} => {
format!("{base_path}.{member_name}")
}
TagPath::StringLength { base_path } => {
format!("{base_path}.LEN")
}
TagPath::StringData { base_path, index } => {
format!("{base_path}.DATA[{index}]")
}
}
}
pub fn to_cip_path(&self) -> Result<Vec<u8>> {
let mut path = Vec::new();
self.build_cip_path(&mut path)?;
if !path.len().is_multiple_of(2) {
path.push(0x00);
}
Ok(path)
}
fn build_cip_path(&self, path: &mut Vec<u8>) -> Result<()> {
match self {
TagPath::Controller { tag_name } => {
path.push(0x91);
path.push(tag_name.len() as u8);
path.extend_from_slice(tag_name.as_bytes());
}
TagPath::Program {
program_name,
tag_name,
} => {
path.push(0x91);
let program_path = format!("Program:{program_name}");
path.push(program_path.len() as u8);
path.extend_from_slice(program_path.as_bytes());
if !path.len().is_multiple_of(2) {
path.push(0x00);
}
path.push(0x91);
path.push(tag_name.len() as u8);
path.extend_from_slice(tag_name.as_bytes());
}
TagPath::Array { base_path, indices } => {
base_path.build_cip_path(path)?;
if !path.len().is_multiple_of(2) {
path.push(0x00);
}
for &index in indices {
append_element_id_segment(path, index);
}
}
TagPath::Bit {
base_path,
bit_index: _,
} => {
base_path.build_cip_path(path)?;
}
TagPath::Member {
base_path,
member_name,
} => {
base_path.build_cip_path(path)?;
if !path.len().is_multiple_of(2) {
path.push(0x00);
}
path.push(0x91);
path.push(member_name.len() as u8);
path.extend_from_slice(member_name.as_bytes());
}
TagPath::StringLength { base_path } => {
base_path.build_cip_path(path)?;
if !path.len().is_multiple_of(2) {
path.push(0x00);
}
path.push(0x91);
path.push(3); path.extend_from_slice(b"LEN");
}
TagPath::StringData { base_path, index } => {
base_path.build_cip_path(path)?;
if !path.len().is_multiple_of(2) {
path.push(0x00);
}
path.push(0x91);
path.push(4); path.extend_from_slice(b"DATA");
if !path.len().is_multiple_of(2) {
path.push(0x00);
}
append_element_id_segment(path, *index);
}
}
Ok(())
}
pub fn base_tag_name(&self) -> String {
match self {
TagPath::Controller { tag_name } => tag_name.clone(),
TagPath::Program { tag_name, .. } => tag_name.clone(),
TagPath::Array { base_path, .. } => base_path.base_tag_name(),
TagPath::Bit { base_path, .. } => base_path.base_tag_name(),
TagPath::Member { base_path, .. } => base_path.base_tag_name(),
TagPath::StringLength { base_path } => base_path.base_tag_name(),
TagPath::StringData { base_path, .. } => base_path.base_tag_name(),
}
}
pub fn is_program_scoped(&self) -> bool {
match self {
TagPath::Program { .. } => true,
TagPath::Array { base_path, .. } => base_path.is_program_scoped(),
TagPath::Bit { base_path, .. } => base_path.is_program_scoped(),
TagPath::Member { base_path, .. } => base_path.is_program_scoped(),
TagPath::StringLength { base_path } => base_path.is_program_scoped(),
TagPath::StringData { base_path, .. } => base_path.is_program_scoped(),
TagPath::Controller { .. } => false,
}
}
pub fn program_name(&self) -> Option<String> {
match self {
TagPath::Program { program_name, .. } => Some(program_name.clone()),
TagPath::Array { base_path, .. } => base_path.program_name(),
TagPath::Bit { base_path, .. } => base_path.program_name(),
TagPath::Member { base_path, .. } => base_path.program_name(),
TagPath::StringLength { base_path } => base_path.program_name(),
TagPath::StringData { base_path, .. } => base_path.program_name(),
TagPath::Controller { .. } => None,
}
}
}
fn append_element_id_segment(path: &mut Vec<u8>, index: u32) {
if index <= 255 {
path.push(0x28);
path.push(index as u8);
} else if index <= 65535 {
path.push(0x29);
path.push(0x00);
path.extend_from_slice(&(index as u16).to_le_bytes());
} else {
path.push(0x2A);
path.push(0x00);
path.extend_from_slice(&index.to_le_bytes());
}
}
impl fmt::Display for TagPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_string())
}
}
struct TagPathParser<'a> {
input: &'a str,
position: usize,
}
impl<'a> TagPathParser<'a> {
fn new(input: &'a str) -> Self {
Self { input, position: 0 }
}
fn parse(mut self) -> Result<TagPath> {
self.parse_path()
}
fn parse_path(&mut self) -> Result<TagPath> {
if self.input.starts_with("Program:") {
self.parse_program_scoped()
} else {
self.parse_controller_scoped()
}
}
fn parse_program_scoped(&mut self) -> Result<TagPath> {
self.position = 8;
let program_name = self.parse_identifier()?;
if !self.consume_char('.') {
return Err(TagPathError::protocol(
"Expected '.' after program name".to_string(),
));
}
let tag_name = self.parse_identifier()?;
let mut path = TagPath::Program {
program_name,
tag_name,
};
while self.position < self.input.len() {
path = self.parse_qualifier(path)?;
}
Ok(path)
}
fn parse_controller_scoped(&mut self) -> Result<TagPath> {
let tag_name = self.parse_identifier()?;
let mut path = TagPath::Controller { tag_name };
while self.position < self.input.len() {
path = self.parse_qualifier(path)?;
}
Ok(path)
}
fn parse_qualifier(&mut self, base_path: TagPath) -> Result<TagPath> {
match self.peek_char() {
Some('[') => self.parse_array_access(base_path),
Some('.') => self.parse_member_or_bit_access(base_path),
_ => Err(TagPathError::protocol(format!(
"Unexpected character at position {}",
self.position
))),
}
}
fn parse_array_access(&mut self, base_path: TagPath) -> Result<TagPath> {
self.consume_char('[');
let mut indices = Vec::new();
indices.push(self.parse_number()?);
while self.peek_char() == Some(',') {
self.consume_char(',');
indices.push(self.parse_number()?);
}
if !self.consume_char(']') {
return Err(TagPathError::protocol(
"Expected ']' after array indices".to_string(),
));
}
Ok(TagPath::Array {
base_path: Box::new(base_path),
indices,
})
}
fn parse_member_or_bit_access(&mut self, base_path: TagPath) -> Result<TagPath> {
self.consume_char('.');
let remaining = &self.input[self.position..];
if remaining.starts_with("LEN")
&& (remaining.len() == 3
|| remaining.as_bytes()[3] == b'.'
|| remaining.as_bytes()[3] == b'[')
{
self.position += 3;
return Ok(TagPath::StringLength {
base_path: Box::new(base_path),
});
}
if remaining.starts_with("DATA[") {
self.position += 5; let index = self.parse_number()?;
if !self.consume_char(']') {
return Err(TagPathError::protocol(
"Expected ']' after DATA index".to_string(),
));
}
return Ok(TagPath::StringData {
base_path: Box::new(base_path),
index,
});
}
let identifier = self.parse_identifier()?;
if let Ok(bit_index) = identifier.parse::<u8>()
&& bit_index < 32
{
return Ok(TagPath::Bit {
base_path: Box::new(base_path),
bit_index,
});
}
Ok(TagPath::Member {
base_path: Box::new(base_path),
member_name: identifier,
})
}
fn parse_identifier(&mut self) -> Result<String> {
let start = self.position;
while self.position < self.input.len() {
let ch = self.input.as_bytes()[self.position];
if ch.is_ascii_alphanumeric() || ch == b'_' {
self.position += 1;
} else {
break;
}
}
if start == self.position {
return Err(TagPathError::protocol("Expected identifier".to_string()));
}
Ok(self.input[start..self.position].to_string())
}
fn parse_number(&mut self) -> Result<u32> {
let start = self.position;
while self.position < self.input.len() {
let ch = self.input.as_bytes()[self.position];
if ch.is_ascii_digit() {
self.position += 1;
} else {
break;
}
}
if start == self.position {
return Err(TagPathError::protocol("Expected number".to_string()));
}
self.input[start..self.position]
.parse()
.map_err(|_| TagPathError::protocol("Invalid number".to_string()))
}
fn peek_char(&self) -> Option<char> {
self.input
.as_bytes()
.get(self.position)
.map(|byte| *byte as char)
}
fn consume_char(&mut self, expected: char) -> bool {
if self.peek_char() == Some(expected) {
self.position += 1;
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_controller_scoped_tag() {
let path = TagPath::parse("MyTag").unwrap();
assert_eq!(
path,
TagPath::Controller {
tag_name: "MyTag".to_string()
}
);
assert_eq!(path.to_string(), "MyTag");
}
#[test]
fn test_program_scoped_tag() {
let path = TagPath::parse("Program:MainProgram.MyTag").unwrap();
assert_eq!(
path,
TagPath::Program {
program_name: "MainProgram".to_string(),
tag_name: "MyTag".to_string()
}
);
assert_eq!(path.to_string(), "Program:MainProgram.MyTag");
assert!(path.is_program_scoped());
assert_eq!(path.program_name(), Some("MainProgram".to_string()));
}
#[test]
fn test_array_access() {
let path = TagPath::parse("MyArray[5]").unwrap();
if let TagPath::Array { base_path, indices } = path {
assert_eq!(
*base_path,
TagPath::Controller {
tag_name: "MyArray".to_string()
}
);
assert_eq!(indices, vec![5]);
} else {
panic!("Expected Array path");
}
}
#[test]
fn test_multi_dimensional_array() {
let path = TagPath::parse("Matrix[1,2,3]").unwrap();
if let TagPath::Array { base_path, indices } = path {
assert_eq!(
*base_path,
TagPath::Controller {
tag_name: "Matrix".to_string()
}
);
assert_eq!(indices, vec![1, 2, 3]);
} else {
panic!("Expected Array path");
}
}
#[test]
fn test_bit_access() {
let path = TagPath::parse("StatusWord.15").unwrap();
if let TagPath::Bit {
base_path,
bit_index,
} = path
{
assert_eq!(
*base_path,
TagPath::Controller {
tag_name: "StatusWord".to_string()
}
);
assert_eq!(bit_index, 15);
} else {
panic!("Expected Bit path");
}
}
#[test]
fn test_bit_path_emits_base_path_only() {
let bit_path = TagPath::parse("StatusWord.15")
.unwrap()
.to_cip_path()
.unwrap();
let base_path = TagPath::parse("StatusWord").unwrap().to_cip_path().unwrap();
assert_eq!(
bit_path, base_path,
"bit path should encode identically to its parent tag"
);
let mut expected = vec![0x91, 0x0A];
expected.extend_from_slice(b"StatusWord");
assert_eq!(bit_path, expected);
assert!(!bit_path.contains(&0x29));
}
#[test]
fn test_member_access() {
let path = TagPath::parse("MotorData.Speed").unwrap();
if let TagPath::Member {
base_path,
member_name,
} = path
{
assert_eq!(
*base_path,
TagPath::Controller {
tag_name: "MotorData".to_string()
}
);
assert_eq!(member_name, "Speed");
} else {
panic!("Expected Member path");
}
}
#[test]
fn test_string_length() {
let path = TagPath::parse("MyString.LEN").unwrap();
if let TagPath::StringLength { base_path } = path {
assert_eq!(
*base_path,
TagPath::Controller {
tag_name: "MyString".to_string()
}
);
} else {
panic!("Expected StringLength path");
}
}
#[test]
fn test_string_data() {
let path = TagPath::parse("MyString.DATA[5]").unwrap();
if let TagPath::StringData { base_path, index } = path {
assert_eq!(
*base_path,
TagPath::Controller {
tag_name: "MyString".to_string()
}
);
assert_eq!(index, 5);
} else {
panic!("Expected StringData path");
}
}
#[test]
fn test_string_data_cip_path_uses_8_bit_element_segment() {
let path = TagPath::parse("MyString.DATA[5]")
.unwrap()
.to_cip_path()
.unwrap();
let mut expected = vec![0x91, 0x08];
expected.extend_from_slice(b"MyString");
expected.extend_from_slice(&[0x91, 0x04]);
expected.extend_from_slice(b"DATA");
expected.extend_from_slice(&[0x28, 0x05]);
assert_eq!(path, expected);
}
#[test]
fn test_string_data_cip_path_uses_16_bit_element_segment() {
let path = TagPath::parse("MyString.DATA[300]")
.unwrap()
.to_cip_path()
.unwrap();
let mut expected = vec![0x91, 0x08];
expected.extend_from_slice(b"MyString");
expected.extend_from_slice(&[0x91, 0x04]);
expected.extend_from_slice(b"DATA");
expected.extend_from_slice(&[0x29, 0x00]);
expected.extend_from_slice(&300u16.to_le_bytes());
assert_eq!(path, expected);
}
#[test]
fn test_complex_nested_path() {
let path = TagPath::parse("Program:Safety.Devices[2].Status.15").unwrap();
if let TagPath::Bit {
base_path,
bit_index,
} = path
{
assert_eq!(bit_index, 15);
if let TagPath::Member {
base_path,
member_name,
} = *base_path
{
assert_eq!(member_name, "Status");
if let TagPath::Array { base_path, indices } = *base_path {
assert_eq!(indices, vec![2]);
if let TagPath::Program {
program_name,
tag_name,
} = *base_path
{
assert_eq!(program_name, "Safety");
assert_eq!(tag_name, "Devices");
} else {
panic!("Expected Program path");
}
} else {
panic!("Expected Array path");
}
} else {
panic!("Expected Member path");
}
} else {
panic!("Expected Bit path");
}
}
#[test]
fn test_cip_path_generation() {
let path = TagPath::parse("MyTag").unwrap();
let cip_path = path.to_cip_path().unwrap();
assert_eq!(cip_path[0], 0x91); assert_eq!(cip_path[1], 5); assert_eq!(&cip_path[2..7], b"MyTag");
assert_eq!(cip_path[7], 0x00); }
#[test]
fn test_array_cip_path_generation() {
let path = TagPath::parse("MyArray[5]").unwrap();
let cip_path = path.to_cip_path().unwrap();
assert_eq!(cip_path[0], 0x91); assert_eq!(cip_path[1], 7); assert_eq!(&cip_path[2..9], b"MyArray");
assert_eq!(cip_path[9], 0x00);
assert_eq!(cip_path[10], 0x28); assert_eq!(cip_path[11], 0x05); assert_eq!(cip_path.len(), 12); }
#[test]
fn test_program_array_cip_path_generation() {
let path = TagPath::parse("Program:MainProgram.ArrayTest[0]").unwrap();
let cip_path = path.to_cip_path().unwrap();
tracing::debug!(
"Program array CIP path ({} bytes): {:02X?}",
cip_path.len(),
cip_path
);
assert_eq!(cip_path[0], 0x91);
assert_eq!(cip_path[1], 19); assert_eq!(&cip_path[2..21], b"Program:MainProgram");
assert_eq!(cip_path[21], 0x00);
assert_eq!(cip_path[22], 0x91);
assert_eq!(cip_path[23], 9); assert_eq!(&cip_path[24..33], b"ArrayTest");
assert_eq!(cip_path[33], 0x00);
assert_eq!(cip_path[34], 0x28); assert_eq!(cip_path[35], 0x00);
assert_eq!(cip_path.len(), 36);
}
#[test]
fn test_base_tag_name() {
let path = TagPath::parse("Program:Main.MotorData[1].Speed.15").unwrap();
assert_eq!(path.base_tag_name(), "MotorData");
}
#[test]
fn test_invalid_paths() {
assert!(TagPath::parse("").is_err());
assert!(TagPath::parse("Program:").is_err());
assert!(TagPath::parse("MyArray[").is_err());
assert!(TagPath::parse("MyArray]").is_err());
assert!(TagPath::parse("MyTag.").is_err());
}
}