u-boot/tools/binman/fip_util.py
Simon Glass 4583c00236 patman: Move library functions into a library directory
The patman directory has a number of modules which are used by other tools
in U-Boot. This makes it hard to package the tools using pypi since the
common files must be copied along with the tool that uses them.

To address this, move these files into a new u_boot_pylib library. This
can be packaged separately and listed as a dependency of each tool.

Signed-off-by: Simon Glass <sjg@chromium.org>
2023-03-08 11:40:49 -08:00

628 lines
22 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0+
# Copyright 2021 Google LLC
# Written by Simon Glass <sjg@chromium.org>
"""Support for ARM's Firmware Image Package (FIP) format
FIP is a format similar to FMAP[1] but with fewer features and an obscure UUID
instead of the region name.
It consists of a header and a table of entries, each pointing to a place in the
firmware image where something can be found.
[1] https://chromium.googlesource.com/chromiumos/third_party/flashmap/+/refs/heads/master/lib/fmap.h
If ATF updates, run this program to update the FIT_TYPE_LIST.
ARM Trusted Firmware is available at:
https://github.com/ARM-software/arm-trusted-firmware.git
"""
from argparse import ArgumentParser
import collections
import io
import os
import re
import struct
import sys
from uuid import UUID
OUR_FILE = os.path.realpath(__file__)
OUR_PATH = os.path.dirname(OUR_FILE)
# Bring in the patman and dtoc libraries (but don't override the first path
# in PYTHONPATH)
sys.path.insert(2, os.path.join(OUR_PATH, '..'))
# pylint: disable=C0413
from u_boot_pylib import command
from u_boot_pylib import tools
# The TOC header, at the start of the FIP
HEADER_FORMAT = '<IIQ'
HEADER_LEN = 0x10
HEADER_MAGIC = 0xaA640001
HEADER_SERIAL = 0x12345678
# The entry header (a table of these comes after the TOC header)
UUID_LEN = 16
ENTRY_FORMAT = f'<{UUID_LEN}sQQQ'
ENTRY_SIZE = 0x28
HEADER_NAMES = (
'name',
'serial',
'flags',
)
ENTRY_NAMES = (
'uuid',
'offset',
'size',
'flags',
)
# Set to True to enable output from running fiptool for debugging
VERBOSE = False
# Use a class so we can convert the bytes, making the table more readable
# pylint: disable=R0903
class FipType:
"""A FIP entry type that we understand"""
def __init__(self, name, desc, uuid_bytes):
"""Create up a new type
Args:
name (str): Short name for the type
desc (str): Longer description for the type
uuid_bytes (bytes): List of 16 bytes for the UUID
"""
self.name = name
self.desc = desc
self.uuid = bytes(uuid_bytes)
# This is taken from tbbr_config.c in ARM Trusted Firmware
FIP_TYPE_LIST = [
# ToC Entry UUIDs
FipType('scp-fwu-cfg', 'SCP Firmware Updater Configuration FWU SCP_BL2U',
[0x65, 0x92, 0x27, 0x03, 0x2f, 0x74, 0xe6, 0x44,
0x8d, 0xff, 0x57, 0x9a, 0xc1, 0xff, 0x06, 0x10]),
FipType('ap-fwu-cfg', 'AP Firmware Updater Configuration BL2U',
[0x60, 0xb3, 0xeb, 0x37, 0xc1, 0xe5, 0xea, 0x41,
0x9d, 0xf3, 0x19, 0xed, 0xa1, 0x1f, 0x68, 0x01]),
FipType('fwu', 'Firmware Updater NS_BL2U',
[0x4f, 0x51, 0x1d, 0x11, 0x2b, 0xe5, 0x4e, 0x49,
0xb4, 0xc5, 0x83, 0xc2, 0xf7, 0x15, 0x84, 0x0a]),
FipType('fwu-cert', 'Non-Trusted Firmware Updater certificate',
[0x71, 0x40, 0x8a, 0xb2, 0x18, 0xd6, 0x87, 0x4c,
0x8b, 0x2e, 0xc6, 0xdc, 0xcd, 0x50, 0xf0, 0x96]),
FipType('tb-fw', 'Trusted Boot Firmware BL2',
[0x5f, 0xf9, 0xec, 0x0b, 0x4d, 0x22, 0x3e, 0x4d,
0xa5, 0x44, 0xc3, 0x9d, 0x81, 0xc7, 0x3f, 0x0a]),
FipType('scp-fw', 'SCP Firmware SCP_BL2',
[0x97, 0x66, 0xfd, 0x3d, 0x89, 0xbe, 0xe8, 0x49,
0xae, 0x5d, 0x78, 0xa1, 0x40, 0x60, 0x82, 0x13]),
FipType('soc-fw', 'EL3 Runtime Firmware BL31',
[0x47, 0xd4, 0x08, 0x6d, 0x4c, 0xfe, 0x98, 0x46,
0x9b, 0x95, 0x29, 0x50, 0xcb, 0xbd, 0x5a, 0x00]),
FipType('tos-fw', 'Secure Payload BL32 (Trusted OS)',
[0x05, 0xd0, 0xe1, 0x89, 0x53, 0xdc, 0x13, 0x47,
0x8d, 0x2b, 0x50, 0x0a, 0x4b, 0x7a, 0x3e, 0x38]),
FipType('tos-fw-extra1', 'Secure Payload BL32 Extra1 (Trusted OS Extra1)',
[0x0b, 0x70, 0xc2, 0x9b, 0x2a, 0x5a, 0x78, 0x40,
0x9f, 0x65, 0x0a, 0x56, 0x82, 0x73, 0x82, 0x88]),
FipType('tos-fw-extra2', 'Secure Payload BL32 Extra2 (Trusted OS Extra2)',
[0x8e, 0xa8, 0x7b, 0xb1, 0xcf, 0xa2, 0x3f, 0x4d,
0x85, 0xfd, 0xe7, 0xbb, 0xa5, 0x02, 0x20, 0xd9]),
FipType('nt-fw', 'Non-Trusted Firmware BL33',
[0xd6, 0xd0, 0xee, 0xa7, 0xfc, 0xea, 0xd5, 0x4b,
0x97, 0x82, 0x99, 0x34, 0xf2, 0x34, 0xb6, 0xe4]),
FipType('rmm-fw', 'Realm Monitor Management Firmware',
[0x6c, 0x07, 0x62, 0xa6, 0x12, 0xf2, 0x4b, 0x56,
0x92, 0xcb, 0xba, 0x8f, 0x63, 0x36, 0x06, 0xd9]),
# Key certificates
FipType('rot-cert', 'Root Of Trust key certificate',
[0x86, 0x2d, 0x1d, 0x72, 0xf8, 0x60, 0xe4, 0x11,
0x92, 0x0b, 0x8b, 0xe7, 0x62, 0x16, 0x0f, 0x24]),
FipType('trusted-key-cert', 'Trusted key certificate',
[0x82, 0x7e, 0xe8, 0x90, 0xf8, 0x60, 0xe4, 0x11,
0xa1, 0xb4, 0x77, 0x7a, 0x21, 0xb4, 0xf9, 0x4c]),
FipType('scp-fw-key-cert', 'SCP Firmware key certificate',
[0x02, 0x42, 0x21, 0xa1, 0xf8, 0x60, 0xe4, 0x11,
0x8d, 0x9b, 0xf3, 0x3c, 0x0e, 0x15, 0xa0, 0x14]),
FipType('soc-fw-key-cert', 'SoC Firmware key certificate',
[0x8a, 0xb8, 0xbe, 0xcc, 0xf9, 0x60, 0xe4, 0x11,
0x9a, 0xd0, 0xeb, 0x48, 0x22, 0xd8, 0xdc, 0xf8]),
FipType('tos-fw-key-cert', 'Trusted OS Firmware key certificate',
[0x94, 0x77, 0xd6, 0x03, 0xfb, 0x60, 0xe4, 0x11,
0x85, 0xdd, 0xb7, 0x10, 0x5b, 0x8c, 0xee, 0x04]),
FipType('nt-fw-key-cert', 'Non-Trusted Firmware key certificate',
[0x8a, 0xd5, 0x83, 0x2a, 0xfb, 0x60, 0xe4, 0x11,
0x8a, 0xaf, 0xdf, 0x30, 0xbb, 0xc4, 0x98, 0x59]),
# Content certificates
FipType('tb-fw-cert', 'Trusted Boot Firmware BL2 certificate',
[0xd6, 0xe2, 0x69, 0xea, 0x5d, 0x63, 0xe4, 0x11,
0x8d, 0x8c, 0x9f, 0xba, 0xbe, 0x99, 0x56, 0xa5]),
FipType('scp-fw-cert', 'SCP Firmware content certificate',
[0x44, 0xbe, 0x6f, 0x04, 0x5e, 0x63, 0xe4, 0x11,
0xb2, 0x8b, 0x73, 0xd8, 0xea, 0xae, 0x96, 0x56]),
FipType('soc-fw-cert', 'SoC Firmware content certificate',
[0xe2, 0xb2, 0x0c, 0x20, 0x5e, 0x63, 0xe4, 0x11,
0x9c, 0xe8, 0xab, 0xcc, 0xf9, 0x2b, 0xb6, 0x66]),
FipType('tos-fw-cert', 'Trusted OS Firmware content certificate',
[0xa4, 0x9f, 0x44, 0x11, 0x5e, 0x63, 0xe4, 0x11,
0x87, 0x28, 0x3f, 0x05, 0x72, 0x2a, 0xf3, 0x3d]),
FipType('nt-fw-cert', 'Non-Trusted Firmware content certificate',
[0x8e, 0xc4, 0xc1, 0xf3, 0x5d, 0x63, 0xe4, 0x11,
0xa7, 0xa9, 0x87, 0xee, 0x40, 0xb2, 0x3f, 0xa7]),
FipType('sip-sp-cert', 'SiP owned Secure Partition content certificate',
[0x77, 0x6d, 0xfd, 0x44, 0x86, 0x97, 0x4c, 0x3b,
0x91, 0xeb, 0xc1, 0x3e, 0x02, 0x5a, 0x2a, 0x6f]),
FipType('plat-sp-cert', 'Platform owned Secure Partition content certificate',
[0xdd, 0xcb, 0xbf, 0x4a, 0xca, 0xd6, 0x11, 0xea,
0x87, 0xd0, 0x02, 0x42, 0xac, 0x13, 0x00, 0x03]),
# Dynamic configs
FipType('hw-config', 'HW_CONFIG',
[0x08, 0xb8, 0xf1, 0xd9, 0xc9, 0xcf, 0x93, 0x49,
0xa9, 0x62, 0x6f, 0xbc, 0x6b, 0x72, 0x65, 0xcc]),
FipType('tb-fw-config', 'TB_FW_CONFIG',
[0x6c, 0x04, 0x58, 0xff, 0xaf, 0x6b, 0x7d, 0x4f,
0x82, 0xed, 0xaa, 0x27, 0xbc, 0x69, 0xbf, 0xd2]),
FipType('soc-fw-config', 'SOC_FW_CONFIG',
[0x99, 0x79, 0x81, 0x4b, 0x03, 0x76, 0xfb, 0x46,
0x8c, 0x8e, 0x8d, 0x26, 0x7f, 0x78, 0x59, 0xe0]),
FipType('tos-fw-config', 'TOS_FW_CONFIG',
[0x26, 0x25, 0x7c, 0x1a, 0xdb, 0xc6, 0x7f, 0x47,
0x8d, 0x96, 0xc4, 0xc4, 0xb0, 0x24, 0x80, 0x21]),
FipType('nt-fw-config', 'NT_FW_CONFIG',
[0x28, 0xda, 0x98, 0x15, 0x93, 0xe8, 0x7e, 0x44,
0xac, 0x66, 0x1a, 0xaf, 0x80, 0x15, 0x50, 0xf9]),
FipType('fw-config', 'FW_CONFIG',
[0x58, 0x07, 0xe1, 0x6a, 0x84, 0x59, 0x47, 0xbe,
0x8e, 0xd5, 0x64, 0x8e, 0x8d, 0xdd, 0xab, 0x0e]),
] # end
FIP_TYPES = {ftype.name: ftype for ftype in FIP_TYPE_LIST}
def get_type_uuid(fip_type_or_uuid):
"""get_type_uuid() - Convert a type or uuid into both
This always returns a UUID, but may not return a type since it does not do
the reverse lookup.
Args:
fip_type_or_uuid (str or bytes): Either a string containing the name of
an entry (e.g. 'soc-fw') or a bytes(16) containing the UUID
Returns:
tuple:
str: fip type (None if not known)
bytes(16): uuid
Raises:
ValueError: An unknown type was requested
"""
if isinstance(fip_type_or_uuid, str):
fip_type = fip_type_or_uuid
lookup = FIP_TYPES.get(fip_type)
if not lookup:
raise ValueError(f"Unknown FIP entry type '{fip_type}'")
uuid = lookup.uuid
else:
fip_type = None
uuid = fip_type_or_uuid
return fip_type, uuid
# pylint: disable=R0903
class FipHeader:
"""Class to represent a FIP header"""
def __init__(self, name, serial, flags):
"""Set up a new header object
Args:
name (str): Name, i.e. HEADER_MAGIC
serial (str): Serial value, i.e. HEADER_SERIAL
flags (int64): Flags value
"""
self.name = name
self.serial = serial
self.flags = flags
# pylint: disable=R0903
class FipEntry:
"""Class to represent a single FIP entry
This is used to hold the information about an entry, including its contents.
Use the get_data() method to obtain the raw output for writing to the FIP
file.
"""
def __init__(self, uuid, offset, size, flags):
self.uuid = uuid
self.offset = offset
self.size = size
self.flags = flags
self.fip_type = None
self.data = None
self.valid = uuid != tools.get_bytes(0, UUID_LEN)
if self.valid:
# Look up the friendly name
matches = {val for (key, val) in FIP_TYPES.items()
if val.uuid == uuid}
if len(matches) == 1:
self.fip_type = matches.pop().name
@classmethod
def from_type(cls, fip_type_or_uuid, data, flags):
"""Create a FipEntry from a type name
Args:
cls (class): This class
fip_type_or_uuid (str or bytes): Name of the type to create, or
bytes(16) uuid
data (bytes): Contents of entry
flags (int64): Flags value
Returns:
FipEntry: Created 241
"""
fip_type, uuid = get_type_uuid(fip_type_or_uuid)
fent = FipEntry(uuid, None, len(data), flags)
fent.fip_type = fip_type
fent.data = data
return fent
def decode_fip(data):
"""Decode a FIP into a header and list of FIP entries
Args:
data (bytes): Data block containing the FMAP
Returns:
Tuple:
header: FipHeader object
List of FipArea objects
"""
fields = list(struct.unpack(HEADER_FORMAT, data[:HEADER_LEN]))
header = FipHeader(*fields)
fents = []
pos = HEADER_LEN
while True:
fields = list(struct.unpack(ENTRY_FORMAT, data[pos:pos + ENTRY_SIZE]))
fent = FipEntry(*fields)
if not fent.valid:
break
fent.data = data[fent.offset:fent.offset + fent.size]
fents.append(fent)
pos += ENTRY_SIZE
return header, fents
class FipWriter:
"""Class to handle writing a ARM Trusted Firmware's Firmware Image Package
Usage is something like:
fip = FipWriter(size)
fip.add_entry('scp-fwu-cfg', tools.read_file('something.bin'))
...
data = cbw.get_data()
Attributes:
"""
def __init__(self, flags, align):
self._fip_entries = []
self._flags = flags
self._align = align
def add_entry(self, fip_type, data, flags):
"""Add a new entry to the FIP
Args:
fip_type (str): Type to add, e.g. 'tos-fw-config'
data (bytes): Contents of entry
flags (int64): Entry flags
Returns:
FipEntry: entry that was added
"""
fent = FipEntry.from_type(fip_type, data, flags)
self._fip_entries.append(fent)
return fent
def get_data(self):
"""Obtain the full contents of the FIP
Thhis builds the FIP with headers and all required FIP entries.
Returns:
bytes: data resulting from building the FIP
"""
buf = io.BytesIO()
hdr = struct.pack(HEADER_FORMAT, HEADER_MAGIC, HEADER_SERIAL,
self._flags)
buf.write(hdr)
# Calculate the position fo the first entry
offset = len(hdr)
offset += len(self._fip_entries) * ENTRY_SIZE
offset += ENTRY_SIZE # terminating entry
for fent in self._fip_entries:
offset = tools.align(offset, self._align)
fent.offset = offset
offset += fent.size
# Write out the TOC
for fent in self._fip_entries:
hdr = struct.pack(ENTRY_FORMAT, fent.uuid, fent.offset, fent.size,
fent.flags)
buf.write(hdr)
# Write out the entries
for fent in self._fip_entries:
buf.seek(fent.offset)
buf.write(fent.data)
return buf.getvalue()
class FipReader():
"""Class to handle reading a Firmware Image Package (FIP)
Usage is something like:
fip = fip_util.FipReader(data)
fent = fip.get_entry('fwu')
self.WriteFile('ufwu.bin', fent.data)
blob = fip.get_entry(
bytes([0xe3, 0xb7, 0x8d, 0x9e, 0x4a, 0x64, 0x11, 0xec,
0xb4, 0x5c, 0xfb, 0xa2, 0xb9, 0xb4, 0x97, 0x88]))
self.WriteFile('blob.bin', blob.data)
"""
def __init__(self, data, read=True):
"""Set up a new FitReader
Args:
data (bytes): data to read
read (bool): True to read the data now
"""
self.fents = collections.OrderedDict()
self.data = data
if read:
self.read()
def read(self):
"""Read all the files in the FIP and add them to self.files"""
self.header, self.fents = decode_fip(self.data)
def get_entry(self, fip_type_or_uuid):
"""get_entry() - Find an entry by type or UUID
Args:
fip_type_or_uuid (str or bytes): Name of the type to create, or
bytes(16) uuid
Returns:
FipEntry: if found
Raises:
ValueError: entry type not found
"""
fip_type, uuid = get_type_uuid(fip_type_or_uuid)
for fent in self.fents:
if fent.uuid == uuid:
return fent
label = fip_type
if not label:
label = UUID(bytes=uuid)
raise ValueError(f"Cannot find FIP entry '{label}'")
def parse_macros(srcdir):
"""parse_macros: Parse the firmware_image_package.h file
Args:
srcdir (str): 'arm-trusted-firmware' source directory
Returns:
dict:
key: UUID macro name, e.g. 'UUID_TRUSTED_FWU_CERT'
value: list:
file comment, e.g. 'ToC Entry UUIDs'
macro name, e.g. 'UUID_TRUSTED_FWU_CERT'
uuid as bytes(16)
Raises:
ValueError: a line cannot be parsed
"""
re_uuid = re.compile('0x[0-9a-fA-F]{2}')
re_comment = re.compile(r'^/\* (.*) \*/$')
fname = os.path.join(srcdir, 'include/tools_share/firmware_image_package.h')
data = tools.read_file(fname, binary=False)
macros = collections.OrderedDict()
comment = None
for linenum, line in enumerate(data.splitlines()):
if line.startswith('/*'):
mat = re_comment.match(line)
if mat:
comment = mat.group(1)
else:
# Example: #define UUID_TOS_FW_CONFIG \
if 'UUID' in line:
macro = line.split()[1]
elif '{{' in line:
mat = re_uuid.findall(line)
if not mat or len(mat) != 16:
raise ValueError(
f'{fname}: Cannot parse UUID line {linenum + 1}: Got matches: {mat}')
uuid = bytes([int(val, 16) for val in mat])
macros[macro] = comment, macro, uuid
if not macros:
raise ValueError(f'{fname}: Cannot parse file')
return macros
def parse_names(srcdir):
"""parse_names: Parse the tbbr_config.c file
Args:
srcdir (str): 'arm-trusted-firmware' source directory
Returns:
tuple: dict of entries:
key: UUID macro, e.g. 'UUID_NON_TRUSTED_FIRMWARE_BL33'
tuple: entry information
Description of entry, e.g. 'Non-Trusted Firmware BL33'
UUID macro, e.g. 'UUID_NON_TRUSTED_FIRMWARE_BL33'
Name of entry, e.g. 'nt-fw'
Raises:
ValueError: the file cannot be parsed
"""
# Extract the .name, .uuid and .cmdline_name values
re_data = re.compile(r'\.name = "([^"]*)",\s*\.uuid = (UUID_\w*),\s*\.cmdline_name = "([^"]+)"',
re.S)
fname = os.path.join(srcdir, 'tools/fiptool/tbbr_config.c')
data = tools.read_file(fname, binary=False)
# Example entry:
# {
# .name = "Secure Payload BL32 Extra2 (Trusted OS Extra2)",
# .uuid = UUID_SECURE_PAYLOAD_BL32_EXTRA2,
# .cmdline_name = "tos-fw-extra2"
# },
mat = re_data.findall(data)
if not mat:
raise ValueError(f'{fname}: Cannot parse file')
names = {uuid: (desc, uuid, name) for desc, uuid, name in mat}
return names
def create_code_output(macros, names):
"""create_code_output() - Create the new version of this Python file
Args:
macros (dict):
key (str): UUID macro name, e.g. 'UUID_TRUSTED_FWU_CERT'
value: list:
file comment, e.g. 'ToC Entry UUIDs'
macro name, e.g. 'UUID_TRUSTED_FWU_CERT'
uuid as bytes(16)
names (dict): list of entries, each
tuple: entry information
Description of entry, e.g. 'Non-Trusted Firmware BL33'
UUID macro, e.g. 'UUID_NON_TRUSTED_FIRMWARE_BL33'
Name of entry, e.g. 'nt-fw'
Returns:
str: Table of FipType() entries
"""
def _to_hex_list(data):
"""Convert bytes into C code
Args:
bytes to convert
Returns:
str: in the format '0x12, 0x34, 0x56...'
"""
# Use 0x instead of %# since the latter ignores the 0 modifier in
# Python 3.8.10
return ', '.join(['0x%02x' % byte for byte in data])
out = ''
last_comment = None
for comment, macro, uuid in macros.values():
name_entry = names.get(macro)
if not name_entry:
print(f"Warning: UUID '{macro}' is not mentioned in tbbr_config.c file")
continue
desc, _, name = name_entry
if last_comment != comment:
out += f' # {comment}\n'
last_comment = comment
out += """ FipType('%s', '%s',
[%s,
%s]),
""" % (name, desc, _to_hex_list(uuid[:8]), _to_hex_list(uuid[8:]))
return out
def parse_atf_source(srcdir, dstfile, oldfile):
"""parse_atf_source(): Parse the ATF source tree and update this file
Args:
srcdir (str): Path to 'arm-trusted-firmware' directory. Get this from:
https://github.com/ARM-software/arm-trusted-firmware.git
dstfile (str): File to write new code to, if an update is needed
oldfile (str): Python source file to compare against
Raises:
ValueError: srcdir readme.rst is missing or the first line does not
match what is expected
"""
# We expect a readme file
readme_fname = os.path.join(srcdir, 'readme.rst')
if not os.path.exists(readme_fname):
raise ValueError(
f"Expected file '{readme_fname}' - try using -s to specify the "
'arm-trusted-firmware directory')
readme = tools.read_file(readme_fname, binary=False)
first_line = 'Trusted Firmware-A'
if readme.splitlines()[0] != first_line:
raise ValueError(f"'{readme_fname}' does not start with '{first_line}'")
macros = parse_macros(srcdir)
names = parse_names(srcdir)
output = create_code_output(macros, names)
orig = tools.read_file(oldfile, binary=False)
re_fip_list = re.compile(r'(.*FIP_TYPE_LIST = \[).*?( ] # end.*)', re.S)
mat = re_fip_list.match(orig)
new_code = mat.group(1) + '\n' + output + mat.group(2) if mat else output
if new_code == orig:
print(f"Existing code in '{oldfile}' is up-to-date")
else:
tools.write_file(dstfile, new_code, binary=False)
print(f'Needs update, try:\n\tmeld {dstfile} {oldfile}')
def main(argv, oldfile):
"""Main program for this tool
Args:
argv (list): List of str command-line arguments
oldfile (str): Python source file to compare against
Returns:
int: 0 (exit code)
"""
parser = ArgumentParser(epilog='''Creates an updated version of this code,
with a table of FIP-entry types parsed from the arm-trusted-firmware source
directory''')
parser.add_argument(
'-D', '--debug', action='store_true',
help='Enabling debugging (provides a full traceback on error)')
parser.add_argument(
'-o', '--outfile', type=str, default='fip_util.py.out',
help='Output file to write new fip_util.py file to')
parser.add_argument(
'-s', '--src', type=str, default='.',
help='Directory containing the arm-trusted-firmware source')
args = parser.parse_args(argv)
if not args.debug:
sys.tracebacklimit = 0
parse_atf_source(args.src, args.outfile, oldfile)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:], OUR_FILE)) # pragma: no cover