use bacnet_ops::format::{Datum, Floats, PLAIN_ELEMENTS, StateTexts, ValueFormat, decode_all};
use bacnet_ops::label::kebab;
use bacnet_types::enums::{EngineeringUnits, ObjectType, PropertyIdentifier};
use bacnet_types::primitives::PropertyValue;
use serde_json::{Value, json};
use crate::untrusted::envelope;
pub(crate) fn property_value(
object_type: ObjectType,
property: PropertyIdentifier,
index: Option<u32>,
data: &[u8],
units: Option<EngineeringUnits>,
source: &str,
cap: usize,
) -> Value {
let texts = StateTexts::default();
let format = ValueFormat::new(object_type, &texts, Floats::Display);
let text = match index {
None => format.with_units(property, data, units),
Some(_) => format.value_at(property, index, data),
};
let mut typed = datum_json(&format.data_at(property, index, data));
let display = if envelope_leaves(&mut typed, source, cap) {
envelope(&text, source, cap)
} else {
json!(text)
};
json!({ "display": display, "typed": typed })
}
fn datum_json(datum: &Datum) -> Value {
serde_json::to_value(datum).expect("a datum serializes")
}
pub(crate) fn enveloped(value: &impl serde::Serialize, source: &str, cap: usize) -> Value {
let mut json = serde_json::to_value(value).expect("decoded data serializes");
envelope_leaves(&mut json, source, cap);
json
}
const SAFE_SHAPES: [(&[&str], &[&str]); 8] = [
(&["float"], &[]),
(&["octets"], &[]),
(&["value"], &["name"]),
(&["bits"], &["names"]),
(&["date"], &[]),
(&["time"], &[]),
(&["type", "type_number", "instance"], &[]),
(&["raw"], &[]),
];
fn safe_shape(map: &serde_json::Map<String, Value>) -> bool {
SAFE_SHAPES.iter().any(|(required, optional)| {
required.iter().all(|key| map.contains_key(*key))
&& map
.keys()
.all(|key| required.contains(&key.as_str()) || optional.contains(&key.as_str()))
})
}
fn envelope_leaves(typed: &mut Value, source: &str, cap: usize) -> bool {
match typed {
Value::String(text) => {
*typed = envelope(text, source, cap);
true
}
Value::Array(items) => {
let mut any = false;
for item in items {
any |= envelope_leaves(item, source, cap);
}
any
}
Value::Object(map) if safe_shape(map) => false,
Value::Object(map) if is_element(map) => {
let plain = map
.get("element")
.and_then(Value::as_str)
.is_some_and(|kind| PLAIN_ELEMENTS.contains(&kind));
let mut any = match map.get_mut("text") {
Some(text) if !plain => envelope_leaves(text, source, cap),
_ => false,
};
if let Some(Value::Object(fields)) = map.get_mut("fields") {
any |= envelope_every(fields, source, cap);
}
any
}
Value::Object(map) => envelope_every(map, source, cap),
_ => false,
}
}
fn is_element(map: &serde_json::Map<String, Value>) -> bool {
map.get("element").is_some_and(Value::is_string)
&& map.contains_key("text")
&& map.get("fields").is_none_or(Value::is_object)
&& map
.keys()
.all(|key| ["element", "text", "fields"].contains(&key.as_str()))
}
fn envelope_every(map: &mut serde_json::Map<String, Value>, source: &str, cap: usize) -> bool {
let mut any = false;
for value in map.values_mut() {
any |= envelope_leaves(value, source, cap);
}
any
}
pub(crate) fn bit_names(property: PropertyIdentifier, value: &PropertyValue) -> Vec<String> {
let texts = StateTexts::default();
let format = ValueFormat::new(ObjectType::DEVICE, &texts, Floats::Display);
match format.element_data(property, value) {
Datum::Bits {
names: Some(names), ..
} => names,
_ => Vec::new(),
}
}
pub(crate) fn text_of(data: &[u8]) -> Option<String> {
match decode_all(data)?.as_slice() {
[PropertyValue::CharacterString(text)] => Some(text.clone()),
_ => None,
}
}
pub(crate) fn row_text(
rows: &[bacnet_ops::format::RawRow],
property: PropertyIdentifier,
) -> Option<String> {
let (_, value) = rows.iter().find(|(p, _)| *p == property)?;
text_of(value.as_deref().ok()?)
}
pub(crate) fn object_text(object_type: ObjectType, instance: u32) -> String {
format!("{}:{instance}", kebab(object_type))
}
#[cfg(test)]
mod tests {
use super::*;
use bacnet_encoding::primitives::encode_property_value;
use bytes::BytesMut;
fn encoded(value: &PropertyValue) -> Vec<u8> {
let mut buf = BytesMut::new();
encode_property_value(&mut buf, value).unwrap();
buf.to_vec()
}
#[test]
fn numbers_carry_units_and_enumerations_their_names() {
let pv = encoded(&PropertyValue::Real(72.5));
let value = property_value(
ObjectType::ANALOG_INPUT,
PropertyIdentifier::PRESENT_VALUE,
None,
&pv,
Some(EngineeringUnits::DEGREES_FAHRENHEIT),
"device 1 analog-input:1 present-value",
256,
);
assert_eq!(value["display"], json!("72.5 ยฐF"));
assert_eq!(value["typed"], json!(72.5));
let units = encoded(&PropertyValue::Enumerated(64));
let value = property_value(
ObjectType::ANALOG_INPUT,
PropertyIdentifier::UNITS,
None,
&units,
None,
"s",
256,
);
assert_eq!(
value["typed"],
json!({ "value": 64, "name": "degrees-fahrenheit" })
);
}
#[test]
fn set_bits_are_named() {
let flags = PropertyValue::BitString {
unused_bits: 4,
data: vec![0b1001_0000],
};
assert_eq!(
bit_names(PropertyIdentifier::STATUS_FLAGS, &flags),
["in-alarm", "out-of-service"]
);
}
#[test]
fn device_text_is_enveloped_in_both_forms() {
let name = encoded(&PropertyValue::CharacterString(
"Fan\u{202E} https://x".into(),
));
let value = property_value(
ObjectType::ANALOG_INPUT,
PropertyIdentifier::DESCRIPTION,
None,
&name,
None,
"device 1 analog-input:1 description",
256,
);
assert_eq!(value["display"]["untrusted"], json!("\"Fan hxxps://x\""));
assert_eq!(value["typed"]["untrusted"], json!("Fan hxxps://x"));
assert_eq!(
value["typed"]["source"],
json!("device 1 analog-input:1 description")
);
}
#[test]
fn only_device_text_elements_are_enveloped() {
let mut stamp = json!([{ "element": "time-stamp", "text": "2026-10-25 07:00" }]);
assert!(!envelope_leaves(&mut stamp, "s", 256));
assert_eq!(stamp[0]["text"], json!("2026-10-25 07:00"));
let mut when = json!({ "element": "date-time", "text": "2026-10-25 07:00:00.00" });
assert!(!envelope_leaves(&mut when, "s", 256));
assert_eq!(when["text"], json!("2026-10-25 07:00:00.00"));
let mut named = json!([{ "element": "name-value", "text": "mode = \"Fan\u{202E}\"" }, 7]);
assert!(envelope_leaves(&mut named, "s", 256));
assert_eq!(named[0]["text"]["untrusted"], json!("mode = \"Fan\""));
assert_eq!(named[1], json!(7));
let mut unknown = json!({ "element": "element", "text": "x" });
assert!(envelope_leaves(&mut unknown, "s", 256));
let mut known = json!([{ "value": 1, "name": "active" }, { "bits": "01", "names": ["a"] }]);
assert!(!envelope_leaves(&mut known, "s", 256));
let mut future = json!({ "label": "Fan\u{202E}", "count": 2 });
assert!(envelope_leaves(&mut future, "s", 256));
assert_eq!(future["label"]["untrusted"], json!("Fan"));
}
}