Dumping this here so I can refer to this when needed and in case anyone is looking for this.
function inet_prefixlen_start(string $inet, int $version, int $prefixlen): ?string
{
switch ($version) {
case 4:
if (filter_var($inet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
$pack = 'c*';
$size = 8;
}
break;
case 6:
if (filter_var($inet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) {
$pack = 'n*';
$size = 16;
}
break;
}
if (!isset($pack)) {
return null;
}
$ipArray = unpack($pack, inet_pton($inet));
$groupMask = (1 << $size) - 1;
$fullSize = $size * count($ipArray);
$groupCount = count($ipArray);
for ($i = 0; $i < $groupCount; $i++) {
$mask = $groupMask;
$indexStart = $i * $size;
for ($j = 0; $j < $size; $j++) {
$fullIndex = $indexStart + $j;
if ($fullIndex < $prefixlen) {
continue;
}
// shifting goes from the end, one less of size:
// (size 8)
// i = 0 -> shift = 7 (1000 0000)
// i = 7 -> shift = 0 (0000 0001)
$index = $size - $j - 1;
$mask ^= 1 << $index;
}
// ipArray is 1-indexed because that's how unpack works
$group = $i + 1;
$ipArray[$group] = $ipArray[$group] & $mask;
}
return inet_ntop(pack($pack, ...$ipArray));
}
/**
some sample data
['1.2.3.4', 4, 24, '1.2.3.0'],
['1.2.3.5', 4, 31, '1.2.3.4'],
['::10.0.0.3', 4, 32, null],
['1:2:3:4::', 4, 24, null],
['invalid', 4, 32, null],
['1:2:3:4::1', 6, 64, '1:2:3:4::'],
['1:2:3::1', 6, 64, '1:2:3::'],
['1:2:3:4:5:6:7:8', 6, 64, '1:2:3:4::'],
['1::4:5:6:7:8', 6, 64, '1:0:0:4::'],
['1:2:3:4::10.0.0.1', 6, 64, '1:2:3:4::'],
['1:2:3:ffff::1', 6, 56, '1:2:3:ff00::'],
['::3', 6, 127, '::2'],
['1.2.3.4', 6, 128, null],
['invalid', 6, 128, null],
*/
Or just use one of those IP handling libraries.