blob: e6e5a16a2031359ff64ac20bc15bf6dc7f108610 [file] [log] [blame]
/*
*
* Copyright (c) 2025 Project CHIP Authors
*
* 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.
*/
#include <app/server/JointFabricDatastore.h>
#include <algorithm>
#include <unordered_set>
namespace chip {
namespace app {
void JointFabricDatastore::CopyGroupKeySetWithOwnedSpans(
const Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type & source,
Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type & destination)
{
auto & storage = mGroupKeySetStorage[source.groupKeySetID];
destination.groupKeySetID = source.groupKeySetID;
destination.groupKeySecurityPolicy = source.groupKeySecurityPolicy;
destination.groupKeyMulticastPolicy = source.groupKeyMulticastPolicy;
CopyByteSpanWithOwnedStorage(source.epochKey0, storage.epochKey0, destination.epochKey0);
CopyByteSpanWithOwnedStorage(source.epochKey1, storage.epochKey1, destination.epochKey1);
CopyByteSpanWithOwnedStorage(source.epochKey2, storage.epochKey2, destination.epochKey2);
CopyNullableValue(source.epochStartTime0, destination.epochStartTime0);
CopyNullableValue(source.epochStartTime1, destination.epochStartTime1);
CopyNullableValue(source.epochStartTime2, destination.epochStartTime2);
}
void JointFabricDatastore::RemoveGroupKeySetStorage(uint16_t groupKeySetId)
{
mGroupKeySetStorage.erase(groupKeySetId);
}
void JointFabricDatastore::SetGroupInformationFriendlyNameWithOwnedStorage(
GroupId groupId, const CharSpan & friendlyName,
Clusters::JointFabricDatastore::Structs::DatastoreGroupInformationEntryStruct::Type & destination)
{
auto & storage = mGroupInformationStorage[groupId];
storage.friendlyName.assign(friendlyName.data(), friendlyName.data() + friendlyName.size());
destination.friendlyName = CharSpan(storage.friendlyName.data(), storage.friendlyName.size());
}
void JointFabricDatastore::RemoveGroupInformationStorage(GroupId groupId)
{
mGroupInformationStorage.erase(groupId);
}
CHIP_ERROR JointFabricDatastore::SetAdminEntryWithOwnedStorage(
NodeId nodeId, const CharSpan & friendlyName, const ByteSpan & icac,
Clusters::JointFabricDatastore::Structs::DatastoreAdministratorInformationEntryStruct::Type & destination)
{
auto & storage = mAdminEntryStorage[nodeId];
storage.friendlyName.assign(friendlyName.data(), friendlyName.data() + friendlyName.size());
destination.friendlyName = CharSpan(storage.friendlyName.data(), storage.friendlyName.size());
storage.icac.assign(icac.data(), icac.data() + icac.size());
destination.icac = ByteSpan(storage.icac.data(), storage.icac.size());
return CHIP_NO_ERROR;
}
void JointFabricDatastore::RemoveAdminEntryStorage(NodeId nodeId)
{
mAdminEntryStorage.erase(nodeId);
}
void JointFabricDatastore::CopyByteSpanWithOwnedStorage(const DataModel::Nullable<ByteSpan> & source,
std::vector<uint8_t> & storage, DataModel::Nullable<ByteSpan> & destination)
{
if (!source.IsNull())
{
storage.assign(source.Value().data(), source.Value().data() + source.Value().size());
destination = ByteSpan(storage.data(), storage.size());
}
else
{
storage.clear();
destination.SetNull();
}
}
void JointFabricDatastore::AddListener(Listener & listener)
{
if (mListeners == nullptr)
{
mListeners = &listener;
listener.mNext = nullptr;
return;
}
for (Listener * l = mListeners; /**/; l = l->mNext)
{
if (l == &listener)
{
return;
}
if (l->mNext == nullptr)
{
l->mNext = &listener;
listener.mNext = nullptr;
return;
}
}
}
void JointFabricDatastore::RemoveListener(Listener & listener)
{
if (mListeners == &listener)
{
mListeners = listener.mNext;
listener.mNext = nullptr;
return;
}
for (Listener * l = mListeners; l != nullptr; l = l->mNext)
{
if (l->mNext == &listener)
{
l->mNext = listener.mNext;
listener.mNext = nullptr;
return;
}
}
}
CHIP_ERROR JointFabricDatastore::AddPendingNode(NodeId nodeId, const CharSpan & friendlyName)
{
VerifyOrReturnError(mNodeInformationEntries.size() < kMaxNodes, CHIP_ERROR_NO_MEMORY);
// check that nodeId does not already exist
VerifyOrReturnError(
std::none_of(mNodeInformationEntries.begin(), mNodeInformationEntries.end(),
[nodeId](const GenericDatastoreNodeInformationEntry & entry) { return entry.nodeID == nodeId; }),
CHIP_IM_GLOBAL_STATUS(ConstraintError));
mNodeInformationEntries.push_back(GenericDatastoreNodeInformationEntry(
nodeId, Clusters::JointFabricDatastore::DatastoreStateEnum::kPending, MakeOptional(friendlyName)));
for (Listener * listener = mListeners; listener != nullptr; listener = listener->mNext)
{
listener->MarkNodeListChanged();
}
return CHIP_NO_ERROR;
}
CHIP_ERROR JointFabricDatastore::UpdateNode(NodeId nodeId, const CharSpan & friendlyName)
{
for (auto & entry : mNodeInformationEntries)
{
if (entry.nodeID == nodeId)
{
entry.Set(MakeOptional(friendlyName));
for (Listener * listener = mListeners; listener != nullptr; listener = listener->mNext)
{
listener->MarkNodeListChanged();
}
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::RemoveNode(NodeId nodeId)
{
for (auto it = mNodeInformationEntries.begin(); it != mNodeInformationEntries.end(); ++it)
{
if (it->nodeID == nodeId)
{
mNodeInformationEntries.erase(it);
for (Listener * listener = mListeners; listener != nullptr; listener = listener->mNext)
{
listener->MarkNodeListChanged();
}
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::RefreshNode(NodeId nodeId)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(mRefreshingNodeId == kUndefinedNodeId, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(mRefreshState == kIdle, CHIP_ERROR_INCORRECT_STATE);
mRefreshingNodeId = nodeId;
ReturnErrorOnFailure(ContinueRefresh());
return CHIP_NO_ERROR;
}
CHIP_ERROR JointFabricDatastore::ContinueRefresh()
{
switch (mRefreshState)
{
case kIdle: {
// 1. Confirm that a Node Information Entry exists for the given NodeID, and if not, return NOT_FOUND.
// 2. Set the Node Information Entry's state to Pending.
ReturnErrorOnFailure(SetNode(mRefreshingNodeId, Clusters::JointFabricDatastore::DatastoreStateEnum::kPending));
// Request endpoints from the device's Descriptor cluster and transition to the
// kRefreshingEndpoints state. The delegate call is asynchronous and will invoke
// the provided callback when complete; the callback stores the received list
// and calls ContinueRefresh() to advance the state machine.
ReturnErrorOnFailure(mDelegate->FetchEndpointList(
mRefreshingNodeId,
[this](CHIP_ERROR err,
const std::vector<Clusters::JointFabricDatastore::Structs::DatastoreEndpointEntryStruct::Type> & endpoints) {
if (err == CHIP_NO_ERROR)
{
// Store the fetched endpoints for processing in the next state.
mRefreshingEndpointsList = endpoints;
// Advance the state machine to process the endpoints.
mRefreshState = kRefreshingEndpoints;
}
else
{
// Leave node as pending but tear down the refresh state.
mRefreshingNodeId = kUndefinedNodeId;
mRefreshState = kIdle;
return;
}
// Continue the state machine (will enter kRefreshingEndpoints branch
// when successful and process mRefreshingEndpointsList).
if (ContinueRefresh() != CHIP_NO_ERROR)
{
// Ignore errors in continuation from within the callback.
}
}));
}
break;
case kRefreshingEndpoints: {
// 3. cycle through mRefreshingEndpointsList and add them to the endpoint entries
for (const auto & endpoint : mRefreshingEndpointsList)
{
auto it = std::find_if(
mEndpointEntries.begin(), mEndpointEntries.end(),
[this, &endpoint](const Clusters::JointFabricDatastore::Structs::DatastoreEndpointEntryStruct::Type & entry) {
return entry.nodeID == mRefreshingNodeId && entry.endpointID == endpoint.endpointID;
});
if (it == mEndpointEntries.end())
{
Clusters::JointFabricDatastore::Structs::DatastoreEndpointEntryStruct::Type newEntry;
newEntry.endpointID = endpoint.endpointID;
newEntry.nodeID = mRefreshingNodeId;
mEndpointEntries.push_back(newEntry);
}
}
// TODO: sync friendly name between datastore entry and basic cluster
// Remove EndpointEntries that are not in the mRefreshingEndpointsList
mEndpointEntries.erase(
std::remove_if(mEndpointEntries.begin(), mEndpointEntries.end(),
[&](const auto & entry) {
if (entry.nodeID != mRefreshingNodeId)
{
return false;
}
return !std::any_of(mRefreshingEndpointsList.begin(), mRefreshingEndpointsList.end(),
[&](const auto & endpoint) { return entry.endpointID == endpoint.endpointID; });
}),
mEndpointEntries.end());
// Start fetching groups from the first endpoint
mRefreshingEndpointIndex = 0;
mRefreshState = kRefreshingGroups;
// Fall through to kRefreshingGroups to start fetching
return ContinueRefresh();
}
break;
case kRefreshingGroups: {
// Check if we still have endpoints to process for group fetching
if (mRefreshingEndpointIndex < mRefreshingEndpointsList.size())
{
// Fetch group list for the current endpoint
EndpointId currentEndpointId = mRefreshingEndpointsList[mRefreshingEndpointIndex].endpointID;
ReturnErrorOnFailure(mDelegate->FetchEndpointGroupList(
mRefreshingNodeId, currentEndpointId,
[this, currentEndpointId](
CHIP_ERROR err,
const std::vector<Clusters::JointFabricDatastore::Structs::DatastoreGroupInformationEntryStruct::Type> &
endpointGroups) {
if (err == CHIP_NO_ERROR)
{
// Convert endpointGroups to mEndpointGroupIDEntries for this specific endpoint
for (const auto & endpointGroup : endpointGroups)
{
auto it = std::find_if(
mEndpointGroupIDEntries.begin(), mEndpointGroupIDEntries.end(),
[this, currentEndpointId, &endpointGroup](
const Clusters::JointFabricDatastore::Structs::DatastoreEndpointGroupIDEntryStruct::Type &
entry) {
return entry.nodeID == mRefreshingNodeId && entry.endpointID == currentEndpointId &&
entry.groupID == endpointGroup.groupID;
});
if (it == mEndpointGroupIDEntries.end())
{
Clusters::JointFabricDatastore::Structs::DatastoreEndpointGroupIDEntryStruct::Type newEntry;
newEntry.nodeID = mRefreshingNodeId;
newEntry.endpointID = currentEndpointId;
newEntry.groupID = static_cast<GroupId>(endpointGroup.groupID);
newEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
mEndpointGroupIDEntries.push_back(newEntry);
}
}
// Remove entries not in endpointGroups for this specific endpoint
mEndpointGroupIDEntries.erase(
std::remove_if(mEndpointGroupIDEntries.begin(), mEndpointGroupIDEntries.end(),
[&, currentEndpointId](const auto & entry) {
if (entry.nodeID != mRefreshingNodeId || entry.endpointID != currentEndpointId)
{
return false;
}
if (entry.statusEntry.state !=
Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted &&
entry.statusEntry.state !=
Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
{
return false;
}
return !std::any_of(endpointGroups.begin(), endpointGroups.end(),
[&](const auto & eg) { return entry.groupID == eg.groupID; });
}),
mEndpointGroupIDEntries.end());
// Move to the next endpoint
mRefreshingEndpointIndex++;
}
else
{
// Leave node as pending but tear down the refresh state.
mRefreshingNodeId = kUndefinedNodeId;
mRefreshState = kIdle;
return;
}
// Continue to process next endpoint or move to syncing phase
if (ContinueRefresh() != CHIP_NO_ERROR)
{
// Ignore errors in continuation from within the callback.
}
}));
// Return here - the callback will call ContinueRefresh() again
return CHIP_NO_ERROR;
}
// All endpoints processed; now sync any pending/delete-pending entries
for (auto it = mEndpointGroupIDEntries.begin(); it != mEndpointGroupIDEntries.end();)
{
if (it->nodeID == mRefreshingNodeId)
{
if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending)
{
size_t idx = static_cast<size_t>(std::distance(mEndpointGroupIDEntries.begin(), it));
auto entryToSync = *it;
ReturnErrorOnFailure(mDelegate->SyncNode(mRefreshingNodeId, entryToSync, [this, idx]() {
if (idx < mEndpointGroupIDEntries.size())
{
mEndpointGroupIDEntries[idx].statusEntry.state =
Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
}
}));
}
else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
{
Clusters::JointFabricDatastore::Structs::DatastoreEndpointGroupIDEntryStruct::Type endpointGroupIdNullEntry{
0
};
auto entryToErase = *it;
ReturnErrorOnFailure(mDelegate->SyncNode(mRefreshingNodeId, endpointGroupIdNullEntry, [this, entryToErase]() {
mEndpointGroupIDEntries.erase(std::remove_if(mEndpointGroupIDEntries.begin(), mEndpointGroupIDEntries.end(),
[&](const auto & entry) {
return entry.nodeID == entryToErase.nodeID &&
entry.endpointID == entryToErase.endpointID &&
entry.groupID == entryToErase.groupID;
}),
mEndpointGroupIDEntries.end());
}));
}
else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed)
{
CHIP_ERROR failureCode(it->statusEntry.failureCode);
if (failureCode == CHIP_IM_GLOBAL_STATUS(ConstraintError) ||
failureCode == CHIP_IM_GLOBAL_STATUS(ResourceExhausted))
{
++it;
continue;
}
// Retry or handle failure - for now skip
++it;
continue;
}
}
++it;
}
// Start fetching groups from the first endpoint
mRefreshingEndpointIndex = 0;
mRefreshState = kRefreshingBindings;
// Fall through to kRefreshingGroups to start fetching
return ContinueRefresh();
}
break;
case kRefreshingBindings: {
// Check if we still have endpoints to process for group fetching
if (mRefreshingEndpointIndex < mRefreshingEndpointsList.size())
{
// Fetch group list for the current endpoint
EndpointId currentEndpointId = mRefreshingEndpointsList[mRefreshingEndpointIndex].endpointID;
ReturnErrorOnFailure(mDelegate->FetchEndpointBindingList(
mRefreshingNodeId, currentEndpointId,
[this](CHIP_ERROR err,
const std::vector<Clusters::JointFabricDatastore::Structs::DatastoreEndpointBindingEntryStruct::Type> &
endpointBindings) {
if (err == CHIP_NO_ERROR)
{
// Convert endpointBindings to mEndpointBindingEntries
for (const auto & endpointBinding : endpointBindings)
{
auto it = std::find_if(
mEndpointBindingEntries.begin(), mEndpointBindingEntries.end(),
[this, &endpointBinding](
const Clusters::JointFabricDatastore::Structs::DatastoreEndpointBindingEntryStruct::Type &
entry) {
return entry.nodeID == mRefreshingNodeId && entry.endpointID == endpointBinding.endpointID &&
BindingMatches(entry.binding, endpointBinding.binding);
});
if (it == mEndpointBindingEntries.end())
{
Clusters::JointFabricDatastore::Structs::DatastoreEndpointBindingEntryStruct::Type newEntry;
newEntry.nodeID = mRefreshingNodeId;
newEntry.endpointID = endpointBinding.endpointID;
newEntry.binding = endpointBinding.binding;
if (GenerateAndAssignAUniqueListID(newEntry.listID) != CHIP_NO_ERROR)
{
// Unable to generate a unique List ID; skip this entry.
continue;
}
newEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
mEndpointBindingEntries.push_back(newEntry);
}
}
// Remove entries not in endpointBindings, but only if they are Committed or DeletePending
mEndpointBindingEntries.erase(
std::remove_if(
mEndpointBindingEntries.begin(), mEndpointBindingEntries.end(),
[&](const auto & entry) {
if (entry.nodeID != mRefreshingNodeId)
{
return false;
}
if (entry.statusEntry.state != Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted &&
entry.statusEntry.state !=
Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
{
return false;
}
return !std::any_of(endpointBindings.begin(), endpointBindings.end(), [&](const auto & eb) {
return entry.endpointID == eb.endpointID && BindingMatches(entry.binding, eb.binding);
});
}),
mEndpointBindingEntries.end());
// Move to the next endpoint
mRefreshingEndpointIndex++;
}
else
{
// Leave node as pending but tear down the refresh state.
mRefreshingNodeId = kUndefinedNodeId;
mRefreshState = kIdle;
return;
}
// Continue the state machine to let the kRefreshingBindings branch process mEndpointBindingList.
if (ContinueRefresh() != CHIP_NO_ERROR)
{
// Ignore errors in continuation from within the callback.
}
}));
// Return here - the callback will call ContinueRefresh() again
return CHIP_NO_ERROR;
}
for (auto it = mEndpointBindingEntries.begin(); it != mEndpointBindingEntries.end();)
{
if (it->nodeID == mRefreshingNodeId)
{
if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending ||
it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted)
{
mRefreshingBindingEntries.push_back(*it);
}
else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed)
{
CHIP_ERROR failureCode(it->statusEntry.failureCode);
if (failureCode == CHIP_IM_GLOBAL_STATUS(ConstraintError) ||
failureCode == CHIP_IM_GLOBAL_STATUS(ResourceExhausted))
{
// remove entry from the list
it = mEndpointBindingEntries.erase(it);
continue;
}
mRefreshingBindingEntries.push_back(*it);
}
}
++it;
}
ReturnErrorOnFailure(mDelegate->SyncNode(mRefreshingNodeId, mRefreshingBindingEntries, [this]() {
for (auto & entry : mEndpointBindingEntries)
{
if (entry.nodeID == mRefreshingNodeId &&
(entry.statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending ||
entry.statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed))
{
for (const auto & bindingEntry : mRefreshingBindingEntries)
{
if (entry.endpointID == bindingEntry.endpointID && BindingMatches(entry.binding, bindingEntry.binding))
{
entry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
break;
}
}
}
}
// Remove all DeletePending entries for mRefreshingNodeId
mEndpointBindingEntries.erase(std::remove_if(mEndpointBindingEntries.begin(), mEndpointBindingEntries.end(),
[this](const auto & entry) {
return entry.nodeID == mRefreshingNodeId &&
entry.statusEntry.state ==
Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending;
}),
mEndpointBindingEntries.end());
// After syncing bindings, move to fetching group key sets
mRefreshState = kFetchingGroupKeySets;
if (ContinueRefresh() != CHIP_NO_ERROR)
{
// Ignore errors in continuation from within the callback.
}
}));
}
break;
case kFetchingGroupKeySets: {
// Request Group Key Set List from the device and transition to kRefreshingGroupKeySets.
ReturnErrorOnFailure(mDelegate->FetchGroupKeySetList(
mRefreshingNodeId,
[this](CHIP_ERROR err,
const std::vector<Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type> & groupKeySets) {
if (err == CHIP_NO_ERROR)
{
// Convert groupKeySets to mGroupKeySetList entries
for (const auto & groupKeySet : groupKeySets)
{
auto it = std::find_if(
mGroupKeySetList.begin(), mGroupKeySetList.end(),
[&groupKeySet](
const Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type & entry) {
return entry.groupKeySetID == groupKeySet.groupKeySetID;
});
if (it == mGroupKeySetList.end())
{
Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type copiedKeySet;
CopyGroupKeySetWithOwnedSpans(groupKeySet, copiedKeySet);
mGroupKeySetList.push_back(copiedKeySet);
}
else
{
// Update existing entry
CopyGroupKeySetWithOwnedSpans(groupKeySet, *it);
}
}
// Remove entries not in groupKeySets
for (auto it = mGroupKeySetList.begin(); it != mGroupKeySetList.end();)
{
const bool existsOnNode = std::any_of(groupKeySets.begin(), groupKeySets.end(), [&](const auto & gks) {
return it->groupKeySetID == gks.groupKeySetID;
});
if (!existsOnNode)
{
RemoveGroupKeySetStorage(it->groupKeySetID);
it = mGroupKeySetList.erase(it);
}
else
{
++it;
}
}
// Advance the state machine to process the group key sets.
mRefreshState = kRefreshingGroupKeySets;
}
else
{
// Leave node as pending but tear down the refresh state.
mRefreshingNodeId = kUndefinedNodeId;
mRefreshState = kIdle;
return;
}
// Continue the state machine to let the kRefreshingGroupKeySets branch process mGroupKeySetList.
if (ContinueRefresh() != CHIP_NO_ERROR)
{
// Ignore errors in continuation from within the callback.
}
}));
}
break;
case kRefreshingGroupKeySets: {
// 4. Ensure per-node key-set entries for each GroupKeySet are synced to devices.
for (auto gksIt = mGroupKeySetList.begin(); gksIt != mGroupKeySetList.end(); ++gksIt)
{
const uint16_t groupKeySetId = gksIt->groupKeySetID;
for (auto nkIt = mNodeKeySetEntries.begin(); nkIt != mNodeKeySetEntries.end();)
{
if (nkIt->groupKeySetID != groupKeySetId)
{
++nkIt;
continue;
}
// nkIt references the current groupKeySetId
if (nkIt->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending)
{
// Make a copy of the group key set to send to the node.
size_t idx = static_cast<size_t>(std::distance(mNodeKeySetEntries.begin(), nkIt));
auto groupKeySet = *gksIt;
ReturnErrorOnFailure(mDelegate->SyncNode(nkIt->nodeID, groupKeySet, [this, idx]() {
if (idx < mNodeKeySetEntries.size())
{
mNodeKeySetEntries[idx].statusEntry.state =
Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
}
}));
++nkIt;
}
else if (nkIt->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
{
// zero-initialized struct to indicate deletion for the SyncNode call
Clusters::JointFabricDatastore::Structs::DatastoreNodeKeySetEntryStruct::Type nullEntry{ 0 };
auto nodeIdToErase = nkIt->nodeID;
auto groupKeySetIdToErase = nkIt->groupKeySetID;
ReturnErrorOnFailure(
mDelegate->SyncNode(nkIt->nodeID, nullEntry, [this, nodeIdToErase, groupKeySetIdToErase]() {
mNodeKeySetEntries.erase(std::remove_if(mNodeKeySetEntries.begin(), mNodeKeySetEntries.end(),
[&](const auto & entry) {
return entry.nodeID == nodeIdToErase &&
entry.groupKeySetID == groupKeySetIdToErase;
}),
mNodeKeySetEntries.end());
}));
}
else if (nkIt->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed)
{
CHIP_ERROR failureCode(nkIt->statusEntry.failureCode);
if (failureCode == CHIP_IM_GLOBAL_STATUS(ConstraintError) ||
failureCode == CHIP_IM_GLOBAL_STATUS(ResourceExhausted))
{
// remove entry from the list
nkIt = mNodeKeySetEntries.erase(nkIt);
}
else
{
// Retry the failed commit by attempting to SyncNode again.
size_t idx = static_cast<size_t>(std::distance(mNodeKeySetEntries.begin(), nkIt));
auto groupKeySet = *gksIt;
ReturnErrorOnFailure(mDelegate->SyncNode(nkIt->nodeID, groupKeySet, [this, idx]() {
if (idx < mNodeKeySetEntries.size())
{
mNodeKeySetEntries[idx].statusEntry.state =
Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
}
}));
++nkIt;
}
}
else
{
++nkIt;
}
}
}
// Request ACL List from the device and transition to kRefreshingACLs.
ReturnErrorOnFailure(mDelegate->FetchACLList(
mRefreshingNodeId,
[this](CHIP_ERROR err,
const std::vector<Clusters::JointFabricDatastore::Structs::DatastoreACLEntryStruct::Type> & acls) {
if (err == CHIP_NO_ERROR)
{
// Convert acls to mACLEntries
for (const auto & acl : acls)
{
auto it = std::find_if(mACLEntries.begin(), mACLEntries.end(),
[this, &acl](const datastore::ACLEntryStruct & entry) {
return entry.nodeID == mRefreshingNodeId && entry.listID == acl.listID;
});
if (it == mACLEntries.end())
{
datastore::ACLEntryStruct newEntry;
newEntry.nodeID = mRefreshingNodeId;
newEntry.listID = acl.listID;
newEntry.ACLEntry.authMode = acl.ACLEntry.authMode;
newEntry.ACLEntry.privilege = acl.ACLEntry.privilege;
for (size_t subjectsIndex = 0; subjectsIndex < acl.ACLEntry.subjects.Value().size(); ++subjectsIndex)
{
newEntry.ACLEntry.subjects.push_back(acl.ACLEntry.subjects.Value()[subjectsIndex]);
}
for (size_t targetsIndex = 0; targetsIndex < acl.ACLEntry.targets.Value().size(); ++targetsIndex)
{
newEntry.ACLEntry.targets.push_back(acl.ACLEntry.targets.Value()[targetsIndex]);
}
newEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
mACLEntries.push_back(newEntry);
}
}
// Remove entries not in acls, but only if they are Committed or DeletePending
mACLEntries.erase(std::remove_if(mACLEntries.begin(), mACLEntries.end(),
[&](const auto & entry) {
if (entry.nodeID != mRefreshingNodeId)
{
return false;
}
if (entry.statusEntry.state !=
Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted &&
entry.statusEntry.state !=
Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
{
return false;
}
return !std::any_of(acls.begin(), acls.end(), [&](const auto & acl) {
return entry.listID == acl.listID;
});
}),
mACLEntries.end());
// Advance the state machine to process the ACLs.
mRefreshState = kRefreshingACLs;
}
else
{
// Leave node as pending but tear down the refresh state.
mRefreshingNodeId = kUndefinedNodeId;
mRefreshState = kIdle;
return;
}
// Continue the state machine to let the kRefreshingACLs branch process mACLList.
if (ContinueRefresh() != CHIP_NO_ERROR)
{
// Ignore errors in continuation from within the callback.
}
}));
}
break;
case kRefreshingACLs: {
// 5.
for (auto it = mACLEntries.begin(); it != mACLEntries.end();)
{
if (it->nodeID == mRefreshingNodeId)
{
if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending ||
it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted)
{
{
// Prepare an encoded ACL entry to send to the node.
Clusters::JointFabricDatastore::Structs::DatastoreACLEntryStruct::Type entryToSync;
entryToSync.nodeID = it->nodeID;
entryToSync.listID = it->listID;
entryToSync.ACLEntry.authMode = it->ACLEntry.authMode;
entryToSync.ACLEntry.privilege = it->ACLEntry.privilege;
entryToSync.ACLEntry.subjects =
DataModel::List<const uint64_t>(it->ACLEntry.subjects.data(), it->ACLEntry.subjects.size());
entryToSync.ACLEntry.targets = DataModel::List<
const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlTargetStruct::Type>(
it->ACLEntry.targets.data(), it->ACLEntry.targets.size());
entryToSync.statusEntry = it->statusEntry;
mRefreshingACLEntries.push_back(entryToSync);
}
}
else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed)
{
CHIP_ERROR failureCode(it->statusEntry.failureCode);
if (failureCode == CHIP_IM_GLOBAL_STATUS(ConstraintError) ||
failureCode == CHIP_IM_GLOBAL_STATUS(ResourceExhausted))
{
// remove entry from the list
it = mACLEntries.erase(it);
continue;
}
// Prepare an encoded ACL entry to retry the failed commit.
Clusters::JointFabricDatastore::Structs::DatastoreACLEntryStruct::Type entryToSync;
entryToSync.nodeID = it->nodeID;
entryToSync.listID = it->listID;
entryToSync.ACLEntry.authMode = it->ACLEntry.authMode;
entryToSync.ACLEntry.privilege = it->ACLEntry.privilege;
entryToSync.ACLEntry.subjects =
DataModel::List<const uint64_t>(it->ACLEntry.subjects.data(), it->ACLEntry.subjects.size());
entryToSync.ACLEntry.targets =
DataModel::List<const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlTargetStruct::Type>(
it->ACLEntry.targets.data(), it->ACLEntry.targets.size());
entryToSync.statusEntry = it->statusEntry;
mRefreshingACLEntries.push_back(entryToSync);
}
}
++it;
}
ReturnErrorOnFailure(mDelegate->SyncNode(mRefreshingNodeId, mRefreshingACLEntries, [this]() {
for (auto & entry : mACLEntries)
{
if (entry.nodeID == mRefreshingNodeId &&
(entry.statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending ||
entry.statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed))
{
for (const auto & aclEntry : mRefreshingACLEntries)
{
if (entry.listID == aclEntry.listID)
{
entry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
break;
}
}
}
}
// Remove all DeletePending entries for mRefreshingNodeId
mACLEntries.erase(std::remove_if(mACLEntries.begin(), mACLEntries.end(),
[this](const auto & entry) {
return entry.nodeID == mRefreshingNodeId &&
entry.statusEntry.state ==
Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending;
}),
mACLEntries.end());
}));
// 6.
ReturnErrorOnFailure(SetNode(mRefreshingNodeId, Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted));
for (Listener * listener = mListeners; listener != nullptr; listener = listener->mNext)
{
listener->MarkNodeListChanged();
}
mRefreshingNodeId = kUndefinedNodeId;
mRefreshState = kIdle;
}
break;
}
return CHIP_NO_ERROR;
}
CHIP_ERROR JointFabricDatastore::SetNode(NodeId nodeId, Clusters::JointFabricDatastore::DatastoreStateEnum state)
{
size_t index = 0;
ReturnErrorOnFailure(IsNodeIDInDatastore(nodeId, index));
mNodeInformationEntries[index].commissioningStatusEntry.state = state;
return CHIP_NO_ERROR;
}
CHIP_ERROR JointFabricDatastore::IsNodeIDInDatastore(NodeId nodeId, size_t & index)
{
for (auto & entry : mNodeInformationEntries)
{
if (entry.nodeID == nodeId)
{
index = static_cast<size_t>(&entry - &mNodeInformationEntries[0]);
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR
JointFabricDatastore::AddGroupKeySetEntry(
const Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type & groupKeySet)
{
VerifyOrReturnError(IsGroupKeySetEntryPresent(groupKeySet.groupKeySetID) == false, CHIP_IM_GLOBAL_STATUS(ConstraintError));
VerifyOrReturnError(mGroupKeySetList.size() < kMaxGroupKeySet, CHIP_ERROR_NO_MEMORY);
Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type copiedKeySet;
CopyGroupKeySetWithOwnedSpans(groupKeySet, copiedKeySet);
mGroupKeySetList.push_back(copiedKeySet);
return CHIP_NO_ERROR;
}
bool JointFabricDatastore::IsGroupKeySetEntryPresent(uint16_t groupKeySetId)
{
for (auto & entry : mGroupKeySetList)
{
if (entry.groupKeySetID == groupKeySetId)
{
return true;
}
}
return false;
}
CHIP_ERROR JointFabricDatastore::RemoveGroupKeySetEntry(uint16_t groupKeySetId)
{
VerifyOrReturnValue(groupKeySetId != 0, CHIP_IM_GLOBAL_STATUS(ConstraintError));
for (auto it = mGroupKeySetList.begin(); it != mGroupKeySetList.end(); ++it)
{
if (it->groupKeySetID == groupKeySetId)
{
RemoveGroupKeySetStorage(groupKeySetId);
mGroupKeySetList.erase(it);
return CHIP_NO_ERROR;
}
}
return CHIP_IM_GLOBAL_STATUS(NotFound);
}
CHIP_ERROR
JointFabricDatastore::UpdateGroupKeySetEntry(
Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type & groupKeySet)
{
for (auto & entry : mGroupKeySetList)
{
if (entry.groupKeySetID == groupKeySet.groupKeySetID)
{
LogErrorOnFailure(UpdateNodeKeySetList(groupKeySet));
VerifyOrReturnValue(groupKeySet.groupKeySecurityPolicy <
Clusters::JointFabricDatastore::DatastoreGroupKeySecurityPolicyEnum::kUnknownEnumValue &&
groupKeySet.groupKeyMulticastPolicy <
Clusters::JointFabricDatastore::DatastoreGroupKeyMulticastPolicyEnum::kUnknownEnumValue,
CHIP_IM_GLOBAL_STATUS(ConstraintError));
CopyGroupKeySetWithOwnedSpans(groupKeySet, entry);
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR
JointFabricDatastore::AddAdmin(
Clusters::JointFabricDatastore::Structs::DatastoreAdministratorInformationEntryStruct::Type & adminId)
{
VerifyOrReturnError(IsAdminEntryPresent(adminId.nodeID) == false, CHIP_IM_GLOBAL_STATUS(ConstraintError));
VerifyOrReturnError(mAdminEntries.size() < kMaxAdminNodes, CHIP_ERROR_NO_MEMORY);
ReturnErrorOnFailure(SetAdminEntryWithOwnedStorage(adminId.nodeID, adminId.friendlyName, adminId.icac, adminId));
mAdminEntries.push_back(adminId);
return CHIP_NO_ERROR;
}
bool JointFabricDatastore::IsAdminEntryPresent(NodeId nodeId)
{
for (auto & entry : mAdminEntries)
{
if (entry.nodeID == nodeId)
{
return true;
}
}
return false;
}
CHIP_ERROR JointFabricDatastore::UpdateAdmin(NodeId nodeId, CharSpan friendlyName, ByteSpan icac)
{
for (auto & entry : mAdminEntries)
{
if (entry.nodeID == nodeId)
{
ReturnErrorOnFailure(SetAdminEntryWithOwnedStorage(nodeId, friendlyName, icac, entry));
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::RemoveAdmin(NodeId nodeId)
{
for (auto it = mAdminEntries.begin(); it != mAdminEntries.end(); ++it)
{
if (it->nodeID == nodeId)
{
mAdminEntries.erase(it);
RemoveAdminEntryStorage(nodeId);
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR
JointFabricDatastore::UpdateNodeKeySetList(Clusters::JointFabricDatastore::Structs::DatastoreGroupKeySetStruct::Type & groupKeySet)
{
bool entryUpdated = false;
for (size_t i = 0; i < mNodeKeySetEntries.size(); ++i)
{
auto & entry = mNodeKeySetEntries[i];
if (entry.groupKeySetID == groupKeySet.groupKeySetID)
{
if (groupKeySet.groupKeySecurityPolicy <
Clusters::JointFabricDatastore::DatastoreGroupKeySecurityPolicyEnum::kUnknownEnumValue &&
groupKeySet.groupKeyMulticastPolicy <
Clusters::JointFabricDatastore::DatastoreGroupKeyMulticastPolicyEnum::kUnknownEnumValue)
{
size_t index = i;
LogErrorOnFailure(mDelegate->SyncNode(entry.nodeID, groupKeySet, [this, index]() {
mNodeKeySetEntries[index].statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
}));
if (entryUpdated == false)
{
entryUpdated = true;
}
}
else
{
entry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed;
return CHIP_IM_GLOBAL_STATUS(ConstraintError);
}
}
}
return entryUpdated ? CHIP_NO_ERROR : CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::RemoveKeySet(uint16_t groupKeySetId)
{
for (auto it = mNodeKeySetEntries.begin(); it != mNodeKeySetEntries.end(); ++it)
{
if (it->groupKeySetID == groupKeySetId)
{
if (it->statusEntry.state != Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
{
return CHIP_IM_GLOBAL_STATUS(ConstraintError); // Cannot remove a key set that is not pending
}
ReturnErrorOnFailure(RemoveGroupKeySetEntry(groupKeySetId));
return CHIP_NO_ERROR;
}
}
return CHIP_IM_GLOBAL_STATUS(NotFound);
}
CHIP_ERROR JointFabricDatastore::AddGroup(const Clusters::JointFabricDatastore::Commands::AddGroup::DecodableType & commandData)
{
size_t index = 0;
// Check if the group ID already exists in the datastore
VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_ERROR_NOT_FOUND,
CHIP_IM_GLOBAL_STATUS(ConstraintError));
if (commandData.groupCAT.ValueOr(0) == kAdminCATIdentifier || commandData.groupCAT.ValueOr(0) == kAnchorCATIdentifier)
{
// If the group is an AdminCAT or AnchorCAT, we cannot add it
return CHIP_IM_GLOBAL_STATUS(ConstraintError);
}
Clusters::JointFabricDatastore::Structs::DatastoreGroupInformationEntryStruct::Type groupEntry;
groupEntry.groupID = commandData.groupID;
groupEntry.groupKeySetID = commandData.groupKeySetID;
groupEntry.groupCAT = commandData.groupCAT;
groupEntry.groupCATVersion = commandData.groupCATVersion;
groupEntry.groupPermission = commandData.groupPermission;
SetGroupInformationFriendlyNameWithOwnedStorage(commandData.groupID, commandData.friendlyName, groupEntry);
// Add the group entry to the datastore
mGroupInformationEntries.push_back(groupEntry);
return CHIP_NO_ERROR;
}
CHIP_ERROR
JointFabricDatastore::ForceAddGroup(const Clusters::JointFabricDatastore::Commands::AddGroup::DecodableType & commandData)
{
size_t index = 0;
// Check if the group ID already exists in the datastore
VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_ERROR_NOT_FOUND,
CHIP_IM_GLOBAL_STATUS(ConstraintError));
Clusters::JointFabricDatastore::Structs::DatastoreGroupInformationEntryStruct::Type groupEntry;
groupEntry.groupID = commandData.groupID;
groupEntry.groupKeySetID = commandData.groupKeySetID;
groupEntry.groupCAT = commandData.groupCAT;
groupEntry.groupCATVersion = commandData.groupCATVersion;
groupEntry.groupPermission = commandData.groupPermission;
SetGroupInformationFriendlyNameWithOwnedStorage(commandData.groupID, commandData.friendlyName, groupEntry);
// Add the group entry to the datastore
mGroupInformationEntries.push_back(groupEntry);
return CHIP_NO_ERROR;
}
CHIP_ERROR
JointFabricDatastore::UpdateGroup(const Clusters::JointFabricDatastore::Commands::UpdateGroup::DecodableType & commandData)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
size_t index = 0;
// Check if the group ID exists in the datastore
VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(ConstraintError));
if (mGroupInformationEntries[index].groupCAT.ValueOr(0) == kAdminCATIdentifier ||
mGroupInformationEntries[index].groupCAT.ValueOr(0) == kAnchorCATIdentifier)
{
// If the group is an AdminCAT or AnchorCAT, we cannot update it
return CHIP_IM_GLOBAL_STATUS(ConstraintError);
}
// Update the group entry with the new data
if (commandData.friendlyName.IsNull() == false)
{
if (mGroupInformationEntries[index].friendlyName.data_equal(commandData.friendlyName.Value()) == false)
{
// Friendly name changed. For every endpoint that references this group, mark the endpoint's
// GroupIDList entry as pending and attempt to push the change to the node. If the push
// fails, leave the entry as pending so a subsequent Refresh can apply it.
const GroupId updatedGroupId = commandData.groupID;
for (size_t i = 0; i < mEndpointGroupIDEntries.size(); ++i)
{
auto & epGroupEntry = mEndpointGroupIDEntries[i];
if (epGroupEntry.groupID == updatedGroupId)
{
epGroupEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
// Make a copy to send to the node. Do not fail the entire UpdateGroup if SyncNode
// returns an error; leave the entry pending for a later refresh per spec.
auto entryToSync = epGroupEntry;
CHIP_ERROR syncErr = mDelegate->SyncNode(epGroupEntry.nodeID, entryToSync, [this, i]() {
mEndpointGroupIDEntries[i].statusEntry.state =
Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
});
if (syncErr != CHIP_NO_ERROR)
{
ChipLogError(DataManagement,
"Failed to sync node for group friendly name update, leaving as pending: %" CHIP_ERROR_FORMAT,
syncErr.Format());
}
}
}
// Update the friendly name in the datastore
SetGroupInformationFriendlyNameWithOwnedStorage(static_cast<GroupId>(mGroupInformationEntries[index].groupID),
commandData.friendlyName.Value(), mGroupInformationEntries[index]);
}
}
if (commandData.groupKeySetID.IsNull() == false)
{
if (mGroupInformationEntries[index].groupKeySetID.Value() != commandData.groupKeySetID.Value())
{
// If the groupKeySetID is being updated, we need to ensure that the new key set exists
ReturnErrorOnFailure(AddNodeKeySetEntry(commandData.groupID, commandData.groupKeySetID.Value()));
if (!mGroupInformationEntries[index].groupKeySetID.IsNull())
{
LogErrorOnFailure(RemoveNodeKeySetEntry(
commandData.groupID, mGroupInformationEntries[index].groupKeySetID.Value())); // Remove the old key set
}
}
mGroupInformationEntries[index].groupKeySetID = commandData.groupKeySetID;
}
bool anyGroupCATFieldUpdated = false;
if (commandData.groupCAT.IsNull() == false)
{
if (mGroupInformationEntries[index].groupCAT.Value() != commandData.groupCAT.Value())
{
anyGroupCATFieldUpdated = true;
}
// Update the groupCAT
mGroupInformationEntries[index].groupCAT = commandData.groupCAT;
}
if (commandData.groupCATVersion.IsNull() == false)
{
if (mGroupInformationEntries[index].groupCATVersion.Value() != commandData.groupCATVersion.Value())
{
anyGroupCATFieldUpdated = true;
}
mGroupInformationEntries[index].groupCATVersion = commandData.groupCATVersion;
}
if (commandData.groupPermission != Clusters::JointFabricDatastore::DatastoreAccessControlEntryPrivilegeEnum::kUnknownEnumValue)
{
if (mGroupInformationEntries[index].groupPermission != commandData.groupPermission)
{
anyGroupCATFieldUpdated = true;
}
// If the groupPermission is not set to kUnknownEnumValue, update it
mGroupInformationEntries[index].groupPermission = commandData.groupPermission;
}
if (anyGroupCATFieldUpdated)
{
const GroupId updatedGroupId = commandData.groupID;
for (size_t i = 0; i < mACLEntries.size(); ++i)
{
auto & acl = mACLEntries[i];
// Determine if this ACL entry references the updated group
bool referencesGroup = false;
for (const auto & subject : acl.ACLEntry.subjects)
{
// If the target has a group field and it matches the updated group, mark for update.
// Use IsNull() to match other usages in this file.
if (subject == static_cast<uint64_t>(updatedGroupId))
{
referencesGroup = true;
break;
}
}
if (!referencesGroup)
{
continue;
}
// Update the ACL entry in the datastore to reflect the new group permission and mark Pending.
acl.ACLEntry.privilege = mGroupInformationEntries[index].groupPermission;
acl.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
// Prepare an encoded entry to send to the node.
Clusters::JointFabricDatastore::Structs::DatastoreACLEntryStruct::Type entryToEncode;
entryToEncode.nodeID = acl.nodeID;
entryToEncode.listID = acl.listID;
entryToEncode.ACLEntry.authMode = acl.ACLEntry.authMode;
entryToEncode.ACLEntry.privilege = acl.ACLEntry.privilege;
entryToEncode.ACLEntry.subjects =
DataModel::List<const uint64_t>(acl.ACLEntry.subjects.data(), acl.ACLEntry.subjects.size());
entryToEncode.ACLEntry.targets =
DataModel::List<const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlTargetStruct::Type>(
acl.ACLEntry.targets.data(), acl.ACLEntry.targets.size());
entryToEncode.statusEntry = acl.statusEntry;
// Attempt to update the ACL on the node. On success, mark the ACL entry as Committed.
// Capture index 'i' to safely identify the entry inside the callback.
ReturnErrorOnFailure(mDelegate->SyncNode(acl.nodeID, entryToEncode, [this, i]() {
if (i < mACLEntries.size())
{
mACLEntries[i].statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
}
}));
}
}
return CHIP_NO_ERROR;
}
CHIP_ERROR
JointFabricDatastore::RemoveGroup(const Clusters::JointFabricDatastore::Commands::RemoveGroup::DecodableType & commandData)
{
size_t index = 0;
// Check if the group ID exists in the datastore
VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(ConstraintError));
// Remove the group entry from the datastore
auto it = mGroupInformationEntries.begin();
std::advance(it, index);
if (it->groupCAT.ValueOr(0) == kAdminCATIdentifier || it->groupCAT.ValueOr(0) == kAnchorCATIdentifier)
{
// If the group is an AdminCAT or AnchorCAT, we cannot remove it
return CHIP_IM_GLOBAL_STATUS(ConstraintError);
}
const GroupId removedGroupId = static_cast<GroupId>(it->groupID);
mGroupInformationEntries.erase(it);
RemoveGroupInformationStorage(removedGroupId);
return CHIP_NO_ERROR;
}
CHIP_ERROR JointFabricDatastore::IsGroupIDInDatastore(chip::GroupId groupId, size_t & index)
{
for (auto & entry : mGroupInformationEntries)
{
if (entry.groupID == groupId)
{
index = static_cast<size_t>(&entry - &mGroupInformationEntries[0]);
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::IsNodeIdInNodeInformationEntries(NodeId nodeId, size_t & index)
{
for (auto & entry : mNodeInformationEntries)
{
if (entry.nodeID == nodeId)
{
index = static_cast<size_t>(&entry - &mNodeInformationEntries[0]);
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::UpdateEndpointForNode(NodeId nodeId, chip::EndpointId endpointId, CharSpan friendlyName)
{
for (auto & entry : mEndpointEntries)
{
if (entry.nodeID == nodeId && entry.endpointID == endpointId)
{
entry.friendlyName = friendlyName;
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::IsNodeIdAndEndpointInEndpointInformationEntries(NodeId nodeId, EndpointId endpointId,
size_t & index)
{
for (auto & entry : mEndpointEntries)
{
if (entry.nodeID == nodeId && entry.endpointID == endpointId)
{
index = static_cast<size_t>(&entry - &mEndpointEntries[0]);
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::AddGroupIDToEndpointForNode(NodeId nodeId, chip::EndpointId endpointId, chip::GroupId groupId)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
size_t index = 0;
ReturnErrorOnFailure(IsNodeIdAndEndpointInEndpointInformationEntries(nodeId, endpointId, index));
VerifyOrReturnError(IsGroupIDInDatastore(groupId, index) == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(ConstraintError));
if (mGroupInformationEntries[index].groupKeySetID.IsNull() == false)
{
uint16_t groupKeySetID = mGroupInformationEntries[index].groupKeySetID.Value();
// make sure mNodeKeySetEntries contains an entry for this keyset and node, else add one and update device
bool nodeKeySetExists = false;
for (auto & entry : mNodeKeySetEntries)
{
if (entry.nodeID == nodeId && entry.groupKeySetID == groupKeySetID)
{
nodeKeySetExists = true;
break; // Found the group key set, no need to add it again
}
}
if (!nodeKeySetExists)
{
// Create a new group key set entry if it doesn't exist
Clusters::JointFabricDatastore::Structs::DatastoreNodeKeySetEntryStruct::Type newNodeKeySet;
newNodeKeySet.nodeID = nodeId;
newNodeKeySet.groupKeySetID = groupKeySetID;
newNodeKeySet.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
mNodeKeySetEntries.push_back(newNodeKeySet);
ReturnErrorOnFailure(mDelegate->SyncNode(nodeId, newNodeKeySet, [this]() {
mNodeKeySetEntries.back().statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
}));
}
}
// Check if the group ID already exists for the endpoint
for (auto & entry : mEndpointGroupIDEntries)
{
if (entry.nodeID == nodeId && entry.endpointID == endpointId && entry.groupID == groupId)
{
return CHIP_NO_ERROR;
}
}
VerifyOrReturnError(mEndpointGroupIDEntries.size() < kMaxGroups, CHIP_ERROR_NO_MEMORY);
// Create a new endpoint group ID entry
Clusters::JointFabricDatastore::Structs::DatastoreEndpointGroupIDEntryStruct::Type newGroupEntry;
newGroupEntry.nodeID = nodeId;
newGroupEntry.endpointID = endpointId;
newGroupEntry.groupID = groupId;
newGroupEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
// Add the new ACL entry to the datastore
mEndpointGroupIDEntries.push_back(newGroupEntry);
return mDelegate->SyncNode(nodeId, newGroupEntry, [this]() {
mEndpointGroupIDEntries.back().statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
});
}
CHIP_ERROR JointFabricDatastore::RemoveGroupIDFromEndpointForNode(NodeId nodeId, chip::EndpointId endpointId, chip::GroupId groupId)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
size_t index = 0;
ReturnErrorOnFailure(IsNodeIdAndEndpointInEndpointInformationEntries(nodeId, endpointId, index));
for (auto it = mEndpointGroupIDEntries.begin(); it != mEndpointGroupIDEntries.end(); ++it)
{
if (it->nodeID == nodeId && it->endpointID == endpointId && it->groupID == groupId)
{
it->statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending;
// zero-initialized struct to indicate deletion for the SyncNode call
Clusters::JointFabricDatastore::Structs::DatastoreEndpointGroupIDEntryStruct::Type endpointGroupIdNullEntry{ 0 };
ReturnErrorOnFailure(
mDelegate->SyncNode(nodeId, endpointGroupIdNullEntry, [this, it]() { mEndpointGroupIDEntries.erase(it); }));
if (IsGroupIDInDatastore(groupId, index) == CHIP_NO_ERROR)
{
for (auto it2 = mNodeKeySetEntries.begin(); it2 != mNodeKeySetEntries.end();)
{
bool incrementIndex = true;
if (it2->nodeID == nodeId && mGroupInformationEntries[index].groupKeySetID.IsNull() == false &&
it2->groupKeySetID == mGroupInformationEntries[index].groupKeySetID.Value())
{
it2->statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending;
// zero-initialized struct to indicate deletion for the SyncNode call
Clusters::JointFabricDatastore::Structs::DatastoreNodeKeySetEntryStruct::Type nodeKeySetNullEntry{ 0 };
ReturnErrorOnFailure(
mDelegate->SyncNode(nodeId, nodeKeySetNullEntry, [this, it2]() { mNodeKeySetEntries.erase(it2); }));
incrementIndex = false;
}
if (incrementIndex)
{
++it2;
}
else
{
incrementIndex = true;
}
}
}
return CHIP_NO_ERROR;
}
}
return CHIP_ERROR_NOT_FOUND;
}
// look-up the highest listId used so far, from Endpoint Binding Entries and ACL Entries
CHIP_ERROR JointFabricDatastore::GenerateAndAssignAUniqueListID(uint16_t & listId)
{
uint16_t highestListID = 0;
for (auto & entry : mEndpointBindingEntries)
{
if (entry.listID >= highestListID)
{
highestListID = entry.listID + 1;
}
}
for (auto & entry : mACLEntries)
{
if (entry.listID >= highestListID)
{
highestListID = entry.listID + 1;
}
}
listId = highestListID;
return CHIP_NO_ERROR;
}
bool JointFabricDatastore::BindingMatches(
const Clusters::JointFabricDatastore::Structs::DatastoreBindingTargetStruct::Type & binding1,
const Clusters::JointFabricDatastore::Structs::DatastoreBindingTargetStruct::Type & binding2)
{
if (binding1.node.HasValue() && binding2.node.HasValue())
{
if (binding1.node.Value() != binding2.node.Value())
{
return false;
}
}
else if (binding1.node.HasValue() || binding2.node.HasValue())
{
return false;
}
if (binding1.group.HasValue() && binding2.group.HasValue())
{
if (binding1.group.Value() != binding2.group.Value())
{
return false;
}
}
else if (binding1.group.HasValue() || binding2.group.HasValue())
{
return false;
}
if (binding1.endpoint.HasValue() && binding2.endpoint.HasValue())
{
if (binding1.endpoint.Value() != binding2.endpoint.Value())
{
return false;
}
}
else if (binding1.endpoint.HasValue() || binding2.endpoint.HasValue())
{
return false;
}
if (binding1.cluster.HasValue() && binding2.cluster.HasValue())
{
if (binding1.cluster.Value() != binding2.cluster.Value())
{
return false;
}
}
else if (binding1.cluster.HasValue() || binding2.cluster.HasValue())
{
return false;
}
return true;
}
CHIP_ERROR
JointFabricDatastore::AddBindingToEndpointForNode(
NodeId nodeId, chip::EndpointId endpointId,
const Clusters::JointFabricDatastore::Structs::DatastoreBindingTargetStruct::Type & binding)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
size_t index = 0;
ReturnErrorOnFailure(IsNodeIdAndEndpointInEndpointInformationEntries(nodeId, endpointId, index));
// Check if the group ID already exists for the endpoint
for (auto & entry : mEndpointBindingEntries)
{
if (entry.nodeID == nodeId && entry.endpointID == endpointId)
{
if (BindingMatches(entry.binding, binding))
{
return CHIP_NO_ERROR;
}
}
}
VerifyOrReturnError(mEndpointBindingEntries.size() < kMaxGroups, CHIP_ERROR_NO_MEMORY);
// Create a new binding entry
Clusters::JointFabricDatastore::Structs::DatastoreEndpointBindingEntryStruct::Type newBindingEntry;
newBindingEntry.nodeID = nodeId;
newBindingEntry.endpointID = endpointId;
newBindingEntry.binding = binding;
ReturnErrorOnFailure(GenerateAndAssignAUniqueListID(newBindingEntry.listID));
newBindingEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
// Add the new binding entry to the datastore
mEndpointBindingEntries.push_back(newBindingEntry);
return mDelegate->SyncNode(nodeId, newBindingEntry, [this]() {
mEndpointBindingEntries.back().statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
});
}
CHIP_ERROR
JointFabricDatastore::RemoveBindingFromEndpointForNode(uint16_t listId, NodeId nodeId, chip::EndpointId endpointId)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
size_t index = 0;
ReturnErrorOnFailure(IsNodeIdAndEndpointInEndpointInformationEntries(nodeId, endpointId, index));
for (auto it = mEndpointBindingEntries.begin(); it != mEndpointBindingEntries.end(); ++it)
{
if (it->nodeID == nodeId && it->listID == listId && it->endpointID == endpointId)
{
it->statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending;
// zero-initialized struct to indicate deletion for the SyncNode call
Clusters::JointFabricDatastore::Structs::DatastoreEndpointBindingEntryStruct::Type nullEntry{ 0 };
return mDelegate->SyncNode(nodeId, nullEntry, [this, it]() { mEndpointBindingEntries.erase(it); });
}
}
return CHIP_ERROR_NOT_FOUND;
}
bool JointFabricDatastore::ACLTargetMatches(
const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlTargetStruct::Type & target1,
const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlTargetStruct::Type & target2)
{
if (!target1.cluster.IsNull() && !target2.cluster.IsNull())
{
if (target1.cluster.Value() != target2.cluster.Value())
{
return false;
}
}
else if (!target1.cluster.IsNull() || !target2.cluster.IsNull())
{
return false;
}
if (!target1.endpoint.IsNull() && !target2.endpoint.IsNull())
{
if (target1.endpoint.Value() != target2.endpoint.Value())
{
return false;
}
}
else if (!target1.endpoint.IsNull() || !target2.endpoint.IsNull())
{
return false;
}
if (!target1.deviceType.IsNull() && !target2.deviceType.IsNull())
{
if (target1.deviceType.Value() != target2.deviceType.Value())
{
return false;
}
}
else if (!target1.deviceType.IsNull() || !target2.deviceType.IsNull())
{
return false;
}
return true;
}
bool JointFabricDatastore::ACLMatches(
const datastore::AccessControlEntryStruct & acl1,
const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlEntryStruct::DecodableType & acl2)
{
if (acl1.privilege != acl2.privilege)
{
return false;
}
if (acl1.authMode != acl2.authMode)
{
return false;
}
{
auto it1 = acl1.subjects.begin();
auto it2 = acl2.subjects.Value().begin();
while (it1 != acl1.subjects.end() && it2.Next())
{
if (*it1 != it2.GetValue())
{
return false;
}
++it1;
}
if (it2.Next())
{
return false; // acl2 has more subjects
}
}
{
auto it1 = acl1.targets.begin();
auto it2 = acl2.targets.Value().begin();
while (it1 != acl1.targets.end() && it2.Next())
{
if (ACLTargetMatches(*it1, it2.GetValue()) == false)
{
return false;
}
++it1;
}
if (it2.Next())
{
return false; // acl2 has more targets
}
}
return true;
}
CHIP_ERROR
JointFabricDatastore::AddACLToNode(
NodeId nodeId, const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlEntryStruct::DecodableType & aclEntry)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
size_t index = 0;
ReturnErrorOnFailure(IsNodeIdInNodeInformationEntries(nodeId, index));
// Check if the ACL entry already exists for the node
for (auto & entry : mACLEntries)
{
if (entry.nodeID == nodeId)
{
if (ACLMatches(entry.ACLEntry, aclEntry))
{
return CHIP_NO_ERROR;
}
}
}
VerifyOrReturnError(mACLEntries.size() < kMaxACLs, CHIP_ERROR_NO_MEMORY);
// Create a new ACL entry
datastore::ACLEntryStruct newACLEntry;
newACLEntry.nodeID = nodeId;
newACLEntry.ACLEntry.privilege = aclEntry.privilege;
newACLEntry.ACLEntry.authMode = aclEntry.authMode;
newACLEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
if (!aclEntry.subjects.IsNull())
{
auto iter = aclEntry.subjects.Value().begin();
while (iter.Next())
{
newACLEntry.ACLEntry.subjects.push_back(iter.GetValue());
}
ReturnErrorOnFailure(iter.GetStatus());
}
if (!aclEntry.targets.IsNull())
{
auto iter = aclEntry.targets.Value().begin();
while (iter.Next())
{
newACLEntry.ACLEntry.targets.push_back(iter.GetValue());
}
ReturnErrorOnFailure(iter.GetStatus());
}
ReturnErrorOnFailure(GenerateAndAssignAUniqueListID(newACLEntry.listID));
// Add the new ACL entry to the datastore
mACLEntries.push_back(newACLEntry);
Clusters::JointFabricDatastore::Structs::DatastoreACLEntryStruct::Type entryToEncode;
entryToEncode.nodeID = newACLEntry.nodeID;
entryToEncode.listID = newACLEntry.listID;
entryToEncode.ACLEntry.authMode = newACLEntry.ACLEntry.authMode;
entryToEncode.ACLEntry.privilege = newACLEntry.ACLEntry.privilege;
entryToEncode.ACLEntry.subjects =
DataModel::List<const uint64_t>(newACLEntry.ACLEntry.subjects.data(), newACLEntry.ACLEntry.subjects.size());
entryToEncode.ACLEntry.targets =
DataModel::List<const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlTargetStruct::Type>(
newACLEntry.ACLEntry.targets.data(), newACLEntry.ACLEntry.targets.size());
entryToEncode.statusEntry = newACLEntry.statusEntry;
return mDelegate->SyncNode(nodeId, entryToEncode, [this]() {
mACLEntries.back().statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
});
}
CHIP_ERROR JointFabricDatastore::RemoveACLFromNode(uint16_t listId, NodeId nodeId)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
size_t index = 0;
ReturnErrorOnFailure(IsNodeIdInNodeInformationEntries(nodeId, index));
for (auto it = mACLEntries.begin(); it != mACLEntries.end(); ++it)
{
if (it->nodeID == nodeId && it->listID == listId)
{
it->statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending;
// zero-initialized struct to indicate deletion for the SyncNode call
Clusters::JointFabricDatastore::Structs::DatastoreACLEntryStruct::Type nullEntry{ 0 };
return mDelegate->SyncNode(nodeId, nullEntry, [this, it]() { mACLEntries.erase(it); });
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::AddNodeKeySetEntry(GroupId groupId, uint16_t groupKeySetId)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
// Find all nodes that are members of this group
std::unordered_set<NodeId> nodesInGroup;
for (const auto & entry : mEndpointGroupIDEntries)
{
if (entry.groupID == groupId)
{
nodesInGroup.insert(entry.nodeID);
}
}
if (!nodesInGroup.empty())
{
for (const auto nodeId : nodesInGroup)
{
// Skip if a matching NodeKeySet entry already exists for this node
bool exists = false;
for (const auto & nkse : mNodeKeySetEntries)
{
if (nkse.nodeID == nodeId && nkse.groupKeySetID == groupKeySetId)
{
exists = true;
break;
}
}
if (exists)
{
continue;
}
Clusters::JointFabricDatastore::Structs::DatastoreNodeKeySetEntryStruct::Type newEntry;
newEntry.nodeID = nodeId;
newEntry.groupKeySetID = groupKeySetId;
newEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
mNodeKeySetEntries.push_back(newEntry);
size_t index = mNodeKeySetEntries.size() - 1;
// Sync to the node and mark committed on success
ReturnErrorOnFailure(mDelegate->SyncNode(nodeId, newEntry, [this, index]() {
if (index < mNodeKeySetEntries.size())
{
mNodeKeySetEntries[index].statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
}
}));
}
}
return CHIP_NO_ERROR;
}
CHIP_ERROR JointFabricDatastore::RemoveNodeKeySetEntry(GroupId groupId, uint16_t groupKeySetId)
{
// NOTE: this method assumes its ok to remove the keyset from each node (its not in use by any group)
// Find all nodes that are members of this group
std::unordered_set<NodeId> nodesInGroup;
for (const auto & entry : mEndpointGroupIDEntries)
{
if (entry.groupID == groupId)
{
nodesInGroup.insert(entry.nodeID);
}
}
for (auto it = mNodeKeySetEntries.begin(); it != mNodeKeySetEntries.end(); ++it)
{
for (const auto & nodeId : nodesInGroup)
{
if (it->nodeID == nodeId && it->groupKeySetID == groupKeySetId)
{
// zero-initialized struct to indicate deletion for the SyncNode call
Clusters::JointFabricDatastore::Structs::DatastoreNodeKeySetEntryStruct::Type nullEntry{ 0 };
auto nodeIdToErase = it->nodeID;
auto groupKeySetIdToErase = it->groupKeySetID;
ReturnErrorOnFailure(mDelegate->SyncNode(nodeId, nullEntry, [this, nodeIdToErase, groupKeySetIdToErase]() {
mNodeKeySetEntries.erase(std::remove_if(mNodeKeySetEntries.begin(), mNodeKeySetEntries.end(),
[&](const auto & entry) {
return entry.nodeID == nodeIdToErase &&
entry.groupKeySetID == groupKeySetIdToErase;
}),
mNodeKeySetEntries.end());
}));
return CHIP_NO_ERROR;
}
}
}
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR JointFabricDatastore::TestAddNodeKeySetEntry(GroupId groupId, uint16_t groupKeySetId, NodeId nodeId)
{
VerifyOrReturnError(mDelegate != nullptr, CHIP_ERROR_INCORRECT_STATE);
Clusters::JointFabricDatastore::Structs::DatastoreNodeKeySetEntryStruct::Type newEntry;
newEntry.nodeID = nodeId;
newEntry.groupKeySetID = groupKeySetId;
newEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
mNodeKeySetEntries.push_back(newEntry);
size_t index = mNodeKeySetEntries.size() - 1;
// Sync to the node and mark committed on success
return mDelegate->SyncNode(nodeId, newEntry, [this, index]() {
if (index < mNodeKeySetEntries.size())
{
mNodeKeySetEntries[index].statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
}
});
}
CHIP_ERROR JointFabricDatastore::TestAddEndpointEntry(EndpointId endpointId, NodeId nodeId, CharSpan friendlyName)
{
Clusters::JointFabricDatastore::Structs::DatastoreEndpointEntryStruct::Type newEntry;
newEntry.nodeID = nodeId;
newEntry.endpointID = endpointId;
newEntry.friendlyName = friendlyName;
mEndpointEntries.push_back(newEntry);
return CHIP_NO_ERROR;
}
CHIP_ERROR JointFabricDatastore::ForceAddNodeKeySetEntry(uint16_t groupKeySetId, NodeId nodeId)
{
Clusters::JointFabricDatastore::Structs::DatastoreNodeKeySetEntryStruct::Type newEntry;
newEntry.nodeID = nodeId;
newEntry.groupKeySetID = groupKeySetId;
newEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
mNodeKeySetEntries.push_back(newEntry);
return CHIP_NO_ERROR;
}
} // namespace app
} // namespace chip