Skip to content

UuidRegistry.clear_custom_registrations() does not restore SIG name aliases shadowed by a runtime registration #217

Description

@RonanB96

Summary

UuidRegistry.clear_custom_registrations() permanently corrupts the registry when a custom characteristic (or service) is registered under a name that aliases an existing SIG entry but with a different UUID. After the custom registration is cleared, the SIG name alias is lost and never restored, so subsequent name lookups return None.

Reproduction

from bluetooth_sig.gatt.uuid_registry import get_uuid_registry
from bluetooth_sig.gatt.characteristics.custom import CustomBaseCharacteristic
from bluetooth_sig.types import CharacteristicInfo
from bluetooth_sig.types.uuid import BluetoothUUID

reg = get_uuid_registry()
assert reg.get_characteristic_info("Temperature") is not None  # SIG 2A6E

class TemperatureCharacteristic(CustomBaseCharacteristic):
    _info = CharacteristicInfo(
        uuid=BluetoothUUID("AA112A6E-0000-1000-8000-00805F9B34FB"),  # custom UUID
        name="Temperature",                                          # SIG name
        unit="C", python_type=float,
    )
    def _decode_value(self, data, ctx=None, *, validate=True): return 0.0
    def _encode_value(self, data): return bytearray([0, 0])

TemperatureCharacteristic()                       # auto-registers
get_uuid_registry().clear_custom_registrations()

print(reg.get_characteristic_info("Temperature")) # -> None  (BUG: should be SIG 2A6E)

Root cause

  • register_characteristic() stores the custom info and, via _store_characteristic() / _generate_aliases(), writes the name alias "temperature" -> <custom canonical UUID>, overwriting the SIG mapping "temperature" -> 2A6E.
  • The SIG-entry preservation machinery (_characteristic_overrides) only triggers on a canonical UUID conflict (canonical_key in self._characteristics). A same-name/different-UUID registration is not a canonical conflict, so nothing is preserved.
  • clear_custom_registrations() deletes aliases whose canonical target is a runtime UUID (correctly removing the custom "temperature" alias) but has no record of the SIG alias it shadowed, so it cannot restore "temperature" -> 2A6E. The SIG canonical entry still exists; only the alias is gone.

Impact

  • Any register -> clear cycle of a name-colliding custom characteristic/service leaves the SIG registry with a missing name alias for the rest of the process.
  • In the test suite this manifests as cross-test pollution: tests/registry/test_registry_validation.py::TestNameResolutionFallback registers custom chars named "Temperature" and "Model Number String", which then breaks later tests that look those names up:
    • tests/registry/test_yaml_units.py::TestYAMLUnitParsing::test_yaml_unit_loading_basic
    • tests/static_analysis/test_characteristic_registry_completeness.py::TestCharacteristicEnumCompleteness::test_all_sig_characteristics_have_enum_entries
    • tests/static_analysis/test_characteristic_registry_completeness.py::TestCharacteristicEnumCompleteness::test_characteristic_enum_names_match_yaml

This regression was introduced when custom auto-registration began writing custom info (including the name alias) into the global uuid_registry via RegistrationManager (previously the translator path did not touch the uuid_registry alias index).

Current workaround

The polluting fallback tests instantiate their inline custom characteristics with auto_register=False so they don't write to the global uuid_registry. This keeps the suite green but does not fix the underlying registry defect.

Proposed fix

Make alias handling symmetric with canonical-entry handling. Rebuild the alias indices from the canonical stores at the end of clear_custom_registrations() (aliases are fully derivable from canonical infos), e.g.:

def _rebuild_aliases(self) -> None:
    self._service_aliases.clear()
    self._characteristic_aliases.clear()
    self._descriptor_aliases.clear()
    for info in self._services.values():
        for alias in self._generate_aliases(info):
            self._service_aliases[alias.lower()] = info.uuid.normalized
    for info in self._characteristics.values():
        for alias in self._generate_aliases(info):
            self._characteristic_aliases[alias.lower()] = info.uuid.normalized
    for info in self._descriptors.values():
        for alias in self._generate_aliases(info):
            self._descriptor_aliases[alias.lower()] = info.uuid.normalized

Alternatively, preserve overwritten aliases at registration time and restore them on clear.

Acceptance criteria

  • The reproduction above returns the SIG Temperature info after clear.
  • Registering then clearing a custom service/characteristic/descriptor with a SIG-colliding name leaves all SIG name aliases intact.
  • The auto_register=False workaround can be removed from tests/registry/test_registry_validation.py::TestNameResolutionFallback and the full suite still passes in a single process.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions