#!/bin/sh -efu
### This file is covered by the GNU General Public License,
### which should be included with libshell as the file LICENSE.
### All copyright information are listed in the COPYING.

if [ -z "${__included_shell_ip_address-}" ]; then
__included_shell_ip_address=1

. shell-var

### Regexp for single byte
readonly regex_byte='([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])'

### Regexp for 4-byte address
readonly regex_ipaddr="$regex_byte(\.$regex_byte){3}"

### Regexp for IPv4 address
###
### (http://en.wikipedia.org/wiki/IP_address)
###
### Some first-octet values have special meanings:
###
### * First octet 127 represents the local computer, regardless of what network
###   it is really in. This is useful when testing internal operations.
###
### * First octet 224 and above are reserved for special purposes such as
###   multicasting.
###
### Octets 0 and 255 are not acceptable values in some situations, but 0 can be used
### as the second and/or third octet (e.g. 10.2.0.100).
###
readonly __regex_fbyte='([1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])'
readonly __regex_sbyte='([1]?[0-9][0-9]?|2[0-4][0-9]|25[0-4])'
readonly __regex_lbyte='([1]?[0-9][0-9]?|2[0-4][0-9]|25[0-4])'

readonly regex_ipv4="${__regex_fbyte}(\.${__regex_sbyte}){2}\.${__regex_lbyte}"

### Checks that given option value is a valid IPv4 address.
valid_ipv4()
{
	local ipaddr="$1"
	local i=0 byte

	byte="${ipaddr##*.}"
	ipaddr="${ipaddr%.$byte}"

	[ "$byte" -gt 0 ] && [ "$byte" -lt 255 ] 2>/dev/null ||
			return 1

	while [ $i -lt 3 ]; do
		byte="${ipaddr##*.}"

		[ "$byte" != "$ipaddr" ] ||
			break

		ipaddr="${ipaddr%.$byte}"

		[ "$byte" -ge 0 ] && [ "$byte" -lt 255 ] 2>/dev/null ||
			return 1

		i=$(($i+1))
	done

	[ $i -eq 2 ] &&
		[ "$byte" -ne 127 ] && [ "$byte" -gt 0 ] && [ "$byte" -lt 224 ] 2>/dev/null ||
		return 1
}

__ipv4_hex()
{
	[ -n "${1-}" ] ||
		return 2

	local IFS=.
	set -- $1

	local i=0
	for b; do
		[ "$b" -ge 0 ] && [ "$b" -le 255 ] 2>/dev/null ||
			return 2
		i=$(($i + 1))
	done

	[ "$i" -eq 4 ] ||
		return 2

	printf '0x'
	printf '%02x' "$@"
}

__len2mask_32()
{
	local len
	len="${1-}"

	[ -n "$len" ] ||
		return 2

	local mask
	local pos=0 n nbits
	while [ "$len" -lt 32 ]; do
		n=$(( ($len | 7) + 1 - $len ))
		nbits=$(((1 << $n) - 1))
		len=$(($len + $n))
		pos=$(( ($pos << $n) | $nbits ))
	done
	mask=$((0xFFFFFFFF - $pos))
	printf '%s' "$mask"
}

### Checks that IPv4 address is in subnet
### Usage example:
### ipv4_ip_subnet 172.16.1.2 172.16.1.0/24; echo res=$?
### res=0
###
### ipv4_ip_subnet 172.16.3.2 172.16.1.0/24; echo res=$?
### res=1
ipv4_ip_subnet()
{
	local ip net prefix
	ip="${1-}"; shift
	net="${1-}"; shift
	prefix="${net##*/}"

	[ -n "$prefix" ] && [ "$prefix" -ge 0 ] 2>/dev/null ||
		return 2

	local hex_addr hex_net hex_mask p

	hex_addr="$(__ipv4_hex "$ip")" &&
	hex_net="$(__ipv4_hex "${net%%/*}")" ||
		return 2

	p=$((0xFFFFFFFF))
	hex_mask="$(($p - ($p >> $prefix)))"
	[ "$(($hex_net & $hex_mask))" -eq "$(($hex_addr & $hex_mask))" ] ||
		return 1
}

### Convert netmask to routing prefix.
### Usage example:
### ipv4_mask2prefix 255.255.0.0
### 16
###
### ipv4_prefix2mask 255.255.255.0
### 24
ipv4_mask2prefix()
{
	local hex_mask
	hex_mask="$(__ipv4_hex "${1-}")" ||
		return 2

	local p i=0 prefix=''

	p=$((~$hex_mask & 0xFFFFFFFF))

	while [ "$p" -ne 0 ]; do
		p=$(($p >> 1 & 0xFFFFFFFF))
		i=$(($i + 1))
	done
	prefix=$((32 - $i))

	[ "$prefix" -ge 0 ] && [ "$prefix" -le 32 ] ||
		return 1
	echo "$prefix"
}

### Convert routing prefix to netmask.
### Usage example:
### ipv4_prefix2mask 16
### 255.255.0.0
###
### ipv4_prefix2mask 24
### 255.255.255.0
ipv4_prefix2mask()
{
	local len mask
	len="${1-}"

	[ "$len" = 0 ] || shell_var_is_number "$len" ||
		return 1
	[ "$len" -ge 0 ] && [ "$len" -le 32 ] ||
		return 1

	mask="$(__len2mask_32 "$len")"

	printf '%s.%s.%s.%s\n' \
	    "$(($mask >> 24 & 0xFF))" \
	    "$(($mask >> 16 & 0xFF))" \
	    "$(($mask >> 8  & 0xFF))" \
	    "$(($mask       & 0xFF))"
}

### Outputs the serialized address in network byte order
### as 8 hexadecimal digits.
ipv4_ptonx()
{
	local repr="$1"
	local octets=

	local i d
	i=0
	while :; do
		d="${repr%%.*}"
		[ -n "$d" ] ||
			return 1
		[ "$d" = 0 ] || shell_var_is_number "$d" ||
			return 1
		[ "$d" -le 255 ] ||
			return 1
		octets="$octets$(printf %02x $d)"
		repr="${repr#$d}"
		[ -n "$repr" ] || [ "$i" -ne 3 ] ||
			break
		[ -z "${repr##.*}" ] ||
			return 1
		i=$(($i + 1))
		repr="${repr#.}"
		[ "$i" -lt 4 ] ||
			return 1
	done
	printf '%s\n' "$octets"
	return 0
}

### Outputs the serialized address in network byte order
### as 32 hexadecimal digits.
### Bitwise operations are easy enough to implement over this
### address representation.
ipv6_ptonx()
{
	local repr="$1"
	local skip=''
	local pref='' impz='' suff=''
	[ -n "$repr" ] ||
		return 1
	[ -z "${repr##??*}" ] ||
		return 1
	[ -n "${repr##:[!:]*}" ] ||
		return 1

	local i d
	i=0
	while :; do
		if [ -z "${repr##::*}" ]; then
			# Encountered '::'.
			[ -z "$skip" ] ||
				return 1
			skip=1
			repr="${repr#::}"
			[ -n "$repr" ] ||
				break

			i=$(($i + 1))
			[ "$i" -lt 8 ] ||
				return 1
			continue
		fi
		# Did not encounter '::'.
		repr="${repr#:}"

		d="${repr%%:*}"
		repr="${repr#$d}"
		[ -n "$d" ] ||
			return 1
		if [ -z "${d##*.*.*.*}" ]; then
			# Embedded IPv4 representation.
			[ -z "$repr" ] ||
				return 1
			[ "$i" -eq 6 ] || [ -n "$skip" ] ||
				return 1
			d="$(ipv4_ptonx "$d")" ||
				return 1
			suff="$suff$d"
			break
		fi
		[ -n "${d##*[!0-9a-fA-F]*}" ] ||
			return 1
		[ $(( 0x0$d <= 65535 )) -gt 0 ] ||
			return 1
		if [ -n "$skip" ]; then
			suff="$suff$(printf %04x 0x0$d)"
		else
			pref="$pref$(printf %04x 0x0$d)"
		fi
		if [ -z "$repr" ]; then
			[ "$i" -eq 7 ] || [ -n "$skip" ] &&
				break
			return 1
		fi

		i=$(($i + 1))
		[ "$i" -lt 8 ] ||
			return 1
	done
	impz=$((32 - ${#pref} - ${#suff}))
	[ "$impz" -ne 0 ] && impz="$(printf "%0${impz}x" 0)" || impz=
	printf "%s%s\n" "$pref" "$impz$suff" | tr '[:upper:]' '[:lower:]'
	return 0
}

### Reads an IPv6 address from the option value and determines its type,
### in regard to any special properties derived from that type.
### If an address was recognized successfully, outputs a value
### belonging to the following enum:
### - unspec
### - loopback
### - ipv4-mapped
### - link-local
### - multicast
### - other
### Otherwise, returns non-zero with empty output.
ipv6_addr_type()
{
	local na hi lo atype
	local hi0 hi32 lo64 lo96

	na="$(ipv6_ptonx "$1")" ||
		return 1
	hi="${na%????????}"
	hi="${hi%????????}"
	lo="${na#$hi}"

	case "$hi" in
	ff*) atype=multicast ;;
	fe[89ab]*) atype=link-local ;;
	*) ;;
	esac
	if [ -z "${atype-}" ]; then
		hi0="0x${hi%????????}"
		hi32="0x${hi#????????}"
		lo64="0x${lo%????????}"
		lo96="0x${lo#????????}"

		case "$(($hi0)),$(($hi32)),$(($lo64)),$(($lo96))" in
		"0,0,0,0") atype=unspec ;;
		"0,0,0,1") atype=loopback ;;
		"0,0,65535,"*) atype=ipv4-mapped ;;
		*) ;;
		esac
	fi
	[ -n "${atype-}" ] || atype=other

	printf '%s\n' "$atype"
	return 0
}

### Checks that IPv6 address is in a prefix.
### Usage example:
### ipv6_ip_matches 3fff:e:b:1::2 3fff:e:b:1::/64; echo res=$?
### res=0
###
### ipv6_ip_matches 3fff:e:b:3::2 3fff:e:b:1::/64; echo res=$?
### res=1
###
### ipv6_ip_matches 3fff:e:b:3::2 3fff:e:b::/48; echo res=$?
### res=0
ipv6_ip_matches()
{
	local addr pref preflen
	addr="${1-}"; shift
	pref="${1-}"; shift
	preflen="${pref##*/}"

	[ -n "$preflen" ] ||
		return 2
	[ "$preflen" = 0 ] || shell_var_is_number "$preflen" ||
		return 2
	[ "$preflen" -le 128 ] ||
		return 2

	local hex_addr hex_pref hex_mask

	hex_addr="$(ipv6_ptonx "$addr")" &&
	hex_pref="$(ipv6_ptonx "${pref%%/*}")" ||
		return 2

	local i_addr i_pref w_addr w_pref
	while [ "$preflen" -gt 32 ]; do
		i_addr="${hex_addr#????????}"
		i_pref="${hex_pref#????????}"
		w_addr="${hex_addr%$i_addr}"
		w_pref="${hex_pref%$i_pref}"
		[ "$w_pref" = "$w_addr" ] ||
			return 1

		hex_addr="$i_addr"
		hex_pref="$i_pref"
		preflen="$(($preflen - 32))"
	done
	i_addr="${hex_addr#????????}"
	i_pref="${hex_pref#????????}"
	w_addr="${hex_addr%$i_addr}"
	w_pref="${hex_pref%$i_pref}"
	hex_mask="$(__len2mask_32 "$preflen")"
	[ "$((0x$w_pref & $hex_mask))" -eq "$((0x$w_addr & $hex_mask))" ] ||
		return 1
}

fi #__included_shell_ip_address
