208 lines
5.6 KiB
Python
208 lines
5.6 KiB
Python
# Copyright 2026 jCloud Services GbR
|
|
#
|
|
# 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
|
|
#
|
|
# https://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.
|
|
|
|
import os
|
|
from typing import Tuple, Literal, Sequence
|
|
import ipaddress
|
|
|
|
__all__ = [
|
|
'is_number',
|
|
'int_to_bytes',
|
|
'bits_to_byte',
|
|
'byte_to_bits',
|
|
'get_next_lower_integer_multiple',
|
|
'find_nearest_lower_number',
|
|
'create_file_if_not_exists',
|
|
'is_valid_host',
|
|
'is_valid_port',
|
|
'validate_address_port'
|
|
]
|
|
|
|
def is_number(string: str, base: int = 10) -> bool:
|
|
'''
|
|
Checks whether the string is a number.
|
|
|
|
:param string: The string
|
|
:type string: str
|
|
:param base: The base
|
|
:type base: int
|
|
|
|
:return: ``True`` if the string is a number, otherwise ``False``
|
|
:rtype: bool
|
|
'''
|
|
if base == 0:
|
|
return False
|
|
if not string:
|
|
return False
|
|
try:
|
|
int(string, base)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
def int_to_bytes(n: int, signed: bool = False) -> bytes:
|
|
'''
|
|
Converts the integer into bytes (dynamic length).
|
|
|
|
:param n: The integer
|
|
:type n: int
|
|
:param signed: Whether the integer is signed or not.
|
|
:type signed: bool
|
|
|
|
:return: The bytes
|
|
:rtype: bytes
|
|
'''
|
|
n = int(n)
|
|
length = (n.bit_length() + 7) // 8
|
|
if length == 0:
|
|
length = 1
|
|
return n.to_bytes(length, signed=signed)
|
|
|
|
def _bits_to_byte(bits: Tuple[Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1]]) -> bytes:
|
|
if len(bits) != 8:
|
|
raise ValueError('bits must be a tuple of 8 bits')
|
|
byte = 0
|
|
for bit in bits:
|
|
if int(bit) not in (0, 1):
|
|
raise ValueError('each bit must be 0 or 1')
|
|
byte = (byte << 1) | int(bit)
|
|
return byte
|
|
|
|
def bits_to_byte(*bits: Literal[0, 1]) -> bytes:
|
|
'''
|
|
Converts the bits into a single byte.
|
|
|
|
:param bits: The bits
|
|
:type bits: int
|
|
|
|
:raises ValueError: If there are more than 8 bits specified or a bit is neither ``0`` nor ``1``.
|
|
|
|
:return: The byte
|
|
:rtype: bytes
|
|
'''
|
|
if len(bits) > 8:
|
|
raise ValueError('a maximum of 8 bits can be converted to a byte')
|
|
while len(bits) < 8:
|
|
bits = (0,) + bits
|
|
return bytes([_bits_to_byte(bits)])
|
|
|
|
def byte_to_bits(b: bytes) -> Tuple[Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1], Literal[0, 1]]:
|
|
'''
|
|
Converts the byte to a tuple of bits.
|
|
|
|
:param b: The byte
|
|
:type b: bytes
|
|
|
|
:raises ValueError: If more than one byte is specified or ``b`` is neither a bytestring nor a bytearray,
|
|
|
|
:return: The bits
|
|
:rtype: tuple
|
|
'''
|
|
if not isinstance(b, (bytes, bytearray)) or len(b) != 1:
|
|
raise ValueError('input must be a single byte')
|
|
byte = b[0]
|
|
return tuple((byte >> i) & 1 for i in reversed(range(8)))
|
|
|
|
def get_next_lower_integer_multiple(value: int, multiple: int) -> int:
|
|
'''
|
|
Returns the next lower integer multiple.
|
|
|
|
:param value: The value
|
|
:type value: int
|
|
:param multiple: The factor
|
|
:type multiple: int
|
|
|
|
:return: The next lower integer multiple
|
|
:rtype: int
|
|
'''
|
|
return value - (value % multiple)
|
|
|
|
def find_nearest_lower_number(number_list: Sequence, target: int | float) -> int | float:
|
|
'''
|
|
Finds the nearest lower number to the target from the list.
|
|
|
|
:param number_list: The list
|
|
:type number_list: Sequence
|
|
:param target: The target
|
|
:type target: int | float
|
|
|
|
:return: The nearest lower number to the target from the list.
|
|
:rtype: int | float
|
|
'''
|
|
nearest = None
|
|
for number in number_list:
|
|
if number <= target:
|
|
if nearest is None or number > nearest:
|
|
nearest = number
|
|
return nearest
|
|
|
|
def create_file_if_not_exists(path: str, content: bytes = b''):
|
|
'''
|
|
Creates a file if it does not exist.
|
|
|
|
:param path: The file path
|
|
:type path: str
|
|
:param content: Optional, the content of the file if it is newly created.
|
|
:type content: bytes
|
|
'''
|
|
if not os.path.exists(path):
|
|
with open(path, 'wb') as f:
|
|
f.write(content)
|
|
f.close()
|
|
|
|
def is_valid_host(host: str) -> bool:
|
|
'''
|
|
Checks whether the host is a valid IP address.
|
|
|
|
:param host: The host
|
|
:type host: str
|
|
|
|
:return: ``True`` if the host is a valid IP address, otherwise ``False``
|
|
:rtype: bool
|
|
'''
|
|
try:
|
|
ipaddress.ip_address(host)
|
|
return True
|
|
except ValueError:
|
|
pass
|
|
|
|
def is_valid_port(port: str) -> bool:
|
|
'''
|
|
Checks whether the port is a valid port number.
|
|
|
|
:param port: The port
|
|
:type port: str
|
|
|
|
:return: ``True`` if the port is a valid port number, otherwise ``False``
|
|
:rtype: bool
|
|
'''
|
|
if not port.isdigit():
|
|
return False
|
|
port_num = int(port)
|
|
return 1 <= port_num <= 65535
|
|
|
|
def validate_address_port(addr: str) -> bool:
|
|
if ':' not in addr:
|
|
return False
|
|
|
|
if addr.startswith('['):
|
|
try:
|
|
host, port = addr.rsplit(']:', 1)
|
|
host = host[1:]
|
|
except ValueError:
|
|
return False
|
|
else:
|
|
host, port = addr.rsplit(':', 1)
|
|
|
|
return is_valid_host(host) and is_valid_port(port) |