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