Line data Source code
1 : /*
2 : *
3 : * Copyright (c) 2021-2022 Project CHIP Authors
4 : * All rights reserved.
5 : *
6 : * Licensed under the Apache License, Version 2.0 (the "License");
7 : * you may not use this file except in compliance with the License.
8 : * You may obtain a copy of the License at
9 : *
10 : * http://www.apache.org/licenses/LICENSE-2.0
11 : *
12 : * Unless required by applicable law or agreed to in writing, software
13 : * distributed under the License is distributed on an "AS IS" BASIS,
14 : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 : * See the License for the specific language governing permissions and
16 : * limitations under the License.
17 : */
18 :
19 : #pragma once
20 :
21 : #include <access/SubjectDescriptor.h>
22 : #include <app/ConcreteAttributePath.h>
23 : #include <app/MessageDef/AttributeReportIBs.h>
24 : #include <app/data-model/DecodableList.h>
25 : #include <app/data-model/Decode.h>
26 : #include <app/data-model/Encode.h>
27 : #include <app/data-model/FabricScoped.h>
28 : #include <app/data-model/List.h> // So we can encode lists
29 : #include <app/data-model/TagBoundEncoder.h>
30 : #include <app/util/basic-types.h>
31 : #include <lib/core/Optional.h>
32 : #include <lib/core/TLV.h>
33 : #include <lib/support/logging/CHIPLogging.h>
34 :
35 : /**
36 : * Callback class that clusters can implement in order to interpose custom
37 : * attribute-handling logic. An AttributeAccessInterface instance is associated
38 : * with some specific cluster. A single instance may be used for a specific
39 : * endpoint or for all endpoints.
40 : *
41 : * Instances of AttributeAccessInterface that are registered via
42 : * registerAttributeAccessOverride will be consulted before taking the normal
43 : * attribute access codepath and can use that codepath as a fallback if desired.
44 : */
45 : namespace chip {
46 : namespace app {
47 :
48 : /**
49 : * The AttributeReportBuilder is a helper class for filling a single report in AttributeReportIBs.
50 : *
51 : * Possible usage of AttributeReportBuilder might be:
52 : *
53 : * AttributeReportBuilder builder;
54 : * ReturnErrorOnFailure(builder.PrepareAttribute(...));
55 : * ReturnErrorOnFailure(builder.Encode(...));
56 : * ReturnErrorOnFailure(builder.FinishAttribute());
57 : */
58 : class AttributeReportBuilder
59 : {
60 : public:
61 : /**
62 : * PrepareAttribute encodes the "header" part of an attribute report including the path and data version.
63 : * Path will be encoded according to section 10.5.4.3.1 in the spec.
64 : * Note: Only append is supported currently (encode a null list index), other operations won't encode a list index in the
65 : * attribute path field.
66 : * TODO: Add support for encoding a single element in the list (path with a valid list index).
67 : */
68 : CHIP_ERROR PrepareAttribute(AttributeReportIBs::Builder & aAttributeReportIBs, const ConcreteDataAttributePath & aPath,
69 : DataVersion aDataVersion);
70 :
71 : /**
72 : * FinishAttribute encodes the "footer" part of an attribute report (it closes the containers opened in PrepareAttribute)
73 : */
74 : CHIP_ERROR FinishAttribute(AttributeReportIBs::Builder & aAttributeReportIBs);
75 :
76 : /**
77 : * EncodeValue encodes the value field of the report, it should be called exactly once.
78 : */
79 : template <typename T, std::enable_if_t<!DataModel::IsFabricScoped<T>::value, bool> = true, typename... Ts>
80 1882 : CHIP_ERROR EncodeValue(AttributeReportIBs::Builder & aAttributeReportIBs, TLV::Tag tag, T && item, Ts &&... aArgs)
81 : {
82 1882 : return DataModel::Encode(*(aAttributeReportIBs.GetAttributeReport().GetAttributeData().GetWriter()), tag, item,
83 1882 : std::forward<Ts>(aArgs)...);
84 : }
85 :
86 : template <typename T, std::enable_if_t<DataModel::IsFabricScoped<T>::value, bool> = true, typename... Ts>
87 : CHIP_ERROR EncodeValue(AttributeReportIBs::Builder & aAttributeReportIBs, TLV::Tag tag, FabricIndex accessingFabricIndex,
88 : T && item, Ts &&... aArgs)
89 : {
90 : return DataModel::EncodeForRead(*(aAttributeReportIBs.GetAttributeReport().GetAttributeData().GetWriter()), tag,
91 : accessingFabricIndex, item, std::forward<Ts>(aArgs)...);
92 : }
93 : };
94 :
95 : /**
96 : * The AttributeValueEncoder is a helper class for filling report payloads into AttributeReportIBs.
97 : * The attribute value encoder can be initialized with a AttributeEncodeState for saving and recovering its state between encode
98 : * sessions (chunkings).
99 : *
100 : * When Encode returns recoverable errors (e.g. CHIP_ERROR_NO_MEMORY) the state can be used to initialize the AttributeValueEncoder
101 : * for future use on the same attribute path.
102 : */
103 : class AttributeValueEncoder
104 : {
105 : public:
106 : class ListEncodeHelper
107 : {
108 : public:
109 1114 : ListEncodeHelper(AttributeValueEncoder & encoder) : mAttributeValueEncoder(encoder) {}
110 :
111 : template <typename T, std::enable_if_t<DataModel::IsFabricScoped<T>::value, bool> = true>
112 : CHIP_ERROR Encode(T && aArg) const
113 : {
114 : VerifyOrReturnError(aArg.GetFabricIndex() != kUndefinedFabricIndex, CHIP_ERROR_INVALID_FABRIC_INDEX);
115 :
116 : // If we are encoding for a fabric filtered attribute read and the fabric index does not match that present in the
117 : // request, skip encoding this list item.
118 : VerifyOrReturnError(!mAttributeValueEncoder.mIsFabricFiltered ||
119 : aArg.GetFabricIndex() == mAttributeValueEncoder.mAccessingFabricIndex,
120 : CHIP_NO_ERROR);
121 : return mAttributeValueEncoder.EncodeListItem(mAttributeValueEncoder.mAccessingFabricIndex, std::forward<T>(aArg));
122 : }
123 :
124 : template <typename T, std::enable_if_t<!DataModel::IsFabricScoped<T>::value, bool> = true>
125 2078 : CHIP_ERROR Encode(T && aArg) const
126 : {
127 2078 : return mAttributeValueEncoder.EncodeListItem(std::forward<T>(aArg));
128 : }
129 :
130 : private:
131 : AttributeValueEncoder & mAttributeValueEncoder;
132 : };
133 :
134 : class AttributeEncodeState
135 : {
136 : public:
137 4553 : AttributeEncodeState() : mAllowPartialData(false), mCurrentEncodingListIndex(kInvalidListIndex) {}
138 391 : bool AllowPartialData() const { return mAllowPartialData; }
139 :
140 : private:
141 : friend class AttributeValueEncoder;
142 : /**
143 : * When an attempt to encode an attribute returns an error, the buffer may contain tailing dirty data
144 : * (since the put was aborted). The report engine normally rolls back the buffer to right before encoding
145 : * of the attribute started on errors.
146 : *
147 : * When chunking a list, EncodeListItem will atomically encode list items, ensuring that the
148 : * state of the buffer is valid to send (i.e. contains no trailing garbage), and return an error
149 : * if the list doesn't entirely fit. In this situation, mAllowPartialData is set to communicate to the
150 : * report engine that it should not roll back the list items.
151 : *
152 : * TODO: There might be a better name for this variable.
153 : */
154 : bool mAllowPartialData = false;
155 : /**
156 : * If set to kInvalidListIndex, indicates that we have not encoded any data for the list yet and
157 : * need to start by encoding an empty list before we start encoding any list items.
158 : *
159 : * When set to a valid ListIndex value, indicates the index of the next list item that needs to be
160 : * encoded (i.e. the count of items encoded so far).
161 : */
162 : ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
163 : };
164 :
165 2471 : AttributeValueEncoder(AttributeReportIBs::Builder & aAttributeReportIBsBuilder, FabricIndex aAccessingFabricIndex,
166 : const ConcreteAttributePath & aPath, DataVersion aDataVersion, bool aIsFabricFiltered = false,
167 2471 : const AttributeEncodeState & aState = AttributeEncodeState()) :
168 2471 : mAttributeReportIBsBuilder(aAttributeReportIBsBuilder),
169 2471 : mAccessingFabricIndex(aAccessingFabricIndex), mPath(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId),
170 2471 : mDataVersion(aDataVersion), mIsFabricFiltered(aIsFabricFiltered), mEncodeState(aState)
171 2471 : {}
172 :
173 : /**
174 : * Encode a single value. This value will not be chunked; it will either be
175 : * entirely encoded or fail to be encoded. Consumers are allowed to make
176 : * either one call to Encode or one call to EncodeList to handle a read.
177 : */
178 : template <typename... Ts>
179 : CHIP_ERROR Encode(Ts &&... aArgs)
180 : {
181 : mTriedEncode = true;
182 : return EncodeAttributeReportIB(std::forward<Ts>(aArgs)...);
183 : }
184 :
185 : /**
186 : * Encode an explicit null value.
187 : */
188 : CHIP_ERROR EncodeNull()
189 : {
190 : // Doesn't matter what type Nullable we use here.
191 : return Encode(DataModel::Nullable<uint8_t>());
192 : }
193 :
194 : /**
195 : * Encode an explicit empty list.
196 : */
197 : CHIP_ERROR EncodeEmptyList()
198 : {
199 : // Doesn't matter what type List we use here.
200 : return Encode(DataModel::List<uint8_t>());
201 : }
202 :
203 : /**
204 : * aCallback is expected to take a const auto & argument and Encode() on it as many times as needed to encode all the list
205 : * elements one by one. If any of those Encode() calls returns failure, aCallback must stop encoding and return failure. When
206 : * all items are encoded aCallback is expected to return success.
207 : *
208 : * aCallback may not be called. Consumers must not assume it will be called.
209 : *
210 : * When EncodeList returns an error, the consumers must abort the encoding, and return the exact error to the caller.
211 : *
212 : * TODO: Can we hold a error state in the AttributeValueEncoder itself so functions in ember-compatibility-functions don't have
213 : * to rely on the above assumption?
214 : *
215 : * Consumers are allowed to make either one call to EncodeList or one call to Encode to handle a read.
216 : *
217 : */
218 : template <typename ListGenerator>
219 715 : CHIP_ERROR EncodeList(ListGenerator aCallback)
220 : {
221 715 : mTriedEncode = true;
222 : // Spec 10.5.4.3.1, 10.5.4.6 (Replace a list w/ Multiple IBs)
223 : // EmptyList acts as the beginning of the whole array type attribute report.
224 : // An empty list is encoded iff both mCurrentEncodingListIndex and mEncodeState.mCurrentEncodingListIndex are invalid
225 : // values. After encoding the empty list, mEncodeState.mCurrentEncodingListIndex and mCurrentEncodingListIndex are set to 0.
226 715 : ReturnErrorOnFailure(EnsureListStarted());
227 665 : CHIP_ERROR err = aCallback(ListEncodeHelper(*this));
228 :
229 : // Even if encoding list items failed, make sure we EnsureListEnded().
230 : // Since we encode list items atomically, in the case when we just
231 : // didn't fit the next item we want to make sure our list is properly
232 : // ended before the reporting engine starts chunking.
233 665 : EnsureListEnded();
234 665 : if (err == CHIP_NO_ERROR)
235 : {
236 : // The Encode procedure finished without any error, clear the state.
237 621 : mEncodeState = AttributeEncodeState();
238 : }
239 665 : return err;
240 : }
241 :
242 2243 : bool TriedEncode() const { return mTriedEncode; }
243 :
244 : /**
245 : * The accessing fabric index for this read or subscribe interaction.
246 : */
247 : FabricIndex AccessingFabricIndex() const { return mAccessingFabricIndex; }
248 :
249 : /**
250 : * AttributeValueEncoder is a short lived object, and the state is persisted by mEncodeState and restored by constructor.
251 : */
252 228 : const AttributeEncodeState & GetState() const { return mEncodeState; }
253 :
254 : private:
255 : // We made EncodeListItem() private, and ListEncoderHelper will expose it by Encode()
256 : friend class ListEncodeHelper;
257 :
258 : template <typename... Ts>
259 2078 : CHIP_ERROR EncodeListItem(Ts &&... aArgs)
260 : {
261 : // EncodeListItem must be called after EnsureListStarted(), thus mCurrentEncodingListIndex and
262 : // mEncodeState.mCurrentEncodingListIndex are not invalid values.
263 2078 : if (mCurrentEncodingListIndex < mEncodeState.mCurrentEncodingListIndex)
264 : {
265 : // We have encoded this element in previous chunks, skip it.
266 196 : mCurrentEncodingListIndex++;
267 196 : return CHIP_NO_ERROR;
268 : }
269 :
270 1882 : TLV::TLVWriter backup;
271 1882 : mAttributeReportIBsBuilder.Checkpoint(backup);
272 :
273 : CHIP_ERROR err;
274 1882 : if (mEncodingInitialList)
275 : {
276 : // Just encode a single item, with an anonymous tag.
277 : AttributeReportBuilder builder;
278 1718 : err = builder.EncodeValue(mAttributeReportIBsBuilder, TLV::AnonymousTag(), std::forward<Ts>(aArgs)...);
279 : }
280 : else
281 : {
282 164 : err = EncodeAttributeReportIB(std::forward<Ts>(aArgs)...);
283 : }
284 1882 : if (err != CHIP_NO_ERROR)
285 : {
286 : // For list chunking, ReportEngine should not rollback the buffer when CHIP_ERROR_NO_MEMORY or similar error occurred.
287 : // However, the error might be raised in the middle of encoding procedure, then the buffer may contain partial data,
288 : // unclosed containers etc. This line clears all possible partial data and makes EncodeListItem is atomic.
289 44 : mAttributeReportIBsBuilder.Rollback(backup);
290 44 : return err;
291 : }
292 :
293 1838 : mCurrentEncodingListIndex++;
294 1838 : mEncodeState.mCurrentEncodingListIndex++;
295 1838 : mEncodedAtLeastOneListItem = true;
296 1838 : return CHIP_NO_ERROR;
297 : }
298 :
299 : /**
300 : * Builds a single AttributeReportIB in AttributeReportIBs. The caller is
301 : * responsible for setting up mPath correctly.
302 : *
303 : * In particular, when we are encoding a single element in the list, mPath
304 : * must indicate a null list index to represent an "append" operation.
305 : * operation.
306 : */
307 : template <typename... Ts>
308 164 : CHIP_ERROR EncodeAttributeReportIB(Ts &&... aArgs)
309 : {
310 : AttributeReportBuilder builder;
311 164 : ReturnErrorOnFailure(builder.PrepareAttribute(mAttributeReportIBsBuilder, mPath, mDataVersion));
312 164 : ReturnErrorOnFailure(builder.EncodeValue(mAttributeReportIBsBuilder, TLV::ContextTag(AttributeDataIB::Tag::kData),
313 : std::forward<Ts>(aArgs)...));
314 :
315 164 : return builder.FinishAttribute(mAttributeReportIBsBuilder);
316 : }
317 :
318 : /**
319 : * EnsureListStarted sets our mCurrentEncodingListIndex to 0, and:
320 : *
321 : * * If we are just starting the list, gets us ready to encode list items.
322 : *
323 : * * If we are continuing a chunked list, guarantees that mPath.mListOp is
324 : * AppendItem after it returns.
325 : */
326 : CHIP_ERROR EnsureListStarted();
327 :
328 : /**
329 : * EnsureListEnded writes out the end of the list and our attribute data IB,
330 : * if we were encoding our initial list
331 : */
332 : void EnsureListEnded();
333 :
334 : bool mTriedEncode = false;
335 : AttributeReportIBs::Builder & mAttributeReportIBsBuilder;
336 : const FabricIndex mAccessingFabricIndex;
337 : ConcreteDataAttributePath mPath;
338 : DataVersion mDataVersion;
339 : bool mIsFabricFiltered = false;
340 : // mEncodingInitialList is true if we're encoding a list and we have not
341 : // started chunking it yet, so we're encoding a single attribute report IB
342 : // for the whole list, not one per item.
343 : bool mEncodingInitialList = false;
344 : // mEncodedAtLeastOneListItem becomes true once we successfully encode a list item.
345 : bool mEncodedAtLeastOneListItem = false;
346 : AttributeEncodeState mEncodeState;
347 : ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
348 : };
349 :
350 : class AttributeValueDecoder
351 : {
352 : public:
353 2434 : AttributeValueDecoder(TLV::TLVReader & aReader, const Access::SubjectDescriptor & aSubjectDescriptor) :
354 2434 : mReader(aReader), mSubjectDescriptor(aSubjectDescriptor)
355 2434 : {}
356 :
357 : template <typename T, typename std::enable_if_t<!DataModel::IsFabricScoped<T>::value, bool> = true>
358 : CHIP_ERROR Decode(T & aArg)
359 : {
360 : mTriedDecode = true;
361 : return DataModel::Decode(mReader, aArg);
362 : }
363 :
364 : template <typename T, typename std::enable_if_t<DataModel::IsFabricScoped<T>::value, bool> = true>
365 : CHIP_ERROR Decode(T & aArg)
366 : {
367 : mTriedDecode = true;
368 : // The WriteRequest comes with no fabric index, this will happen when receiving a write request on a PASE session before
369 : // AddNOC.
370 : VerifyOrReturnError(AccessingFabricIndex() != kUndefinedFabricIndex, CHIP_IM_GLOBAL_STATUS(UnsupportedAccess));
371 : ReturnErrorOnFailure(DataModel::Decode(mReader, aArg));
372 : aArg.SetFabricIndex(AccessingFabricIndex());
373 : return CHIP_NO_ERROR;
374 : }
375 :
376 2429 : bool TriedDecode() const { return mTriedDecode; }
377 :
378 : /**
379 : * The accessing fabric index for this write interaction.
380 : */
381 : FabricIndex AccessingFabricIndex() const { return mSubjectDescriptor.fabricIndex; }
382 :
383 : /**
384 : * The accessing subject descriptor for this write interaction.
385 : */
386 : const Access::SubjectDescriptor & GetSubjectDescriptor() const { return mSubjectDescriptor; }
387 :
388 : private:
389 : TLV::TLVReader & mReader;
390 : bool mTriedDecode = false;
391 : const Access::SubjectDescriptor mSubjectDescriptor;
392 : };
393 :
394 : class AttributeAccessInterface
395 : {
396 : public:
397 : /**
398 : * aEndpointId can be Missing to indicate that this object is meant to be
399 : * used with all endpoints.
400 : */
401 2474 : AttributeAccessInterface(Optional<EndpointId> aEndpointId, ClusterId aClusterId) :
402 2474 : mEndpointId(aEndpointId), mClusterId(aClusterId)
403 2474 : {}
404 2474 : virtual ~AttributeAccessInterface() {}
405 :
406 : /**
407 : * Callback for reading attributes.
408 : *
409 : * @param [in] aPath indicates which exact data is being read.
410 : * @param [in] aEncoder the AttributeValueEncoder to use for encoding the
411 : * data.
412 : *
413 : * The implementation can do one of three things:
414 : *
415 : * 1) Return a failure. This is treated as a failed read and the error is
416 : * returned to the client, by converting it to a StatusIB.
417 : * 2) Return success and attempt to encode data using aEncoder. The data is
418 : * returned to the client.
419 : * 3) Return success and not attempt to encode any data using aEncoder. In
420 : * this case, Ember attribute access will happen for the read. This may
421 : * involve reading from the attribute store or external attribute
422 : * callbacks.
423 : */
424 : virtual CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) = 0;
425 :
426 : /**
427 : * Callback for writing attributes.
428 : *
429 : * @param [in] aPath indicates which exact data is being written.
430 : * @param [in] aDecoder the AttributeValueDecoder to use for decoding the
431 : * data.
432 : *
433 : * The implementation can do one of three things:
434 : *
435 : * 1) Return a failure. This is treated as a failed write and the error is
436 : * sent to the client, by converting it to a StatusIB.
437 : * 2) Return success and attempt to decode from aDecoder. This is
438 : * treated as a successful write.
439 : * 3) Return success and not attempt to decode from aDecoder. In
440 : * this case, Ember attribute access will happen for the write. This may
441 : * involve writing to the attribute store or external attribute
442 : * callbacks.
443 : */
444 0 : virtual CHIP_ERROR Write(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder) { return CHIP_NO_ERROR; }
445 :
446 : /**
447 : * Indicates the start of a series of list operations. This function will be called before the first Write operation of a series
448 : * of consequence attribute data of the same attribute.
449 : *
450 : * 1) This function will be called if the client tries to set a nullable list attribute to null.
451 : * 2) This function will only be called once for a series of consequent attribute data (regardless the kind of list operation)
452 : * of the same attribute.
453 : *
454 : * @param [in] aPath indicates the path of the modified list.
455 : */
456 0 : virtual void OnListWriteBegin(const ConcreteAttributePath & aPath) {}
457 :
458 : /**
459 : * Indicates the end of a series of list operations. This function will be called after the last Write operation of a series
460 : * of consequence attribute data of the same attribute.
461 : *
462 : * 1) This function will be called if the client tries to set a nullable list attribute to null.
463 : * 2) This function will only be called once for a series of consequent attribute data (regardless the kind of list operation)
464 : * of the same attribute.
465 : * 3) When aWriteWasSuccessful is true, the data written must be consistent or the list is untouched.
466 : *
467 : * @param [in] aPath indicates the path of the modified list
468 : * @param [in] aWriteWasSuccessful indicates whether the delivered list is complete.
469 : *
470 : */
471 0 : virtual void OnListWriteEnd(const ConcreteAttributePath & aPath, bool aWriteWasSuccessful) {}
472 :
473 : /**
474 : * Mechanism for keeping track of a chain of AttributeAccessInterfaces.
475 : */
476 3 : void SetNext(AttributeAccessInterface * aNext) { mNext = aNext; }
477 15 : AttributeAccessInterface * GetNext() const { return mNext; }
478 :
479 : /**
480 : * Check whether a this AttributeAccessInterface is relevant for a
481 : * particular endpoint+cluster. An AttributeAccessInterface will be used
482 : * for a read from a particular cluster only when this function returns
483 : * true.
484 : */
485 5002 : bool Matches(EndpointId aEndpointId, ClusterId aClusterId) const
486 : {
487 5002 : return (!mEndpointId.HasValue() || mEndpointId.Value() == aEndpointId) && mClusterId == aClusterId;
488 : }
489 :
490 : /**
491 : * Check whether an AttributeAccessInterface is relevant for a particular
492 : * specific endpoint. This is used to clean up overrides registered for an
493 : * endpoint that becomes disabled.
494 : */
495 15 : bool MatchesEndpoint(EndpointId aEndpointId) const { return mEndpointId.HasValue() && mEndpointId.Value() == aEndpointId; }
496 :
497 : /**
498 : * Check whether another AttributeAccessInterface wants to handle the same set of
499 : * attributes as we do.
500 : */
501 4 : bool Matches(const AttributeAccessInterface & aOther) const
502 : {
503 8 : return mClusterId == aOther.mClusterId &&
504 8 : (!mEndpointId.HasValue() || !aOther.mEndpointId.HasValue() || mEndpointId.Value() == aOther.mEndpointId.Value());
505 : }
506 :
507 : private:
508 : Optional<EndpointId> mEndpointId;
509 : ClusterId mClusterId;
510 : AttributeAccessInterface * mNext = nullptr;
511 : };
512 :
513 : } // namespace app
514 : } // namespace chip
|