1use crate::proto::s7::header::{Area, TransportSize};
2
3use crate::error::{Error, Result};
4
5#[derive(Debug, Clone)]
6pub struct TagAddress {
7 pub area: Area,
8 pub db_number: u16,
9 pub byte_offset: u32,
10 pub bit_offset: u8,
11 pub transport: TransportSize,
12 pub element_count: u16,
13}
14
15pub fn parse_tag(tag: &str) -> Result<TagAddress> {
16 let upper = tag.to_uppercase();
17
18 if upper.starts_with('T') && !upper.starts_with("TM") && !upper.contains(',') {
21 let idx_str = upper.strip_prefix('T').unwrap_or("");
22 let index: u32 = idx_str.parse().map_err(|_| Error::PlcError {
23 code: 0,
24 message: format!("invalid timer index in tag: {}", tag),
25 })?;
26 return Ok(TagAddress {
27 area: Area::Timer,
28 db_number: 0,
29 byte_offset: index,
30 bit_offset: 0,
31 transport: TransportSize::Timer,
32 element_count: 1,
33 });
34 }
35 if upper.starts_with('C') && !upper.starts_with("CT") && !upper.contains(',') {
36 let idx_str = upper.strip_prefix('C').unwrap_or("");
37 let index: u32 = idx_str.parse().map_err(|_| Error::PlcError {
38 code: 0,
39 message: format!("invalid counter index in tag: {}", tag),
40 })?;
41 return Ok(TagAddress {
42 area: Area::Counter,
43 db_number: 0,
44 byte_offset: index,
45 bit_offset: 0,
46 transport: TransportSize::Counter,
47 element_count: 1,
48 });
49 }
50 if upper.starts_with('M') && !upper.starts_with("MK") && !upper.contains(',') {
51 return parse_marker_tag(&upper, "", tag);
52 }
53
54 let normalized: std::borrow::Cow<str> = if tag.contains(',') {
58 std::borrow::Cow::Borrowed(tag)
59 } else {
60 let bytes = tag.as_bytes();
64 let sep = bytes.windows(3).enumerate().find_map(|(i, w)| {
65 if w[0].is_ascii_digit() && w[1] == b'.' && w[2].is_ascii_alphabetic() {
66 Some(i + 1) } else {
68 None
69 }
70 });
71 if let Some(pos) = sep {
72 let mut s = tag.to_string();
73 s.replace_range(pos..pos + 1, ",");
74 std::borrow::Cow::Owned(s)
75 } else {
76 std::borrow::Cow::Borrowed(tag)
77 }
78 };
79
80 let parts: Vec<&str> = normalized.splitn(2, ',').collect();
81 if parts.len() != 2 {
82 return Err(Error::PlcError {
83 code: 0,
84 message: format!("invalid tag: {}", tag),
85 });
86 }
87 let area_part = parts[0].to_uppercase();
88 let type_part = parts[1].to_uppercase();
89
90 let (area, db_number) = if let Some(rest) = area_part.strip_prefix("DB") {
91 let n: u16 = rest.parse().map_err(|_| Error::PlcError {
92 code: 0,
93 message: format!("invalid DB number in tag: {}", tag),
94 })?;
95 (Area::DataBlock, n)
96 } else {
97 return Err(Error::PlcError {
98 code: 0,
99 message: format!("unsupported area in tag: {}", tag),
100 });
101 };
102
103 if type_part.starts_with(|c: char| c.is_ascii_digit()) {
107 let bits: Vec<&str> = type_part.split('.').collect();
108 if bits.len() == 2 {
109 let byte_offset: u32 = bits[0].parse().map_err(|_| Error::PlcError {
110 code: 0,
111 message: format!("invalid byte offset in tag: {}", tag),
112 })?;
113 let bit_offset: u8 = bits[1].parse().map_err(|_| Error::PlcError {
114 code: 0,
115 message: format!("invalid bit offset in tag: {}", tag),
116 })?;
117 if bit_offset > 7 {
118 return Err(Error::PlcError {
119 code: 0,
120 message: format!("bit offset must be 0-7: {}", tag),
121 });
122 }
123 return Ok(TagAddress {
124 area,
125 db_number,
126 byte_offset,
127 bit_offset,
128 transport: TransportSize::Bit,
129 element_count: 1,
130 });
131 }
132 }
133
134 let (transport, byte_offset) = parse_typed_offset(&type_part, tag)?;
135
136 Ok(TagAddress {
137 area,
138 db_number,
139 byte_offset,
140 bit_offset: 0,
141 transport,
142 element_count: 1,
143 })
144}
145
146fn parse_marker_tag(area_part: &str, _type_part: &str, tag: &str) -> Result<TagAddress> {
153 let rest = area_part.strip_prefix('M').unwrap_or("");
155
156 let (is_bit, offset_str) = if let Some(r) = rest.strip_prefix('X') {
158 (true, r)
159 } else if rest.starts_with(|c: char| c.is_ascii_digit()) {
160 (true, rest)
161 } else {
162 (false, rest)
163 };
164
165 if is_bit {
166 let parts: Vec<&str> = offset_str.split('.').collect();
168 if parts.len() == 2 {
169 let byte_offset: u32 = parts[0].parse().map_err(|_| Error::PlcError {
170 code: 0,
171 message: format!("invalid byte offset in marker tag: {}", tag),
172 })?;
173 let bit_offset: u8 = parts[1].parse().map_err(|_| Error::PlcError {
174 code: 0,
175 message: format!("invalid bit offset in marker tag: {}", tag),
176 })?;
177 if bit_offset > 7 {
178 return Err(Error::PlcError {
179 code: 0,
180 message: format!("bit offset must be 0-7: {}", tag),
181 });
182 }
183 return Ok(TagAddress {
184 area: Area::Marker,
185 db_number: 0,
186 byte_offset,
187 bit_offset,
188 transport: TransportSize::Bit,
189 element_count: 1,
190 });
191 }
192 return Err(Error::PlcError {
193 code: 0,
194 message: format!("invalid marker bit tag (expected M<byte>.<bit>): {}", tag),
195 });
196 }
197
198 let (transport, offset_str) = if let Some(r) = rest.strip_prefix('B') {
200 (TransportSize::Byte, r)
201 } else if let Some(r) = rest.strip_prefix('W') {
202 (TransportSize::Word, r)
203 } else if let Some(r) = rest.strip_prefix('D') {
204 (TransportSize::DWord, r)
205 } else {
206 return Err(Error::PlcError {
207 code: 0,
208 message: format!("unsupported marker type in tag: {} (use MB/MW/MD/MX or M<byte>.<bit>)", tag),
209 });
210 };
211
212 let byte_offset: u32 = offset_str.parse().map_err(|_| Error::PlcError {
213 code: 0,
214 message: format!("invalid offset in marker tag: {}", tag),
215 })?;
216
217 Ok(TagAddress {
218 area: Area::Marker,
219 db_number: 0,
220 byte_offset,
221 bit_offset: 0,
222 transport,
223 element_count: 1,
224 })
225}
226
227fn parse_typed_offset(type_part: &str, tag: &str) -> Result<(TransportSize, u32)> {
228 if let Some(rest) = type_part.strip_prefix("REAL") {
229 Ok((TransportSize::Real, rest.parse().unwrap_or(0)))
230 } else if let Some(rest) = type_part.strip_prefix("DWORD") {
231 Ok((TransportSize::DWord, rest.parse().unwrap_or(0)))
232 } else if let Some(rest) = type_part.strip_prefix("DINT") {
233 Ok((TransportSize::DInt, rest.parse().unwrap_or(0)))
234 } else if let Some(rest) = type_part.strip_prefix("WORD") {
235 Ok((TransportSize::Word, rest.parse().unwrap_or(0)))
236 } else if let Some(rest) = type_part.strip_prefix("INT") {
237 Ok((TransportSize::Int, rest.parse().unwrap_or(0)))
238 } else if let Some(rest) = type_part.strip_prefix("BYTE") {
239 Ok((TransportSize::Byte, rest.parse().unwrap_or(0)))
240 } else {
241 Err(Error::PlcError {
242 code: 0,
243 message: format!("unsupported type in tag: {}", tag),
244 })
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn parse_db_real() {
254 let tag = parse_tag("DB1,REAL4").unwrap();
255 assert_eq!(tag.db_number, 1);
256 assert_eq!(tag.byte_offset, 4);
257 assert_eq!(tag.transport, TransportSize::Real);
258 }
259
260 #[test]
261 fn parse_db_word() {
262 let tag = parse_tag("DB2,WORD10").unwrap();
263 assert_eq!(tag.db_number, 2);
264 assert_eq!(tag.byte_offset, 10);
265 assert_eq!(tag.transport, TransportSize::Word);
266 }
267
268 #[test]
269 fn parse_db_dint() {
270 let tag = parse_tag("DB70,DINT0").unwrap();
271 assert_eq!(tag.db_number, 70);
272 assert_eq!(tag.byte_offset, 0);
273 assert_eq!(tag.transport, TransportSize::DInt);
274 assert_eq!(tag.bit_offset, 0);
275 }
276
277 #[test]
278 fn parse_db_bit_access() {
279 let tag = parse_tag("DB70,332.0").unwrap();
280 assert_eq!(tag.db_number, 70);
281 assert_eq!(tag.byte_offset, 332);
282 assert_eq!(tag.bit_offset, 0);
283 assert_eq!(tag.transport, TransportSize::Bit);
284 }
285
286 #[test]
287 fn parse_db_bit_access_bit7() {
288 let tag = parse_tag("DB70,332.7").unwrap();
289 assert_eq!(tag.db_number, 70);
290 assert_eq!(tag.byte_offset, 332);
291 assert_eq!(tag.bit_offset, 7);
292 assert_eq!(tag.transport, TransportSize::Bit);
293 }
294
295 #[test]
296 fn parse_db_bit_invalid_bit() {
297 let result = parse_tag("DB70,332.8");
298 assert!(result.is_err());
299 }
300
301 #[test]
302 fn parse_invalid_returns_err() {
303 assert!(parse_tag("NOTVALID").is_err());
304 }
305
306 #[test]
307 fn parse_dot_separator_real() {
308 let tag = parse_tag("DB170.REAL262").unwrap();
309 assert_eq!(tag.db_number, 170);
310 assert_eq!(tag.byte_offset, 262);
311 assert_eq!(tag.transport, TransportSize::Real);
312 }
313
314 #[test]
315 fn parse_dot_separator_word() {
316 let tag = parse_tag("DB1.WORD10").unwrap();
317 assert_eq!(tag.db_number, 1);
318 assert_eq!(tag.byte_offset, 10);
319 assert_eq!(tag.transport, TransportSize::Word);
320 }
321
322 #[test]
323 fn parse_comma_separator_unchanged() {
324 let a = parse_tag("DB170,REAL262").unwrap();
325 let b = parse_tag("DB170.REAL262").unwrap();
326 assert_eq!(a.db_number, b.db_number);
327 assert_eq!(a.byte_offset, b.byte_offset);
328 assert_eq!(a.transport, b.transport);
329 }
330
331 #[test]
332 fn parse_bit_access_dot_not_confused() {
333 let tag = parse_tag("DB70,332.0").unwrap();
335 assert_eq!(tag.transport, TransportSize::Bit);
336 assert_eq!(tag.byte_offset, 332);
337 assert_eq!(tag.bit_offset, 0);
338 }
339
340 #[test]
341 fn parse_timer_single_part() {
342 let tag = parse_tag("T5").unwrap();
343 assert_eq!(tag.area, Area::Timer);
344 assert_eq!(tag.byte_offset, 5);
345 assert_eq!(tag.transport, TransportSize::Timer);
346 assert_eq!(tag.db_number, 0);
347 }
348
349 #[test]
350 fn parse_counter_single_part() {
351 let tag = parse_tag("C3").unwrap();
352 assert_eq!(tag.area, Area::Counter);
353 assert_eq!(tag.byte_offset, 3);
354 assert_eq!(tag.transport, TransportSize::Counter);
355 assert_eq!(tag.db_number, 0);
356 }
357
358 #[test]
359 fn parse_marker_byte_single_part() {
360 let tag = parse_tag("MB10").unwrap();
361 assert_eq!(tag.area, Area::Marker);
362 assert_eq!(tag.byte_offset, 10);
363 assert_eq!(tag.transport, TransportSize::Byte);
364 }
365
366 #[test]
367 fn parse_marker_word_single_part() {
368 let tag = parse_tag("MW20").unwrap();
369 assert_eq!(tag.area, Area::Marker);
370 assert_eq!(tag.byte_offset, 20);
371 assert_eq!(tag.transport, TransportSize::Word);
372 }
373
374 #[test]
375 fn parse_marker_dword_single_part() {
376 let tag = parse_tag("MD4").unwrap();
377 assert_eq!(tag.area, Area::Marker);
378 assert_eq!(tag.byte_offset, 4);
379 assert_eq!(tag.transport, TransportSize::DWord);
380 }
381
382 #[test]
383 fn parse_marker_bit_single_part() {
384 let tag = parse_tag("M10.3").unwrap();
385 assert_eq!(tag.area, Area::Marker);
386 assert_eq!(tag.byte_offset, 10);
387 assert_eq!(tag.bit_offset, 3);
388 assert_eq!(tag.transport, TransportSize::Bit);
389 }
390
391 #[test]
392 fn parse_marker_bit_mx_prefix() {
393 let tag = parse_tag("MX5.7").unwrap();
394 assert_eq!(tag.area, Area::Marker);
395 assert_eq!(tag.byte_offset, 5);
396 assert_eq!(tag.bit_offset, 7);
397 assert_eq!(tag.transport, TransportSize::Bit);
398 }
399}