diff --git a/spine/feature_local.go b/spine/feature_local.go index d3f9d2c..880db3b 100644 --- a/spine/feature_local.go +++ b/spine/feature_local.go @@ -457,6 +457,9 @@ func (r *FeatureLocal) SubscribeToRemote(remoteAddress *model.FeatureAddressType } remoteFeature := remoteDevice.FeatureByAddress(remoteAddress) + if remoteFeature == nil { + return nil, model.NewErrorTypeFromString("feature not found") + } remoteFeatureType := remoteFeature.Type() if remoteFeature.Role() == model.RoleTypeClient { return nil, model.NewErrorTypeFromString(fmt.Sprintf("remote feature '%s' is not a server", remoteFeature.String())) @@ -580,6 +583,9 @@ func (r *FeatureLocal) BindToRemote(remoteAddress *model.FeatureAddressType) (*m } remoteFeature := remoteDevice.FeatureByAddress(remoteAddress) + if remoteFeature == nil { + return nil, model.NewErrorTypeFromString("feature not found") + } remoteFeatureType := remoteFeature.Type() if remoteFeature.Role() == model.RoleTypeClient { return nil, model.NewErrorTypeFromString(fmt.Sprintf("remote feature '%s' is not a server", remoteFeature.String())) diff --git a/spine/feature_local_test.go b/spine/feature_local_test.go index 5d42317..c06983e 100644 --- a/spine/feature_local_test.go +++ b/spine/feature_local_test.go @@ -1240,3 +1240,48 @@ func (s *LocalFeatureTestSuite) Test_Operations_NoPartialReadSupport() { assert.True(s.T(), exists) assert.False(s.T(), operation.ReadPartial()) } + +// TestSubscribeToRemote_NilFeature verifies that SubscribeToRemote +// returns a proper error when FeatureByAddress() returns nil for the +// requested address, instead of panicking on a nil-pointer dereference. +// +// This protects callers (notably eebus-go's eg/lpc.connected() event +// handler) from crashing in the Publish() goroutine when a remote +// device advertises a feature address that is not yet materialized in +// the local entity model. +func (s *LocalFeatureTestSuite) TestSubscribeToRemote_NilFeature() { + s.localFeature.Device().AddRemoteDeviceForSki(s.remoteFeature.Device().Ski(), s.remoteFeature.Device()) + + // Build an address that targets the remote device but a non-existent + // (entity, feature) tuple. FeatureByAddress() will return nil for it. + bogusFeatureAddr := &model.FeatureAddressType{ + Device: s.remoteFeature.Address().Device, + Entity: []model.AddressEntityType{99999}, + Feature: util.Ptr(model.AddressFeatureType(99999)), + } + + // Must not panic; must return a proper ErrorType. + assert.NotPanics(s.T(), func() { + msgCounter, err := s.localFeature.SubscribeToRemote(bogusFeatureAddr) + assert.Nil(s.T(), msgCounter) + assert.NotNil(s.T(), err) + }) +} + +// TestBindToRemote_NilFeature mirrors TestSubscribeToRemote_NilFeature +// for the BindToRemote() code path. +func (s *LocalFeatureTestSuite) TestBindToRemote_NilFeature() { + s.localFeature.Device().AddRemoteDeviceForSki(s.remoteFeature.Device().Ski(), s.remoteFeature.Device()) + + bogusFeatureAddr := &model.FeatureAddressType{ + Device: s.remoteFeature.Address().Device, + Entity: []model.AddressEntityType{99999}, + Feature: util.Ptr(model.AddressFeatureType(99999)), + } + + assert.NotPanics(s.T(), func() { + msgCounter, err := s.localFeature.BindToRemote(bogusFeatureAddr) + assert.Nil(s.T(), msgCounter) + assert.NotNil(s.T(), err) + }) +}