#!/usr/bin/env bash
# dependent: bash curl coreutils-cksum getopt
#
# A random MAC address generator
# Author: muink
# Github: https://github.com/muink/rgmac
#

# Init
VERSION=1.5.1

#
OUIURL='https://www.wireshark.org/download/automated/data/manuf'
OUIFILE=/usr/share/rgmac/oui.txt
#
VENDORDIR=/usr/share/rgmac/Vendor/
pushd $VENDORDIR >/dev/null && VENDOR=`ls -1 *.txt` && popd >/dev/null

# Get OS type
OS="`uname`" # just Linux no Darwin

# Get options
GETOPT=$(getopt -n $(basename $0) -o a:ut:s:e:l::UV -l format:,upcase,device:,assign:,query:,list::,update,version,help -- "$@")
[ $? -ne 0 ] && >&2 echo -e "\tUse the --help option get help" && exit 1
eval set -- "$GETOPT"
OPTIONS=$(sed "s|'[^']*'||g; s| -- .+$||; s| --$||" <<< "$GETOPT")

# Duplicate options
for ru in --help\|--help -V\|--version -U\|--update -l\|--list -e\|--query -s\|--assign -t\|--device -u\|--upcase -a\|--format; do
  eval "grep -qE \" ${ru%|*}[ .+]* ($ru)| ${ru#*|}[ .+]* ($ru)\" <<< \"\$OPTIONS\" && >&2 echo \"\$(basename \$0): Option '\$ru' option is repeated\" && exit 1"
done
# Independent options
for ru in --help\|--help -V\|--version -U\|--update -l\|--list -e\|--query; do
  eval "grep -qE \"^ ($ru) .+|.+ ($ru) .+|.+ ($ru) *\$\" <<< \"\$OPTIONS\" && >&2 echo \"\$(basename \$0): Option '\$(sed -E \"s,^.*($ru).*\$,\\1,\" <<< \"\$OPTIONS\")' cannot be used with other options\" && exit 1"
done
# Conflicting options
echo "$OPTIONS" | grep -E " (-s|--assign)\b" | grep -E " (-t|--device)\b" >/dev/null && >&2 echo "$(basename $0): Option '-s|--assign' cannot be used with option '-t|--device'" && exit 1



# Sub function
_help() {
printf "\n\
Usage: rgmac [OPTION]... \n\
A random MAC address generator\n\
\n\
  e.g. rgmac                          -- Locally administered address (Like WiFi MAC Randomization)\n\
  e.g. rgmac -us 3C:E0:72             -- Make a Apple MAC with Uppercase (Fake MAC)\n\
  e.g. rgmac -ac -t console:Sony      -- Make a SonyPS MAC (Fake MAC)\n\
\n\
Options:\n\
  -a, --format <outformat>            -- Format for MAC output\n\
  -u, --upcase                        -- Uppercase MAC output\n\
  -s, --assign <xx:xx:xx>             -- Specify OUI manually\n\
  -e, --query <xx:xx:xx>              -- Query the OUI of the MAC address\n\
  -t, --device <VendorType:NameID>    -- Use IEEE public OUI, See 'Vendor/<VendorType>.txt'\n\
  -l, --list[VendorType]              -- List valid VendorType and NameID\n\
  -U, --update                        -- Update locale OUI database\n\
  -V, --version                       -- Returns version\n\
  --help                              -- Returns help info\n\
\n\
OptFormat:\n\
  <xx:xx:xx>    Valid: 06fcee, 06-fc-ee, 06:fc:ee, 06fcee5f3355, 06-fc-ee-5f-33-55, 06:fc:ee:5f:33:55\n\
  <outformat>   Valid: (C)olon, (D)ash\n\
\n"
}

_version() {
  echo "$(basename $0) version: v$VERSION"
}

update_oui() {
  curl -LSso "$OUIFILE" "$OUIURL"
}

## randhex <str length>
#randhex() {
#  local length=$[ $1 + 0 ]
#  [ "$length" -ge "1" ] || length=1
#  local round=$[ $length / 64 + 1 ]
#  echo $(for _ in $(seq 1 $round); do echo $(cat /dev/urandom 2>/dev/null | head -n 8 | sha256sum) | sed 's|[^a-zA-Z0-9]||g'; done) | sed 's|\s||g' | cut -c 1-$length
#}

# randhex <[1-32]>
randhex() {
  echo $(cat /dev/urandom 2>/dev/null | head -n 8 | md5sum | cut -c 1-${1})
}

# randnum <min> <max>
randnum() {
    local min=$1
    local max=$[ $2 - $min + 1 ]
    local num=$(cat /dev/urandom 2>/dev/null | head -n 8 | cksum | cut -f1 -d' ')
    echo $[ $num % $max + $min ]
}

# pick_oui <VendorType:NameID>
pick_oui() {
  grep -qEi "^.+:.+$" <<< "$1" || return 1
  local VENDOR="$VENDORDIR${1%%:*}.txt"
  local NAME="${1##*:}"
  local ID

  if [ -e "$VENDOR" ]; then
    ID=$(cat "$VENDOR" | cut -f1 | grep -wn "$NAME" | cut -f1 -d':')
    [ -z "$ID" ] && return 1
  else return 1;
  fi

  local rawpar=$(sed -n "${ID}p" "$VENDOR" | cut -f2-)
  local two=$(printf "$rawpar" | sed -En "s|\t+|\n|g p" | sed -n "/^2=.*/ s|2=|| p")
  local three=$(printf "$rawpar" | sed -En "s|\t+|\n|g p" | sed -n "/^3=.*/ s|3=|| p" | sed -n 's|^"||; s|"$|| p')
  local str par
  for par in two three; do
    if   [ -z "$str" ]; then str=${!par};
    elif [ -n "${!par}" ]; then str="$str|${!par}"
    fi
  done

  local ouilist=$(grep -E "$str" "$OUIFILE" | sed -En '/^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){2}\s+/ p' | cut -f1)
  local listcount=$(grep -E "$str" "$OUIFILE" | sed -En '/^[[:xdigit:]]{2}(:[[:xdigit:]]{2}){2}\s+/ p' | sed -n '$=')
  echo $ouilist | cut -f$(randnum 1 $listcount) -d' '
}



# Main
#   +-----------+-----------+-----------+-----------+-----------+-----------+-----------+
#   |  device   |  assign   |   query   |   list    |  update   |  version  |   help    |
#   +-----------+-----------+-----------+-----------+-----------+-----------+-----------+
# Get options
while [ -n "$1" ]; do
  case "$1" in
    --help)
      _help
      exit
    ;;
    -V|--version)
      _version
      exit
    ;;
    -U|--update)
      update_oui && exit
      [ $? -ne 0 ] && >&2 echo "$(basename $0): OUI database update failed" && exit 1
    ;;
    -l|--list)
      if [ -n "$2" ]; then
        if [ "$(grep -E "\b$2.txt\b" <<< "$VENDOR")" ]; then
          cat "$VENDORDIR$2.txt" | cut -f1
        else 
          >&2 echo -e "$(basename $0): Option '$1' requires a valid argument\n\tUse the -l/--list option list them" && exit 1
        fi
      else
        for l in $VENDOR; do echo "${l%.txt}"; done
      fi
      exit
    ;;
    -e|--query)
      QUERY=$(echo "${2,,}" | grep -Ei "^([0-f]{6}|[0-f]{12})$|^[0-f]{2}((-[0-f]{2}){2}|(-[0-f]{2}){5})$|^[0-f]{2}((:[0-f]{2}){2}|(:[0-f]{2}){5})$" | sed 's|[:-]||g' | cut -c-6)
      [ -z "$QUERY" ] && >&2 echo -e "$(basename $0): Option '$1' requires a valid argument\n\tUse the --help option get help" && exit 1
      if   [ "$[ 0x${QUERY:0:2} & 0x01 ]" -eq "$[0x01]" ]; then
        >&2 echo -e "$(basename $0): Option '$1' requires a valid unicast MAC address\n\tUse the --help option get help" && exit 1
      elif [ "$[ 0x${QUERY:0:2} & 0x02 ]" -eq "$[0x02]" ]; then
        echo "$2: Locally administered address" && exit
      else
        echo "$2: Globally unique address"
        GLOBALLY=$(grep -i "^${QUERY:0:2}:${QUERY:2:2}:${QUERY:4:2}" "$OUIFILE")
        if [ -n "$GLOBALLY" ]; then
          echo "Record: $GLOBALLY"
          exit
        else
          echo "No matching record found"
          exit
        fi
      fi
    ;;
    -s|--assign)
      ASSIGN=$(echo "${2,,}" | grep -Ei "^([0-f]{6}|[0-f]{12})$|^[0-f]{2}((-[0-f]{2}){2}|(-[0-f]{2}){5})$|^[0-f]{2}((:[0-f]{2}){2}|(:[0-f]{2}){5})$" | sed 's|[:-]||g' | cut -c-6)
      [ -z "$ASSIGN" ] && >&2 echo -e "$(basename $0): Option '$1' requires a valid argument\n\tUse the --help option get help" && exit 1
      shift
    ;;
    -t|--device)
      DEVICE=`pick_oui "$2" | sed 's|[:-]||g'`
      [ -z "$DEVICE" ] && >&2 echo -e "$(basename $0): Option '$1' requires a valid argument\n\tUse the --help option get help" && exit 1
      shift
    ;;
    -a|--format)
      LINKSYM=$(echo "$2" | sed -En "/^c$|^d$/ {s|^c$|:|; s|^d$|-|; p}")
      [ -z "$LINKSYM" ] && >&2 echo -e "$(basename $0): Option '$1' requires a valid argument\n\tUse the --help option get help" && exit 1
      shift
    ;;
    -u|--upcase)
      UPPER=true
    ;;
    --)
      shift
      break
    ;;
    *)
      >&2 echo -e "$(basename $0): '$1' is not an option\n\tUse the --help option get help"
      exit 1
    ;;
  esac
  shift
done
# Get parameters
# ...

# Output
[ ! -e "$OUIFILE" ] && >&2 echo -e "$(basename $0): OUI database not exist, please update DB first\n\tUse the --help option get help" && exit 1
if   [ -n "$ASSIGN" ]; then OUI=$ASSIGN;
elif [ -n "$DEVICE" ]; then OUI=$DEVICE;
else OUI=$(printf %x $[ 0x$(randhex 2) & 0xFE | 0x02 ] | sed -E 's|^([0-9a-fA-F])$|0\1|')$(randhex 4); # ccccccLI
fi
NIC=`randhex 6`
OUTPUT=${OUI:0:2}$LINKSYM${OUI:2:2}$LINKSYM${OUI:4:2}$LINKSYM${NIC:0:2}$LINKSYM${NIC:2:2}$LINKSYM${NIC:4:2}

[ "$UPPER" == "true" ] && echo ${OUTPUT^^} || echo ${OUTPUT,,}
