mirror of https://github.com/ARMmbed/mbed-os.git
Add TF-M autogen tool
parent
771cdaa3e7
commit
f5af459d4c
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright (c) 2017-2018 ARM Limited
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .generate_partition_code import \
|
||||||
|
generate_partitions_sources, generate_psa_setup
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'generate_partitions_sources',
|
||||||
|
'generate_psa_setup',
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,689 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# Copyright (c) 2017-2018 ARM Limited
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from os.path import join as path_join
|
||||||
|
from six import integer_types, string_types
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
||||||
|
from jsonschema import validate
|
||||||
|
|
||||||
|
__version__ = '1.0'
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
TEMPLATES_LIST_FILE = path_join(SCRIPT_DIR, 'tfm_generated_file_list.json')
|
||||||
|
MANIFEST_FILE_PATTERN = '*_psa.json'
|
||||||
|
MBED_OS_ROOT = os.path.abspath(path_join(SCRIPT_DIR, os.pardir, os.pardir))
|
||||||
|
SERVICES_DIR = os.path.join(MBED_OS_ROOT, "components", "TARGET_PSA", "services")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_int(num):
|
||||||
|
"""
|
||||||
|
Tries to parse an integer num from a given string
|
||||||
|
|
||||||
|
:param num: Number in int/string type
|
||||||
|
:return: Numeric value
|
||||||
|
"""
|
||||||
|
if isinstance(num, int):
|
||||||
|
return num
|
||||||
|
num_str = str(num)
|
||||||
|
radix = 16 if num_str.lower().startswith('0x') else 10
|
||||||
|
res = int(num_str, radix)
|
||||||
|
# Python converts str to int as a signed integer
|
||||||
|
if res > 0x7FFFFFFF:
|
||||||
|
res -= 0x100000000
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class RotService(object):
|
||||||
|
MINOR_POLICIES = ['STRICT', 'RELAXED']
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
identifier,
|
||||||
|
signal,
|
||||||
|
non_secure_clients,
|
||||||
|
minor_version=1,
|
||||||
|
minor_policy='STRICT'
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Root of Trust Service C'tor (Aligned with json schema)
|
||||||
|
|
||||||
|
:param name: Root of Trust Service identifier (available to user)
|
||||||
|
:param identifier: Root of Trust Service numeric enumeration.
|
||||||
|
:param signal: Root of Trust Service identifier inside the partition
|
||||||
|
:param non_secure_clients: True to allow connections from non-secure
|
||||||
|
partitions
|
||||||
|
:param minor_version: Root of Trust Service version
|
||||||
|
:param minor_policy: Enforcement level of minor version
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.id = identifier
|
||||||
|
self.signal = signal
|
||||||
|
|
||||||
|
assert assert_int(identifier)
|
||||||
|
|
||||||
|
assert isinstance(non_secure_clients, bool), \
|
||||||
|
'non_secure_clients parameter must be of boolean type'
|
||||||
|
self.nspe_callable = non_secure_clients
|
||||||
|
|
||||||
|
self.minor_version = assert_int(minor_version)
|
||||||
|
assert self.minor_version > 0, 'minor_version parameter is invalid'
|
||||||
|
|
||||||
|
assert minor_policy in self.MINOR_POLICIES, \
|
||||||
|
'minor_policy parameter is invalid'
|
||||||
|
self.minor_policy = minor_policy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def numeric_id(self):
|
||||||
|
return assert_int(self.id)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
(self.name == other.name) and
|
||||||
|
(self.id == other.id) and
|
||||||
|
(self.signal == other.signal) and
|
||||||
|
(self.nspe_callable == other.nspe_callable) and
|
||||||
|
(self.minor_version == other.minor_version) and
|
||||||
|
(self.minor_policy == other.minor_policy)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MmioRegion(object):
|
||||||
|
MMIO_PERMISSIONS = {
|
||||||
|
'READ-ONLY': 'PSA_MMIO_PERM_READ_ONLY',
|
||||||
|
'READ-WRITE': 'PSA_MMIO_PERM_READ_WRITE'
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""
|
||||||
|
MMIO Region C'tor (Aligned with json schema)
|
||||||
|
|
||||||
|
Supports both named and numeric regions
|
||||||
|
In case of named region the acceptable params are name and permission
|
||||||
|
In case of numeric region the acceptable params are name, size and
|
||||||
|
permission
|
||||||
|
|
||||||
|
:param name: C definition name of the region (size will be
|
||||||
|
auto-generated)
|
||||||
|
:param base: C hex string defining a memory address (must be 32bit)
|
||||||
|
:param size: size of a region (Applicable only for numbered regions)
|
||||||
|
:param permission: Access permissions to the described region (R/RW)
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert 'permission' in kwargs
|
||||||
|
self.permission = self.MMIO_PERMISSIONS[kwargs['permission']]
|
||||||
|
if 'name' in kwargs:
|
||||||
|
self.base = kwargs['name']
|
||||||
|
self.size = '(sizeof(*({})))'.format(kwargs['name'])
|
||||||
|
if 'base' in kwargs:
|
||||||
|
self.base = kwargs['base']
|
||||||
|
self.size = assert_int(kwargs['size'])
|
||||||
|
|
||||||
|
assert 'partition_id' in kwargs
|
||||||
|
self.partition_id = assert_int(kwargs['partition_id'])
|
||||||
|
|
||||||
|
assert hasattr(self, 'base')
|
||||||
|
assert hasattr(self, 'size')
|
||||||
|
assert hasattr(self, 'permission')
|
||||||
|
assert hasattr(self, 'partition_id')
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
(self.base == other.base) and
|
||||||
|
(self.size == other.size) and
|
||||||
|
(self.permission == other.permission)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Irq(object):
|
||||||
|
def __init__(self, line_num, signal):
|
||||||
|
"""
|
||||||
|
IRQ line C'tor (Aligned with json schema)
|
||||||
|
|
||||||
|
:param line_num: number of interrupt used by the partition
|
||||||
|
:param signal: IRQ line identifier inside the partition
|
||||||
|
"""
|
||||||
|
self.line_num = assert_int(line_num)
|
||||||
|
assert isinstance(signal, string_types)
|
||||||
|
self.signal = signal
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (self.line_num == other.line_num) and \
|
||||||
|
(self.signal == other.signal)
|
||||||
|
|
||||||
|
|
||||||
|
class Manifest(object):
|
||||||
|
PRIORITY = ['LOW', 'NORMAL', 'HIGH']
|
||||||
|
PARTITION_TYPES = ['APPLICATION-ROT', 'PSA-ROT']
|
||||||
|
# The following signal bits cannot be used:
|
||||||
|
# bit[0-2] | Reserved
|
||||||
|
# bit[3] | PSA Doorbell
|
||||||
|
# bit[31] | RTX error bit
|
||||||
|
RESERVED_SIGNALS = 5
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
manifest_file,
|
||||||
|
name,
|
||||||
|
partition_id,
|
||||||
|
partition_type,
|
||||||
|
priority,
|
||||||
|
entry_point,
|
||||||
|
heap_size,
|
||||||
|
stack_size,
|
||||||
|
source_files,
|
||||||
|
mmio_regions=None,
|
||||||
|
rot_services=None,
|
||||||
|
extern_sids=None,
|
||||||
|
irqs=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Manifest C'tor (Aligned with json schema)
|
||||||
|
|
||||||
|
:param manifest_file: Path to json manifest
|
||||||
|
:param name: Partition unique name
|
||||||
|
:param partition_id: Partition identifier
|
||||||
|
:param partition_type: Whether the partition is unprivileged or part
|
||||||
|
of the trusted computing base
|
||||||
|
:param priority: Priority of the partition's thread
|
||||||
|
:param entry_point: C symbol name of the partition's main function
|
||||||
|
:param heap_size: Size of heap required for the partition
|
||||||
|
:param stack_size: Size of stack required for the partition
|
||||||
|
:param source_files: List of files assembling the partition
|
||||||
|
(relative paths)
|
||||||
|
:param mmio_regions: List of MMIO regions used by the partition
|
||||||
|
:param rot_services: List of Root of Trust Services declared by the
|
||||||
|
partition
|
||||||
|
:param extern_sids: List of Root of Trust Services the partition can call
|
||||||
|
:param irqs: List of interrupts the partition can handle
|
||||||
|
"""
|
||||||
|
assert manifest_file is not None
|
||||||
|
assert name is not None
|
||||||
|
assert partition_id is not None
|
||||||
|
assert partition_type is not None
|
||||||
|
assert entry_point is not None
|
||||||
|
assert priority is not None
|
||||||
|
assert heap_size is not None
|
||||||
|
assert stack_size is not None
|
||||||
|
assert source_files is not None
|
||||||
|
|
||||||
|
mmio_regions = [] if mmio_regions is None else mmio_regions
|
||||||
|
rot_services = [] if rot_services is None else rot_services
|
||||||
|
extern_sids = [] if extern_sids is None else extern_sids
|
||||||
|
irqs = [] if irqs is None else irqs
|
||||||
|
|
||||||
|
assert os.path.isfile(manifest_file)
|
||||||
|
assert isinstance(partition_id, integer_types)
|
||||||
|
assert isinstance(heap_size, int)
|
||||||
|
assert isinstance(stack_size, int)
|
||||||
|
assert isinstance(entry_point, string_types)
|
||||||
|
assert partition_type in self.PARTITION_TYPES
|
||||||
|
assert priority in self.PRIORITY
|
||||||
|
assert partition_id > 0
|
||||||
|
|
||||||
|
self.file = manifest_file
|
||||||
|
self.name = name
|
||||||
|
self.id = partition_id
|
||||||
|
self.type = partition_type
|
||||||
|
self.priority = priority
|
||||||
|
self.heap_size = heap_size
|
||||||
|
self.stack_size = stack_size
|
||||||
|
self.entry_point = entry_point
|
||||||
|
if isinstance(source_files, list):
|
||||||
|
self.source_files = source_files
|
||||||
|
else:
|
||||||
|
self.source_files = [source_files]
|
||||||
|
|
||||||
|
self.mmio_regions = mmio_regions
|
||||||
|
self.rot_services = rot_services
|
||||||
|
self.extern_sids = extern_sids
|
||||||
|
self.irqs = irqs
|
||||||
|
|
||||||
|
for src_file in self.source_files:
|
||||||
|
assert os.path.isfile(src_file), \
|
||||||
|
"The source file {} mentioned in {} doesn't exist.".format(
|
||||||
|
src_file, self.file
|
||||||
|
)
|
||||||
|
|
||||||
|
for rot_srv in self.rot_services:
|
||||||
|
assert isinstance(rot_srv, RotService)
|
||||||
|
|
||||||
|
for extern_sid in self.extern_sids:
|
||||||
|
assert isinstance(extern_sid, string_types)
|
||||||
|
|
||||||
|
assert len(self.extern_sids) == len(set(self.extern_sids)), \
|
||||||
|
'Detected duplicates external SIDs in {}'.format(self.file)
|
||||||
|
|
||||||
|
for irq in self.irqs:
|
||||||
|
assert isinstance(irq, Irq)
|
||||||
|
|
||||||
|
total_signals = len(self.rot_services) + len(self.irqs)
|
||||||
|
assert total_signals <= 32 - self.RESERVED_SIGNALS, \
|
||||||
|
'Manifest {} - {} exceeds limit of RoT services and IRQs allowed ' \
|
||||||
|
'({}).'.format(
|
||||||
|
self.name, self.file, 32 - self.RESERVED_SIGNALS
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
(self.file == other.file) and
|
||||||
|
(self.name == other.name) and
|
||||||
|
(self.id == other.id) and
|
||||||
|
(self.type == other.type) and
|
||||||
|
(self.priority == other.priority) and
|
||||||
|
(self.heap_size == other.heap_size) and
|
||||||
|
(self.stack_size == other.stack_size) and
|
||||||
|
(self.entry_point == other.entry_point) and
|
||||||
|
(self.source_files == other.source_files) and
|
||||||
|
(self.mmio_regions == other.mmio_regions) and
|
||||||
|
(self.rot_services == other.rot_services) and
|
||||||
|
(self.extern_sids == other.extern_sids) and
|
||||||
|
(self.irqs == other.irqs)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, manifest_file, skip_src=False):
|
||||||
|
"""
|
||||||
|
Load a partition manifest file
|
||||||
|
|
||||||
|
:param manifest_file: Manifest file path
|
||||||
|
:param skip_src: Ignore the `source_files` entry
|
||||||
|
:return: Manifest object
|
||||||
|
"""
|
||||||
|
|
||||||
|
partition_schema_path = path_join(
|
||||||
|
SCRIPT_DIR,
|
||||||
|
'partition_description_schema.json'
|
||||||
|
)
|
||||||
|
with open(partition_schema_path) as schema_fh:
|
||||||
|
partition_schema = json.load(schema_fh)
|
||||||
|
|
||||||
|
# Load partition manifest file.
|
||||||
|
with open(manifest_file) as fh:
|
||||||
|
manifest = json.load(fh)
|
||||||
|
|
||||||
|
validate(manifest, partition_schema)
|
||||||
|
manifest_dir = os.path.dirname(manifest_file)
|
||||||
|
|
||||||
|
source_files = []
|
||||||
|
if not skip_src:
|
||||||
|
for src_file in manifest['source_files']:
|
||||||
|
source_files.append(
|
||||||
|
os.path.normpath(path_join(manifest_dir, src_file)))
|
||||||
|
|
||||||
|
mmio_regions = []
|
||||||
|
for mmio_region in manifest.get('mmio_regions', []):
|
||||||
|
mmio_regions.append(
|
||||||
|
MmioRegion(partition_id=manifest['id'], **mmio_region))
|
||||||
|
|
||||||
|
rot_services = []
|
||||||
|
for rot_srv in manifest.get('services', []):
|
||||||
|
rot_services.append(RotService(**rot_srv))
|
||||||
|
|
||||||
|
irqs = []
|
||||||
|
for irq in manifest.get('irqs', []):
|
||||||
|
irqs.append(Irq(**irq))
|
||||||
|
|
||||||
|
return Manifest(
|
||||||
|
manifest_file=manifest_file,
|
||||||
|
name=manifest['name'],
|
||||||
|
partition_id=assert_int(manifest['id']),
|
||||||
|
partition_type=manifest['type'],
|
||||||
|
priority=manifest['priority'],
|
||||||
|
heap_size=assert_int(manifest['heap_size']),
|
||||||
|
stack_size=assert_int(manifest['stack_size']),
|
||||||
|
entry_point=manifest['entry_point'],
|
||||||
|
source_files=source_files,
|
||||||
|
mmio_regions=mmio_regions,
|
||||||
|
rot_services=rot_services,
|
||||||
|
extern_sids=manifest.get('extern_sids', []),
|
||||||
|
irqs=irqs
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sids(self):
|
||||||
|
return [rot_srv.name for rot_srv in self.rot_services]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def autogen_folder(self):
|
||||||
|
return os.path.abspath(os.path.dirname(self.file))
|
||||||
|
|
||||||
|
def find_dependencies(self, manifests):
|
||||||
|
"""
|
||||||
|
Find other manifests which holds Root of Trust Services that
|
||||||
|
are declared as extern in this manifest
|
||||||
|
|
||||||
|
:param manifests: list of manifests to filter
|
||||||
|
:return: list of manifest's names that holds current
|
||||||
|
extern Root of Trust Services
|
||||||
|
"""
|
||||||
|
|
||||||
|
manifests = [man for man in manifests if man != self]
|
||||||
|
extern_sids_set = set(self.extern_sids)
|
||||||
|
return [manifest.name for manifest in manifests
|
||||||
|
if extern_sids_set.intersection(set(manifest.sids))]
|
||||||
|
|
||||||
|
def templates_to_files(self, templates, templates_base, output_dir):
|
||||||
|
"""
|
||||||
|
Translates a list of partition templates to file names
|
||||||
|
|
||||||
|
:param templates: List of partition templates
|
||||||
|
:param output_dir: Output directory (Default is autogen folder property)
|
||||||
|
:return: Dictionary of template to output file translation
|
||||||
|
"""
|
||||||
|
|
||||||
|
generated_files = {}
|
||||||
|
for t in templates:
|
||||||
|
fname = os.path.relpath(t, templates_base)
|
||||||
|
_tpl = fname.replace('NAME', self.name.lower())
|
||||||
|
full_path = path_join(
|
||||||
|
output_dir,
|
||||||
|
os.path.splitext(_tpl)[0]
|
||||||
|
)
|
||||||
|
generated_files[t] = full_path
|
||||||
|
|
||||||
|
return generated_files
|
||||||
|
|
||||||
|
|
||||||
|
def check_circular_call_dependencies(manifests):
|
||||||
|
"""
|
||||||
|
Check if there is a circular dependency between the partitions
|
||||||
|
described by the manifests.
|
||||||
|
A circular dependency might happen if there is a scenario in which a
|
||||||
|
partition calls a Root of Trust Service in another partition which than
|
||||||
|
calls another Root of Trust Service which resides in the
|
||||||
|
originating partition.
|
||||||
|
For example: Partition A has a Root of Trust Service A1 and extern sid B1,
|
||||||
|
partition B has a Root of Trust Service B1 and extern sid A1.
|
||||||
|
|
||||||
|
:param manifests: List of the partition manifests.
|
||||||
|
:return: True if a circular dependency exists, false otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Construct a call graph.
|
||||||
|
call_graph = {}
|
||||||
|
for manifest in manifests:
|
||||||
|
call_graph[manifest.name] = {
|
||||||
|
'calls': manifest.find_dependencies(manifests),
|
||||||
|
'called_by': set()
|
||||||
|
}
|
||||||
|
for manifest_name in call_graph:
|
||||||
|
for called in call_graph[manifest_name]['calls']:
|
||||||
|
call_graph[called]['called_by'].add(manifest_name)
|
||||||
|
|
||||||
|
# Run topological sort on the call graph.
|
||||||
|
while len(call_graph) > 0:
|
||||||
|
# Find all the nodes that aren't called by anyone and
|
||||||
|
# therefore can be removed.
|
||||||
|
nodes_to_remove = [x for x in list(call_graph.keys()) if
|
||||||
|
len(call_graph[x]['called_by']) == 0]
|
||||||
|
|
||||||
|
# If no node can be removed we have a circle.
|
||||||
|
if not nodes_to_remove:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Remove the nodes.
|
||||||
|
for node in nodes_to_remove:
|
||||||
|
for called in call_graph[node]['calls']:
|
||||||
|
call_graph[called]['called_by'].remove(node)
|
||||||
|
call_graph.pop(node)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_partition_manifests(manifests):
|
||||||
|
"""
|
||||||
|
Check the correctness of the manifests list
|
||||||
|
(no conflicts, no missing elements, etc.)
|
||||||
|
|
||||||
|
:param manifests: List of the partition manifests
|
||||||
|
"""
|
||||||
|
for manifest in manifests:
|
||||||
|
assert isinstance(manifest, Manifest)
|
||||||
|
|
||||||
|
partitions_names = {}
|
||||||
|
partitions_ids = {}
|
||||||
|
rot_service_ids = {}
|
||||||
|
rot_service_names = {}
|
||||||
|
rot_service_signals = {}
|
||||||
|
irq_signals = {}
|
||||||
|
irq_numbers = {}
|
||||||
|
all_extern_sids = set()
|
||||||
|
spe_contained_manifests = []
|
||||||
|
|
||||||
|
for manifest in manifests:
|
||||||
|
# Make sure the partition names are unique.
|
||||||
|
if manifest.name in partitions_names:
|
||||||
|
raise ValueError(
|
||||||
|
'Partition name {} is not unique, '
|
||||||
|
'found in both {} and {}.'.format(
|
||||||
|
manifest.name,
|
||||||
|
partitions_names[manifest.name],
|
||||||
|
manifest.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
partitions_names[manifest.name] = manifest.file
|
||||||
|
|
||||||
|
# Make sure the partition ID's are unique.
|
||||||
|
if manifest.id in partitions_ids:
|
||||||
|
raise ValueError(
|
||||||
|
'Partition id {} is not unique, '
|
||||||
|
'found in both {} and {}.'.format(
|
||||||
|
manifest.id,
|
||||||
|
partitions_ids[manifest.id],
|
||||||
|
manifest.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
partitions_ids[manifest.id] = manifest.file
|
||||||
|
|
||||||
|
is_nspe_callabale = False
|
||||||
|
|
||||||
|
# Make sure all the Root of Trust Service IDs and signals are unique.
|
||||||
|
for rot_service in manifest.rot_services:
|
||||||
|
if rot_service.name in rot_service_names:
|
||||||
|
raise ValueError(
|
||||||
|
'Root of Trust Service name {} is found '
|
||||||
|
'in both {} and {}.'.format(
|
||||||
|
rot_service.name,
|
||||||
|
rot_service_names[rot_service.name],
|
||||||
|
manifest.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rot_service_names[rot_service.name] = manifest.file
|
||||||
|
|
||||||
|
if rot_service.signal in rot_service_signals:
|
||||||
|
raise ValueError(
|
||||||
|
'Root of Trust Service signal {} is found '
|
||||||
|
'in both {} and {}.'.format(
|
||||||
|
rot_service.signal,
|
||||||
|
rot_service_signals[rot_service.signal],
|
||||||
|
manifest.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rot_service_signals[rot_service.signal] = manifest.file
|
||||||
|
|
||||||
|
if rot_service.numeric_id in rot_service_ids:
|
||||||
|
raise ValueError(
|
||||||
|
'Root of Trust Service identifier {} is found '
|
||||||
|
'in both {} and {}.'.format(
|
||||||
|
rot_service.numeric_id,
|
||||||
|
rot_service_ids[rot_service.numeric_id],
|
||||||
|
manifest.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rot_service_ids[rot_service.numeric_id] = manifest.file
|
||||||
|
is_nspe_callabale |= rot_service.nspe_callable
|
||||||
|
|
||||||
|
if not is_nspe_callabale:
|
||||||
|
spe_contained_manifests.append(manifest)
|
||||||
|
|
||||||
|
# Make sure all the IRQ signals and line-numbers are unique.
|
||||||
|
for irq in manifest.irqs:
|
||||||
|
if irq.signal in irq_signals:
|
||||||
|
raise ValueError(
|
||||||
|
'IRQ signal {} is found in both {} and {}.'.format(
|
||||||
|
irq.signal,
|
||||||
|
irq_signals[irq.signal],
|
||||||
|
manifest.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
irq_signals[irq.signal] = manifest.file
|
||||||
|
|
||||||
|
if irq.line_num in irq_numbers:
|
||||||
|
raise ValueError(
|
||||||
|
'IRQ line number {} is found in both {} and {}.'.format(
|
||||||
|
irq.line_num,
|
||||||
|
irq_numbers[irq.line_num],
|
||||||
|
manifest.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
irq_numbers[irq.line_num] = manifest.file
|
||||||
|
|
||||||
|
all_extern_sids.update(manifest.extern_sids)
|
||||||
|
|
||||||
|
# Check that all the external SIDs can be found.
|
||||||
|
declared_sids = set(rot_service_names.keys())
|
||||||
|
for manifest in manifests:
|
||||||
|
extern_sids = set(manifest.extern_sids)
|
||||||
|
if not extern_sids.issubset(declared_sids):
|
||||||
|
missing_sids = extern_sids.difference(declared_sids)
|
||||||
|
raise ValueError(
|
||||||
|
"External SID(s) {} required by {} can't be found in "
|
||||||
|
"any partition manifest.".format(
|
||||||
|
', '.join(missing_sids), manifest.file)
|
||||||
|
)
|
||||||
|
|
||||||
|
if check_circular_call_dependencies(manifests):
|
||||||
|
raise ValueError(
|
||||||
|
"Detected a circular call dependency between the partitions.")
|
||||||
|
|
||||||
|
for manifest in spe_contained_manifests:
|
||||||
|
rot_services = set([service.name for service in manifest.rot_services])
|
||||||
|
if not rot_services.intersection(all_extern_sids) and len(
|
||||||
|
manifest.irqs) == 0:
|
||||||
|
raise ValueError(
|
||||||
|
'Partition {} (defined by {}) is not accessible from NSPE '
|
||||||
|
'and not referenced by any other partition.'.format(
|
||||||
|
manifest.name,
|
||||||
|
manifest.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_source_files(
|
||||||
|
templates_dict,
|
||||||
|
render_args,
|
||||||
|
extra_filters=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generate SPM common C code from manifests using given templates
|
||||||
|
|
||||||
|
:param templates: Dictionary of template and their auto-generated products
|
||||||
|
:param render_args: Dictionary of arguments that should be passed to render
|
||||||
|
:param extra_filters: Dictionary of extra filters to use in the rendering
|
||||||
|
process
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Load templates for the code generation.
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(MBED_OS_ROOT),
|
||||||
|
lstrip_blocks=True,
|
||||||
|
trim_blocks=True,
|
||||||
|
undefined=StrictUndefined
|
||||||
|
)
|
||||||
|
|
||||||
|
if extra_filters:
|
||||||
|
env.filters.update(extra_filters)
|
||||||
|
|
||||||
|
for tpl in templates_dict:
|
||||||
|
template = env.get_template(tpl['template'])
|
||||||
|
data = template.render(**render_args)
|
||||||
|
output_path = os.path.join(MBED_OS_ROOT, tpl['output'])
|
||||||
|
output_folder = os.path.dirname(output_path)
|
||||||
|
|
||||||
|
if not os.path.exists(output_folder):
|
||||||
|
os.makedirs(output_folder)
|
||||||
|
|
||||||
|
with open(output_path, 'wt') as fh:
|
||||||
|
fh.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_tfm_setup(manifest_files, extra_filters=None):
|
||||||
|
"""
|
||||||
|
Process all the given manifest files and generate C setup code from them
|
||||||
|
:param manifest_files: List of manifest files
|
||||||
|
:param output_dir: Output directory for the generated files
|
||||||
|
:param weak_setup: Is the functions/data in the setup file weak
|
||||||
|
(can be overridden by another setup file)
|
||||||
|
:param extra_filters: Dictionary of extra filters to use in the rendering
|
||||||
|
process
|
||||||
|
:return: path to the setup generated files
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Construct lists of all the manifests and mmio_regions.
|
||||||
|
region_list = []
|
||||||
|
manifests = []
|
||||||
|
for manifest_file in manifest_files:
|
||||||
|
manifest_obj = Manifest.from_json(manifest_file)
|
||||||
|
manifests.append(manifest_obj)
|
||||||
|
for region in manifest_obj.mmio_regions:
|
||||||
|
region_list.append(region)
|
||||||
|
|
||||||
|
# Validate the correctness of the manifest collection.
|
||||||
|
validate_partition_manifests(manifests)
|
||||||
|
|
||||||
|
with open(TEMPLATES_LIST_FILE, 'r') as fh:
|
||||||
|
templates_data = json.load(fh)
|
||||||
|
|
||||||
|
render_args = {
|
||||||
|
'partitions': manifests,
|
||||||
|
}
|
||||||
|
|
||||||
|
return generate_source_files(
|
||||||
|
templates_data,
|
||||||
|
render_args,
|
||||||
|
extra_filters=extra_filters
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def manifests_discovery(root_dir):
|
||||||
|
manifest_files = set()
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(root_dir):
|
||||||
|
to_add = [path_join(root, f) for f in
|
||||||
|
fnmatch.filter(files, MANIFEST_FILE_PATTERN) if
|
||||||
|
'TARGET_IGNORE' not in root]
|
||||||
|
manifest_files.update(to_add)
|
||||||
|
|
||||||
|
return list(manifest_files)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_tfm_code():
|
||||||
|
# Find all manifest files in the mbed-os tree
|
||||||
|
manifest_files = manifests_discovery(SERVICES_DIR)
|
||||||
|
|
||||||
|
# Generate default system psa setup file (only system partitions)
|
||||||
|
generate_tfm_setup(manifest_files)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
generate_tfm_code()
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"description": "schema for a partition description.",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "type", "priority", "id", "entry_point", "stack_size", "heap_size", "source_files"],
|
||||||
|
"anyOf": [
|
||||||
|
{"required" : ["services"]},
|
||||||
|
{"required" : ["irqs"]}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"description": "Alphanumeric C macro for referring to a partition. (all capital)",
|
||||||
|
"$ref": "#/definitions/c_macro"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "Whether the partition is unprivileged or part of the trusted computing base.",
|
||||||
|
"enum": ["APPLICATION-ROT", "PSA-ROT"]
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"description": "Partition task priority.",
|
||||||
|
"enum": ["LOW", "NORMAL", "HIGH"]
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "Partition numeric unique positive identifier. (must be a positive 8 bytes hex string)",
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^0x[0-7][0-9a-fA-F]{7}$"
|
||||||
|
},
|
||||||
|
"entry_point": {
|
||||||
|
"description": "C symbol name of the partition's entry point. (unmangled, use extern C if needed)",
|
||||||
|
"$ref": "#/definitions/c_symbol"
|
||||||
|
},
|
||||||
|
"stack_size": {
|
||||||
|
"description": "Partition's task stack size in bytes.",
|
||||||
|
"$ref": "#/definitions/positive_integer_or_hex_string"
|
||||||
|
},
|
||||||
|
"heap_size": {
|
||||||
|
"description": "Partition's task heap size in bytes.",
|
||||||
|
"$ref": "#/definitions/positive_integer_or_hex_string"
|
||||||
|
},
|
||||||
|
"mmio_regions": {
|
||||||
|
"description": "List of Memory-Mapped IO region objects which the partition has access to.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [{
|
||||||
|
"$ref": "#/definitions/named_region"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/numbered_region"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"description": "List of RoT Service objects which the partition implements.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/service"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"extern_sids": {
|
||||||
|
"description": "List of SID which the partition code depends on and allowed to access.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/c_macro"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"source_files": {
|
||||||
|
"description": "List of source files relative to PSA Manifest file. A Secure Partition is built from explicit file list.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z0-9-_./]+$"
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"irqs": {
|
||||||
|
"description": "List of IRQ objects which the partition implements.",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/irq"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"c_macro": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[A-Z_][A-Z0-9_]*$"
|
||||||
|
},
|
||||||
|
"c_symbol": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
|
||||||
|
},
|
||||||
|
"hex_string": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^0x(0*[1-9a-fA-F][0-9a-fA-F]*)$",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 10
|
||||||
|
},
|
||||||
|
"positive_integer": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": true,
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"positive_integer_or_hex_string": {
|
||||||
|
"oneOf": [{
|
||||||
|
"$ref": "#/definitions/positive_integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/hex_string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"named_region": {
|
||||||
|
"description": "MMIO region which is described by it's C macro name and access permissions.",
|
||||||
|
"required": ["name", "permission"],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"description": "Alphanumeric C macro for referring to the region.",
|
||||||
|
"$ref": "#/definitions/c_macro"
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"description": "Access permissions for the region.",
|
||||||
|
"enum": ["READ-ONLY", "READ-WRITE"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"numbered_region": {
|
||||||
|
"description": "MMIO region which is described by it's base address, size and access permissions.",
|
||||||
|
"required": ["base", "size", "permission"],
|
||||||
|
"properties": {
|
||||||
|
"base": {
|
||||||
|
"description": "The base address of the region.",
|
||||||
|
"$ref": "#/definitions/hex_string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"description": "Size in bytes of the region.",
|
||||||
|
"$ref": "#/definitions/positive_integer_or_hex_string"
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"description": "Access permissions for the region.",
|
||||||
|
"enum": ["READ-ONLY", "READ-WRITE"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"service": {
|
||||||
|
"required": ["name", "identifier", "non_secure_clients", "signal"],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"description": "Alphanumeric C macro for referring to a RoT Service from source code (all capital)",
|
||||||
|
"$ref": "#/definitions/c_macro"
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"description": "The integer value of the NAME field",
|
||||||
|
"$ref": "#/definitions/positive_integer_or_hex_string"
|
||||||
|
},
|
||||||
|
"non_secure_clients": {
|
||||||
|
"description": "Denote whether the RoT Service is exposed to non-secure clients.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"signal": {
|
||||||
|
"description": "Alphanumeric C macro for referring to the RoT Service's signal value. (all capital)",
|
||||||
|
"$ref": "#/definitions/c_macro"
|
||||||
|
},
|
||||||
|
"minor_version": {
|
||||||
|
"description": "Optional: Minor version number of the RoT Service's interface.",
|
||||||
|
"$ref": "#/definitions/positive_integer",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"minor_policy": {
|
||||||
|
"description": "Optional: Minor version policy to apply on connections to the RoT Service.",
|
||||||
|
"enum": ["STRICT", "RELAXED"],
|
||||||
|
"default": "STRICT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"irq": {
|
||||||
|
"required": ["line_num", "signal"],
|
||||||
|
"properties": {
|
||||||
|
"line_num": {
|
||||||
|
"description": "Interrupt line number for registering to ISR table entry and enable/disable the specific IRQ once received.",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"signal": {
|
||||||
|
"description": "Alphanumeric C macro for referring to the IRQ's signal value. (all capital)",
|
||||||
|
"$ref": "#/definitions/c_macro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* Copyright (c) 2017-2019 ARM Limited
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*********** WARNING: This is an auto-generated file. Do not edit! ***********/
|
||||||
|
|
||||||
|
#ifndef __TFM_PARTITION_DEFS_INC__
|
||||||
|
#define __TFM_PARTITION_DEFS_INC__
|
||||||
|
|
||||||
|
{% for partition in partitions %}
|
||||||
|
{% set partition_loop = loop %}
|
||||||
|
#ifdef TFM_PSA_API
|
||||||
|
#define {{partition.name|upper}}_ID (TFM_SP_BASE + {{ partition_loop.index0 }})
|
||||||
|
#endif
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
#define TFM_MAX_USER_PARTITIONS ({{partitions|count}})
|
||||||
|
|
||||||
|
#endif /* __TFM_PARTITION_DEFS_INC__ */
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, Arm Limited. All rights reserved.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*********** WARNING: This is an auto-generated file. Do not edit! ***********/
|
||||||
|
|
||||||
|
#ifndef __TFM_PARTITION_LIST_INC__
|
||||||
|
#define __TFM_PARTITION_LIST_INC__
|
||||||
|
|
||||||
|
{% for partition in partitions %}
|
||||||
|
#ifdef TFM_PSA_API
|
||||||
|
/******** {{partition.name|upper}} ********/
|
||||||
|
PARTITION_DECLARE({{partition.name|upper}}, 0
|
||||||
|
| SPM_PART_FLAG_IPC
|
||||||
|
, "{{partition.type}}", {{partition.id}}, {{partition.priority}}, {{partition.stack_size}});
|
||||||
|
PARTITION_ADD_INIT_FUNC({{partition.name|upper}}, {{partition.entry_point}});
|
||||||
|
#endif /* TFM_PSA_API */
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
#endif /* __TFM_PARTITION_LIST_INC__ */
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018-2019, Arm Limited. All rights reserved.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*********** WARNING: This is an auto-generated file. Do not edit! ***********/
|
||||||
|
|
||||||
|
#ifndef __TFM_SERVICE_LIST_INC__
|
||||||
|
#define __TFM_SERVICE_LIST_INC__
|
||||||
|
|
||||||
|
{% for partition in partitions %}
|
||||||
|
#ifdef TFM_PSA_API
|
||||||
|
/******** {{partition.name|upper}} ********/
|
||||||
|
{% for rot_srv in partition.rot_services %}
|
||||||
|
{"{{rot_srv.name|upper}}", {{partition.name|upper}}_ID, {{rot_srv.signal|upper}}, {{rot_srv.id}}, {% if rot_srv.nspe_callable %}true{% else %}false{% endif %}, {{rot_srv.minor_version}}, TFM_VERSION_POLICY_{{rot_srv.minor_policy|upper}}},
|
||||||
|
{% endfor %}
|
||||||
|
#endif /* TFM_PSA_API */
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
#endif /* __TFM_SERVICE_LIST_INC__ */
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018, Arm Limited. All rights reserved.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#ifndef __TFM_SPM_SIGNAL_DEFS_H__
|
||||||
|
#define __TFM_SPM_SIGNAL_DEFS_H__
|
||||||
|
|
||||||
|
{% for partition in partitions %}
|
||||||
|
{% for rot_srv in partition.rot_services %}
|
||||||
|
#define {{rot_srv.signal|upper}}_POS ({{loop.index + 3}}UL)
|
||||||
|
#define {{rot_srv.signal|upper}} (1UL << {{rot_srv.signal|upper}}_POS)
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Secure Partition ID definitions",
|
||||||
|
"template": "tools/tfm/templates/tfm_partition_defs.inc.tpl",
|
||||||
|
"output": "components/TARGET_PSA/TARGET_TFM/COMPONENT_SPE/autogen/tfm_partition_defs.inc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Secure Partition declarations",
|
||||||
|
"template": "tools/tfm/templates/tfm_partition_list.inc.tpl",
|
||||||
|
"output": "components/TARGET_PSA/TARGET_TFM/COMPONENT_SPE/autogen/tfm_partition_list.inc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Secure Service list",
|
||||||
|
"template": "tools/tfm/templates/tfm_service_list.inc.tpl",
|
||||||
|
"output": "components/TARGET_PSA/TARGET_TFM/COMPONENT_SPE/autogen/tfm_service_list.inc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Secure Service siganls list",
|
||||||
|
"template": "tools/tfm/templates/tfm_spm_signal_defs.h.tpl",
|
||||||
|
"output": "components/TARGET_PSA/TARGET_TFM/COMPONENT_SPE/autogen/tfm_spm_signal_defs.h"
|
||||||
|
}
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue