#!/bin/sh

# Turn of auto expansion
set -f

po_domain="alterator-ports-access"
alterator_api_version=1

. alterator-sh-functions
. shell-config
. shell-quote
. shell-var
. alterator-ports-access-lib.sh
. alterator-service-functions

readonly USB_IDS="/usr/share/misc/usb.ids"

# APPLY CHANGES
apply_changes() {
  echo "[backend-portsctrl] apply_changes." 1>&2
  alterator-ports-access -u -s
}

extract_pre_access() {
    local desc="${1%;}"

    case "$desc" in
        *\;[*])
            echo "${desc%;*}"
            ;;
    esac
}

extract_access() {
    local desc="${1%;}"
    local access=

    case "$desc" in
        *\;[*])
            access="${desc##*;[}"
            echo "${access%]}"
            ;;
    esac
}

yesno_label() {
    if shell_var_is_yes "$1"; then
        echo "`_ 'Yes'`"
    else
        echo "`_ 'No'`"
    fi
}

access_label() {
    echo "${1:-`_ 'Default'`}"
}

# LIST CONFIGURED SERIAL PORTS
_list_serial() {
    printf "(\"%s\" label_serial_port \"%s\" label_serial_enabled \"%s\" label_serial_access \"%s\")\n" "$1" "$1" "$(yesno_label "$2")" "$(access_label "$3")" >&3
}
list_serial() {
    portsctrl_iterate_serial _list_serial
}

cat_e()
{
	[ -e "$1" ] || return 0
	cat "$1"
}

find_usb_dir() {
    local vendor_id="$1"
    local product_id="${2:-}"
    local serial="${3:-}"

    for i in $(find /sys -name authorized); do
        local dir="$(dirname "$i")"
        local devclass="$(cat_e "$dir/bDeviceClass")"

        # Ignore USB hubs
        [ "$devclass" = "09" ] && continue
        [ -z "$devclass" ] && continue

        [ "$(cat "$dir/idVendor")" = "$vendor_id" ] || continue
        [ -z "$product_id" -o "$(cat "$dir/idProduct")" = "$product_id" ] || continue
        [ -z "$serial" -o "$(cat "$dir/serial")" = "$serial" ] || continue

        [ -e "$dir/manufacturer" ] || continue

        echo "$dir"
        break
    done
}

get_usb_manufacturer() {
    if [ -r "$USB_IDS" -a -n "$1" ]; then
        local manufacturer="$(sed -n -e "/^0*$1[[:space:]]\\+/ { s///p; q; }" "$USB_IDS")"
	fi
    if [ -z "$manufacturer" ]; then
        local dir="$(find_usb_dir "$1")"
        if [ -n "$dir" ]; then
            cat "$dir/manufacturer" 2>/dev/null
        fi
    else
        echo "$manufacturer"
    fi
}

get_usb_product_name() {
    if [ -r "$USB_IDS" -a -n "$1" -a -n "$2" ]; then
        local prodname="$(sed -n -e "/^0*$1[[:space:]]\\+/,/^[^[:space:]]/{ /^[[:space:]]\\+0*$2[[:space:]]\\+/ { s///p; q; } }" "$USB_IDS")"
	fi
    if [ -z "$prodname" ]; then
        local dir="$(find_usb_dir "$1" "$2")"
        if [ -n "$dir" ]; then
            cat "$dir/product" 2>/dev/null
        fi
    else
        echo "$prodname"
    fi
}

get_usb_class_name() {
	if [ -r "$USB_IDS" -a -n "$1" ]; then
		sed -n -e "/^C[[:space:]]\\+0*$1[[:space:]]\\+\\([^#]\\+\\).*\$/ { s//\\1/; p; q }" "$USB_IDS"
	fi
}

get_usb_subclass_name() {
	if [ -r "$USB_IDS" -a -n "$1" -a -n "$2" ]; then
		sed -n -e "/^C[[:space:]]\\+0*$1[[:space:]]/,/^[^\\t]/ {
			/^\\t0*$2[[:space:]]\\+\\([^#]\\+\\).*\$/ { s//\\1/; p; q }
		}" "$USB_IDS"
	fi
}

get_usb_protocol_name() {
	if [ -r "$USB_IDS" -a -n "$1" -a -n "$2" -a -n "$3" ]; then
		sed -n -e "/^C[[:space:]]\\+0*$1[[:space:]]/,/^[^\\t]/ {
			/^\\t0*$2[[:space:]]/,/^\\([^\\t]\\|\\t[^\\t]\\)/ {
				/^\\t\\t0*$3[[:space:]]\\+\\([^#]\\+\\).*\$/ {
					s//\\1/; p; q;
				}
			}
		}" "$USB_IDS"
	fi
}

# LIST USB WHITE LIST
_list_usb_rule() {
    local num="$1"
    local vendor_id="$2"
    local prod_id="$3"
    local serial="$4"
	local class="$5"
	local subclass="$6"
	local proto="$7"
    local info="$8"
    local access="$9"

	local vendor_name="$(get_usb_manufacturer "$vendor_id")"
	local product_name="$(get_usb_product_name "$vendor_id" "$prod_id")"
	local class_name="$(get_usb_class_name "$class")"
	local subclass_name="$(get_usb_subclass_name "$class" "$subclass")"
	local proto_name="$(get_usb_protocol_name "$class" "$subclass" "$proto")"

    printf "(\"%s\" label_usb_rule_number \"%s\" label_usb_vendor_id \"%s\" label_usb_vendor_name \"%s\" label_usb_prod_id \"%s\" label_usb_prod_name \"%s\" label_usb_serial \"%s\" label_usb_class \"%s\" label_usb_class_name \"%s\" label_usb_subclass \"%s\" label_usb_subclass_name \"%s\" label_usb_proto \"%s\" label_usb_proto_name \"%s\" label_usb_access \"%s\")\n" \
		   "$num" "$num" \
		   "$vendor_id" "$vendor_name" \
		   "$prod_id" "$product_name" \
		   "$serial" \
		   "$class" "$class_name" \
		   "$subclass" "$subclass_name" \
		   "$proto" "$proto_name" \
		   "$(access_label "$access")" >&3
}
list_usb_rules() {
    portsctrl_iterate_usb _list_usb_rule
}

# LIST PRESENT USB DEVICES
list_prsnt_devices() {
	# Force scan, temporary disable USB control.
	if shell_var_is_yes "$in_force"; then
		echo "[backend-portsctrl] Temporary disable USB control." 1>&2
		alterator-ports-access -e
	fi

	for i in $(find /sys -name authorized); do
		local dir="$(dirname "$i")"

		local bDeviceClass="$(cat_e "$dir/bDeviceClass")"
		[ -n "$bDeviceClass" ] || continue

		# Ignore USB hubs
		[ "$bDeviceClass" = "09" ] && continue

		local idVendor="$(cat_e "$dir/idVendor")"
		local idProduct="$(cat_e "$dir/idProduct")"
		local serial="$(cat_e "$dir/serial")"

		[ -n "$idVendor" ] || continue

		local bDeviceSubClass="$(cat_e "$dir/bDeviceSubClass")"
		local bDeviceProtocol="$(cat_e "$dir/bDeviceProtocol")"

		local manufacturer="$(get_usb_manufacturer "$idVendor")"
		local product="$(get_usb_product_name "$idVendor" "$idProduct")"

		[ -n "$manufacturer" -a -z "${manufacturer##*unauthorized*}" ] && continue
		[ -n "$product" -a -z "${product##*unauthorized*}" ] && continue

		local dev_class_name="$(get_usb_class_name "$bDeviceClass")"
		local dev_subclass_name="$(get_usb_subclass_name "$bDeviceClass" "$bDeviceSubClass")"
		local dev_proto_name="$(get_usb_protocol_name "$bDeviceClass" "$bDeviceSubClass" "$bDeviceProtocol")"

		printf "(\"%s\" label_prsnt_usb_vendor \"%s\" label_prsnt_usb_product \"%s\" label_prsnt_usb_idvendor \"%s\" label_prsnt_usb_idproduct \"%s\" label_prsnt_usb_serial \"%s\" label_prsnt_usb_class \"%s\" label_prsnt_usb_class_name \"%s\" label_prsnt_usb_subclass \"%s\" label_prsnt_usb_subclass_name \"%s\" label_prsnt_usb_proto \"%s\" label_prsnt_usb_proto_name \"%s\" label_prsnt_usb_iface \"\")\n" \
			   "$dir" "$manufacturer" \
			   "$product" "$idVendor" "$idProduct" "$serial" \
			   "$bDeviceClass" "$dev_class_name" \
			   "$bDeviceSubClass" "$dev_subclass_name" \
			   "$bDeviceProtocol" "$dev_proto_name" \
			   >&3

		if shell_var_is_yes "$in_list_ifaces"; then
			for j in $(find "$dir" -name bInterfaceClass); do
				local sdir="$(dirname "$j")"

				local bInterfaceClass="$(cat_e "$sdir/bInterfaceClass")"
				[ -n "$bInterfaceClass" ] || continue

				local bInterfaceNumber="$(cat_e "$sdir/bInterfaceNumber")"
				[ -n "$bInterfaceNumber" ] || continue

				local bInterfaceSubClass="$(cat_e "$sdir/bInterfaceSubClass")"
				local bInterfaceProtocol="$(cat_e "$sdir/bInterfaceProtocol")"

				local iface_class_name="$(get_usb_class_name "$bInterfaceClass")"
				local iface_subclass_name="$(get_usb_subclass_name "$bInterfaceClass" "$bInterfaceSubClass")"
				local iface_proto_name="$(get_usb_protocol_name "$bInterfaceClass" "$bInterfaceSubClass" "$bInterfaceProtocol")"

				printf "(\"%s\" label_prsnt_usb_vendor \"%s\" label_prsnt_usb_product \"%s\" label_prsnt_usb_idvendor \"%s\" label_prsnt_usb_idproduct \"%s\" label_prsnt_usb_serial \"%s\" label_prsnt_usb_class \"%s\" label_prsnt_usb_class_name \"%s\" label_prsnt_usb_subclass \"%s\" label_prsnt_usb_subclass_name \"%s\" label_prsnt_usb_proto \"%s\" label_prsnt_usb_proto_name \"%s\" label_prsnt_usb_iface \"%s\")\n" \
					   "$sdir" "$manufacturer" \
					   "$product" "$idVendor" "$idProduct" "$serial" \
					   "$bInterfaceClass" "$iface_class_name" \
					   "$bInterfaceSubClass" "$iface_subclass_name" \
					   "$bInterfaceProtocol" "$iface_proto_name" \
					   "$bInterfaceNumber" \
					   >&3
			done
		fi
	done

	# Renew USB control.
	if shell_var_is_yes "$in_force"; then
		echo "[backend-portsctrl] renew USB control" 1>&2
		alterator-ports-access -u
	fi
}

# READ SERIAL PORT STATUS
_print_serial_access() {
    write_string_param "input_serial_owner" "$1"
    write_string_param "input_serial_group" "$2"
    write_string_param "list_serial_mode" "${3:-default}"
}
_print_serial() {
    if [ "$1" = "$4" ]; then
        write_string_param "label_serial_selected" "$1"
        write_string_param "list_serial_enabled" "$2"
        portsctrl_parse_access _print_serial_access "$3"
    fi
}
print_serial() {
    if [ -z "$in_port" ]; then
        write_error "Serial port name isn't specified."
        return 1
    fi

    # Check config file exists:
    if ! portsctrl_config_exists; then
        _print_serial "$in_port" 'yes' '' "$in_port"
        return
    fi

    portsctrl_iterate_serial _print_serial "$in_port"
}

# READ USB DEVICE STATUS
_print_usb_access() {
    write_string_param "input_usb_owner" "$1"
    write_string_param "input_usb_group" "$2"
    write_string_param "list_usb_mode" "${3:-default}"
}
_print_usb() {
    local num="$1"
    local vendor_id="$2"
    local prod_id="$3"
    local serial="$4"
	local class="$5"
	local subclass="$6"
	local proto="$7"
    local info="$8"
    local access="$9"

	local filter="${10:-}"

	[ -z "$filter" -o "$filter" = "$num" ] || \
		return 0

    write_string_param "input_usb_vendor_id" "$vendor_id"
    write_string_param "input_usb_product_id" "$prod_id"
    write_string_param "input_usb_serial" "$serial"
    write_string_param "list_usb_class" "$class"
    write_string_param "list_usb_subclass" "$subclass"
    write_string_param "list_usb_protocol" "$proto"

    portsctrl_parse_access _print_usb_access "$access"
}
print_usb() {
    if [ -z "$in_num" ]; then
        write_error "USB rule number isn't specified."
        return 1
    fi

    # Check config file exists:
    portsctrl_config_exists || return 0

    portsctrl_iterate_usb _print_usb "$in_num"
}

# READ CONTROL STATUS FOR USB & SERIAL
print_status () {
    # Check config file exists:
    if ! portsctrl_config_exists; then
        write_bool_param "serial_ctrl_on" 0
        write_bool_param "usb_ctrl_on" 0
        return
    fi

    # Create subshell & include config file.
    (
        . "$CONFIG"

        if shell_var_is_yes "$SERIAL_CONTROL"; then
            write_bool_param "serial_ctrl_on" 1
        else
            write_bool_param "serial_ctrl_on" 0
        fi

        if shell_var_is_yes "$USB_CONTROL"; then
            write_bool_param "usb_ctrl_on" 1
        else
            write_bool_param "usb_ctrl_on" 0
        fi

        if shell_var_is_yes "$USB_ALLOW_HID"; then
            write_bool_param "usb_hid" 1
        else
            write_bool_param "usb_hid" 0
        fi
    )
}

# CHECK CONFIG EVAILABLE
assert_config () {
    if ! portsctrl_config_exists; then
        # Create config:
        if ! touch "$CONFIG"; then
            write_error "Can't create config file: ($CONFIG)"
            return 1
        else
            echo "[backend-portsctrl] new config file created ($CONFIG)" 1>&2
        fi
    fi
}

# TURN ON/OFF CONTROL
control_ports() {
    # Validate invocation.
    if [ -z "$in_subsys" -o -z "$in_enabled" ]; then
        write_error "Subsystem and the control flag should be specified."
        return 1
    fi

    local status=
    if [ "$in_enabled" = "#t" ]; then
        status="yes"
    else
        status="no"
    fi

    # Check config file exists:
    assert_config || return $?

    # Update config
    local subsys="$(echo "$in_subsys" | tr '[:lower:]' '[:upper:]')"
    shell_config_set "$CONFIG" "${subsys}_CONTROL" "\"$(quote_shell "$status")\""

    # Apply changes:
    apply_changes

    # Enforce changes in udisks2:
    service_control 'udevd' stop
    service_control 'udisks2' restart
    service_control 'udevd' start
}


# NEW SERIAL PORT PARAMETERS
write_serial_options() {
    # Validate the invocation:
    if [ -z "$in_port" -o -z "$in_enabled" ]; then
        write_error "Port and the control flag should be specified."
        return 1
    fi

    # Check config file exists:
    assert_config || return $?

    # Create subshell & include config file
    (
        . "$CONFIG"

        # Remove any occurrence of the port:
        shell_config_set "$CONFIG" 'SERIAL_DISABLED' "($(for i in "${SERIAL_DISABLED[@]}"; do [ "$in_port" = "${i%%;*}" ] && continue; echo -n "\"$(quote_shell "$i")\" "; done))"
        shell_config_set "$CONFIG" 'SERIAL_ENABLED' "($(for i in "${SERIAL_ENABLED[@]}"; do [ "$in_port" = "${i%%;*}" ] && continue; echo -n "\"$(quote_shell "$i")\" "; done))"

        # Re-read config:
        . "$CONFIG"

        desc="$in_port"
        if [ -n "$in_owner" -o -n "$in_group" -o -n "$in_mode" ]; then
            desc="$desc;[$in_owner:$in_group:$in_mode]"
        fi

        # Add port to the list:
        if shell_var_is_yes "$in_enabled"; then
            shell_config_set "$CONFIG" 'SERIAL_ENABLED' "($(for i in "${SERIAL_ENABLED[@]}" "$desc"; do echo -n "\"$(quote_shell "$i")\" "; done))"
            echo "Enabled access to serial port(s) $desc." | logger -p user.warning
        elif shell_var_is_no "$in_enabled"; then
            shell_config_set "$CONFIG" 'SERIAL_DISABLED' "($(for i in "${SERIAL_DISABLED[@]}" "$desc"; do echo -n "\"$(quote_shell "$i")\" "; done))"
            echo "Disabled access to serial port(s) $desc." | logger -p user.warning
        fi
    )

    # Apply the changes:
    apply_changes
}


# REMOVE ENTRY FROM WHITE LIST
_usb_rm_rule() {
    local num="$1"

    portsctrl_config_exists || return

    # Create subshell & include the config file:
    (
        . "$CONFIG"

        USB_WHITE_LIST[$num]=
        shell_config_set "$CONFIG" 'USB_WHITE_LIST' "($(for i in "${USB_WHITE_LIST[@]}"; do test -z "$i" && continue; echo -n "\"$(quote_shell "$i")\" "; done))"
    )
}
usb_rm_rule() {
    # Validate the invocation:
    if [ -z "$in_rule_num" ]; then
        write_error "Rule number should be specified."
        return 1
    fi

    # Check config file exists:
    assert_config || return $?

    # Remove the rule:
    _usb_rm_rule "$in_rule_num"

    # Apply the changes:
    apply_changes
}


# ADD OR MODIFY USB WHITE LIST ENTRY
usb_save_rule() {
    # Validate the invocation:
    if [ -z "$in_vendor_id$in_product_id$in_serial$in_class$in_subclass$in_protocol" ]; then
        write_error "No values specified — nothing to add!"
        return 1
    fi

    # Check config file exists:
    assert_config || return $?

    # Delete existing rule if rule number is specified:
	if [ -n "${in_num:-}" ]; then
		_usb_rm_rule "$in_num"
	fi

    # Create subshell & include the config file:
    (
        . "$CONFIG"

        if [ "$in_mode" = 'default' ]; then
            in_mode=
        fi

        rule="$in_vendor_id;$in_product_id;$in_serial;${in_class:+C$in_class;}${in_subclass:+S$in_subclass;}${in_protocol:+P$in_protocol;}$in_info"

        if [ -n "$in_owner" -o -n "$in_group" -o -n "$in_mode" ]; then
            rule="$rule;[$in_owner:$in_group:$in_mode]"
        fi

        shell_config_set "$CONFIG" 'USB_WHITE_LIST' "($(for i in "${USB_WHITE_LIST[@]}" "$rule"; do echo -n "\"$(quote_shell "$i")\" "; done))"

        echo "(Re)Configured USB port: $rule." | logger -p user.warning
    )

    # Apply the changes:
    apply_changes
}


# Control USB HID devices
usb_hid() {
    # Check config file exists:
    assert_config || return $?

    # Update the config:
    if test_bool "$in_allow_hid"; then
        allow='yes'
    else
        allow='no'
    fi

    shell_config_set "$CONFIG" "USB_ALLOW_HID" "\"$allow\""

    # Apply the changes:
    apply_changes
}


# Add present USB device to whitelist
usb_add_prsnt() {
	local dir="$in_dev_path"

	local idVendor="$(cat_e "$dir/idVendor")"
	local idProduct="$(cat_e "$dir/idProduct")"
	local serial="$(cat_e "$dir/serial")"

	local bDeviceClass="$(cat_e "$dir/bDeviceClass")"
	local bDeviceSubClass="$(cat_e "$dir/bDeviceSubClass")"
	local bDeviceProtocol="$(cat_e "$dir/bDeviceProtocol")"

	local bInterfaceClass="$(cat_e "$dir/bInterfaceClass")"
	local bInterfaceSubClass="$(cat_e "$dir/bInterfaceSubClass")"
	local bInterfaceProtocol="$(cat_e "$dir/bInterfaceProtocol")"

    in_vendor_id="$idVendor" \
	in_product_id="$idProduct" \
	in_serial="$serial" \
	in_class="${bDeviceClass:-$bInterfaceClass}" \
	in_subclass="${bDeviceSubClass:-$bInterfaceSubClass}" \
	in_protocol="${bDeviceProtocol:-$bInterfaceProtocol}" \
	in_info="$(get_usb_product_name "$idVendor" "$idProduct")" \
	usb_save_rule
}

list_usb_classes() {
	if [ -r "$USB_IDS" ]; then
		sed -n -e 's/^C[[:space:]]\+\([0-9a-f][0-9a-f]\)[[:space:]]\+\([^#]*\)\(#.*\)\?$/\1\t\2/p' "$USB_IDS" | write_enum
	fi
}

list_usb_subclasses() {
	local class="${1:-$in_class}"
	if [ -r "$USB_IDS" ]; then
		sed -n -e "/^C[[:space:]]\\+$class[[:space:]]/,/^[^\\t]/ {
			s/^\\t\\([0-9a-f][0-9a-f]\\)[[:space:]]\\+\\([^#]*\\)\\(#.*\\)\\?\$/\\1\\t\\2/p;
		}" "$USB_IDS" | write_enum
	fi
}

list_usb_protocols() {
	local class="${1:-$in_class}"
	local subclass="${2:-$in_subclass}"
	if [ -r "$USB_IDS" ]; then
		sed -n -e "/^C[[:space:]]\\+$class[[:space:]]/,/^[^\\t]/ {
			/^\\t$subclass[[:space:]]/,/^\\([^\\t]\\|\\t[^\\t]\\)/ {
				s/^\\t\\t\\([0-9a-f][0-9a-f]\\)[[:space:]]\\+\\([^#]*\\)\\(#.*\\)\\?\$/\\1\\t\\2/p;
			}
		}" "$USB_IDS" | write_enum
	fi
}

# MAIN LOOP
on_message() {
  case "$in_action" in

    read)

      case "$in__objects" in
        serial_port)
          # READ SERIAL PORT STATUS
          echo "[backend-portsctrl] Read serial port settings." >&2
          print_serial
          ;;

        usb_device)
          # READ USB DEVICE STATUS
          echo "[backend-portsctrl] Read USB device settings." >&2
          print_usb
          ;;

        status)
          # READ CONTROL STATUS FOR USB & SERIAL
          echo "[backend-portsctrl] Read the status." >&2
          print_status
          ;;

        *)
          # UNDEFINED READ REQUEST
          write_error "Undefined read request: $in__objects." >&2
          ;;
      esac
      ;;

    write)

      case "$in__objects" in
        ctrl)
          # TURN ON/OFF CONTROL
          echo "[backend-portsctrl] Erite ports control status." >&2
          control_ports
          ;;

        serial)
          # NEW SERIAL PORT PARAMETERS
          echo "[backend-portsctrl] Write down serial port options." >&2
          write_serial_options
          ;;

        usb_rm)
          # REMOVE ENTRY FROM WHITE LIST
          echo "[backend-portsctrl] Remove rule from the whitelist." >&2
          usb_rm_rule
          ;;

        usb_save)
          # ADD NEW ENTRY TO WHITE LIST
          echo "[backend-portsctrl] Add/save USB whitelist rule." >&2
          usb_save_rule
          ;;

        usb_add_prsnt)
          # ADD PRESENT USB DEVICE TO WHITE LIST
          echo "[backend-portsctrl] Add present USB device to the whitelist." >&2
          usb_add_prsnt
          ;;

        usb_hid)
          # CONTROL USB HID
          echo "[backend-portsctrl] Save USB HID status." >&2
          usb_hid
          ;;

        *)
          # UNDEFINED WRITE REQUEST
          write_error "Undefined write request: $in__objects." >&2
          ;;

      esac
      ;;

    list)

      case "$in__objects" in

        list_serial)
          # LIST SERIAL PORTS
          echo "[backend-portsctrl] List serial ports." >&2
          list_serial
          ;;

        list_usb_rules)
          # LIST USB WHITE LIST
          echo "[backend-portsctrl] List USB whitelist rules." >&2
          list_usb_rules
          ;;

        list_prsnt_devices)
          # LIST PRESENT USB DEVICES
          echo "[backend-portsctrl] List present devices." >&2
          list_prsnt_devices
          ;;

		list_usb_classes)
		  list_usb_classes
		  ;;

		list_usb_subclasses)
		  list_usb_subclasses
		  ;;

		list_usb_protocols)
		  list_usb_protocols
		  ;;

        *)
          # UNDEFINED LIST REQUEST
          write_error "Undefined list request: $in__objects." >&2
          ;;

      esac
      ;;

    *)
      # UNDEFINED ACTION
      write_error "Undefined list request: $in_action." >&2
      ;;
  esac
}

message_loop

# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=sh
