diff --git a/build/scripts/Targets/TargetDescriptionFiles/Services/ValidationService.php b/build/scripts/Targets/TargetDescriptionFiles/Services/ValidationService.php new file mode 100644 index 00000000..62094bdf --- /dev/null +++ b/build/scripts/Targets/TargetDescriptionFiles/Services/ValidationService.php @@ -0,0 +1,821 @@ +getName())) { + $failures[] = 'Target name not found'; + } + + if (str_contains($tdf->getName(), ' ')) { + $failures[] = 'Target name cannot contain whitespaces'; + } + + if (empty($tdf->getFamily())) { + $failures[] = 'Missing/invalid target family'; + } + + if (empty($tdf->getConfigurationValue())) { + $failures[] = 'Missing configuration value'; + } + + if (empty($tdf->variants)) { + $failures[] = 'Missing target variants'; + } + + $processedPropertyGroupKeys = []; + foreach ($tdf->propertyGroups as $propertyGroup) { + $failures = array_merge($failures, $this->validatePropertyGroup($propertyGroup)); + + if ($propertyGroup->key !== null && in_array($propertyGroup->key, $processedPropertyGroupKeys)) { + $failures[] = 'Duplicate property group key ("' . $propertyGroup->key . '") detected'; + } + + $processedPropertyGroupKeys[] = $propertyGroup->key; + } + + if (empty($tdf->addressSpaces)) { + $failures[] = 'Missing address spaces'; + } + + $processedAddressSpaceKeys = []; + foreach ($tdf->addressSpaces as $addressSpace) { + $failures = array_merge($failures, $this->validateAddressSpace($addressSpace)); + + if ($addressSpace->key !== null && in_array($addressSpace->key, $processedAddressSpaceKeys)) { + $failures[] = 'Duplicate address space key ("' . $addressSpace->key . '") detected'; + } + + $processedAddressSpaceKeys[] = $addressSpace->key; + } + + if ($tdf->getMemorySegmentsFromAnyAddressSpace('internal_program_memory') === null) { + $failures[] = 'Missing "internal_program_memory" memory segment'; + } + + if ($tdf->getMemorySegmentsFromAnyAddressSpace('internal_ram') === null) { + $failures[] = 'Missing "internal_ram" memory segment'; + } + + if ($tdf->getMemorySegmentsFromAnyAddressSpace('internal_eeprom') === null) { + $failures[] = 'Missing "internal_eeprom" memory segment'; + } + + if (empty($tdf->modules)) { + $failures[] = 'Missing modules'; + } + + $processedModuleKeys = []; + foreach ($tdf->modules as $module) { + $failures = array_merge($failures, $this->validateModule($module, $tdf)); + + if ($module->key !== null && in_array($module->key, $processedModuleKeys)) { + $failures[] = 'Duplicate module key ("' . $module->key . '") detected'; + } + + $processedModuleKeys[] = $module->key; + } + + if (empty($tdf->getModule('gpio_port'))) { + $failures[] = 'Missing GPIO port module'; + } + + if (empty($tdf->modules)) { + $failures[] = 'Missing peripherals'; + } + + $processedPeripheralKeys = []; + foreach ($tdf->peripherals as $peripheral) { + $failures = array_merge($failures, $this->validatePeripheral($peripheral, $tdf)); + + if ($peripheral->key !== null && in_array($peripheral->key, $processedPeripheralKeys)) { + $failures[] = 'Duplicate peripheral key ("' . $peripheral->key . '") detected'; + } + + if ($peripheral->moduleKey !== null && $tdf->getModule($peripheral->moduleKey) === null) { + $failures[] = 'Invalid module key ("' . $peripheral->moduleKey . '") on peripheral "' + . $peripheral->key . '"'; + } + + $processedPeripheralKeys[] = $peripheral->key; + } + + if (empty($tdf->getPeripheralsOfModule('gpio_port'))) { + $failures[] = 'Missing GPIO port peripherals'; + } + + if (empty($tdf->pinouts)) { + $failures[] = 'Missing pinouts'; + } + + $processedPinoutKeys = []; + foreach ($tdf->pinouts as $pinout) { + $failures = array_merge($failures, $this->validatePinout($pinout)); + + if ($pinout->key !== null && in_array($pinout->key, $processedPinoutKeys)) { + $failures[] = 'Duplicate pinout key ("' . $pinout->key . '") detected'; + } + + $processedPinoutKeys[] = $pinout->key; + } + + $processedVariantNames = []; + foreach ($tdf->variants as $variant) { + $failures = array_merge($failures, $this->validateVariant($variant, $tdf)); + + if ($variant->name !== null && in_array($variant->name, $processedVariantNames)) { + $failures[] = 'Duplicate variant name ("' . $variant->name . '") detected'; + } + + $processedVariantNames[] = $variant->name; + } + + return $failures; + } + + protected function validatePropertyGroup(PropertyGroup $propertyGroup): array + { + $failures = []; + + if (empty($propertyGroup->key)) { + $failures[] = 'Missing key'; + } + + if (!mb_check_encoding((string) $propertyGroup->key, 'ASCII')) { + $failures[] = 'Key contains non ASCII characters'; + } + + if (str_contains((string) $propertyGroup->key, ' ')) { + $failures[] = 'Key contains at least one white character'; + } + + if (str_contains((string) $propertyGroup->key, '.')) { + $failures[] = 'Key contains at least one period (".") character'; + } + + if (empty($propertyGroup->subPropertyGroups) && empty($propertyGroup->properties)) { + $failures[] = 'Empty property group'; + } + + $processedSubPropertyGroupKeys = []; + foreach ($propertyGroup->subPropertyGroups as $subPropertyGroup) { + $failures = array_merge($failures, $this->validatePropertyGroup($subPropertyGroup)); + + if ($subPropertyGroup->key !== null && in_array($subPropertyGroup->key, $processedSubPropertyGroupKeys)) { + $failures[] = 'Duplicate property group key ("' . $subPropertyGroup->key . '") detected'; + } + + $processedSubPropertyGroupKeys[] = $subPropertyGroup->key; + } + + $processedPropertyKeys = []; + foreach ($propertyGroup->properties as $property) { + $failures = array_merge($failures, $this->validateProperty($property)); + + if ($property->key !== null && in_array($property->key, $processedPropertyKeys)) { + $failures[] = 'Duplicate property key ("' . $property->key . '") detected'; + } + + $processedPropertyKeys[] = $property->key; + } + + return array_map( + fn (string $failure): string => 'Property group (id: "' . $propertyGroup->key . '") validation failure: ' + . $failure, + $failures + ); + } + + protected function validateProperty(Property $property): array + { + $failures = []; + + if (empty($property->key)) { + $failures[] = 'Missing key'; + } + + if (!mb_check_encoding((string) $property->key, 'ASCII')) { + $failures[] = 'Key contains non ASCII characters'; + } + + if (str_contains((string) $property->key, ' ')) { + $failures[] = 'Key contains at least white space character'; + } + + if (str_contains((string) $property->key, '.')) { + $failures[] = 'Key contains at least one period (".") character'; + } + + if ($property->value === null) { + $failures[] = 'Missing value'; + } + + return array_map( + fn (string $failure): string => 'Property (key: "' . $property->key . '") validation failure: ' . $failure, + $failures + ); + } + + protected function validateAddressSpace(AddressSpace $addressSpace): array + { + $failures = []; + + if (empty($addressSpace->key)) { + $failures[] = 'Missing key'; + } + + if ($addressSpace->startAddress === null) { + $failures[] = 'Missing start address'; + } + + if ($addressSpace->size === null) { + $failures[] = 'Missing size'; + } + + if (empty($addressSpace->memorySegments)) { + $failures[] = 'Missing memory segments - all address spaces must contain at least one memory segment'; + } + + $processedSegmentKeys = []; + foreach ($addressSpace->memorySegments as $segment) { + $failures = array_merge($failures, $this->validateMemorySegment($segment)); + + if ($segment->key !== null && in_array($segment->key, $processedSegmentKeys)) { + $failures[] = 'Duplicate segment key ("' . $segment->key . '") detected'; + } + + foreach ($addressSpace->memorySegments as $segmentOther) { + if ($segment->key === $segmentOther->key) { + continue; + } + + if ($segment->intersectsWith($segmentOther)) { + $failures[] = 'Segment "' . $segment->key . '" overlaps with segment "' . $segmentOther->key . '"'; + } + } + + $processedSegmentKeys[] = $segment->key; + } + + $totalSegmentSize = $addressSpace->totalSegmentSize(); + if ($totalSegmentSize > $addressSpace->size) { + $failures[] = 'Total size of all contained segments (' . $totalSegmentSize . ' bytes) exceeds the total' + . ' size of the address space (' . $addressSpace->size . ' bytes)'; + } + + return array_map( + fn (string $failure): string => 'Address space (key: "' . $addressSpace->key . '") validation failure: ' + . $failure, + $failures + ); + } + + protected function validateMemorySegment(MemorySegment $segment): array + { + $failures = []; + + if (empty($segment->key)) { + $failures[] = 'Missing key'; + } + + if (!preg_match('/^[a-z_]+$/',$segment->key)) { + $failures[] = 'Key must contain only lowercase letters'; + } + + if (!mb_check_encoding((string) $segment->key, 'ASCII')) { + $failures[] = 'Key must contain only ASCII characters'; + } + + if (empty($segment->name)) { + $failures[] = 'Missing name'; + } + + if (empty($segment->type)) { + $failures[] = 'Missing/invalid type'; + } + + if ($segment->startAddress === null) { + $failures[] = 'Missing start address'; + } + + if ($segment->size === null) { + $failures[] = 'Missing size'; + } + + $processedSectionKeys = []; + foreach ($segment->sections as $section) { + $failures = array_merge($failures, $this->validateMemorySegmentSection($section)); + + if ($section->key !== null && in_array($section->key, $processedSectionKeys)) { + $failures[] = 'Duplicate section key ("' . $section->key . '") detected'; + } + + foreach ($segment->sections as $sectionOther) { + if ($section->key === $sectionOther->key) { + continue; + } + + if ($section->intersectsWith($sectionOther)) { + $failures[] = 'Section "' . $section->key . '" overlaps with section "' . $sectionOther->key . '"'; + } + } + + $processedSectionKeys[] = $section->key; + } + + $totalSectionSize = $segment->totalSectionSize(); + if ($totalSectionSize > $segment->size) { + $failures[] = 'Total size of all contained sections (' . $totalSectionSize . ' bytes) exceeds the total' + . ' size of the memory segment (' . $segment->size . ' bytes)'; + } + + return array_map( + fn (string $failure): string => 'Memory segment (key: "' . $segment->key . '") validation failure: ' + . $failure, + $failures + ); + } + + protected function validateMemorySegmentSection(MemorySegmentSection $section): array + { + $failures = []; + + if (empty($section->key)) { + $failures[] = 'Missing key'; + } + + if (empty($section->name)) { + $failures[] = 'Missing name'; + } + + if ($section->startAddress === null) { + $failures[] = 'Missing start address'; + } + + if ($section->size === null) { + $failures[] = 'Missing size'; + } + + $processedSectionKeys = []; + foreach ($section->subSections as $subSection) { + $failures = array_merge($failures, $this->validateMemorySegmentSection($subSection)); + + if ($subSection->key !== null && in_array($subSection->key, $processedSectionKeys)) { + $failures[] = 'Duplicate section key ("' . $subSection->key . '") detected'; + } + + foreach ($section->subSections as $sectionOther) { + if ($subSection->key === $sectionOther->key) { + continue; + } + + if ($subSection->intersectsWith($sectionOther)) { + $failures[] = 'Section "' . $subSection->key . '" overlaps with section "' . $sectionOther->key . '"'; + } + } + + $processedSectionKeys[] = $subSection->key; + } + + return array_map( + fn (string $failure): string => 'Section (key: "' . $section->key . '") validation failure: ' . $failure, + $failures + ); + } + + protected function validateRegisterGroup( + RegisterGroup $registerGroup, + string $moduleKey, + TargetDescriptionFile $tdf + ): array + { + $failures = []; + + if (empty($registerGroup->key)) { + $failures[] = 'Missing key'; + } + + if (!mb_check_encoding((string) $registerGroup->key, 'ASCII')) { + $failures[] = 'Key contains non ASCII characters'; + } + + if (str_contains((string) $registerGroup->key, ' ')) { + $failures[] = 'Key contains at least one white character'; + } + + if (str_contains((string) $registerGroup->key, '.')) { + $failures[] = 'Key contains at least one period (".") character'; + } + + if (empty($registerGroup->name)) { + $failures[] = 'Missing name'; + } + + $processedChildKeys = []; + + foreach ($registerGroup->registers as $register) { + $failures = array_merge($failures, $this->validateRegister($register)); + + if ($register->key !== null && in_array($register->key, $processedChildKeys)) { + $failures[] = 'Duplicate register key ("' . $register->key . '") detected'; + } + + if ($register->alternative !== true) { + foreach ($registerGroup->registers as $registerOther) { + if ($register->key === $registerOther->key || $registerOther->alternative === true) { + continue; + } + + if ($register->intersectsWith($registerOther)) { + $failures[] = 'Register "' . $register->key . '" overlaps with register "' + . $registerOther->key . '"'; + } + } + } + + $processedChildKeys[] = $register->key; + } + + foreach ($registerGroup->subGroups as $subGroup) { + $failures = array_merge( + $failures, + $this->validateRegisterGroup($subGroup, $moduleKey, $tdf) + ); + + if ($subGroup->key !== null && in_array($subGroup->key, $processedChildKeys)) { + $failures[] = 'Duplicate register sub group key ("' . $subGroup->key . '") detected'; + } + + $processedChildKeys[] = $subGroup->key; + } + + foreach ($registerGroup->subGroupReferences as $subGroupReference) { + $failures = array_merge( + $failures, + $this->validateRegisterGroupReference($subGroupReference, $moduleKey, $tdf) + ); + + if ($subGroupReference->key !== null && in_array($subGroupReference->key, $processedChildKeys)) { + $failures[] = 'Duplicate register group reference key ("' . $subGroupReference->key . '") detected'; + } + + $processedChildKeys[] = $subGroupReference->key; + } + + return array_map( + fn (string $failure): string => 'Register group (key: "' . $registerGroup->key . '") validation failure: ' + . $failure, + $failures + ); + } + + protected function validateRegisterGroupReference( + RegisterGroupReference $registerGroupReference, + string $moduleKey, + TargetDescriptionFile $tdf + ): array { + $failures = []; + + if (empty($registerGroupReference->key)) { + $failures[] = 'Missing key'; + } + + if (!mb_check_encoding((string) $registerGroupReference->key, 'ASCII')) { + $failures[] = 'Key contains non ASCII characters'; + } + + if (str_contains((string) $registerGroupReference->key, ' ')) { + $failures[] = 'Key contains at least one period space'; + } + + if (str_contains((string) $registerGroupReference->key, '.')) { + $failures[] = 'Key contains at least one period (".") character'; + } + + if (empty($registerGroupReference->registerGroupKey)) { + $failures[] = 'Missing register group key'; + } + + if (empty($registerGroupReference->name)) { + $failures[] = 'Missing name'; + } + + if ($registerGroupReference->offset === null) { + $failures[] = 'Missing offset'; + } + + if ($tdf->resolveRegisterGroupReference($registerGroupReference, $moduleKey) === null) { + $failures[] = 'Could not resolve register group reference "' . $registerGroupReference->key + . '" - check register group key ("' . $registerGroupReference->registerGroupKey . '")'; + } + + return array_map( + fn (string $failure): string => 'Register group reference (group key: "' + . $registerGroupReference->registerGroupKey . '") validation failure: ' . $failure, + $failures + ); + } + + protected function validateRegister(Register $register): array + { + $failures = []; + + if (empty($register->key)) { + $failures[] = 'Missing key'; + } + + if (!mb_check_encoding((string) $register->key, 'ASCII')) { + $failures[] = 'Key contains non ASCII character'; + } + + if (str_contains((string) $register->key, ' ')) { + $failures[] = 'Key contains at least white space character'; + } + + if (str_contains((string) $register->key, '.')) { + $failures[] = 'Key contains at least one period (".") character'; + } + + if (empty($register->name)) { + $failures[] = 'Missing name'; + } + + if ($register->offset === null) { + $failures[] = 'Missing offset'; + } + + if ($register->size === null) { + $failures[] = 'Missing size'; + } + + $processedBitFieldKeys = []; + foreach ($register->bitFields as $bitField) { + $failures = array_merge($failures, $this->validateBitField($bitField)); + + if ($bitField->key !== null && in_array($bitField->key, $processedBitFieldKeys)) { + $failures[] = 'Duplicate bit field key ("' . $bitField->key . '") detected'; + } + + $processedBitFieldKeys[] = $bitField->key; + } + + return array_map( + fn (string $failure): string => 'Register (name: "' . $register->name . '") validation failure: ' + . $failure, + $failures + ); + } + + protected function validateBitField(BitField $bitField): array + { + $failures = []; + + if (empty($bitField->key)) { + $failures[] = 'Missing key'; + } + + if (empty($bitField->name)) { + $failures[] = 'Missing name'; + } + + if ($bitField->mask === null) { + $failures[] = 'Missing mask'; + } + + return array_map( + fn (string $failure): string => 'Bit field (name: "' . $bitField->name . '") validation failure: ' + . $failure, + $failures + ); + } + + protected function validateModule(Module $module, TargetDescriptionFile $tdf): array + { + $failures = []; + + if (empty($module->key)) { + $failures[] = 'Missing key'; + } + + if (empty($module->name)) { + $failures[] = 'Missing name'; + } + + if (empty($module->description)) { + $failures[] = 'Missing description'; + } + + $processedChildKeys = []; + foreach ($module->registerGroups as $registerGroup) { + $failures = array_merge( + $failures, + $this->validateRegisterGroup($registerGroup, $module->key ?? '', $tdf) + ); + + if ($registerGroup->key !== null && in_array($registerGroup->key, $processedChildKeys)) { + $failures[] = 'Duplicate register group key ("' . $registerGroup->key . '") detected'; + } + + $processedChildKeys[] = $registerGroup->key; + } + + return array_map( + fn (string $failure): string => 'Module (key: "' . $module->key . '") validation failure: ' . $failure, + $failures + ); + } + + protected function validatePeripheral(Peripheral $peripheral, TargetDescriptionFile $tdf): array + { + $failures = []; + + if (empty($peripheral->key)) { + $failures[] = 'Missing key'; + } + + if (empty($peripheral->name)) { + $failures[] = 'Missing name'; + } + + if (empty($peripheral->moduleKey)) { + $failures[] = 'Missing module key'; + } + + if (empty($peripheral->registerGroupReferences) && empty($peripheral->signals)) { + $failures[] = 'Empty - no register group references or signals'; + } + + $processedChildKeys = []; + foreach ($peripheral->registerGroupReferences as $registerGroupReference) { + $failures = array_merge( + $failures, + $this->validateRegisterGroupReference( + $registerGroupReference, + $peripheral->moduleKey ?? '', + $tdf + ) + ); + + if ($registerGroupReference->key !== null && in_array($registerGroupReference->key, $processedChildKeys)) { + $failures[] = 'Duplicate register group reference key ("' . $registerGroupReference->key . '") detected'; + } + + $processedChildKeys[] = $registerGroupReference->key; + } + + foreach ($peripheral->signals as $signal) { + $failures = array_merge($failures, $this->validateSignal($signal)); + } + + return array_map( + fn (string $failure): string => 'Peripheral "' . $peripheral->name . '" validation failure: ' . $failure, + $failures + ); + } + + protected function validateSignal(Signal $signal): array + { + $failures = []; + + if (empty($signal->padId)) { + $failures[] = 'Missing pad ID'; + } + + return array_map( + fn (string $failure): string => 'Signal validation failure: ' . $failure, + $failures + ); + } + + protected function validatePinout(Pinout $pinout): array + { + $failures = []; + + if (empty($pinout->key)) { + $failures[] = 'Missing key'; + } + + if (empty($pinout->name)) { + $failures[] = 'Missing name'; + } + + if (empty($pinout->type)) { + $failures[] = 'Unknown type'; + } + + if ( + in_array($pinout->type, [PinoutType::SOIC, PinoutType::DIP, PinoutType::SSOP]) + && count($pinout->pins) % 2 != 0 + ) { + $failures[] = 'Pin count for ' . $pinout->type->value . ' pinout is not a multiple of two'; + } + + if ( + in_array($pinout->type, [PinoutType::QFP, PinoutType::QFN]) + && count($pinout->pins) % 2 != 0 + ) { + $failures[] = 'Pin count for ' . $pinout->type->value . ' pinout is not a multiple of four'; + } + + // It's common for the pin count to be included in the name - we verify it here + $impliedPinCount = filter_var($pinout->name, FILTER_SANITIZE_NUMBER_INT); + if (is_numeric($impliedPinCount) && (int) $impliedPinCount !== count($pinout->pins)) { + $failures[] = 'Implied pin count (' . $impliedPinCount . ') does not match actual pin count (' + . count($pinout->pins) . ')'; + } + + $processedPositions = []; + foreach ($pinout->pins as $pin) { + $failures = array_merge($failures, $this->validatePin($pin, $pinout)); + + if ($pin->position !== null && in_array($pin->position, $processedPositions)) { + $failures[] = 'Duplicate pin position (' . $pin->position . ') detected'; + } + + $processedPositions[] = $pin->position; + } + + return array_map( + fn (string $failure): string => 'Pinout "' . $pinout->key . '" validation failure: ' . $failure, + $failures + ); + } + + protected function validatePin(Pin $pin, Pinout $pinout): array + { + $failures = []; + + if (empty($pin->position)) { + $failures[] = 'Missing position'; + + } elseif ( + in_array( + $pinout->type, + [PinoutType::QFN, PinoutType::QFP, PinoutType::MLF, PinoutType::DIP, PinoutType::SOIC] + ) + && !is_numeric($pin->position) + ) { + $failures[] = 'Position for ' . $pinout->type->value . ' pinouts must be numerical (current value: "' + . $pin->position . '")'; + } + + if (empty($pin->pad)) { + $failures[] = 'Missing pad'; + } + + return array_map( + fn (string $failure): string => 'Pin validation failure: ' . $failure, + $failures + ); + } + + protected function validateVariant(Variant $variant, TargetDescriptionFile $tdf): array + { + $failures = []; + + if (empty($variant->name)) { + $failures[] = 'Missing name'; + } + + if (empty($variant->pinoutKey)) { + $failures[] = 'Missing pinout key'; + + } elseif (!$tdf->getPinout($variant->pinoutKey) instanceof Pinout) { + $failures[] = 'Could not resolve pinout from key "' . $variant->pinoutKey + . '" - check pinout key'; + } + + if (empty($variant->package)) { + $failures[] = 'Missing package'; + } + + return array_map( + fn (string $failure): string => 'Variant "' . $variant->name . '" validation failure: ' . $failure, + $failures + ); + } +}