1use bacnet_encoding::constructed::{decode_name_value, encode_name_value};
35use bacnet_encoding::tags::Tag;
36use bacnet_types::constructed::BACnetNameValue;
37use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier as P};
38use bacnet_types::error::Error;
39use bacnet_types::primitives::PropertyValue;
40use bytes::BytesMut;
41
42use crate::common;
43use crate::property_metadata::{
44 PropertyConformance::Optional,
45 PropertyMetadata,
46 PropertyWriteCapability::{Always, ReadOnly},
47};
48
49mod persistence;
50mod saving;
51pub use persistence::{
52 FileTagsPersistence, TagsPersistence, TagsSnapshot, MAX_TAGS_SNAPSHOT_BYTES,
53};
54pub(crate) use saving::ProfileState;
55
56pub const MAX_TAGS: usize = 1024;
59
60#[derive(Debug, Clone, Default, PartialEq)]
66pub struct ObjectProfile {
67 pub tags: Option<Vec<BACnetNameValue>>,
69 pub profile_location: Option<String>,
71 pub profile_name: Option<String>,
73}
74
75impl ObjectProfile {
76 pub fn check(&self) -> Result<(), Error> {
82 if let Some(tags) = &self.tags {
83 check_tags(tags)?;
84 }
85 if self
86 .profile_location
87 .as_deref()
88 .is_some_and(|location| !location_scheme_allowed(location))
89 {
90 return Err(common::value_out_of_range_error());
91 }
92 if self
93 .profile_name
94 .as_deref()
95 .is_some_and(|name| !vendor_prefixed(name))
96 {
97 return Err(common::value_out_of_range_error());
98 }
99 Ok(())
100 }
101
102 pub(crate) fn metadata(&self) -> impl Iterator<Item = PropertyMetadata> {
105 [
106 (P::TAGS, self.tags.is_some(), Always),
107 (
108 P::PROFILE_LOCATION,
109 self.profile_location.is_some(),
110 ReadOnly,
111 ),
112 (P::PROFILE_NAME, self.profile_name.is_some(), ReadOnly),
113 ]
114 .into_iter()
115 .filter(|&(_, present, _)| present)
116 .map(|(property, _, write)| PropertyMetadata::new(property, Optional, None, write))
117 }
118
119 pub(crate) fn read(
121 &self,
122 property: P,
123 index: Option<u32>,
124 ) -> Option<Result<PropertyValue, Error>> {
125 let text = |value: &Option<String>| match value {
126 None => Err(common::unknown_property_error()),
127 Some(_) if index.is_some() => Err(common::property_is_not_an_array_error()),
128 Some(text) => Ok(PropertyValue::CharacterString(text.clone())),
129 };
130 Some(match property {
131 P::TAGS => match &self.tags {
132 None => Err(common::unknown_property_error()),
133 Some(tags) => read_tags(tags, index),
134 },
135 P::PROFILE_LOCATION => text(&self.profile_location),
136 P::PROFILE_NAME => text(&self.profile_name),
137 _ => return None,
138 })
139 }
140
141 pub(crate) fn write(
144 &mut self,
145 property: P,
146 index: Option<u32>,
147 value: &PropertyValue,
148 ) -> Option<Result<(), Error>> {
149 let read_only = |present: bool| match index {
150 _ if !present => Err(common::unknown_property_error()),
151 Some(_) => Err(common::property_is_not_an_array_error()),
152 None => Err(common::write_access_denied_error()),
153 };
154 Some(match property {
155 P::TAGS => match &mut self.tags {
156 None => Err(common::unknown_property_error()),
157 Some(tags) => write_tags(tags, index, value),
158 },
159 P::PROFILE_LOCATION => read_only(self.profile_location.is_some()),
160 P::PROFILE_NAME => read_only(self.profile_name.is_some()),
161 _ => return None,
162 })
163 }
164}
165
166fn encoded(tag: &BACnetNameValue) -> Result<PropertyValue, Error> {
168 let mut bytes = BytesMut::new();
169 encode_name_value(&mut bytes, tag)?;
170 Ok(PropertyValue::ApplicationData(bytes.to_vec()))
171}
172
173fn read_tags(tags: &[BACnetNameValue], index: Option<u32>) -> Result<PropertyValue, Error> {
174 match index {
175 None => Ok(PropertyValue::List(
176 tags.iter().map(encoded).collect::<Result<_, _>>()?,
177 )),
178 Some(0) => Ok(PropertyValue::Unsigned(tags.len() as u64)),
179 Some(index) => encoded(&tags[slot(index, tags.len())?]),
180 }
181}
182
183fn slot(index: u32, len: usize) -> Result<usize, Error> {
186 usize::try_from(index - 1)
187 .ok()
188 .filter(|slot| *slot < len)
189 .ok_or_else(common::invalid_array_index_error)
190}
191
192fn starts_name_value(tag: &Tag) -> bool {
194 tag.is_context(0)
195}
196
197fn no_space_error() -> Error {
198 common::protocol_error(ErrorClass::RESOURCES, ErrorCode::NO_SPACE_TO_WRITE_PROPERTY)
199}
200
201fn write_tags(
205 tags: &mut Vec<BACnetNameValue>,
206 index: Option<u32>,
207 value: &PropertyValue,
208) -> Result<(), Error> {
209 match index {
210 None => {
211 let written = common::decode_elements(value, starts_name_value, decode_name_value)?;
212 check_tags(&written)?;
213 *tags = written;
214 }
215 Some(0) => {
216 let PropertyValue::Unsigned(size) = value else {
217 return Err(common::invalid_data_type_error());
218 };
219 let size = usize::try_from(*size)
220 .ok()
221 .filter(|size| *size <= MAX_TAGS)
222 .ok_or_else(no_space_error)?;
223 tags.resize_with(size, || BACnetNameValue::semantic(""));
224 }
225 Some(index) => {
226 let slot = slot(index, tags.len())?;
227 let tag = common::decode_single_element(value, starts_name_value, decode_name_value)?;
228 check_tag(&tag)?;
229 tags[slot] = tag;
230 }
231 }
232 Ok(())
233}
234
235fn check_tags(tags: &[BACnetNameValue]) -> Result<(), Error> {
236 if tags.len() > MAX_TAGS {
237 return Err(no_space_error());
238 }
239 tags.iter().try_for_each(check_tag)
240}
241
242fn check_tag(tag: &BACnetNameValue) -> Result<(), Error> {
244 if let Some(value) = &tag.value {
245 if !value.is_primitive() {
246 return Err(common::invalid_data_type_error());
247 }
248 }
249 if tag.name.contains(';') {
250 return Err(common::value_out_of_range_error());
251 }
252 Ok(())
253}
254
255fn location_scheme_allowed(location: &str) -> bool {
258 location.is_empty()
259 || location.split_once(':').is_some_and(|(scheme, _)| {
260 ["http", "https", "bacnet"]
261 .iter()
262 .any(|allowed| scheme.eq_ignore_ascii_case(allowed))
263 })
264}
265
266fn vendor_prefixed(name: &str) -> bool {
269 name.split_once('-').is_some_and(|(vendor, _)| {
270 !vendor.is_empty()
271 && vendor.bytes().all(|b| b.is_ascii_digit())
272 && vendor.parse::<u16>().is_ok()
273 })
274}
275
276#[cfg(test)]
277#[path = "object_profile_tests.rs"]
278mod tests;
279
280#[cfg(test)]
281mod persistence_tests;
282#[cfg(test)]
283mod saving_tests;
284#[cfg(test)]
285mod test_support;
286
287#[cfg(test)]
288mod value_tests;
289
290#[cfg(test)]
291mod input_tests;
292
293#[cfg(test)]
294mod output_tests;
295
296#[cfg(test)]
297mod device_tests;