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