#!/bin/bash
# Copyright 2021 Google LLC
#
# 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.


# List of options the script accepts. Trailing column means that the option
# requires an argument.
ARGUMENT_LIST=(
    "help"
    "product-id:"
    "product-name:"
    "host-mac:"
    "bind-device:"
    "dev-mac:"
    "dev-type:"
    "gadget-dir-name:"
    "iface-name:"
)

print_usage() {
    cat <<HELP
$0 [OPTIONS] [stop|start]
    Create USB Gadget Configuration
        --product-id USB Product Id for the gadget.
        --product-name Product name string (en) for the gadget.
        --host-mac MAC address of the host part of the connection. Optional.
        --dev-mac MAC address of the device (gadget) part of the connection. Optional.
        --dev-type Type of gadget to instantiate. Default: "eem"
        --bind-device Name of the device to bind, as listed in /sys/class/udc/
        --gadget-dir-name Optional base name for gadget directory. Default: iface-name
        --iface-name name of the network interface.
        --help  Print this help and exit.
HELP
}

gadget_start() {
    # Always provide a basic network configuration
    mkdir -p /run/systemd/network || return
    cat >/run/systemd/network/+-bmc-"${IFACE_NAME}".network <<EOF
[Match]
Name=${IFACE_NAME}
EOF

    # Add the gbmcbr configuration if this is a relevant device
    if (( ID_VENDOR == 0x18d1 && ID_PRODUCT == 0x22b )); then
        cat >>/run/systemd/network/+-bmc-"${IFACE_NAME}".network <<EOF
[Network]
Bridge=gbmcbr
[Bridge]
Cost=85
EOF
    fi

    # Ignore any failures due to systemd being unavailable at boot
    networkctl reload || true

    local gadget_dir="${CONFIGFS_HOME}/usb_gadget/${GADGET_DIR_NAME}"
    mkdir -p "${gadget_dir}" || return
    echo "${ID_VENDOR}" >"${gadget_dir}/idVendor" || return
    echo "${ID_PRODUCT}" >"${gadget_dir}/idProduct" || return

    local str_en_dir="${gadget_dir}/strings/0x409"
    mkdir -p "${str_en_dir}" || return
    echo "${STR_EN_VENDOR}" >"${str_en_dir}/manufacturer" || return
    echo "${STR_EN_PRODUCT}" >"${str_en_dir}/product" || return

    local config_dir="${gadget_dir}/configs/c.1"
    mkdir -p "${config_dir}" || return
    echo 100 > "${config_dir}/MaxPower" || return
    mkdir -p "${config_dir}/strings/0x409" || return
    echo "${DEV_TYPE^^}" > "${config_dir}/strings/0x409/configuration" || return

    local func_dir="${gadget_dir}/functions/${DEV_TYPE}.${IFACE_NAME}"
    mkdir -p "${func_dir}" || return

    if [[ -n $HOST_MAC_ADDR ]]; then
        echo "${HOST_MAC_ADDR}" >"${func_dir}"/host_addr || return
    fi

    if [[ -n $DEV_MAC_ADDR ]]; then
        echo "${DEV_MAC_ADDR}" >"${func_dir}"/dev_addr || return
    fi

    ln -s "${func_dir}" "${config_dir}" || return

    # This only works on kernel 5.12+, we have to ignore failures for now
    echo "$IFACE_NAME" >"${func_dir}"/ifname || true

    echo "${BIND_DEVICE}" >"${gadget_dir}"/UDC || return
    # Try to reconfigure a few times in case we race with systemd-networkd
    local start=$SECONDS
    while (( SECONDS - start < 5 )); do
        local ifname
        ifname="$(<"${func_dir}"/ifname)" || return
        [ "${IFACE_NAME}" = "$ifname" ] && break
        ip link set dev "$ifname" down && \
            ip link set dev "$ifname" name "${IFACE_NAME}" && break
        sleep 1
    done
    ip link set dev "$IFACE_NAME" up || return
}

gadget_stop() {
    local gadget_dir="${CONFIGFS_HOME}/usb_gadget/${GADGET_DIR_NAME}"
    rm -f "${gadget_dir}/configs/c.1/${DEV_TYPE}.${IFACE_NAME}"
    rmdir "${gadget_dir}/functions/${DEV_TYPE}.${IFACE_NAME}" \
      "${gadget_dir}/configs/c.1/strings/0x409" \
      "${gadget_dir}/configs/c.1" \
      "${gadget_dir}/strings/0x409" \
      "${gadget_dir}" || true

    rm -f /run/systemd/network/+-bmc-"${IFACE_NAME}".network
    networkctl reload || true
}

opts="$(getopt \
    --longoptions "$(printf "%s," "${ARGUMENT_LIST[@]}")" \
    --name "$(basename "$0")" \
    --options "" \
    -- "$@"
)"

eval set -- "$opts"

CONFIGFS_HOME=${CONFIGFS_HOME:-/sys/kernel/config}
ID_VENDOR="0x18d1" # Google
ID_PRODUCT=""
STR_EN_VENDOR="Google"
STR_EN_PRODUCT=""
DEV_MAC_ADDR=""
DEV_TYPE="eem"
HOST_MAC_ADDR=""
BIND_DEVICE=""
ACTION="start"
GADGET_DIR_NAME=""
IFACE_NAME=""
while [[ $# -gt 0 ]]; do
    case "$1" in
        --product-id)
            ID_PRODUCT=$2
            shift 2
            ;;
        --product-name)
            STR_EN_PRODUCT=$2
            shift 2
            ;;
        --host-mac)
            HOST_MAC_ADDR=$2
            shift 2
            ;;
        --dev-mac)
            DEV_MAC_ADDR=$2
            shift 2
            ;;
        --dev-type)
            DEV_TYPE=$2
            shift 2
            ;;
        --bind-device)
            BIND_DEVICE=$2
            shift 2
            ;;
        --gadget-dir-name)
            GADGET_DIR_NAME=$2
            shift 2
            ;;
        --iface-name)
            IFACE_NAME=$2
            shift 2
            ;;
        --help)
            print_usage
            exit 0
            ;;
        start)
            ACTION="start"
            shift 1
            break
            ;;
        stop)
            ACTION="stop"
            shift 1
            break
            ;;
        --)
            shift 1
            ;;
        *)
            break
            ;;
    esac
done

if [ -z "$GADGET_DIR_NAME" ]; then
    GADGET_DIR_NAME="$IFACE_NAME"
fi

if [[ $ACTION == "stop" ]]; then
    gadget_stop
else
    if [ -z "$ID_PRODUCT" ]; then
        echo "Product ID is missing" >&2
        exit 1
    fi

    if [ -z "$IFACE_NAME" ]; then
        echo "Interface name is missing" >&2
        exit 1
    fi

    if [ -z "$BIND_DEVICE" ]; then
        echo "Bind device is missing" >&2
        exit 1
    fi

    rc=0
    gadget_start || rc=$?
    (( rc == 0 )) || gadget_stop || true
    exit $rc
fi
