# -*- coding: utf-8 -*- # Copyright 2021 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Objects for describing template code to be generated from structured.xml.""" import hashlib import os import re import struct import templates_validator as validator_tmpl class Util: """Helpers for generating C++.""" @staticmethod def sanitize_name(name): return re.sub('[^0-9a-zA-Z_]', '_', name) @staticmethod def camel_to_snake(name): pat = '((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))' return re.sub(pat, r'_\1', name).lower() @staticmethod def hash_name(name): # This must match the hash function in chromium's # //base/metrics/metric_hashes.cc. >Q means 8 bytes, big endian. name = name.encode('utf-8') md5 = hashlib.md5(name) return struct.unpack('>Q', md5.digest()[:8])[0] @staticmethod def event_name_hash(project_name, event_name): """Make the name hash for an event. This gets uploaded in the StructuredEventProto.event_name_hash field. It is the sole means of recording which event from structured.xml a StructuredEventProto instance represents. To avoid naming collisions, it must contain three pieces of information: - the name of the event itself - the name of the event's project, to avoid collisions with events of the same name in other projects - an identifier that this comes from chromium, to avoid collisions with events and projects of the same name defined in cros's structured.xml This must use sanitized names for the project and event. """ event_name = Util.sanitize_name(event_name) project_name = Util.sanitize_name(project_name) # TODO(crbug.com/1148168): Once the minimum python version is 3.6+, rewrite # this .format and others using f-strings. return Util.hash_name('chrome::{}::{}'.format(project_name, event_name)) class FileInfo: """Codegen-related info about a file.""" def __init__(self, dirname, basename): self.dirname = dirname self.basename = basename self.filepath = os.path.join(dirname, basename) # This takes the last three components of the filepath for use in the # header guard, ie. METRICS_STRUCTURED_STRUCTURED_EVENTS_H_ relative_path = os.sep.join(self.filepath.split(os.sep)[-3:]) self.guard_path = Util.sanitize_name(relative_path).upper() class ProjectInfo: """Codegen-related info about a project.""" def __init__(self, project): self.name = Util.sanitize_name(project.name) self.namespace = Util.camel_to_snake(self.name) self.name_hash = Util.hash_name(self.name) self.validator = '{}ProjectValidator'.format(self.name) self.validator_snake_name = Util.camel_to_snake(self.validator) self.events = project.events # Set ID type. if project.id == 'uma': self.id_type = 'kUmaId' elif project.id == 'per-project': self.id_type = 'kProjectId' elif project.id == 'none': self.id_type = 'kUnidentified' else: raise ValueError('Invalid id type.') # Set ID scope if project.scope == 'profile': self.id_scope = 'kPerProfile' elif project.scope == 'device': self.id_scope = 'kPerDevice' else: raise ValueError('Invalid id scope.') # Set event type. This is inferred by checking all metrics within the # project. If any of a project's metrics is a raw string, then its events # are considered raw string events, even if they also contain non-strings. self.event_type = 'REGULAR' for event in project.events: for metric in event.metrics: if metric.type == 'raw-string': self.event_type = 'RAW_STRING' break def build_event_map(self) -> str: event_infos = (EventInfo(event, self) for event in self.events) # Generate map entries. validator_map_str = ',\n '.join( '{{"{}", &{}}}'.format(event_info.name, event_info.validator_snake_name) for event_info in event_infos) return validator_tmpl.IMPL_PROJECT_EVENT_MAP_TEMPLATE.format( project=self, event_validator_map=validator_map_str) def build_event_validators(self) -> str: event_infos = (EventInfo(event, self) for event in self.events) return '\n'.join(event.build_validator_init() for event in event_infos) def build_project_init(self) -> str: return 'static {} {};'.format(self.validator, self.validator_snake_name) def build_validator_code(self) -> str: return validator_tmpl.IMPL_PROJECT_VALIDATOR_TEMPLATE.format(project=self) class EventInfo: """Codegen-related info about an event.""" def __init__(self, event, project_info): self.name = Util.sanitize_name(event.name) self.name_hash = Util.event_name_hash(project_info.name, self.name) self.validator_name = '{}EventValidator'.format(self.name) self.validator_snake_name = Util.camel_to_snake(self.validator_name) self.metrics = event.metrics def build_metric_hash_map(self) -> str: metric_infos = (MetricInfo(metric) for metric in self.metrics) return ',\n '.join('{{\"{}\", {}::kEventNameHash}}'.format( metric_info.name, self.validator_name) for metric_info in metric_infos) def build_validator_init(self) -> str: return ('static {} {};').format(self.validator_name, self.validator_snake_name) def build_validator_code(self) -> str: return validator_tmpl.IMPL_EVENT_VALIDATOR_TEMPLATE.format( event=self, metric_hash_map=self.build_metric_hash_map()) class MetricInfo: """Codegen-related info about a metric.""" def __init__(self, metric): self.name = Util.sanitize_name(metric.name) self.hash = Util.hash_name(metric.name) if metric.type == 'hmac-string': self.type = 'std::string&' self.setter = 'AddHmacMetric' elif metric.type == 'int': self.type = 'int64_t' self.setter = 'AddIntMetric' elif metric.type == 'raw-string': self.type = 'std::string&' self.setter = 'AddRawStringMetric' else: raise ValueError('Invalid metric type.') class Template: """Template for producing code from structured.xml.""" def __init__(self, model, dirname, basename, file_template, project_template, event_template, metric_template): self.model = model self.dirname = dirname self.basename = basename self.file_template = file_template self.project_template = project_template self.event_template = event_template self.metric_template = metric_template def write_file(self): file_info = FileInfo(self.dirname, self.basename) with open(file_info.filepath, 'w') as f: f.write(self._stamp_file(file_info)) def _stamp_file(self, file_info): project_code = ''.join( self._stamp_project(file_info, p) for p in self.model.projects) return self.file_template.format(file=file_info, project_code=project_code) def _stamp_project(self, file_info, project): project_info = ProjectInfo(project) event_code = ''.join( self._stamp_event(file_info, project_info, event) for event in project.events) return self.project_template.format(file=file_info, project=project_info, event_code=event_code) def _stamp_event(self, file_info, project_info, event): event_info = EventInfo(event, project_info) metric_code = ''.join( self._stamp_metric(file_info, event_info, metric) for metric in event.metrics) return self.event_template.format(file=file_info, project=project_info, event=event_info, metric_code=metric_code) def _stamp_metric(self, file_info, event_info, metric): return self.metric_template.format(file=file_info, event=event_info, metric=MetricInfo(metric)) class ValidatorHeaderTemplate: """Template for generating header validator code from structured.xml.""" def __init__(self, model, dirname, basename): self.model = model self.dirname = dirname self.basename = basename def write_file(self) -> None: file_info = FileInfo(self.dirname, self.basename) with open(file_info.filepath, 'w') as f: f.write(self._stamp_file(file_info)) def _stamp_file(self, file_info) -> str: return validator_tmpl.HEADER_FILE_TEMPLATE.format(file=file_info) class ValidatorImplTemplate: """Template for generating implementation validator code from structured.xml. The generated file will store a static map containing all the validators mapped by event name. All validators are initialized statically. Almost everything is generated in an anonymous namespace as the generated map should not be exposed. The generated code will be in the following order: 1) EventValidator class implementation. 2) EventValidator static initialization. 3) Project map initialization mapping event name to corresponding EventValidator. 4) Project class implementation. 5) Project validator static initialization. 6) Map initialization mapping project name to ProjectValidator. """ def __init__(self, model, dirname, basename): self.model = model self.dirname = dirname self.basename = basename def write_file(self) -> None: file_info = FileInfo(self.dirname, self.basename) with open(file_info.filepath, 'w') as f: f.write(self._stamp_file(file_info)) def _stamp_file(self, file_info) -> str: event_code = [] event_validators = [] project_event_maps = [] project_code = [] project_validators = [] for project in self.model.projects: project_info = ProjectInfo(project) event_infos = (EventInfo(event, project_info) for event in project.events) project_event_code = '\n'.join(event_info.build_validator_code() for event_info in event_infos) event_code.append(project_event_code) event_validators.append(project_info.build_event_validators()) project_event_maps.append(project_info.build_event_map()) project_code.append(project_info.build_validator_code()) project_validators.append(project_info.build_project_init()) # Turn all lists into strings. events_code_str = ''.join(event_code) event_validators_str = '\n'.join(event_validators) project_event_maps_str = '\n'.join(project_event_maps) project_code_str = ''.join(project_code) project_validators_str = '\n'.join(project_validators) return validator_tmpl.IMPL_FILE_TEMPLATE.format( file=file_info, projects_code=project_code_str, event_code=events_code_str, event_validators=event_validators_str, project_event_maps=project_event_maps_str, project_validators=project_validators_str, project_map=self._build_project_map()) def _build_project_map(self) -> str: project_infos = (ProjectInfo(project) for project in self.model.projects) project_map = ',\n '.join( '{{"{}", &{}}}'.format(project.name, project.validator_snake_name) for project in project_infos) return validator_tmpl.IMPL_PROJECT_MAP_TEMPLATE.format( project_map=project_map)