Line data Source code
1 : /*
2 : * Copyright (c) 2021-2024 Project CHIP Authors
3 : * All rights reserved.
4 : *
5 : * Licensed under the Apache License, Version 2.0 (the "License");
6 : * you may not use this file except in compliance with the License.
7 : * You may obtain a copy of the License at
8 : *
9 : * http://www.apache.org/licenses/LICENSE-2.0
10 : *
11 : * Unless required by applicable law or agreed to in writing, software
12 : * distributed under the License is distributed on an "AS IS" BASIS,
13 : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 : * See the License for the specific language governing permissions and
15 : * limitations under the License.
16 : */
17 : #pragma once
18 :
19 : #include <access/SubjectDescriptor.h>
20 : #include <app/AttributeEncodeState.h>
21 : #include <app/AttributeReportBuilder.h>
22 : #include <app/ConcreteAttributePath.h>
23 : #include <app/MessageDef/AttributeReportIBs.h>
24 : #include <app/data-model/FabricScoped.h>
25 : #include <app/data-model/List.h>
26 : #include <lib/support/BitFlags.h>
27 : #include <lib/support/BitMask.h>
28 : #include <lib/support/TypeTraits.h>
29 :
30 : #include <type_traits>
31 :
32 : namespace chip {
33 : namespace app {
34 :
35 : /**
36 : * The AttributeValueEncoder is a helper class for filling report payloads into AttributeReportIBs.
37 : * The attribute value encoder can be initialized with a AttributeEncodeState for saving and recovering its state between encode
38 : * sessions (chunkings).
39 : *
40 : * When Encode returns recoverable errors (e.g. CHIP_ERROR_NO_MEMORY) the state can be used to initialize the AttributeValueEncoder
41 : * for future use on the same attribute path.
42 : */
43 : class AttributeValueEncoder
44 : {
45 : private:
46 : // Attempt to save flash by reducing the number of instantiations of the
47 : // Encode methods for AttributeValueEncoder and ListEncodeHelper. The idea
48 : // here is that some types actually end up encoded as other types anyway
49 : // (e.g. all integers are encoded are uint64_t or int64_t), so we might as
50 : // well avoid generating template instantiations of our methods (which have
51 : // extra logic) for all the different types that end up encoded the same in
52 : // the end.
53 : //
54 : // A type is a "base" type if it can't be treated as any other type for
55 : // encoding purposes. Overloads of BaseEncodableValue can be added for
56 : // "non-base" types to return values of a base type.
57 : //
58 : // It's important here to not collapse together types for which
59 : // DataModel::Encode in fact has different behavior (e.g. enum types).
60 : template <typename T>
61 : static constexpr const T & BaseEncodableValue(const T & aArg)
62 : {
63 : return aArg;
64 : }
65 : template <typename T>
66 0 : static constexpr auto BaseEncodableValue(const BitFlags<T> & aArg)
67 : {
68 0 : return BaseEncodableValue(aArg.Raw());
69 : }
70 : template <typename T>
71 0 : static constexpr auto BaseEncodableValue(const BitMask<T> & aArg)
72 : {
73 0 : return BaseEncodableValue(aArg.Raw());
74 : }
75 15 : static constexpr uint64_t BaseEncodableValue(uint32_t aArg) { return aArg; }
76 301 : static constexpr uint64_t BaseEncodableValue(uint16_t aArg) { return aArg; }
77 3340 : static constexpr uint64_t BaseEncodableValue(uint8_t aArg) { return aArg; }
78 : static constexpr int64_t BaseEncodableValue(int32_t aArg) { return aArg; }
79 : static constexpr int64_t BaseEncodableValue(int16_t aArg) { return aArg; }
80 : static constexpr int64_t BaseEncodableValue(int8_t aArg) { return aArg; }
81 :
82 : // Determines whether a type should be encoded as-is (if IsBaseType<T> is
83 : // true) or transformed to a different type by calling BaseEncodableValue()
84 : // on it.
85 : template <typename T>
86 : static constexpr bool IsBaseType = std::is_same_v<const T &, decltype(BaseEncodableValue(std::declval<const T &>()))>;
87 :
88 : template <typename T>
89 : static constexpr bool IsMatterEnum = std::is_enum_v<T> && DataModel::detail::HasUnknownValue<T>;
90 :
91 : public:
92 : class ListEncodeHelper
93 : {
94 : public:
95 2359 : ListEncodeHelper(AttributeValueEncoder & encoder) : mAttributeValueEncoder(encoder) {}
96 :
97 : template <typename T, std::enable_if_t<IsBaseType<T> && DataModel::IsFabricScoped<T>::value, bool> = true>
98 156 : CHIP_ERROR Encode(const T & aArg) const
99 : {
100 156 : VerifyOrReturnError(aArg.GetFabricIndex() != kUndefinedFabricIndex, CHIP_ERROR_INVALID_FABRIC_INDEX);
101 :
102 : // If we are encoding for a fabric filtered attribute read and the fabric index does not match that present in the
103 : // request, skip encoding this list item.
104 156 : VerifyOrReturnError(!mAttributeValueEncoder.mIsFabricFiltered ||
105 : aArg.GetFabricIndex() == mAttributeValueEncoder.AccessingFabricIndex(),
106 : CHIP_NO_ERROR);
107 81 : return mAttributeValueEncoder.EncodeListItem(mCheckpoint, aArg, mAttributeValueEncoder.AccessingFabricIndex());
108 : }
109 :
110 : template <typename T,
111 : std::enable_if_t<IsBaseType<T> && !DataModel::IsFabricScoped<T>::value && !IsMatterEnum<T>, bool> = true>
112 6741 : CHIP_ERROR Encode(const T & aArg) const
113 : {
114 6741 : return mAttributeValueEncoder.EncodeListItem(mCheckpoint, aArg);
115 : }
116 :
117 : // Specialization for enums to share as much code as possible in attribute encoding while
118 : // still doing the "unknown value" checks that might be needed..
119 : template <typename T, std::enable_if_t<IsBaseType<T> && IsMatterEnum<T>, bool> = true>
120 0 : CHIP_ERROR Encode(const T & aArg) const
121 : {
122 : static_assert(!DataModel::IsFabricScoped<T>::value, "How do we have a fabric-scoped enum?");
123 : using UnderlyingType = std::remove_cv_t<std::remove_reference_t<decltype(to_underlying(aArg))>>;
124 : static_assert(!std::is_same_v<UnderlyingType, T>, "Encode will call itself recursively");
125 :
126 0 : CHIP_DM_ENCODING_MAYBE_FAIL_UNKNOWN_ENUM_VALUE(aArg);
127 :
128 0 : return Encode(to_underlying(aArg));
129 : }
130 :
131 : template <typename T, std::enable_if_t<!IsBaseType<T>, bool> = true>
132 2016 : CHIP_ERROR Encode(const T & aArg) const
133 : {
134 2016 : return Encode(BaseEncodableValue(aArg));
135 : }
136 :
137 : private:
138 : AttributeValueEncoder & mAttributeValueEncoder;
139 : // Avoid calling the TLVWriter constructor for every instantiation of
140 : // EncodeListItem. We treat encoding as a const operation, so either
141 : // have to put this on the stack (in which case it's per-instantiation),
142 : // or have it as mutable state.
143 : mutable TLV::TLVWriter mCheckpoint;
144 : };
145 :
146 5975 : AttributeValueEncoder(AttributeReportIBs::Builder & aAttributeReportIBsBuilder, Access::SubjectDescriptor subjectDescriptor,
147 : const ConcreteAttributePath & aPath, DataVersion aDataVersion, bool aIsFabricFiltered = false,
148 5975 : const AttributeEncodeState & aState = AttributeEncodeState()) :
149 5975 : mAttributeReportIBsBuilder(aAttributeReportIBsBuilder),
150 5975 : mSubjectDescriptor(subjectDescriptor), mPath(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId),
151 5975 : mDataVersion(aDataVersion), mIsFabricFiltered(aIsFabricFiltered), mEncodeState(aState)
152 5975 : {}
153 :
154 : /**
155 : * Encode a single value. This value will not be chunked; it will either be
156 : * entirely encoded or fail to be encoded. Consumers are allowed to make
157 : * either one call to Encode or one call to EncodeList to handle a read.
158 : */
159 : template <typename T, std::enable_if_t<IsBaseType<T> && !IsMatterEnum<T>, bool> = true>
160 1714 : CHIP_ERROR Encode(const T & aArg)
161 : {
162 1714 : mTriedEncode = true;
163 1714 : return EncodeAttributeReportIB(aArg);
164 : }
165 :
166 : template <typename T, std::enable_if_t<IsBaseType<T> && IsMatterEnum<T>, bool> = true>
167 5 : CHIP_ERROR Encode(const T & aArg)
168 : {
169 : using UnderlyingType = std::remove_cv_t<std::remove_reference_t<decltype(to_underlying(aArg))>>;
170 : static_assert(!std::is_same_v<UnderlyingType, T>, "Encode will call itself recursively");
171 :
172 5 : CHIP_DM_ENCODING_MAYBE_FAIL_UNKNOWN_ENUM_VALUE(aArg);
173 :
174 5 : return Encode(to_underlying(aArg));
175 : }
176 :
177 : template <typename T, std::enable_if_t<!IsBaseType<T>, bool> = true>
178 1640 : CHIP_ERROR Encode(const T & aArg)
179 : {
180 1640 : return Encode(BaseEncodableValue(aArg));
181 : }
182 :
183 : /**
184 : * Encode an explicit null value.
185 : */
186 0 : CHIP_ERROR EncodeNull()
187 : {
188 : // Doesn't matter what type Nullable we use here.
189 0 : return Encode(DataModel::Nullable<uint8_t>());
190 : }
191 :
192 : /**
193 : * Encode an explicit empty list.
194 : */
195 22 : CHIP_ERROR EncodeEmptyList()
196 : {
197 : // Doesn't matter what type List we use here.
198 22 : return Encode(DataModel::List<uint8_t>());
199 : }
200 :
201 : /**
202 : * aCallback is expected to take a const auto & argument and Encode() on it as many times as needed to encode all the list
203 : * elements one by one. If any of those Encode() calls returns failure, aCallback must stop encoding and return failure. When
204 : * all items are encoded aCallback is expected to return success.
205 : *
206 : * aCallback may not be called. Consumers must not assume it will be called.
207 : *
208 : * When EncodeList returns an error, the consumers must abort the encoding, and return the exact error to the caller.
209 : *
210 : * TODO: Can we hold a error state in the AttributeValueEncoder itself?
211 : *
212 : * Consumers are allowed to make either one call to EncodeList or one call to Encode to handle a read.
213 : *
214 : */
215 : template <typename ListGenerator>
216 2479 : CHIP_ERROR EncodeList(ListGenerator aCallback)
217 : {
218 2479 : mTriedEncode = true;
219 : // Spec 10.5.4.3.1, 10.5.4.6 (Replace a list w/ Multiple IBs)
220 : // EmptyList acts as the beginning of the whole array type attribute report.
221 : // An empty list is encoded iff both mCurrentEncodingListIndex and mEncodeState.mCurrentEncodingListIndex are invalid
222 : // values. After encoding the empty list, mEncodeState.mCurrentEncodingListIndex and mCurrentEncodingListIndex are set to 0.
223 2479 : ReturnErrorOnFailure(EnsureListStarted());
224 2359 : CHIP_ERROR err = aCallback(ListEncodeHelper(*this));
225 :
226 : // Even if encoding list items failed, make sure we EnsureListEnded().
227 : // Since we encode list items atomically, in the case when we just
228 : // didn't fit the next item we want to make sure our list is properly
229 : // ended before the reporting engine starts chunking.
230 2359 : EnsureListEnded();
231 2359 : if (err == CHIP_NO_ERROR)
232 : {
233 : // The Encode procedure finished without any error, clear the state.
234 2088 : mEncodeState.Reset();
235 : }
236 2359 : return err;
237 : }
238 :
239 1625 : bool TriedEncode() const { return mTriedEncode; }
240 :
241 232 : const Access::SubjectDescriptor & GetSubjectDescriptor() const { return mSubjectDescriptor; }
242 :
243 : /**
244 : * The accessing fabric index for this read or subscribe interaction.
245 : */
246 232 : FabricIndex AccessingFabricIndex() const { return GetSubjectDescriptor().fabricIndex; }
247 :
248 : /**
249 : * AttributeValueEncoder is a short lived object, and the state is persisted by mEncodeState and restored by constructor.
250 : */
251 2347 : const AttributeEncodeState & GetState() const { return mEncodeState; }
252 :
253 : private:
254 : // We made EncodeListItem() private, and ListEncoderHelper will expose it by Encode()
255 : friend class ListEncodeHelper;
256 : friend class TestOnlyAttributeValueEncoderAccessor;
257 :
258 : // Returns true if the list item should be encoded. If it should, the
259 : // passed-in TLVWriter will be used to checkpoint the current state of our
260 : // attribute report list builder.
261 : bool ShouldEncodeListItem(TLV::TLVWriter & aCheckpoint);
262 :
263 : // Does any cleanup work needed after attempting to encode a list item.
264 : void PostEncodeListItem(CHIP_ERROR aEncodeStatus, const TLV::TLVWriter & aCheckpoint);
265 :
266 : // EncodeListItem may be given an extra FabricIndex argument as a second
267 : // arg, or not. Represent that via a parameter pack (which might be
268 : // empty). In practice, for any given ItemType the extra arg is either there
269 : // or not, so we don't get more template explosion due to aExtraArgs.
270 : template <typename ItemType, typename... ExtraArgTypes>
271 6822 : CHIP_ERROR EncodeListItem(TLV::TLVWriter & aCheckpoint, const ItemType & aItem, ExtraArgTypes &&... aExtraArgs)
272 : {
273 6822 : if (!ShouldEncodeListItem(aCheckpoint))
274 : {
275 602 : return CHIP_NO_ERROR;
276 : }
277 :
278 : CHIP_ERROR err;
279 6220 : if (mEncodingInitialList)
280 : {
281 : // Just encode a single item, with an anonymous tag.
282 : AttributeReportBuilder builder;
283 5855 : err = builder.EncodeValue(mAttributeReportIBsBuilder, TLV::AnonymousTag(), aItem,
284 81 : std::forward<ExtraArgTypes>(aExtraArgs)...);
285 : }
286 : else
287 : {
288 365 : err = EncodeAttributeReportIB(aItem, std::forward<ExtraArgTypes>(aExtraArgs)...);
289 : }
290 :
291 6220 : PostEncodeListItem(err, aCheckpoint);
292 6220 : return err;
293 : }
294 :
295 : /**
296 : * Builds a single AttributeReportIB in AttributeReportIBs. The caller is
297 : * responsible for setting up mPath correctly.
298 : *
299 : * In particular, when we are encoding a single element in the list, mPath
300 : * must indicate a null list index to represent an "append" operation.
301 : * operation.
302 : *
303 : * EncodeAttributeReportIB may be given an extra FabricIndex argument as a second
304 : * arg, or not. Represent that via a parameter pack (which might be
305 : * empty). In practice, for any given ItemType the extra arg is either
306 : * there or not, so we don't get more template explosion due to aExtraArgs.
307 : */
308 : template <typename ItemType, typename... ExtraArgTypes>
309 2079 : CHIP_ERROR EncodeAttributeReportIB(const ItemType & aItem, ExtraArgTypes &&... aExtraArgs)
310 : {
311 : AttributeReportBuilder builder;
312 2079 : ReturnErrorOnFailure(builder.PrepareAttribute(mAttributeReportIBsBuilder, mPath, mDataVersion));
313 2047 : ReturnErrorOnFailure(builder.EncodeValue(mAttributeReportIBsBuilder, TLV::ContextTag(AttributeDataIB::Tag::kData), aItem,
314 : std::forward<ExtraArgTypes>(aExtraArgs)...));
315 :
316 2047 : return builder.FinishAttribute(mAttributeReportIBsBuilder);
317 : }
318 :
319 : /**
320 : * EnsureListStarted sets our mCurrentEncodingListIndex to 0, and:
321 : *
322 : * * If we are just starting the list, gets us ready to encode list items.
323 : *
324 : * * If we are continuing a chunked list, guarantees that mPath.mListOp is
325 : * AppendItem after it returns.
326 : */
327 : CHIP_ERROR EnsureListStarted();
328 :
329 : /**
330 : * EnsureListEnded writes out the end of the list and our attribute data IB,
331 : * if we were encoding our initial list
332 : */
333 : void EnsureListEnded();
334 :
335 : AttributeReportIBs::Builder & mAttributeReportIBsBuilder;
336 : const Access::SubjectDescriptor mSubjectDescriptor;
337 : ConcreteDataAttributePath mPath;
338 : DataVersion mDataVersion;
339 : bool mTriedEncode = false;
340 : bool mIsFabricFiltered = false;
341 : // mEncodingInitialList is true if we're encoding a list and we have not
342 : // started chunking it yet, so we're encoding a single attribute report IB
343 : // for the whole list, not one per item.
344 : bool mEncodingInitialList = false;
345 : // mEncodedAtLeastOneListItem becomes true once we successfully encode a list item.
346 : bool mEncodedAtLeastOneListItem = false;
347 : ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
348 : AttributeEncodeState mEncodeState;
349 : };
350 :
351 : } // namespace app
352 : } // namespace chip
|