blob: c195dd4f8b66ef08c779584bd79589a6b8d78fab [file] [log] [blame]
/*
*
* Copyright (c) 2021-2022 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <access/SubjectDescriptor.h>
#include <app/ConcreteAttributePath.h>
#include <app/MessageDef/AttributeReportIBs.h>
#include <app/data-model/DecodableList.h>
#include <app/data-model/Decode.h>
#include <app/data-model/Encode.h>
#include <app/data-model/FabricScoped.h>
#include <app/data-model/List.h> // So we can encode lists
#include <app/data-model/TagBoundEncoder.h>
#include <app/util/basic-types.h>
#include <lib/core/Optional.h>
#include <lib/core/TLV.h>
#include <lib/support/logging/CHIPLogging.h>
/**
* Callback class that clusters can implement in order to interpose custom
* attribute-handling logic. An AttributeAccessInterface instance is associated
* with some specific cluster. A single instance may be used for a specific
* endpoint or for all endpoints.
*
* Instances of AttributeAccessInterface that are registered via
* registerAttributeAccessOverride will be consulted before taking the normal
* attribute access codepath and can use that codepath as a fallback if desired.
*/
namespace chip {
namespace app {
/**
* The AttributeReportBuilder is a helper class for filling a single report in AttributeReportIBs.
*
* Possible usage of AttributeReportBuilder might be:
*
* AttributeReportBuilder builder;
* ReturnErrorOnFailure(builder.PrepareAttribute(...));
* ReturnErrorOnFailure(builder.Encode(...));
* ReturnErrorOnFailure(builder.FinishAttribute());
*/
class AttributeReportBuilder
{
public:
/**
* PrepareAttribute encodes the "header" part of an attribute report including the path and data version.
* Path will be encoded according to section 10.5.4.3.1 in the spec.
* Note: Only append is supported currently (encode a null list index), other operations won't encode a list index in the
* attribute path field.
* TODO: Add support for encoding a single element in the list (path with a valid list index).
*/
CHIP_ERROR PrepareAttribute(AttributeReportIBs::Builder & aAttributeReportIBs, const ConcreteDataAttributePath & aPath,
DataVersion aDataVersion);
/**
* FinishAttribute encodes the "footer" part of an attribute report (it closes the containers opened in PrepareAttribute)
*/
CHIP_ERROR FinishAttribute(AttributeReportIBs::Builder & aAttributeReportIBs);
/**
* EncodeValue encodes the value field of the report, it should be called exactly once.
*/
template <typename T, std::enable_if_t<!DataModel::IsFabricScoped<T>::value, bool> = true, typename... Ts>
CHIP_ERROR EncodeValue(AttributeReportIBs::Builder & aAttributeReportIBs, TLV::Tag tag, T && item, Ts &&... aArgs)
{
return DataModel::Encode(*(aAttributeReportIBs.GetAttributeReport().GetAttributeData().GetWriter()), tag, item,
std::forward<Ts>(aArgs)...);
}
template <typename T, std::enable_if_t<DataModel::IsFabricScoped<T>::value, bool> = true, typename... Ts>
CHIP_ERROR EncodeValue(AttributeReportIBs::Builder & aAttributeReportIBs, TLV::Tag tag, FabricIndex accessingFabricIndex,
T && item, Ts &&... aArgs)
{
return DataModel::EncodeForRead(*(aAttributeReportIBs.GetAttributeReport().GetAttributeData().GetWriter()), tag,
accessingFabricIndex, item, std::forward<Ts>(aArgs)...);
}
};
/**
* The AttributeValueEncoder is a helper class for filling report payloads into AttributeReportIBs.
* The attribute value encoder can be initialized with a AttributeEncodeState for saving and recovering its state between encode
* sessions (chunkings).
*
* When Encode returns recoverable errors (e.g. CHIP_ERROR_NO_MEMORY) the state can be used to initialize the AttributeValueEncoder
* for future use on the same attribute path.
*/
class AttributeValueEncoder
{
public:
class ListEncodeHelper
{
public:
ListEncodeHelper(AttributeValueEncoder & encoder) : mAttributeValueEncoder(encoder) {}
template <typename T, std::enable_if_t<DataModel::IsFabricScoped<T>::value, bool> = true>
CHIP_ERROR Encode(T && aArg) const
{
VerifyOrReturnError(aArg.GetFabricIndex() != kUndefinedFabricIndex, CHIP_ERROR_INVALID_FABRIC_INDEX);
// If we are encoding for a fabric filtered attribute read and the fabric index does not match that present in the
// request, skip encoding this list item.
VerifyOrReturnError(!mAttributeValueEncoder.mIsFabricFiltered ||
aArg.GetFabricIndex() == mAttributeValueEncoder.mAccessingFabricIndex,
CHIP_NO_ERROR);
return mAttributeValueEncoder.EncodeListItem(mAttributeValueEncoder.mAccessingFabricIndex, std::forward<T>(aArg));
}
template <typename T, std::enable_if_t<!DataModel::IsFabricScoped<T>::value, bool> = true>
CHIP_ERROR Encode(T && aArg) const
{
return mAttributeValueEncoder.EncodeListItem(std::forward<T>(aArg));
}
private:
AttributeValueEncoder & mAttributeValueEncoder;
};
class AttributeEncodeState
{
public:
AttributeEncodeState() : mAllowPartialData(false), mCurrentEncodingListIndex(kInvalidListIndex) {}
bool AllowPartialData() const { return mAllowPartialData; }
private:
friend class AttributeValueEncoder;
/**
* When an attempt to encode an attribute returns an error, the buffer may contain tailing dirty data
* (since the put was aborted). The report engine normally rolls back the buffer to right before encoding
* of the attribute started on errors.
*
* When chunking a list, EncodeListItem will atomically encode list items, ensuring that the
* state of the buffer is valid to send (i.e. contains no trailing garbage), and return an error
* if the list doesn't entirely fit. In this situation, mAllowPartialData is set to communicate to the
* report engine that it should not roll back the list items.
*
* TODO: There might be a better name for this variable.
*/
bool mAllowPartialData = false;
/**
* If set to kInvalidListIndex, indicates that we have not encoded any data for the list yet and
* need to start by encoding an empty list before we start encoding any list items.
*
* When set to a valid ListIndex value, indicates the index of the next list item that needs to be
* encoded (i.e. the count of items encoded so far).
*/
ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
};
AttributeValueEncoder(AttributeReportIBs::Builder & aAttributeReportIBsBuilder, FabricIndex aAccessingFabricIndex,
const ConcreteAttributePath & aPath, DataVersion aDataVersion, bool aIsFabricFiltered = false,
const AttributeEncodeState & aState = AttributeEncodeState()) :
mAttributeReportIBsBuilder(aAttributeReportIBsBuilder),
mAccessingFabricIndex(aAccessingFabricIndex), mPath(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId),
mDataVersion(aDataVersion), mIsFabricFiltered(aIsFabricFiltered), mEncodeState(aState)
{}
/**
* Encode a single value. This value will not be chunked; it will either be
* entirely encoded or fail to be encoded. Consumers are allowed to make
* either one call to Encode or one call to EncodeList to handle a read.
*/
template <typename... Ts>
CHIP_ERROR Encode(Ts &&... aArgs)
{
mTriedEncode = true;
return EncodeAttributeReportIB(std::forward<Ts>(aArgs)...);
}
/**
* Encode an explicit null value.
*/
CHIP_ERROR EncodeNull()
{
// Doesn't matter what type Nullable we use here.
return Encode(DataModel::Nullable<uint8_t>());
}
/**
* Encode an explicit empty list.
*/
CHIP_ERROR EncodeEmptyList()
{
// Doesn't matter what type List we use here.
return Encode(DataModel::List<uint8_t>());
}
/**
* aCallback is expected to take a const auto & argument and Encode() on it as many times as needed to encode all the list
* elements one by one. If any of those Encode() calls returns failure, aCallback must stop encoding and return failure. When
* all items are encoded aCallback is expected to return success.
*
* aCallback may not be called. Consumers must not assume it will be called.
*
* When EncodeList returns an error, the consumers must abort the encoding, and return the exact error to the caller.
*
* TODO: Can we hold a error state in the AttributeValueEncoder itself so functions in ember-compatibility-functions don't have
* to rely on the above assumption?
*
* Consumers are allowed to make either one call to EncodeList or one call to Encode to handle a read.
*
*/
template <typename ListGenerator>
CHIP_ERROR EncodeList(ListGenerator aCallback)
{
mTriedEncode = true;
// Spec 10.5.4.3.1, 10.5.4.6 (Replace a list w/ Multiple IBs)
// EmptyList acts as the beginning of the whole array type attribute report.
// An empty list is encoded iff both mCurrentEncodingListIndex and mEncodeState.mCurrentEncodingListIndex are invalid
// values. After encoding the empty list, mEncodeState.mCurrentEncodingListIndex and mCurrentEncodingListIndex are set to 0.
ReturnErrorOnFailure(EnsureListStarted());
CHIP_ERROR err = aCallback(ListEncodeHelper(*this));
// Even if encoding list items failed, make sure we EnsureListEnded().
// Since we encode list items atomically, in the case when we just
// didn't fit the next item we want to make sure our list is properly
// ended before the reporting engine starts chunking.
EnsureListEnded();
if (err == CHIP_NO_ERROR)
{
// The Encode procedure finished without any error, clear the state.
mEncodeState = AttributeEncodeState();
}
return err;
}
bool TriedEncode() const { return mTriedEncode; }
/**
* The accessing fabric index for this read or subscribe interaction.
*/
FabricIndex AccessingFabricIndex() const { return mAccessingFabricIndex; }
/**
* AttributeValueEncoder is a short lived object, and the state is persisted by mEncodeState and restored by constructor.
*/
const AttributeEncodeState & GetState() const { return mEncodeState; }
private:
// We made EncodeListItem() private, and ListEncoderHelper will expose it by Encode()
friend class ListEncodeHelper;
template <typename... Ts>
CHIP_ERROR EncodeListItem(Ts &&... aArgs)
{
// EncodeListItem must be called after EnsureListStarted(), thus mCurrentEncodingListIndex and
// mEncodeState.mCurrentEncodingListIndex are not invalid values.
if (mCurrentEncodingListIndex < mEncodeState.mCurrentEncodingListIndex)
{
// We have encoded this element in previous chunks, skip it.
mCurrentEncodingListIndex++;
return CHIP_NO_ERROR;
}
TLV::TLVWriter backup;
mAttributeReportIBsBuilder.Checkpoint(backup);
CHIP_ERROR err;
if (mEncodingInitialList)
{
// Just encode a single item, with an anonymous tag.
AttributeReportBuilder builder;
err = builder.EncodeValue(mAttributeReportIBsBuilder, TLV::AnonymousTag(), std::forward<Ts>(aArgs)...);
}
else
{
err = EncodeAttributeReportIB(std::forward<Ts>(aArgs)...);
}
if (err != CHIP_NO_ERROR)
{
// For list chunking, ReportEngine should not rollback the buffer when CHIP_ERROR_NO_MEMORY or similar error occurred.
// However, the error might be raised in the middle of encoding procedure, then the buffer may contain partial data,
// unclosed containers etc. This line clears all possible partial data and makes EncodeListItem is atomic.
mAttributeReportIBsBuilder.Rollback(backup);
return err;
}
mCurrentEncodingListIndex++;
mEncodeState.mCurrentEncodingListIndex++;
mEncodedAtLeastOneListItem = true;
return CHIP_NO_ERROR;
}
/**
* Builds a single AttributeReportIB in AttributeReportIBs. The caller is
* responsible for setting up mPath correctly.
*
* In particular, when we are encoding a single element in the list, mPath
* must indicate a null list index to represent an "append" operation.
* operation.
*/
template <typename... Ts>
CHIP_ERROR EncodeAttributeReportIB(Ts &&... aArgs)
{
AttributeReportBuilder builder;
ReturnErrorOnFailure(builder.PrepareAttribute(mAttributeReportIBsBuilder, mPath, mDataVersion));
ReturnErrorOnFailure(builder.EncodeValue(mAttributeReportIBsBuilder, TLV::ContextTag(AttributeDataIB::Tag::kData),
std::forward<Ts>(aArgs)...));
return builder.FinishAttribute(mAttributeReportIBsBuilder);
}
/**
* EnsureListStarted sets our mCurrentEncodingListIndex to 0, and:
*
* * If we are just starting the list, gets us ready to encode list items.
*
* * If we are continuing a chunked list, guarantees that mPath.mListOp is
* AppendItem after it returns.
*/
CHIP_ERROR EnsureListStarted();
/**
* EnsureListEnded writes out the end of the list and our attribute data IB,
* if we were encoding our initial list
*/
void EnsureListEnded();
bool mTriedEncode = false;
AttributeReportIBs::Builder & mAttributeReportIBsBuilder;
const FabricIndex mAccessingFabricIndex;
ConcreteDataAttributePath mPath;
DataVersion mDataVersion;
bool mIsFabricFiltered = false;
// mEncodingInitialList is true if we're encoding a list and we have not
// started chunking it yet, so we're encoding a single attribute report IB
// for the whole list, not one per item.
bool mEncodingInitialList = false;
// mEncodedAtLeastOneListItem becomes true once we successfully encode a list item.
bool mEncodedAtLeastOneListItem = false;
AttributeEncodeState mEncodeState;
ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
};
class AttributeValueDecoder
{
public:
AttributeValueDecoder(TLV::TLVReader & aReader, const Access::SubjectDescriptor & aSubjectDescriptor) :
mReader(aReader), mSubjectDescriptor(aSubjectDescriptor)
{}
template <typename T, typename std::enable_if_t<!DataModel::IsFabricScoped<T>::value, bool> = true>
CHIP_ERROR Decode(T & aArg)
{
mTriedDecode = true;
return DataModel::Decode(mReader, aArg);
}
template <typename T, typename std::enable_if_t<DataModel::IsFabricScoped<T>::value, bool> = true>
CHIP_ERROR Decode(T & aArg)
{
mTriedDecode = true;
// The WriteRequest comes with no fabric index, this will happen when receiving a write request on a PASE session before
// AddNOC.
VerifyOrReturnError(AccessingFabricIndex() != kUndefinedFabricIndex, CHIP_IM_GLOBAL_STATUS(UnsupportedAccess));
ReturnErrorOnFailure(DataModel::Decode(mReader, aArg));
aArg.SetFabricIndex(AccessingFabricIndex());
return CHIP_NO_ERROR;
}
bool TriedDecode() const { return mTriedDecode; }
/**
* The accessing fabric index for this write interaction.
*/
FabricIndex AccessingFabricIndex() const { return mSubjectDescriptor.fabricIndex; }
/**
* The accessing subject descriptor for this write interaction.
*/
const Access::SubjectDescriptor & GetSubjectDescriptor() const { return mSubjectDescriptor; }
private:
TLV::TLVReader & mReader;
bool mTriedDecode = false;
const Access::SubjectDescriptor mSubjectDescriptor;
};
class AttributeAccessInterface
{
public:
/**
* aEndpointId can be Missing to indicate that this object is meant to be
* used with all endpoints.
*/
AttributeAccessInterface(Optional<EndpointId> aEndpointId, ClusterId aClusterId) :
mEndpointId(aEndpointId), mClusterId(aClusterId)
{}
virtual ~AttributeAccessInterface() {}
/**
* Callback for reading attributes.
*
* @param [in] aPath indicates which exact data is being read.
* @param [in] aEncoder the AttributeValueEncoder to use for encoding the
* data.
*
* The implementation can do one of three things:
*
* 1) Return a failure. This is treated as a failed read and the error is
* returned to the client, by converting it to a StatusIB.
* 2) Return success and attempt to encode data using aEncoder. The data is
* returned to the client.
* 3) Return success and not attempt to encode any data using aEncoder. In
* this case, Ember attribute access will happen for the read. This may
* involve reading from the attribute store or external attribute
* callbacks.
*/
virtual CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) = 0;
/**
* Callback for writing attributes.
*
* @param [in] aPath indicates which exact data is being written.
* @param [in] aDecoder the AttributeValueDecoder to use for decoding the
* data.
*
* The implementation can do one of three things:
*
* 1) Return a failure. This is treated as a failed write and the error is
* sent to the client, by converting it to a StatusIB.
* 2) Return success and attempt to decode from aDecoder. This is
* treated as a successful write.
* 3) Return success and not attempt to decode from aDecoder. In
* this case, Ember attribute access will happen for the write. This may
* involve writing to the attribute store or external attribute
* callbacks.
*/
virtual CHIP_ERROR Write(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder) { return CHIP_NO_ERROR; }
/**
* Indicates the start of a series of list operations. This function will be called before the first Write operation of a series
* of consequence attribute data of the same attribute.
*
* 1) This function will be called if the client tries to set a nullable list attribute to null.
* 2) This function will only be called once for a series of consequent attribute data (regardless the kind of list operation)
* of the same attribute.
*
* @param [in] aPath indicates the path of the modified list.
*/
virtual void OnListWriteBegin(const ConcreteAttributePath & aPath) {}
/**
* Indicates the end of a series of list operations. This function will be called after the last Write operation of a series
* of consequence attribute data of the same attribute.
*
* 1) This function will be called if the client tries to set a nullable list attribute to null.
* 2) This function will only be called once for a series of consequent attribute data (regardless the kind of list operation)
* of the same attribute.
* 3) When aWriteWasSuccessful is true, the data written must be consistent or the list is untouched.
*
* @param [in] aPath indicates the path of the modified list
* @param [in] aWriteWasSuccessful indicates whether the delivered list is complete.
*
*/
virtual void OnListWriteEnd(const ConcreteAttributePath & aPath, bool aWriteWasSuccessful) {}
/**
* Mechanism for keeping track of a chain of AttributeAccessInterfaces.
*/
void SetNext(AttributeAccessInterface * aNext) { mNext = aNext; }
AttributeAccessInterface * GetNext() const { return mNext; }
/**
* Check whether a this AttributeAccessInterface is relevant for a
* particular endpoint+cluster. An AttributeAccessInterface will be used
* for a read from a particular cluster only when this function returns
* true.
*/
bool Matches(EndpointId aEndpointId, ClusterId aClusterId) const
{
return (!mEndpointId.HasValue() || mEndpointId.Value() == aEndpointId) && mClusterId == aClusterId;
}
/**
* Check whether an AttributeAccessInterface is relevant for a particular
* specific endpoint. This is used to clean up overrides registered for an
* endpoint that becomes disabled.
*/
bool MatchesEndpoint(EndpointId aEndpointId) const { return mEndpointId.HasValue() && mEndpointId.Value() == aEndpointId; }
/**
* Check whether another AttributeAccessInterface wants to handle the same set of
* attributes as we do.
*/
bool Matches(const AttributeAccessInterface & aOther) const
{
return mClusterId == aOther.mClusterId &&
(!mEndpointId.HasValue() || !aOther.mEndpointId.HasValue() || mEndpointId.Value() == aOther.mEndpointId.Value());
}
private:
Optional<EndpointId> mEndpointId;
ClusterId mClusterId;
AttributeAccessInterface * mNext = nullptr;
};
} // namespace app
} // namespace chip