meta-google: network-sh: Add library

Provides a utility for working with network type data. Right now this is
just for MAC address parsing and conversion to EUI{48,64}.

Change-Id: I49946d8147f1c7b10cfe3a9e55b20fc30c083eda
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/meta-google/recipes-google/networking/network-sh.bb b/meta-google/recipes-google/networking/network-sh.bb
new file mode 100644
index 0000000..a377b9e
--- /dev/null
+++ b/meta-google/recipes-google/networking/network-sh.bb
@@ -0,0 +1,23 @@
+SUMMARY = "Shell functions for manipulating network addresses"
+PR = "r1"
+LICENSE = "Apache-2.0"
+LIC_FILES_CHKSUM = "file://${COREBASE}/meta/files/common-licenses/Apache-2.0;md5=89aea4e17d99a7cacdbeed46a0096b10"
+
+FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"
+SRC_URI += "file://lib.sh"
+SRC_URI += "file://test.sh"
+S = "${WORKDIR}"
+
+DATA = "${datadir}/network"
+FILES_${PN} += "${DATA}"
+
+DEPENDS += "test-sh"
+
+do_compile() {
+  SYSROOT="$PKG_CONFIG_SYSROOT_DIR" bash test.sh || exit
+}
+
+do_install_append() {
+  install -d -m0755 ${D}${DATA}
+  install -m0644 lib.sh ${D}${DATA}/
+}
diff --git a/meta-google/recipes-google/networking/network-sh/lib.sh b/meta-google/recipes-google/networking/network-sh/lib.sh
new file mode 100644
index 0000000..f37f719
--- /dev/null
+++ b/meta-google/recipes-google/networking/network-sh/lib.sh
@@ -0,0 +1,106 @@
+#!/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.
+
+[ -n "${network_init-}" ] && return
+
+mac_to_bytes() {
+  local -n bytes="$1"
+  local str="$2"
+
+  # Verify that the MAC is Valid
+  [[ "$str" =~ ^[[:xdigit:]]{1,2}(:[[:xdigit:]]{1,2}){5}$ ]] || return
+
+  # Split the mac into hex bytes
+  local oldifs="$IFS"
+  IFS=:
+  local byte
+  for byte in $str; do
+    bytes+=(0x$byte)
+  done
+  IFS="$oldifs"
+}
+
+mac_to_eui48() {
+  local mac_bytes=()
+  mac_to_bytes mac_bytes "$1" || return
+
+  # Return the EUI-64 bytes in the IPv6 format
+  printf '%02x%02x:%02x%02x:%02x%02x\n' "${mac_bytes[@]}"
+}
+
+mac_to_eui64() {
+  local mac_bytes=()
+  mac_to_bytes mac_bytes "$1" || return
+
+  # Using EUI-64 conversion rules, create the suffix bytes from MAC bytes
+  # Invert bit-0 of the first byte, and insert 0xfffe in the middle.
+  local suffix_bytes=(
+    $((mac_bytes[0] ^ 1))
+    ${mac_bytes[@]:1:2}
+    $((0xff)) $((0xfe))
+    ${mac_bytes[@]:3:3}
+  )
+
+  # Return the EUI-64 bytes in the IPv6 format
+  printf '%02x%02x:%02x%02x:%02x%02x:%02x%02x\n' "${suffix_bytes[@]}"
+}
+
+ipv6_pfx_concat() {
+  local pfx="$1"
+  local sfx="$2"
+
+  # Validate the prefix
+  if ! [[ "$pfx" =~ ^(([0-9a-fA-F]{1,4}:)+):/([0-9]+)$ ]]; then
+    echo "Invalid IPv6 prefix: $pfx" >&2
+    return 1
+  fi
+  local addr="${BASH_REMATCH[1]}"
+  local cidr="${BASH_REMATCH[3]}"
+  # Ensure prefix doesn't have too many bytes
+  local nos="${addr//:/}"
+  if (( ${#addr} - ${#nos} > (cidr+7)/16 )); then
+    echo "Too many prefix bytes: $pfx" >&2
+    return 1
+  fi
+
+  # Validate the suffix
+  if ! [[ "$sfx" =~ ^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4})*$ ]]; then
+    echo "Invalid IPv6 suffix: $sfx" >&2
+    return 1
+  fi
+  # Ensure suffix doesn't have too many bytes
+  local nos="${sfx//:/}"
+  if (( ${#sfx} - ${#nos} >= (128-cidr)/16 )); then
+    echo "Too many suffix bytes: $sfx" >&2
+    return 1
+  fi
+
+  local comb="$addr:$sfx"
+  local nos="${comb//:/}"
+  if (( ${#comb} - ${#nos} == 8 )); then
+    comb="$addr$sfx"
+  fi
+  echo "$comb/$cidr"
+}
+
+ipv6_pfx_to_cidr() {
+  [[ "$1" =~ ^[0-9a-fA-F:]+/([0-9]+)$ ]] || return
+  echo "${BASH_REMATCH[1]}"
+}
+
+network_init=1
+return 0 2>/dev/null
+echo "network is a library, not executed directly" >&2
+exit 1
diff --git a/meta-google/recipes-google/networking/network-sh/test.sh b/meta-google/recipes-google/networking/network-sh/test.sh
new file mode 100755
index 0000000..57387c4
--- /dev/null
+++ b/meta-google/recipes-google/networking/network-sh/test.sh
@@ -0,0 +1,84 @@
+#!/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.
+
+cd "$(dirname "$0")"
+if [ -e ../network-sh.bb ]; then
+  source '../../test/test-sh/lib.sh'
+else
+  source "$SYSROOT/usr/share/test/lib.sh"
+fi
+source lib.sh
+
+test_mac_to_bytes() {
+  out=()
+  expect_err 1 mac_to_bytes out ''
+  expect_err 1 mac_to_bytes out '00'
+  expect_err 1 mac_to_bytes out '12:34:56:78:90:'
+  expect_err 1 mac_to_bytes out ':12:34:56:78:90'
+  expect_err 1 mac_to_bytes out '12:34:56:78:90:0:'
+  expect_err 1 mac_to_bytes out '12:34:56:78:90:0:2'
+
+  expect_err 0 mac_to_bytes out 'a2:0:f:de:0:29'
+  expected=(0xa2 0 0xf 0xde 0 0x29)
+  for (( i=0; i < ${#expected[@]}; ++i )); do
+    expect_numeq "${out[$i]}" "${expected[$i]}"
+  done
+}
+
+test_mac_to_eui_48() {
+  str="$(mac_to_eui48 '12:34:56:78:90:af')" || fail
+  expect_streq "$str" '1234:5678:90af'
+}
+
+test_eui_64() {
+  str="$(mac_to_eui64 '12:34:56:78:90:af')" || fail
+  expect_streq "$str" '1334:56ff:fe78:90af'
+}
+
+test_ipv6_pfx_concat() {
+  # Invalid inputs
+  expect_err 1 ipv6_pfx_concat 'fd/64' '1234:5678:90af'
+  expect_err 1 ipv6_pfx_concat 'fd01::' '1234:5678:90af'
+  expect_err 1 ipv6_pfx_concat 'fd01:' '1234:5678:90af'
+  expect_err 1 ipv6_pfx_concat 'fd01::/a0' '1234:5678:90af'
+  expect_err 1 ipv6_pfx_concat 'fd01::/64' ':1234:5678:90af'
+  expect_err 1 ipv6_pfx_concat 'fd01::/64' '::'
+
+  # Too many address bits
+  expect_err 1 ipv6_pfx_concat 'fd01:1:1:1:1::/64' '1234:5678:90af'
+  expect_err 1 ipv6_pfx_concat 'fd01::/64' '1:0:1234:5678:90af'
+  expect_err 1 ipv6_pfx_concat 'fd01::/65' '1:1234:5678:90af'
+  expect_err 1 ipv6_pfx_concat 'fd01::/72' '1:1234:5678:90af'
+
+  str="$(ipv6_pfx_concat 'fd01::/64' '1')" || fail
+  expect_streq "$str" 'fd01::1/64'
+  str="$(ipv6_pfx_concat 'fd01::/72' '1234:5678:90af')" || fail
+  expect_streq "$str" 'fd01::1234:5678:90af/72'
+  str="$(ipv6_pfx_concat 'fd01:eeee:aaaa:cccc::/64' 'a:1234:5678:90af')" || fail
+  expect_streq "$str" 'fd01:eeee:aaaa:cccc:a:1234:5678:90af/64'
+}
+
+test_ipv6_pfx_to_cidr() {
+  expect_err 1 ipv6_pfx_to_cidr 'z/64'
+  expect_err 1 ipv6_pfx_to_cidr '64'
+
+  cidr="$(ipv6_pfx_to_cidr 'fd01::/64')" || fail
+  expect_numeq "$cidr" 64
+  cidr="$(ipv6_pfx_to_cidr 'fd01:eeee:aaaa:cccc:a:1234:5678:90af/128')" || fail
+  expect_numeq "$cidr" 128
+}
+
+return 0 2>/dev/null
+main