mirror of
https://github.com/hyugogirubato/KeyDive.git
synced 2026-07-15 18:40:02 +02:00
214 lines
6.4 KiB
Python
214 lines
6.4 KiB
Python
import base64
|
|
import json
|
|
import logging
|
|
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Union, Optional
|
|
from uuid import UUID
|
|
|
|
import coloredlogs
|
|
import xmltodict
|
|
|
|
from unidecode import unidecode
|
|
|
|
|
|
def unidec(data: str) -> str:
|
|
"""
|
|
Normalizes a string by removing diacritics and accents using ASCII transliteration.
|
|
|
|
Args:
|
|
data (str): The input string to normalize.
|
|
|
|
Returns:
|
|
str: The transliterated and stripped version of the input string.
|
|
"""
|
|
return unidecode(data).strip()
|
|
|
|
|
|
def b64enc(data: Union[bytes, str], safe: bool = False) -> str:
|
|
"""
|
|
Encodes data to Base64 format.
|
|
|
|
Args:
|
|
data (bytes or str): The data to encode.
|
|
safe (bool): Use URL-safe encoding if True.
|
|
|
|
Returns:
|
|
str: Base64-encoded string.
|
|
"""
|
|
if isinstance(data, str):
|
|
data = data.encode('utf-8')
|
|
return (base64.urlsafe_b64encode(data) if safe else base64.b64encode(data)).decode('utf-8')
|
|
|
|
|
|
def b64dec(data: str, safe: bool = False) -> bytes:
|
|
"""
|
|
Decodes Base64-encoded string to bytes.
|
|
|
|
Args:
|
|
data (str): Base64-encoded string.
|
|
safe (bool): Use URL-safe decoding if True.
|
|
|
|
Returns:
|
|
bytes: Decoded byte data.
|
|
"""
|
|
# Fix missing padding
|
|
data = data + '=' * (-len(data) % 4)
|
|
return base64.urlsafe_b64decode(data) if safe else base64.b64decode(data)
|
|
|
|
|
|
def xmldec(data: Union[bytes, str], force_list: Optional[list] = None) -> dict:
|
|
"""
|
|
Parses XML string or bytes into a dictionary.
|
|
|
|
Args:
|
|
data (str or bytes): The XML data to parse.
|
|
force_list (list, optional): Tags to always parse as lists.
|
|
|
|
Returns:
|
|
dict: Parsed XML structure.
|
|
"""
|
|
return xmltodict.parse(data, force_list=set(force_list or []))
|
|
|
|
|
|
def dumps(data: Union[dict, list], beauty: bool = False) -> str:
|
|
"""
|
|
Serializes a Python object into JSON, handling binary, nested, and custom types.
|
|
|
|
Args:
|
|
data (dict or list): The data to serialize.
|
|
beauty (bool): If True, formats JSON with indentation for readability.
|
|
|
|
Returns:
|
|
str: A JSON-formatted string.
|
|
"""
|
|
|
|
def __string(value):
|
|
# Recursively process lists
|
|
if isinstance(value, list):
|
|
return [__string(v) for v in value]
|
|
|
|
# Recursively process dictionaries
|
|
elif isinstance(value, dict):
|
|
return {__string(k): __string(v) for k, v in value.items()}
|
|
|
|
# Handle byte values (e.g., raw data, encoded JSON, binary keys)
|
|
elif isinstance(value, bytes):
|
|
if value:
|
|
# Try decoding as JSON
|
|
try:
|
|
return __string(json.loads(value))
|
|
except ValueError:
|
|
pass
|
|
|
|
# Try decoding as UTF-8 string
|
|
try:
|
|
return value.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
pass
|
|
|
|
# Handle specific known binary formats (e.g., 16-byte AES keys)
|
|
if len(value) == 16:
|
|
return value.hex()
|
|
|
|
# Fallback: encode to base64 string for general binary data
|
|
return b64enc(value)
|
|
|
|
# Return None for empty bytes
|
|
return None
|
|
|
|
# Recurse again for nested structures
|
|
elif isinstance(value, (dict, list)):
|
|
return __string(value)
|
|
|
|
# Convert known non-serializable types to string
|
|
elif isinstance(value, (UUID, datetime, Path)):
|
|
return str(value)
|
|
|
|
# If value is a string, try parsing it as JSON
|
|
elif isinstance(value, str):
|
|
try:
|
|
return __string(json.loads(value))
|
|
except ValueError:
|
|
pass
|
|
|
|
# Return all other values unchanged
|
|
return value
|
|
|
|
# Perform final JSON serialization with optional pretty-printing
|
|
return json.dumps(
|
|
__string(data),
|
|
indent=2 if beauty else None,
|
|
separators=None if beauty else (',', ':') # Compact output if not pretty
|
|
)
|
|
|
|
|
|
def configure_logging(path: Optional[Path] = None, verbose: bool = False) -> Optional[Path]:
|
|
"""
|
|
Configures logging to file and console with optional verbosity.
|
|
|
|
Args:
|
|
path (Path, optional): Directory to save the log file.
|
|
verbose (bool): Enable debug-level logging if True.
|
|
|
|
Returns:
|
|
Path or None: Path to log file if created, otherwise None.
|
|
"""
|
|
# Set up the root logger with the desired logging level
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
|
|
# Clear any existing handlers (optional, to avoid duplicate logs if reconfiguring)
|
|
if root_logger.hasHandlers():
|
|
root_logger.handlers.clear()
|
|
|
|
file_path = None
|
|
if path:
|
|
# Ensure the log directory exists
|
|
if path.is_file():
|
|
path = path.parent
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create a file handler
|
|
file_path = path / ('keydive_%s.log' % datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))
|
|
file_path = file_path.resolve(strict=False)
|
|
file_handler = logging.FileHandler(file_path)
|
|
file_handler.setLevel(logging.DEBUG)
|
|
|
|
# Set log formatting
|
|
formatter = logging.Formatter(
|
|
fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
file_handler.setFormatter(formatter)
|
|
|
|
# Add the file handler to the root logger
|
|
root_logger.addHandler(file_handler)
|
|
|
|
# Configure coloredlogs for console output
|
|
# 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
|
|
coloredlogs.install(
|
|
fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S',
|
|
level=logging.DEBUG if verbose else logging.INFO,
|
|
logger=root_logger,
|
|
field_styles={
|
|
'asctime': {'color': 'green'}, # timestamp
|
|
'hostname': {'color': 'magenta'},
|
|
'levelname': {'bold': True, 'color': 'blue'}, # level e.g., [I]
|
|
'name': {'color': 'magenta'}, # logger name
|
|
'programname': {'color': 'cyan'},
|
|
'message': {'color': 'white'} # message text
|
|
},
|
|
level_styles={
|
|
'debug': {'color': 'cyan'},
|
|
'info': {'color': 'white'},
|
|
'warning': {'color': 'yellow'},
|
|
'error': {'color': 'red'},
|
|
'critical': {'bold': True, 'color': 'red'}
|
|
}
|
|
)
|
|
|
|
return file_path
|