"""RNA-derived property descriptions and validation inside the native Blender job.""" import math MAX_OPERATOR_ARRAY_LENGTH = 64 MAX_OPERATOR_STRING_BYTES = 4096 def operator_property_spec(prop): kind = str(getattr(prop, 'type', '')).upper() if prop.identifier == 'rna_type' or kind not in ('BOOLEAN', 'INT', 'FLOAT', 'DOUBLE', 'ENUM', 'STRING'): return None length = int(getattr(prop, 'array_length', 0)) dimensions = tuple(getattr(prop, 'array_dimensions', ())) if length > MAX_OPERATOR_ARRAY_LENGTH or sum(value > 0 for value in dimensions) > 1: return None if length and kind not in ('BOOLEAN', 'INT', 'FLOAT', 'DOUBLE'): return None flag = kind == 'ENUM' and bool(getattr(prop, 'is_enum_flag', False)) value_type = 'boolean' if kind == 'BOOLEAN' else 'number' if kind in ('INT', 'FLOAT', 'DOUBLE') else 'enum' if kind == 'ENUM' else 'string' if length: value_type += '-array' elif flag: value_type = 'enum-set' spec = {'name': prop.identifier, 'type': value_type, 'required': bool(getattr(prop, 'is_required', False))} for key, attr in (('label', 'name'), ('description', 'description'), ('subtype', 'subtype'), ('unit', 'unit')): value = getattr(prop, attr, None) if value: spec[key] = str(value) if length: spec['arrayLength'] = length if kind == 'INT': spec['integer'] = True try: if length: default = list(prop.default_array) elif flag: default = sorted(prop.default_flag) else: default = prop.default if isinstance(default, (bool, int, float, str, list)): values = default if isinstance(default, list) else [default] if all(not isinstance(v, float) or math.isfinite(v) for v in values): spec['defaultValue'] = default except (AttributeError, TypeError, ValueError): pass for key, attr in (('min', 'hard_min'), ('max', 'hard_max')): value = getattr(prop, attr, None) if type(value) in (int, float) and math.isfinite(value): spec[key] = value if kind == 'ENUM': items = [{'value': str(item.identifier), 'label': str(item.name)} for item in prop.enum_items if item.identifier] if items: spec['enumItems'] = items return spec def validated_operator_properties(target, properties): """Validate against the operator's live RNA immediately before execution.""" specs = {prop.identifier: operator_property_spec(prop) for prop in target.get_rna_type().properties} result = {} for name, value in properties.items(): spec = specs.get(name) if spec is None: raise ValueError('SERVER_OPERATOR_PROPERTY_BLOCKED: ' + name) kind = spec['type'] if kind.endswith('-array'): if not isinstance(value, list) or len(value) != spec['arrayLength']: raise ValueError('SERVER_OPERATOR_PROPERTY_LENGTH: ' + name) scalar_kind = kind.removesuffix('-array') values = value elif kind == 'enum-set': if not isinstance(value, list) or len(value) > MAX_OPERATOR_ARRAY_LENGTH or any(type(v) is not str for v in value) or len(set(value)) != len(value): raise ValueError('SERVER_OPERATOR_PROPERTY_TYPE: ' + name) scalar_kind = 'enum' values = value else: scalar_kind = kind values = [value] for item in values: valid = False if scalar_kind == 'boolean': valid = type(item) is bool elif scalar_kind == 'number': valid = type(item) in (int, float) and math.isfinite(item) if valid and spec.get('integer'): valid = float(item).is_integer() and abs(item) <= 9007199254740991 if valid: valid = spec.get('min', -math.inf) <= item <= spec.get('max', math.inf) elif scalar_kind in ('string', 'enum'): valid = type(item) is str and len(item.encode()) <= MAX_OPERATOR_STRING_BYTES # Context-dependent enum callbacks may expose no items while describing. if valid and scalar_kind == 'enum' and spec.get('enumItems'): valid = item in {entry['value'] for entry in spec['enumItems']} if not valid: raise ValueError('SERVER_OPERATOR_PROPERTY_TYPE_OR_RANGE: ' + name) if kind == 'enum-set': result[name] = set(value) elif spec.get('integer'): result[name] = [int(v) for v in value] if isinstance(value, list) else int(value) else: result[name] = value return result