1mod datetime;
11mod json;
12mod numeric;
13mod typed;
14
15pub use datetime::{parse_date, parse_date_time, parse_time, parse_timestamp};
16pub use typed::{parse_value_for, parse_value_for_with_priority};
17
18use bacnet_types::enums::{ObjectType, PropertyIdentifier};
19use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
20
21#[cfg(test)]
22mod datetime_tests;
23#[cfg(test)]
24mod json_tests;
25#[cfg(test)]
26mod typed_more_tests;
27#[cfg(test)]
28mod typed_tests;
29
30const OBJ_ABBREVS: &[(&str, u32)] = &[
38 ("ai", 0), ("ao", 1), ("av", 2), ("bi", 3), ("bo", 4), ("bv", 5), ("dev", 8), ("msi", 13), ("mso", 14), ("msv", 19), ("lp", 21), ("lsp", 21), ("sc", 17), ("cal", 6), ("nc", 15), ("trn", 20), ("acc", 23), ("pi", 24), ("lo", 12), ("prg", 16), ("cmd", 7), ];
60
61const PROP_ABBREVS: &[(&str, u32)] = &[
63 ("pv", 85), ("on", 77), ("ot", 79), ("desc", 28), ("sf", 111), ("es", 36), ("oos", 81), ("pa", 87), ("rd", 104), ("ol", 76), ("all", 8), ];
75
76pub fn parse_object_type(s: &str) -> Result<ObjectType, String> {
83 let s = s.trim();
84
85 if let Ok(n) = s.parse::<u32>() {
87 return Ok(ObjectType::from_raw(n));
88 }
89
90 let lower = s.to_ascii_lowercase();
92 for &(abbr, raw) in OBJ_ABBREVS {
93 if lower == abbr {
94 return Ok(ObjectType::from_raw(raw));
95 }
96 }
97
98 let normalized = lower.replace('-', "_");
100 for &(name, val) in ObjectType::ALL_NAMED {
101 if name.eq_ignore_ascii_case(&normalized) {
102 return Ok(val);
103 }
104 }
105
106 Err(format!("unknown object type: '{s}'"))
107}
108
109pub fn parse_object_specifier(s: &str) -> Result<(ObjectType, u32), String> {
111 let (type_str, inst_str) = s
112 .rsplit_once(':')
113 .ok_or_else(|| format!("expected 'type:instance' format, got '{s}'"))?;
114
115 let obj_type = parse_object_type(type_str)?;
116 let instance = inst_str
117 .parse::<u32>()
118 .map_err(|_| format!("invalid instance number: '{inst_str}'"))?;
119
120 Ok((obj_type, instance))
121}
122
123pub fn parse_property(s: &str) -> Result<(PropertyIdentifier, Option<u32>), String> {
133 let s = s.trim();
134
135 let (prop_str, array_index) = if let Some(bracket_pos) = s.find('[') {
137 let end = s
138 .find(']')
139 .ok_or_else(|| format!("missing closing ']' in '{s}'"))?;
140 if end <= bracket_pos {
141 return Err(format!("malformed array index in '{s}'"));
142 }
143 let idx_str = &s[bracket_pos + 1..end];
144 let idx = idx_str
145 .parse::<u32>()
146 .map_err(|_| format!("invalid array index: '{idx_str}'"))?;
147 (&s[..bracket_pos], Some(idx))
148 } else {
149 (s, None)
150 };
151
152 let prop = parse_property_name(prop_str)?;
153 Ok((prop, array_index))
154}
155
156fn parse_property_name(s: &str) -> Result<PropertyIdentifier, String> {
158 if let Ok(n) = s.parse::<u32>() {
160 return Ok(PropertyIdentifier::from_raw(n));
161 }
162
163 let lower = s.to_ascii_lowercase();
165 for &(abbr, raw) in PROP_ABBREVS {
166 if lower == abbr {
167 return Ok(PropertyIdentifier::from_raw(raw));
168 }
169 }
170
171 let normalized = lower.replace('-', "_");
173 for &(name, val) in PropertyIdentifier::ALL_NAMED {
174 if name.eq_ignore_ascii_case(&normalized) {
175 return Ok(val);
176 }
177 }
178
179 Err(format!("unknown property: '{s}'"))
180}
181
182pub fn parse_value(s: &str) -> Result<PropertyValue, String> {
202 let s = s.trim();
203
204 if s.eq_ignore_ascii_case("null") {
206 return Ok(PropertyValue::Null);
207 }
208
209 if s.eq_ignore_ascii_case("true") {
211 return Ok(PropertyValue::Boolean(true));
212 }
213 if s.eq_ignore_ascii_case("false") {
214 return Ok(PropertyValue::Boolean(false));
215 }
216
217 if s.eq_ignore_ascii_case("active") {
219 return Ok(PropertyValue::Enumerated(1));
220 }
221 if s.eq_ignore_ascii_case("inactive") {
222 return Ok(PropertyValue::Enumerated(0));
223 }
224
225 if let Some(rest) = s.strip_prefix("date:") {
227 return parse_date(rest).map(PropertyValue::Date);
228 }
229
230 if let Some(rest) = s.strip_prefix("time:") {
232 return parse_time(rest).map(PropertyValue::Time);
233 }
234
235 if s.eq_ignore_ascii_case("any time") || datetime::looks_like_time(s) {
237 return parse_time(s).map(PropertyValue::Time);
238 }
239 let lower = s.to_ascii_lowercase();
240 if datetime::looks_like_date(s) || lower.starts_with("any date") || lower == "unspecified" {
241 return match parse_date_time(s)? {
242 (date, None) => Ok(PropertyValue::Date(date)),
243 (date, Some(time)) => Ok(PropertyValue::List(vec![
244 PropertyValue::Date(date),
245 PropertyValue::Time(time),
246 ])),
247 };
248 }
249
250 if let Some(bits) = s.strip_prefix("bits(").and_then(|b| b.strip_suffix(')')) {
252 if !bits.bytes().all(|b| b == b'0' || b == b'1') {
253 return Err(format!(
254 "invalid bits '{bits}': 0/1 digits, first bit first"
255 ));
256 }
257 let set: Vec<bool> = bits.bytes().map(|b| b == b'1').collect();
258 return Ok(typed::bit_string(&set));
259 }
260
261 if let Some(rest) = s.strip_prefix("object:") {
263 let (obj_type, instance) = parse_object_specifier(rest)?;
264 let oid = ObjectIdentifier::new(obj_type, instance)
265 .map_err(|e| format!("invalid object identifier: {e}"))?;
266 return Ok(PropertyValue::ObjectIdentifier(oid));
267 }
268
269 if let Some(rest) = s.strip_prefix("enumerated:") {
271 let val = rest
272 .parse::<u32>()
273 .map_err(|_| format!("invalid enumerated value: '{rest}'"))?;
274 return Ok(PropertyValue::Enumerated(val));
275 }
276
277 if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
279 let inner = &s[1..s.len() - 1];
280 return Ok(PropertyValue::CharacterString(inner.to_string()));
281 }
282
283 if s.contains('.') {
285 let val = s
286 .parse::<f32>()
287 .map_err(|_| format!("invalid float value: '{s}'"))?;
288 return numeric::finite(PropertyValue::Real(val)).map_err(|e| format!("'{s}': {e}"));
289 }
290
291 if (s.contains('e') || s.contains('E'))
293 && !s.eq_ignore_ascii_case("enumerated")
294 && !s.starts_with("enumerated:")
295 && let Ok(val) = s.parse::<f32>()
296 {
297 return numeric::finite(PropertyValue::Real(val)).map_err(|e| format!("'{s}': {e}"));
298 }
299
300 if s.starts_with('-') {
302 let val = s
303 .parse::<i32>()
304 .map_err(|_| format!("invalid integer value: '{s}'"))?;
305 return Ok(PropertyValue::Signed(val));
306 }
307
308 if let Ok(val) = s.parse::<u64>() {
309 return Ok(PropertyValue::Unsigned(val));
310 }
311
312 Err(format!("cannot parse value: '{s}'"))
313}
314
315fn split_priority(s: &str) -> Result<(&str, Option<u8>), String> {
318 let s = s.trim();
319
320 let at_pos = if let Some(stripped) = s.strip_prefix('"') {
323 if let Some(end_quote) = stripped.find('"') {
325 let after_quote = end_quote + 2; s[after_quote..].find('@').map(|p| p + after_quote)
327 } else {
328 None
329 }
330 } else {
331 s.rfind('@')
332 };
333
334 let Some(pos) = at_pos else {
335 return Ok((s, None));
336 };
337 let priority_str = &s[pos + 1..];
338 let priority = priority_str
339 .parse::<u8>()
340 .map_err(|_| format!("invalid priority: '{priority_str}'"))?;
341 if !(1..=16).contains(&priority) {
342 return Err(format!("priority must be 1-16, got {priority}"));
343 }
344 Ok((&s[..pos], Some(priority)))
345}
346
347pub fn parse_value_with_priority(s: &str) -> Result<(PropertyValue, Option<u8>), String> {
351 let (value, priority) = split_priority(s)?;
352 Ok((parse_value(value)?, priority))
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn parse_object_type_full_name() {
361 assert_eq!(
362 parse_object_type("analog-input").unwrap().to_raw(),
363 ObjectType::ANALOG_INPUT.to_raw()
364 );
365 assert_eq!(
366 parse_object_type("ANALOG_INPUT").unwrap().to_raw(),
367 ObjectType::ANALOG_INPUT.to_raw()
368 );
369 assert_eq!(
370 parse_object_type("Binary-Value").unwrap().to_raw(),
371 ObjectType::BINARY_VALUE.to_raw()
372 );
373 }
374
375 #[test]
376 fn parse_object_type_abbreviation() {
377 assert_eq!(parse_object_type("ai").unwrap().to_raw(), 0);
378 assert_eq!(parse_object_type("bo").unwrap().to_raw(), 4);
379 assert_eq!(parse_object_type("dev").unwrap().to_raw(), 8);
380 assert_eq!(parse_object_type("msv").unwrap().to_raw(), 19);
381 assert_eq!(parse_object_type("AI").unwrap().to_raw(), 0);
382 }
383
384 #[test]
385 fn parse_object_type_numeric() {
386 assert_eq!(parse_object_type("0").unwrap().to_raw(), 0);
387 assert_eq!(parse_object_type("8").unwrap().to_raw(), 8);
388 assert_eq!(parse_object_type("42").unwrap().to_raw(), 42);
389 }
390
391 #[test]
392 fn parse_object_type_error() {
393 assert!(parse_object_type("nonexistent").is_err());
394 }
395
396 #[test]
397 fn parse_object_specifier_works() {
398 let (ot, inst) = parse_object_specifier("ai:1").unwrap();
399 assert_eq!(ot.to_raw(), 0);
400 assert_eq!(inst, 1);
401
402 let (ot, inst) = parse_object_specifier("analog-input:100").unwrap();
403 assert_eq!(ot.to_raw(), 0);
404 assert_eq!(inst, 100);
405
406 let (ot, inst) = parse_object_specifier("dev:1234").unwrap();
407 assert_eq!(ot.to_raw(), 8);
408 assert_eq!(inst, 1234);
409 }
410
411 #[test]
412 fn parse_object_specifier_errors() {
413 assert!(parse_object_specifier("ai").is_err()); assert!(parse_object_specifier("ai:abc").is_err()); }
416
417 #[test]
418 fn parse_property_full_name() {
419 let (p, idx) = parse_property("present-value").unwrap();
420 assert_eq!(p.to_raw(), 85);
421 assert_eq!(idx, None);
422 }
423
424 #[test]
425 fn parse_property_abbreviation() {
426 let (p, _) = parse_property("pv").unwrap();
427 assert_eq!(p.to_raw(), 85);
428 let (p, _) = parse_property("on").unwrap();
429 assert_eq!(p.to_raw(), 77);
430 let (p, _) = parse_property("all").unwrap();
431 assert_eq!(p.to_raw(), 8);
432 }
433
434 #[test]
435 fn parse_property_numeric() {
436 let (p, _) = parse_property("85").unwrap();
437 assert_eq!(p.to_raw(), 85);
438 }
439
440 #[test]
441 fn parse_property_array_index() {
442 let (p, idx) = parse_property("object-list[3]").unwrap();
443 assert_eq!(p.to_raw(), 76);
444 assert_eq!(idx, Some(3));
445
446 let (p, idx) = parse_property("ol[0]").unwrap();
447 assert_eq!(p.to_raw(), 76);
448 assert_eq!(idx, Some(0));
449 }
450
451 #[test]
452 fn parse_value_null() {
453 assert!(matches!(parse_value("null").unwrap(), PropertyValue::Null));
454 assert!(matches!(parse_value("NULL").unwrap(), PropertyValue::Null));
455 }
456
457 #[test]
458 fn parse_value_boolean() {
459 assert!(matches!(
460 parse_value("true").unwrap(),
461 PropertyValue::Boolean(true)
462 ));
463 assert!(matches!(
464 parse_value("false").unwrap(),
465 PropertyValue::Boolean(false)
466 ));
467 }
468
469 #[test]
470 fn parse_value_active_inactive() {
471 assert!(matches!(
472 parse_value("active").unwrap(),
473 PropertyValue::Enumerated(1)
474 ));
475 assert!(matches!(
476 parse_value("inactive").unwrap(),
477 PropertyValue::Enumerated(0)
478 ));
479 }
480
481 #[test]
482 fn parse_value_unsigned() {
483 match parse_value("42").unwrap() {
484 PropertyValue::Unsigned(v) => assert_eq!(v, 42),
485 other => panic!("expected Unsigned, got {other:?}"),
486 }
487 }
488
489 #[test]
490 fn parse_value_signed() {
491 match parse_value("-5").unwrap() {
492 PropertyValue::Signed(v) => assert_eq!(v, -5),
493 other => panic!("expected Signed, got {other:?}"),
494 }
495 }
496
497 #[test]
498 fn parse_value_real() {
499 match parse_value("72.5").unwrap() {
500 PropertyValue::Real(v) => assert!((v - 72.5).abs() < f32::EPSILON),
501 other => panic!("expected Real, got {other:?}"),
502 }
503 }
504
505 #[test]
506 fn parse_value_string() {
507 match parse_value("\"hello world\"").unwrap() {
508 PropertyValue::CharacterString(s) => assert_eq!(s, "hello world"),
509 other => panic!("expected CharacterString, got {other:?}"),
510 }
511 }
512
513 #[test]
514 fn parse_value_enumerated() {
515 match parse_value("enumerated:3").unwrap() {
516 PropertyValue::Enumerated(v) => assert_eq!(v, 3),
517 other => panic!("expected Enumerated, got {other:?}"),
518 }
519 }
520
521 #[test]
522 fn parse_value_with_priority_works() {
523 let (val, pri) = parse_value_with_priority("72.5@8").unwrap();
524 assert!(matches!(val, PropertyValue::Real(_)));
525 assert_eq!(pri, Some(8));
526
527 let (val, pri) = parse_value_with_priority("null@16").unwrap();
528 assert!(matches!(val, PropertyValue::Null));
529 assert_eq!(pri, Some(16));
530
531 let (val, pri) = parse_value_with_priority("42").unwrap();
532 assert!(matches!(val, PropertyValue::Unsigned(42)));
533 assert_eq!(pri, None);
534 }
535
536 #[test]
537 fn parse_value_with_priority_invalid() {
538 assert!(parse_value_with_priority("42@0").is_err()); assert!(parse_value_with_priority("42@17").is_err()); }
541
542 #[test]
543 fn lp_maps_to_life_safety_point() {
544 assert_eq!(parse_object_type("lp").unwrap().to_raw(), 21);
545 assert_eq!(parse_object_type("lsp").unwrap().to_raw(), 21);
546 assert_eq!(parse_object_type("cal").unwrap().to_raw(), 6);
547 }
548
549 #[test]
550 fn bracket_ordering_panic() {
551 assert!(parse_property("pv]3[").is_err());
552 }
553
554 #[test]
555 fn scientific_notation_real() {
556 match parse_value("1e10").unwrap() {
557 PropertyValue::Real(v) => assert!((v - 1e10).abs() < 1e5),
558 other => panic!("expected Real, got {other:?}"),
559 }
560 match parse_value("-2.5e3").unwrap() {
561 PropertyValue::Real(v) => assert!((v - (-2500.0)).abs() < f32::EPSILON),
562 other => panic!("expected Real, got {other:?}"),
563 }
564 }
565
566 #[test]
567 fn parse_date_value() {
568 match parse_value("date:2024-03-15").unwrap() {
569 PropertyValue::Date(d) => {
570 assert_eq!(d.year, 124); assert_eq!(d.month, 3);
572 assert_eq!(d.day, 15);
573 assert_eq!(d.day_of_week, 0xFF);
574 }
575 other => panic!("expected Date, got {other:?}"),
576 }
577 }
578
579 #[test]
580 fn parse_time_value() {
581 match parse_value("time:14:30:00").unwrap() {
582 PropertyValue::Time(t) => {
583 assert_eq!(t.hour, 14);
584 assert_eq!(t.minute, 30);
585 assert_eq!(t.second, 0);
586 assert_eq!(t.hundredths, 0);
587 }
588 other => panic!("expected Time, got {other:?}"),
589 }
590 }
591
592 #[test]
593 fn parse_object_identifier_value() {
594 match parse_value("object:ai:1").unwrap() {
595 PropertyValue::ObjectIdentifier(oid) => {
596 assert_eq!(oid.object_type().to_raw(), 0);
597 assert_eq!(oid.instance_number(), 1);
598 }
599 other => panic!("expected ObjectIdentifier, got {other:?}"),
600 }
601 }
602
603 #[test]
604 fn empty_string_with_priority() {
605 let (val, pri) = parse_value_with_priority("\"\"@8").unwrap();
606 match val {
607 PropertyValue::CharacterString(s) => assert_eq!(s, ""),
608 other => panic!("expected CharacterString, got {other:?}"),
609 }
610 assert_eq!(pri, Some(8));
611 }
612}