#!/usr/bin/env python3
#
# Copyright (c) 2015-2017 Qualcomm Atheros, Inc.
# Copyright (c) 2018-2019, The Linux Foundation. All rights reserved.
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# Run 'ath11k-bdencoder --help' to see the instructions
#

import struct
import ctypes
import os.path
import argparse
import json
import binascii
import hashlib
import tempfile
import subprocess
import logging
import sys
import shutil

MAX_BUF_LEN = 2000000

# the signature length also includes null byte and padding
ATH11K_BOARD_SIGNATURE = b"QCA-ATH11K-BOARD"
ATH11K_BOARD_SIGNATURE_LEN = 20

PADDING_MAGIC = 0x6d
DEFAULT_BD_API = 2
DEFAULT_BOARD_FILE = 'board-%d.bin' % DEFAULT_BD_API
DEFAULT_JSON_FILE = 'board-%d.json' % DEFAULT_BD_API
TYPE_LENGTH_SIZE = 8

ATH11K_BD_IE_BOARD = 0
ATH11K_BD_IE_REGDB = 1

ATH11K_BD_IE_BOARD_NAME = 0
ATH11K_BD_IE_BOARD_DATA = 1

ATH11K_BD_IE_REGDB_NAME = 0
ATH11K_BD_IE_REGDB_DATA = 1


def padding_needed(len):
    if len % 4 != 0:
        return 4 - len % 4
    return 0


def add_ie(buf, offset, id, value):
    length = len(value)
    padding_len = padding_needed(length)
    length = length + padding_len

    padding = ctypes.create_string_buffer(padding_len)

    for i in range(padding_len):
        struct.pack_into('<B', padding, i, PADDING_MAGIC)

    fmt = '<2i%ds%ds' % (len(value), padding_len)
    struct.pack_into(fmt, buf, offset, id, len(value), value, padding.raw)
    offset = offset + TYPE_LENGTH_SIZE + len(value) + padding_len

    return offset


# to workaround annoying python feature of returning negative hex values
def hex32(val):
    return val & 0xffffffff


# match with kernel crc32_le(0, buf, buf_len) implementation
def _crc32(buf):
    return hex32(~(hex32(binascii.crc32(buf, 0xffffffff))))


def pretty_array_str(array):
    return '\',\''.join(array)


class RegdbName():
    @staticmethod
    def parse_ie(buf, offset, length):
        self = RegdbName()
        fmt = '<%ds' % length
        (name, ) = struct.unpack_from(fmt, buf, offset)
        self.name = name.decode()

        logging.debug('RegdbName.parse_ie(): offset %d length %d self %s' %
                      (offset, length, self))

        return self

    def add_to_buf(self, buf, offset):
        return add_ie(buf, offset, ATH11K_BD_IE_REGDB_NAME, self.name.encode())

    def __eq__(self, other):
        return self.name == other.name

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return 'RegdbName(%s)' % self.name

    def __init__(self, name=None):
        self.name = name


class RegdbData():
    @staticmethod
    def parse_ie(buf, offset, length):
        self = RegdbData()
        fmt = '<%ds' % length
        (self.data, ) = struct.unpack_from(fmt, buf, offset)

        logging.debug('RegdbData.parse_ie(): offset %d length %d self %s' %
                      (offset, length, self))

        return self

    def add_to_buf(self, buf, offset):
        return add_ie(buf, offset, ATH11K_BD_IE_REGDB_DATA, self.data)

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        if self.data is not None:
            s = '%d B' % (len(self.data))
        else:
            s = 'n/a'

        return 'RegdbData(%s)' % (s)

    def __init__(self, data=None):
        self.data = data


class Regdb():
    @staticmethod
    def parse_ie(buf, offset, length):
        logging.debug('Regdb.parse_ie(): offset %d length %d' % (offset, length))

        self = Regdb()

        # looping regdb IEs
        while length > 0:
            (ie_id, ie_len) = struct.unpack_from('<2i', buf, offset)

            logging.debug('Regdb.parse_ie(): found ie_id %d ie_len %d offset %d length %d' %
                          (ie_id, ie_len, offset, length))

            if TYPE_LENGTH_SIZE + ie_len > length:
                raise Exception('Error: ie_len too big (%d > %d)' % (ie_len,
                                                                     length))

            offset += TYPE_LENGTH_SIZE
            length -= TYPE_LENGTH_SIZE

            if ie_id == ATH11K_BD_IE_REGDB_NAME:
                self.names.append(RegdbName.parse_ie(buf, offset, ie_len))
            elif ie_id == ATH11K_BD_IE_REGDB_DATA:
                self.data = RegdbData.parse_ie(buf, offset, ie_len)

            offset += ie_len + padding_needed(ie_len)
            length -= ie_len + padding_needed(ie_len)

        return self

    def add_to_buf(self, buf, offset):
        # store position ie header of this element
        ie_offset = offset

        offset += TYPE_LENGTH_SIZE

        for name in self.names:
            offset = name.add_to_buf(buf, offset)

        offset = self.data.add_to_buf(buf, offset)

        # write ie header as now we know the full length
        ie_len = offset - ie_offset - TYPE_LENGTH_SIZE
        struct.pack_into('<2i', buf, ie_offset, ATH11K_BD_IE_REGDB, ie_len)

        return offset

    def get_names_as_str(self):
        names = []
        for regdbname in self.names:
            names.append(regdbname.name)

        return names

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        names = []
        for regdbname in self.names:
            names.append(str(regdbname))

        return 'Regdb(%s, %s)' % (','.join(names), self.data)

    def __init__(self):
        self.data = None
        self.names = []


class BoardName():
    @staticmethod
    def parse_ie(buf, offset, length):
        self = BoardName()
        fmt = '<%ds' % length
        (name, ) = struct.unpack_from(fmt, buf, offset)
        self.name = name.decode()

        logging.debug('BoardName.parse_ie(): offset %d length %d self %s' %
                      (offset, length, self))

        return self

    def add_to_buf(self, buf, offset):
        return add_ie(buf, offset, ATH11K_BD_IE_BOARD_NAME, self.name.encode())

    def __eq__(self, other):
        return self.name == other.name

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return 'BoardName(%s)' % self.name

    def __init__(self, name=None):
        self.name = name


class BoardData():
    @staticmethod
    def parse_ie(buf, offset, length):
        self = BoardData()
        fmt = '<%ds' % length
        (self.data, ) = struct.unpack_from(fmt, buf, offset)

        logging.debug('BoardData.parse_ie(): offset %d length %d self %s' %
                      (offset, length, self))

        return self

    def add_to_buf(self, buf, offset):
        return add_ie(buf, offset, ATH11K_BD_IE_BOARD_DATA, self.data)

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        if self.data is not None:
            s = '%d B' % (len(self.data))
        else:
            s = 'n/a'

        return 'BoardData(%s)' % (s)

    def __init__(self, data=None):
        self.data = data


class Board():
    @staticmethod
    def parse_ie(buf, offset, length):
        logging.debug('Board.parse_ie(): offset %d length %d' % (offset, length))

        self = Board()

        # looping board IEs
        while length > 0:
            (ie_id, ie_len) = struct.unpack_from('<2i', buf, offset)

            logging.debug('Board.parse_ie(): found ie_id %d ie_len %d offset %d length %d' %
                          (ie_id, ie_len, offset, length))

            if TYPE_LENGTH_SIZE + ie_len > length:
                raise Exception('Error: ie_len too big (%d > %d)' % (ie_len,
                                                                     length))

            offset += TYPE_LENGTH_SIZE
            length -= TYPE_LENGTH_SIZE

            if ie_id == ATH11K_BD_IE_BOARD_NAME:
                self.names.append(BoardName.parse_ie(buf, offset, ie_len))
            elif ie_id == ATH11K_BD_IE_BOARD_DATA:
                self.data = BoardData.parse_ie(buf, offset, ie_len)

            offset += ie_len + padding_needed(ie_len)
            length -= ie_len + padding_needed(ie_len)

        return self

    def add_to_buf(self, buf, offset):
        # store position ie header of this element
        ie_offset = offset

        offset += TYPE_LENGTH_SIZE

        for name in self.names:
            offset = name.add_to_buf(buf, offset)

        offset = self.data.add_to_buf(buf, offset)

        # write ie header as now we know the full length
        ie_len = offset - ie_offset - TYPE_LENGTH_SIZE
        struct.pack_into('<2i', buf, ie_offset, ATH11K_BD_IE_BOARD, ie_len)

        return offset

    def get_names_as_str(self):
        names = []
        for boardname in self.names:
            names.append(boardname.name)

        return names

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        names = []
        for boardname in self.names:
            names.append(str(boardname))

        return 'Board(%s, %s)' % (','.join(names), self.data)

    def __init__(self):
        self.data = None
        self.names = []


class BoardContainer:

    def add_board(self, data, names):
        boardnames = []
        for name in names:
            boardnames.append(BoardName(name))

        board = Board()
        board.data = BoardData(data)
        board.names = boardnames

        self.boards.append(board)

    def add_regdb(self, data, names):
        regdbnames = []
        for name in names:
            regdbnames.append(RegdbName(name))

        regdb = Regdb()
        regdb.data = RegdbData(data)
        regdb.names = regdbnames

        self.regdbs.append(regdb)

    @staticmethod
    def open_json(filename):
        self = BoardContainer()

        if not os.path.exists(filename):
            print('mapping file %s not found' % (filename))
            return

        f = open(filename, 'r')
        mapping = json.loads(f.read())
        f.close()

        if 'board' in mapping[0]:
            for b in mapping[0]['board']:
                board_filename = b['data']
                f = open(board_filename, 'rb')
                data = f.read()
                f.close()

                self.add_board(data, b['names'])

        if 'regdb' in mapping[0]:
            for b in mapping[0]['regdb']:
                regdb_filename = b['data']
                f = open(regdb_filename, 'rb')
                data = f.read()
                f.close()

                self.add_regdb(data, b['names'])

        return self

    def validate(self):
        allnames = []

        for board in self.boards:
            for name in board.names:
                if name in allnames:
                    # TODO: Find a better way to report problems,
                    # maybe return a list of strings? Or use an
                    # exception?
                    print('Warning: duplicate board name: %s' % (name.name))
                    return

                allnames.append(name)

    def _add_signature(self, buf, offset):
        signature = ATH11K_BOARD_SIGNATURE + b'\0'
        length = len(signature)
        pad_len = padding_needed(length)
        length = length + pad_len

        padding = ctypes.create_string_buffer(pad_len)

        for i in range(pad_len):
            struct.pack_into('<B', padding, i, PADDING_MAGIC)

        fmt = '<%ds%ds' % (len(signature), pad_len)
        struct.pack_into(fmt, buf, offset, signature, padding.raw)
        offset += length

        # make sure ATH11K_BOARD_SIGNATURE_LEN is correct
        assert(ATH11K_BOARD_SIGNATURE_LEN == length)

        return offset

    @staticmethod
    def open(name):
        self = BoardContainer()

        f = open(name, 'rb')
        buf = f.read()
        f.close()
        buf_len = len(buf)

        logging.debug('BoardContainer.open(): name %s' % (name))

        offset = 0

        fmt = '<%dsb' % (len(ATH11K_BOARD_SIGNATURE))
        (signature, null) = struct.unpack_from(fmt, buf, offset)

        if signature != ATH11K_BOARD_SIGNATURE or null != 0:
            print("invalid signature found in %s" % name)
            return 1

        offset += ATH11K_BOARD_SIGNATURE_LEN

        # looping main IEs
        while offset < buf_len:
            (ie_id, ie_len) = struct.unpack_from('<2i', buf, offset)
            logging.debug('BoardContainer.open(): found offset %d ie_id %d ie_len %d' %
                          (offset, ie_id, ie_len))

            offset += TYPE_LENGTH_SIZE

            if offset + ie_len > buf_len:
                print('Error: Buffer too short (%d + %d > %d)' % (offset,
                                                                  ie_len,
                                                                  buf_len))
                return 1

            if ie_id == ATH11K_BD_IE_BOARD:
                self.boards.append(Board.parse_ie(buf, offset, ie_len))
            elif ie_id == ATH11K_BD_IE_REGDB:
                self.regdbs.append(Regdb.parse_ie(buf, offset, ie_len))

            offset += ie_len + padding_needed(ie_len)

        self.validate()

        return self

    def write(self, name):
        (buf, buf_len) = self.get_bin()

        fd = open(name, 'wb')
        fd.write(buf.raw[0:buf_len])
        fd.close()

        self.validate()

        print("board binary file '%s' is created" % name)

    def get_bin(self):
        buf = ctypes.create_string_buffer(MAX_BUF_LEN)
        offset = 0

        offset = self._add_signature(buf, offset)

        for board in self.boards:
            offset = board.add_to_buf(buf, offset)

        for regdb in self.regdbs:
            offset = regdb.add_to_buf(buf, offset)

        # returns buffer and it's length
        return buf, offset

    def get_summary(self, sort=False):
        (buf, buf_len) = self.get_bin()

        s = ''

        s += 'FileSize: %d\n' % (buf_len)
        s += 'FileCRC32: %08x\n' % (_crc32(buf[0:buf_len]))
        s += 'FileMD5: %s\n' % (hashlib.md5(buf[0:buf_len]).hexdigest())

        boards = self.boards

        if sort:
            boards = sorted(boards, key=lambda board: board.get_names_as_str())

        index = 0
        for board in boards:
            if not sort:
                index_s = '[%d]' % (index)
            else:
                index_s = ''

            s += 'BoardNames%s: \'%s\'\n' % (index_s, pretty_array_str(board.get_names_as_str()))
            s += 'BoardLength%s: %d\n' % (index_s, len(board.data.data))
            s += 'BoardCRC32%s: %08x\n' % (index_s, _crc32(board.data.data))
            s += 'BoardMD5%s: %s\n' % (index_s, hashlib.md5(board.data.data).hexdigest())
            index += 1

        regdbs = self.regdbs

        if sort:
            regdbs = sorted(regdbs, key=lambda regdb: regdb.get_names_as_str())

        index = 0
        for regdb in regdbs:
            if not sort:
                index_s = '[%d]' % (index)
            else:
                index_s = ''

            s += 'RegdbNames%s: \'%s\'\n' % (index_s, pretty_array_str(regdb.get_names_as_str()))
            s += 'RegdbLength%s: %d\n' % (index_s, len(regdb.data.data))
            s += 'RegdbCRC32%s: %08x\n' % (index_s, _crc32(regdb.data.data))
            s += 'RegdbMD5%s: %s\n' % (index_s, hashlib.md5(regdb.data.data).hexdigest())
            index += 1

        return s

    def __init__(self):
        self.boards = []
        self.regdbs = []


def cmd_extract(args):
    cont = BoardContainer().open(args.extract)

    mapping = []
    d = {}
    mapping.append(d)

    mapping_board = []
    d['board'] = mapping_board

    for board in cont.boards:
        filename = board.names[0].name + '.bin'

        b = {}
        b['names'] = board.get_names_as_str()
        b['data'] = filename
        mapping_board.append(b)

        f = open(filename, 'wb')
        f.write(board.data.data)
        f.close()

        print("%s created size: %d" % (filename, len(board.data.data)))

    mapping_regdb = []
    d['regdb'] = mapping_regdb

    for regdb in cont.regdbs:
        filename = regdb.names[0].name + '.regdb'

        b = {}
        b['names'] = regdb.get_names_as_str()
        b['data'] = filename
        mapping_regdb.append(b)

        f = open(filename, 'wb')
        f.write(regdb.data.data)
        f.close()

        print("%s created size: %d" % (filename, len(regdb.data.data)))

    filename = DEFAULT_JSON_FILE
    f = open(filename, 'w')
    f.write(json.dumps(mapping, indent=4))
    f.close()

    print("%s created" % (filename))


def cmd_info(args):
    filename = args.info

    cont = BoardContainer().open(filename)

    print(cont.get_summary())


def cmd_diff(args):
    if args.diff:
        filename1 = args.diff[0]
        filename2 = args.diff[1]
    else:
        filename1 = args.diffstat[0]
        filename2 = args.diffstat[1]

    print(diff_boardfiles(filename1, filename2, args.diff))


def diff_boardfiles(filename1, filename2, diff):
    result = ''

    container1 = BoardContainer().open(filename1)
    (temp1_fd, temp1_pathname) = tempfile.mkstemp()
    os.write(temp1_fd, container1.get_summary(sort=True).encode())

    container2 = BoardContainer().open(filename2)
    (temp2_fd, temp2_pathname) = tempfile.mkstemp()
    os.write(temp2_fd, container2.get_summary(sort=True).encode())

    # this function is used both with --diff and --diffstat
    if diff:
        # '--less-mode' and '--auto-page' would be nice when running on
        # terminal but don't know how to get the control character
        # through. For terminal detection sys.stdout.isatty() can be used.
        cmd = ['wdiff', temp1_pathname, temp2_pathname]

        # wdiff is braindead and returns 1 in a succesfull case
        try:
            output = subprocess.check_output(cmd)
        except subprocess.CalledProcessError as e:
            if e.returncode == 1:
                output = e.output
            else:
                print('Failed to run wdiff: %d\n%s' % (e.returncode, e.output))
                return 1
        except OSError as e:
            print('Failed to run wdiff: %s' % (e))
            return 1

        result += '%s\n' % (output.decode())

    # create simple statistics about changes in board images
    new_boards = {}
    deleted_boards = {}
    changed_boards = {}

    for board in container2.boards:
        # convert the list to a string
        s = pretty_array_str(board.get_names_as_str())
        new_boards[s] = board

    for board in container1.boards:
        # convert the list to a string
        names = pretty_array_str(board.get_names_as_str())

        if names not in new_boards:
            # board image has been deleted
            deleted_boards[names] = board
            continue

        board2 = new_boards[names]
        del new_boards[names]

        if board.data.data == board2.data.data:
            # board image hasn't changed
            continue

        # board image has changed
        changed_boards[names] = board2

    result += 'New board:\n%s\n\n' % ('\n'.join(list(new_boards.keys())))
    result += 'Changed board:\n%s\n\n' % ('\n'.join(list(changed_boards.keys())))
    result += 'Deleted board:\n%s\n' % ('\n'.join(list(deleted_boards.keys())))

    result += '%d board image(s) added, %d changed, %d deleted, %d in total\n\n' % (len(new_boards),
                                                                                    len(changed_boards),
                                                                                    len(deleted_boards),
                                                                                    len(container2.boards))

    # create simple statistics about changes in regdb images
    new_regdbs = {}
    deleted_regdbs = {}
    changed_regdbs = {}

    for regdb in container2.regdbs:
        # convert the list to a string
        s = pretty_array_str(regdb.get_names_as_str())
        new_regdbs[s] = regdb

    for regdb in container1.regdbs:
        # convert the list to a string
        names = pretty_array_str(regdb.get_names_as_str())

        if names not in new_regdbs:
            # regdb image has been deleted
            deleted_regdbs[names] = regdb
            continue

        regdb2 = new_regdbs[names]
        del new_regdbs[names]

        if regdb.data.data == regdb2.data.data:
            # regdb image hasn't changed
            continue

        # regdb image has changed
        changed_regdbs[names] = regdb2

    result += 'New regdb:\n%s\n\n' % ('\n'.join(list(new_regdbs.keys())))
    result += 'Changed regdb:\n%s\n\n' % ('\n'.join(list(changed_regdbs.keys())))
    result += 'Deleted regdb:\n%s\n' % ('\n'.join(list(deleted_regdbs.keys())))

    result += '%d regdb image(s) added, %d changed, %d deleted, %d in total' % (len(new_regdbs),
                                                                                len(changed_regdbs),
                                                                                len(deleted_regdbs),
                                                                                len(container2.regdbs))

    os.close(temp1_fd)
    os.close(temp2_fd)

    return result


def cmd_create(args):
    mapping_file = args.create

    if args.output:
        output = args.output
    else:
        output = DEFAULT_BOARD_FILE

    cont = BoardContainer.open_json(mapping_file)
    cont.write(output)


def cmd_add_board(args):
    if len(args.add_board) < 3:
        print('error: --add-board requires 3 or more arguments, only %d given' % (len(args.add_board)))
        sys.exit(1)

    board_filename = args.add_board[0]
    new_filename = args.add_board[1]
    new_names = args.add_board[2:]

    f = open(new_filename, 'rb')
    new_data = f.read()
    f.close()

    # copy the original file for diff
    (temp_fd, temp_pathname) = tempfile.mkstemp()
    shutil.copyfile(board_filename, temp_pathname)

    container = BoardContainer.open(board_filename)
    container.add_board(new_data, new_names)
    container.write(board_filename)

    print(diff_boardfiles(temp_pathname, board_filename, False))

    os.remove(temp_pathname)


def main():
    description = '''ath11k board-N.bin files manegement tool

ath11k-bdencoder is for creating (--create), listing (--info) and
comparing (--diff, --diffstat) ath11k board-N.bin files. The
board-N.bin is a container format which can have unlimited number of
actual board images ("board files"), each image containing one or
names which ath11k uses to find the correct image.

For creating board files you need a mapping file in JSON which
contains the names and filenames for the actual binary:

[
{
"board": [
        {"names": ["AAA1", "AAAA2"], "data": "A.bin"},
        {"names": ["B"], "data": "B.bin"},
        {"names": ["C"], "data": "C.bin"},
        ],
"regdb": [
        {"names": ["A"], "data": "A.regdb"}
        ]
}
]

In this example the board-N.bin will contain three board files which
are read from files named A.bin (using names AAA1 and AAAA2 in the
board-N.bin file), B.bin (using name B) and C.bin (using name C). The file also contains one regdb (regulatory database) from file A.regdb.

You can use --extract switch to see examples from real board-N.bin
files.
'''

    parser = argparse.ArgumentParser(description=description,
                                     formatter_class=argparse.RawTextHelpFormatter)

    cmd_group = parser.add_mutually_exclusive_group(required=True)
    cmd_group.add_argument("-c", "--create", metavar='JSON_FILE',
                           help='create board-N.bin from a mapping file in JSON format')
    cmd_group.add_argument("-e", "--extract", metavar='BOARD_FILE',
                           help='extract board-N.bin file to a JSON mapping file and individual board files, compatible with the format used with --create command')
    cmd_group.add_argument("-i", "--info", metavar='BOARD_FILE',
                           help='show all details about a board-N.bin file')
    cmd_group.add_argument('-d', '--diff', metavar='BOARD_FILE', nargs=2,
                           help='show differences between two board-N.bin files')
    cmd_group.add_argument('-D', '--diffstat', metavar='BOARD_FILE', nargs=2,
                           help='show a summary of differences between two board-N.bin files')
    cmd_group.add_argument('-a', '--add-board', metavar='NAME', nargs='+',
                           help='add a board file to an existing board-N.bin, first argument is the filename of board-N.bin to add to, second is the filename board file (board.bin) to add and then followed by one or more arguments are names used in board-N.bin')

    parser.add_argument('-v', '--verbose', action='store_true',
                        help='enable verbose (debug) messages')
    parser.add_argument("-o", "--output", metavar="BOARD_FILE",
                        help='name of the output file, otherwise the default is: %s' %
                        (DEFAULT_BOARD_FILE))

    args = parser.parse_args()

    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)

    if args.create:
        return cmd_create(args)
    elif args.extract:
        return cmd_extract(args)
    elif args.info:
        return cmd_info(args)
    elif args.diff:
        return cmd_diff(args)
    elif args.diffstat:
        return cmd_diff(args)
    elif args.add_board:
        return cmd_add_board(args)

if __name__ == "__main__":
    main()
