ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
Utility Functions

🛠️ String, path, IP, UTF-8, CRC32, and formatting utilities More...

Files

file  atomic.c
 Implementation of atomic operations abstraction.
 
file  musl_c23_compat.c
 🔄 musl C23 compatibility wrappers for __isoc23_* symbol aliases (glibc 2.38+ compatibility)
 
file  crc32.c
 ⚡ Hardware-accelerated CRC32 checksum with ARM64 and x86_64 CPU feature detection
 
file  aspect_ratio.c
 📐 Terminal cell aspect ratio calculations for accurate image dimension scaling
 
file  format.c
 📊 Byte size formatting utilities for human-readable output (B, KB, MB, GB, TB, PB, EB)
 
file  image.c
 🖼️ Safe overflow-checked buffer size calculations for images and video frames
 
file  ip.c
 🌍 IPv4/IPv6 address parsing, validation, and formatting utilities
 
file  parsing.c
 🔍 Safe string parsing utilities for integers, sizes, and protocol messages
 
file  password.c
 🔑 Password prompting utilities with secure input and formatting
 
file  path.c
 Cross-platform path manipulation with normalization and Windows/Unix separator handling.
 
file  pcre2.c
 Centralized PCRE2 singleton pattern for efficient regex compilation.
 
file  string.c
 🔤 String manipulation utilities: ASCII escaping, trimming, case conversion, and formatting
 
file  url.c
 Production-grade URL parsing and validation using PCRE2.
 
file  utf8.c
 🔤 UTF-8 encoding and decoding with multi-byte character support
 
file  version.c
 🏷️ Binary-embedded version information in custom ELF/Mach-O sections for runtime inspection
 
file  atomic.h
 ⚛️ Atomic operations abstraction layer with debug tracking
 
file  crc32.h
 Hardware-Accelerated CRC32 Checksum Computation.
 
file  aspect_ratio.h
 📐 Aspect Ratio Calculation Functions
 
file  bits.h
 🔢 Bit Manipulation Utilities
 
file  bytes.h
 🔢 Byte-Level Access and Arithmetic Utilities
 
file  display.h
 🖥️ Display Width and Terminal Centering Utilities
 
file  endian.h
 🔄 Network byte order conversion helpers
 
file  fnv1a.h
 #️⃣ FNV-1a Hash Function Implementation
 
file  format.h
 📊 String Formatting Utilities
 
file  image.h
 🖼️ Safe overflow-checked buffer size calculations for images and video frames
 
file  ip.h
 🌍 IP Address Parsing and Formatting Utilities
 
file  magic.h
 Magic number validation constants and macros.
 
file  math.h
 🔢 Mathematical Utility Functions
 
file  number.h
 🔢 Number Formatting and Conversion Utilities
 
file  overflow.h
 ✅ Safe Integer Arithmetic and Overflow Detection
 
file  parsing.h
 🔍 Safe Parsing Utilities
 
file  password.h
 Password prompting utilities with secure input and formatting.
 
file  path.h
 📂 Path Manipulation Utilities
 
file  pcre2.h
 🔍 Centralized PCRE2 singleton pattern for efficient regex compilation
 
file  string.h
 🔤 String Manipulation and Shell Escaping Utilities
 
file  thread.h
 🧵 Thread lifecycle management helpers
 
file  url.h
 🌐 Production-Grade URL Validation and Parsing
 
file  utf8.h
 🔤 UTF-8 Encoding and Decoding Utilities
 
file  util.h
 🛠️ Master Utility Header
 

Data Structures

struct  atomic_t
 Atomic value wrapper for integral/boolean types. More...
 
struct  atomic_ptr_t
 Atomic pointer wrapper. More...
 
struct  url_parts_t
 Structure containing parsed URL components. More...
 

Macros

#define asciichat_crc32(data, len)   asciichat_crc32_hw((data), (len))
 Main CRC32 dispatcher macro - use this in application code.
 
#define FNV1A_32_OFFSET_BASIS   2166136261ULL
 FNV-1a 32-bit offset basis.
 
#define FNV1A_32_PRIME   16777619ULL
 FNV-1a 32-bit prime.
 
#define FNV1A_32_MASK   0xFFFFFFFFULL
 FNV-1a 32-bit mask.
 
#define FNV1A_INIT(hash)   uint64_t hash = FNV1A_32_OFFSET_BASIS;
 FNV-1a 32-bit hash macro.
 
#define FNV1A_32_HASH(hash, byte)
 FNV-1a 32-bit hash macro for a single byte.
 
#define ROUND(xfloat)   (int)(0.5f + (xfloat))
 Round floating-point value to nearest integer.
 
#define SIZE_MUL_SAFE(a, b)   (size_mul_would_overflow(a, b) ? SIZE_MAX : (a) * (b))
 Multiply with overflow check inline (macro form for tight loops)
 
#define SIZE_ADD_SAFE(a, b)   (size_add_would_overflow(a, b) ? SIZE_MAX : (a) + (b))
 Add with overflow check inline (macro form for tight loops)
 
#define PASSWORD_MAX_LEN   260
 Buffer size for password input.
 
#define PATH_COMPONENT_DOT   '.'
 Path component: current directory (single dot)
 
#define PATH_COMPONENT_DOTDOT   ".."
 Path component: parent directory (double dot)
 
#define PATH_TILDE   '~'
 Path component: home directory tilde.
 
#define PATH_DRIVE_SEPARATOR   ':'
 Path component: Windows drive separator (colon)
 
#define MAX_PATH_BASES   16
 Maximum number of path base directories.
 

Enumerations

enum  path_role_t {
  PATH_ROLE_CONFIG_FILE , PATH_ROLE_LOG_FILE , PATH_ROLE_KEY_PRIVATE , PATH_ROLE_KEY_PUBLIC ,
  PATH_ROLE_CLIENT_KEYS
}
 Classification for user-supplied filesystem paths. More...
 

Functions

long __isoc23_wcstol (const wchar_t *str, wchar_t **endptr, int base)
 Wide string to integer conversion.
 
int __isoc23_scanf (const char *format,...)
 Formatted input functions.
 
int __isoc23_wscanf (const wchar_t *format,...)
 Wide character formatted input functions.
 
void arc4random_buf (void *buf, size_t nbytes)
 glibc-specific random functions for expat and fontconfig compatibility
 
uint32_t asciichat_crc32_hw (const void *data, size_t len)
 Compute CRC32 checksum with hardware acceleration (if available)
 
uint32_t asciichat_crc32_sw (const void *data, size_t len)
 Compute CRC32 checksum using software implementation only.
 
bool crc32_hw_is_available (void)
 Check if hardware CRC32 acceleration is available at runtime.
 
void aspect_ratio (const ssize_t img_width, const ssize_t img_height, const ssize_t width, const ssize_t height, const bool stretch, ssize_t *out_width, ssize_t *out_height)
 Calculate aspect ratio with terminal character correction.
 
void aspect_ratio2 (const ssize_t img_width, const ssize_t img_height, const ssize_t target_width, const ssize_t target_height, ssize_t *out_width, ssize_t *out_height)
 Simple aspect ratio calculation without terminal character correction.
 
void calculate_fit_dimensions_pixel (int img_width, int img_height, int max_width, int max_height, int *out_width, int *out_height)
 Calculate fit dimensions for pixel-based images.
 
int display_width (const char *text)
 Calculate the visible display width of text with ANSI escape codes.
 
int display_center_horizontal (const char *text, int terminal_width)
 Calculate left padding needed to center text horizontally.
 
int display_center_vertical (int content_height, int terminal_height)
 Calculate top padding needed to vertically center content.
 
int display_height (const char *text, int terminal_width)
 Calculate how many display lines text will consume when wrapped.
 
void format_bytes_pretty (size_t bytes, char *out, size_t out_capacity)
 Format byte count into human-readable string.
 
int is_valid_ipv4 (const char *ip)
 Check if a string is a valid IPv4 address.
 
int is_valid_ipv6 (const char *ip)
 Check if a string is a valid IPv6 address.
 
int parse_ipv6_address (const char *input, char *output, size_t output_size)
 Parse IPv6 address, removing brackets if present.
 
int parse_ip_with_port (const char *input, char *ip_output, size_t ip_output_size, uint16_t *port_output)
 Parse IP address and port from string.
 
int parse_address_with_optional_port (const char *input, char *address_output, size_t address_output_size, uint16_t *port_output, uint16_t default_port)
 Parse address with optional port from string.
 
asciichat_error_t format_ip_address (int family, const struct sockaddr *addr, char *output, size_t output_size)
 Format IP address from socket address structure.
 
asciichat_error_t format_ip_with_port (const char *ip, uint16_t port, char *output, size_t output_size)
 Format IP address with port number.
 
int get_ip_version (const char *ip)
 Get IP address version.
 
int is_valid_ip (const char *ip)
 Check if string is a valid IP address (IPv4 or IPv6)
 
int ip_equals (const char *ip1, const char *ip2)
 Compare two IP addresses for equality.
 
int ip_compare (const char *ip1, const char *ip2)
 Compare two IP addresses for sorting.
 
int compare_ip_port_strings (const char *ip_port1, const char *ip_port2)
 Compare two "IP:port" strings with IPv6 normalization.
 
int parse_cidr (const char *cidr, char *ip_out, size_t ip_out_size, int *prefix_out)
 Parse CIDR notation into IP and prefix length.
 
int ip_in_cidr (const char *ip, const char *cidr)
 Check if IP address is within CIDR range.
 
int ip_in_cidr_parsed (const char *ip, const char *network, int prefix_len)
 Check if IP is within CIDR range (parsed form)
 
asciichat_error_t ipv4_to_ipv6_mapped (const char *ipv4, char *ipv6_out, size_t out_size)
 Convert IPv4 address to IPv6-mapped format.
 
asciichat_error_t extract_ipv4_from_mapped_ipv6 (const char *ipv6, char *ipv4_out, size_t out_size)
 Extract IPv4 address from IPv6-mapped format.
 
int is_ipv4_mapped_ipv6 (const char *ipv6)
 Check if IPv6 address is IPv4-mapped.
 
asciichat_error_t canonicalize_ipv6 (const char *ipv6, char *canonical_out, size_t out_size)
 Canonicalize IPv6 address to standard form.
 
asciichat_error_t expand_ipv6 (const char *ipv6, char *expanded_out, size_t out_size)
 Expand IPv6 address to full form.
 
asciichat_error_t compact_ipv6 (const char *ipv6, char *compact_out, size_t out_size)
 Compress IPv6 address using :: notation.
 
int is_anycast_ipv6 (const char *ipv6)
 Check if IPv6 address is anycast.
 
int is_lan_ipv4 (const char *ip)
 Check if IPv4 address is a private/LAN address.
 
int is_lan_ipv6 (const char *ip)
 Check if IPv6 address is a private/LAN address.
 
int is_broadcast_ipv4 (const char *ip)
 Check if IPv4 address is a broadcast address.
 
int is_broadcast_ipv6 (const char *ip)
 Check if IPv6 address is a multicast address.
 
int is_localhost_ipv4 (const char *ip)
 Check if IPv4 address is localhost.
 
int is_localhost_ipv6 (const char *ip)
 Check if IPv6 address is localhost.
 
int is_link_local_ipv4 (const char *ip)
 Check if IPv4 address is a link-local address.
 
int is_link_local_ipv6 (const char *ip)
 Check if IPv6 address is a link-local address.
 
int is_internet_ipv4 (const char *ip)
 Check if IPv4 address is a public internet address.
 
int is_internet_ipv6 (const char *ip)
 Check if IPv6 address is a public internet address.
 
int extract_ip_from_address (const char *addr_with_port, char *ip_out, size_t ip_out_size)
 Extract IP address (without port) from formatted address string.
 
const char * get_ip_type_string (const char *ip)
 Get human-readable string describing IP address type.
 
asciichat_error_t safe_parse_size_message (const char *message, unsigned int *width, unsigned int *height)
 Parse SIZE message format.
 
asciichat_error_t safe_parse_audio_message (const char *message, unsigned int *num_samples)
 Parse AUDIO message format.
 
int prompt_password (const char *prompt, char *password, size_t max_len)
 Prompt the user for a password with secure input.
 
int prompt_password_simple (const char *prompt, char *password, size_t max_len)
 Prompt the user for a password with simple formatting.
 
const char * extract_project_relative_path (const char *file)
 Extract relative path from an absolute path.
 
char * expand_path (const char *path)
 Expand path with tilde (~) support.
 
char * get_config_dir (void)
 Get configuration directory path with XDG_CONFIG_HOME support.
 
char * get_data_dir (void)
 Get application data directory path.
 
char * get_log_dir (void)
 Get log directory path appropriate for current build type.
 
char * get_discovery_database_dir (void)
 Get discovery service database directory with Homebrew-aware system-wide fallback.
 
bool path_normalize_copy (const char *path, char *out, size_t out_len)
 Normalize a path and copy it into the provided buffer.
 
bool path_is_absolute (const char *path)
 Determine whether a path is absolute on the current platform.
 
bool path_is_within_base (const char *path, const char *base)
 Check whether a path resides within a specified base directory.
 
bool path_is_within_any_base (const char *path, const char *const *bases, size_t base_count)
 Check whether a path resides within any of several base directories.
 
bool path_looks_like_path (const char *value)
 Determine if a string is likely intended to reference the filesystem.
 
asciichat_error_t path_validate_user_path (const char *input, path_role_t role, char **normalized_out)
 Validate and canonicalize a user-supplied filesystem path.
 
void path_cleanup_thread_locals (void)
 Cleanup thread-local path resources.
 
void escape_ascii (const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size)
 Escape ASCII characters in a string.
 
bool validate_shell_safe (const char *str, const char *allowed_chars)
 Validate that a string contains only safe characters for shell commands.
 
bool escape_shell_single_quotes (const char *str, char *out_buffer, size_t out_buffer_size)
 Escape a string for safe use in shell commands (single quotes)
 
bool escape_shell_double_quotes (const char *str, char *out_buffer, size_t out_buffer_size)
 Escape a string for safe use in shell commands (double quotes, Windows-compatible)
 
bool string_needs_shell_quoting (const char *str)
 Check if a string needs shell quoting (contains spaces or special chars)
 
bool escape_path_for_shell (const char *path, char *out_buffer, size_t out_buffer_size)
 Escape a path for safe use in shell commands (auto-platform)
 
const char * colored_string (log_color_t color, const char *text)
 Build a colored string for terminal output.
 
void truncate_with_ellipsis (const char *input, char *output, size_t output_size, int max_width)
 Truncate a string to a maximum display width, with ellipsis.
 
void strip_ansi_codes (const char *input, char *output, size_t output_size)
 Strip ANSI escape codes from a string.
 
bool url_is_valid (const char *url)
 Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)
 
asciichat_error_t url_parse (const char *url, url_parts_t *parts_out)
 Parse HTTP(S), WebSocket, and TCP URL into components.
 
void url_parts_destroy (url_parts_t *parts)
 Free parsed URL components.
 
bool url_is_websocket_scheme (const char *scheme)
 Check if URL scheme is WebSocket (ws or wss)
 
bool url_is_websocket (const char *url)
 Check if a URL string is a valid WebSocket URL.
 
bool url_looks_like_websocket (const char *url)
 Quick check if a string is a WebSocket URL.
 
bool url_is_tcp_scheme (const char *scheme)
 Check if URL scheme is TCP.
 
bool url_is_tcp (const char *url)
 Check if a URL string is a valid TCP URL.
 
bool url_looks_like_tcp (const char *url)
 Quick check if a string is a TCP URL.
 
int utf8_decode (const uint8_t *s, uint32_t *codepoint)
 Decode a UTF-8 sequence to a Unicode codepoint.
 
int utf8_display_width (const char *str)
 Calculate terminal display width of a UTF-8 string.
 
int utf8_display_width_n (const char *str, size_t max_bytes)
 Calculate terminal display width of a UTF-8 string up to byte limit.
 
bool utf8_is_valid (const char *str)
 Check if a string is valid UTF-8.
 
bool utf8_is_ascii_only (const char *str)
 Check if a string contains only ASCII characters.
 
size_t utf8_char_count (const char *str)
 Count UTF-8 characters (not bytes)
 
size_t utf8_to_codepoints (const char *str, uint32_t *out_codepoints, size_t max_codepoints)
 Convert UTF-8 string to array of Unicode codepoints.
 
int utf8_next_char_bytes (const char *str, size_t max_bytes)
 Get byte length of next UTF-8 character.
 
int utf8_continuation_bytes_needed (unsigned char first_byte)
 Determine how many additional bytes are needed to complete a UTF-8 character.
 
int utf8_read_and_insert_continuation_bytes (char *buffer, size_t *cursor, size_t *len, size_t max_len, int continuation_bytes, int(*read_byte_fn)(void))
 Read continuation bytes and insert them into buffer at cursor position.
 
const char * utf8_strcasestr (const char *haystack, const char *needle)
 Case-insensitive substring search with full Unicode support.
 
void utf8_write_lowercase (FILE *output, const char *text)
 Write UTF-8 string to file with ASCII lowercase conversion.
 

Detailed Description

🛠️ String, path, IP, UTF-8, CRC32, and formatting utilities

This header provides CRC32 checksum computation with automatic hardware acceleration when available. The system automatically dispatches to hardware implementations (SSE4.2, ARM CRC32) for optimal performance, falling back to a software implementation on platforms without hardware support.

CORE FEATURES:

HARDWARE ACCELERATION:

The system supports:

SOFTWARE FALLBACK:

PERFORMANCE:

Hardware acceleration provides:

Note
Use the asciichat_crc32() macro for automatic hardware dispatch.
Hardware acceleration is detected at runtime, so no compile-time configuration is required.
CRC32 is used primarily for network packet integrity verification to detect transmission errors.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides functions for calculating image dimensions while maintaining aspect ratio. Functions handle terminal character correction for ASCII art rendering and pixel-based calculations for image scaling.

CORE FEATURES:

TERMINAL CHARACTER CORRECTION:

Terminal characters have different aspect ratios than square pixels:

ASPECT RATIO CALCULATIONS:

Functions calculate dimensions that maintain image aspect ratio:

Note
Terminal character aspect ratio correction is essential for proper ASCII art rendering that matches the original image proportions.
All output parameters are modified only on successful calculation.
This file contains functions based on jp2a code: Copyright (C) 2006 Christian Stigen Larsen, http://csl.sublevel3.org Distributed under the GNU General Public License (GPL) v2. Project homepage on http://jp2a.sf.net
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

Portable bit manipulation functions including power-of-two checks, bit counting, and leading/trailing zero detection across platforms.

CORE FEATURES:

PLATFORM SUPPORT:

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

This header provides low-level utilities for safely reading and writing multi-byte values from/to unaligned memory addresses, with overflow detection for size calculations.

CORE FEATURES:

UNALIGNED ACCESS:

Direct pointer casts like *(uint32_t*)ptr cause undefined behavior on architectures requiring aligned access (ARM, SPARC, etc.). These functions use memcpy which compilers optimize to single instructions when possible while remaining safe on all platforms.

Note
These functions are used extensively in network and crypto modules for parsing and generating packets.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

This header provides a shared FNV-1a hash function implementation used throughout the codebase for consistent hashing with UBSan-safe arithmetic.

FNV-1a (Fowler-Noll-Vo) is a fast, non-cryptographic hash function with good distribution properties. This implementation uses 64-bit arithmetic to avoid unsigned overflow, then masks to 32-bit for the final result.

All functions are static inline for performance, allowing the compiler to inline them at call sites.

Note
This hash function is NOT for cryptographic purposes.
All arithmetic is done in 64-bit space to avoid UBSan warnings.

This header provides utilities for formatting various data types into human-readable strings. Functions handle byte counts, sizes, and other numeric values with appropriate units and formatting.

CORE FEATURES:

BYTE FORMATTING:

Byte counts are formatted with appropriate binary units:

Note
All formatting functions ensure buffer overflow protection.
Output strings are null-terminated.
Decimal precision is fixed (2 decimal places for units >= KB).
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides utilities for validating, parsing, and formatting IPv4 and IPv6 addresses with proper bracket notation support. Functions handle various address formats including bracketed IPv6 addresses.

CORE FEATURES:

IP ADDRESS FORMATS:

The system supports various IP address formats:

BRACKET NOTATION:

IPv6 addresses in URLs and socket addresses use bracket notation:

Note
All parsing functions validate input addresses.
IPv6 addresses are normalized (brackets removed) for internal storage.
Port numbers are validated (0-65535 range).
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides lightweight mathematical utilities for image processing, including rounding and RGB value clamping.

CORE FEATURES:

Note
For aspect ratio calculations, see util/aspect_ratio.h
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

Utilities for converting numbers to strings and calculating digit counts. Designed for minimal overhead, suitable for real-time video processing.

CORE FEATURES:

USAGE:

// Count digits needed for allocation int digits = digits_u32(12345); // Returns 5 char buffer[digits]; size_t len = write_decimal(12345, buffer);

// Format u8 into ANSI sequence char ansi_seq[3]; char *end = write_u8(ansi_seq, 255); // Write "255" // ansi_seq now contains "255", end points after last char

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

Safe arithmetic operations that detect and handle integer overflow/underflow without undefined behavior. Essential for buffer size calculations and dimension validations to prevent security issues and crashes.

CORE FEATURES:

MOTIVATION:

Integer overflow in buffer size calculations is a common source of:

USAGE:

// Safe multiplication size_t result; if (checked_size_mul(width, height, &result) != ASCIICHAT_OK) { log_error("Dimension overflow: %zu x %zu", width, height); return ERROR_OVERFLOW; } uint8_t *buffer = SAFE_MALLOC(result, uint8_t *);

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

This header provides safe, validated parsing utilities for protocol message formats. Functions validate input strings, check for overflow conditions, and return structured error codes on failure.

CORE FEATURES:

PROTOCOL MESSAGE FORMATS:

The system supports parsing of ascii-chat protocol messages:

VALIDATION:

All parsing functions validate:

Note
All parsing functions are safe and do not cause undefined behavior.
Invalid input returns error codes (ASCIICHAT_ERROR_*) for proper handling.
Output parameters are only modified on successful parsing.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

Provides high-level password prompting functionality built on top of the platform question API. Includes formatting with visual separators and consistent user experience across the application.

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

This header provides cross-platform utilities for working with file paths, including path expansion, configuration directory resolution, and project relative path extraction.

Core Features

Path Expansion

The system supports:

Configuration Directories

All ascii-chat data files (config, known_hosts, etc.) use a single directory:

Note
All returned paths that require freeing are allocated with malloc() and must be freed by the caller using free().
Path operations handle both Unix (/) and Windows () separators.
Configuration paths include directory separator at the end.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides utilities for string manipulation, shell command escaping, and validation. Functions ensure strings are safe for use in shell commands and command-line interfaces.

CORE FEATURES:

SHELL ESCAPING:

The system provides different escaping strategies:

SHELL METACHARACTERS:

The following characters are considered unsafe for shell commands:

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
February 2026

Utility Functions

Overview

The Utility module provides a collection of helper functions and utilities used throughout ascii-chat. These include string manipulation, path handling, IP address parsing, mathematical operations, and format conversion.

Implementation: lib/util/*.h

Key Features:

  • Hardware-accelerated CRC32 checksums (SSE4.2, ARM CRC32)
  • Aspect ratio calculations and frame fitting
  • IP address parsing and validation (IPv4/IPv6)
  • UTF-8 string handling and validation
  • Path manipulation and normalization
  • String parsing and formatting utilities
  • Cross-platform mathematical operations

Utility Modules

CRC32 Checksums

Hardware Acceleration:

// CRC32 with automatic hardware acceleration
uint32_t checksum = crc32_compute(data, length);
// Check if hardware acceleration is available
if (crc32_hw_supported()) {
log_info("Using hardware CRC32 acceleration");
}
unsigned int uint32_t
Definition common.h:58
#define log_info(...)
Log an INFO message.
Definition log/log.h:561

Platforms:

  • x86/x64: SSE4.2 instructions (CRC32C)
  • ARM: ARM CRC32 instructions
  • Fallback: Software table-based CRC32

Aspect Ratio

Calculate Aspect Ratio:

// Calculate aspect ratio for fitting
int image_width = 1920, image_height = 1080;
dimensions_t fitted = fit_to_aspect_ratio(
image_width, image_height
);
log_info("Fitted size: %dx%d", fitted.width, fitted.height);
int display_width(const char *text)
Calculate the visible display width of text with ANSI escape codes.
int display_height(const char *text, int terminal_width)
Calculate how many display lines text will consume when wrapped.

Maintain Aspect Ratio:

// Get dimensions that maintain aspect ratio
dimensions_t dims = calculate_aspect_ratio_fit(
src_width, src_height,
dst_width, dst_height,
maintain_aspect_ratio // true/false
);

IP Address Utilities

Parse IP Addresses:

// Parse IPv4 address
struct in_addr addr4;
if (parse_ipv4("192.168.1.1", &addr4)) {
log_info("Valid IPv4 address");
}
// Parse IPv6 address
struct in6_addr addr6;
if (parse_ipv6("::1", &addr6)) {
log_info("Valid IPv6 address");
}

Validate IP Addresses:

// Check if string is valid IP
if (is_valid_ipv4("192.168.1.1")) {
log_info("Valid IPv4");
}
if (is_valid_ipv6("2001:db8::1")) {
log_info("Valid IPv6");
}
int is_valid_ipv4(const char *ip)
Check if a string is a valid IPv4 address.
Definition ip.c:58
int is_valid_ipv6(const char *ip)
Check if a string is a valid IPv6 address.
Definition ip.c:105

Format IP Addresses:

char ip_str[INET6_ADDRSTRLEN];
format_ip_address(&addr, ip_str, sizeof(ip_str));
log_info("IP: %s", ip_str);
asciichat_error_t format_ip_address(int family, const struct sockaddr *addr, char *output, size_t output_size)
Format IP address from socket address structure.
Definition ip.c:196

UTF-8 String Handling

Validate UTF-8:

const char *text = "Hello, 世界!";
if (is_valid_utf8(text)) {
log_info("Valid UTF-8 string");
}

Count UTF-8 Characters:

size_t char_count = utf8_strlen(text);
size_t byte_count = strlen(text);
log_info("Characters: %zu, Bytes: %zu", char_count, byte_count);

Iterate UTF-8 Characters:

const char *p = text;
while (*p) {
uint32_t codepoint;
size_t bytes = utf8_decode(p, &codepoint);
log_debug("Codepoint: U+%04X", codepoint);
p += bytes;
}
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
int utf8_decode(const uint8_t *s, uint32_t *codepoint)
Decode a UTF-8 sequence to a Unicode codepoint.
Definition utf8.c:18

Path Utilities

Normalize Paths:

char normalized[PATH_MAX];
normalize_path("/path/to/../file.txt", normalized, sizeof(normalized));
// Result: "/path/file.txt"

Join Paths:

char joined[PATH_MAX];
join_path("/home/user", ".ssh/id_ed25519", joined, sizeof(joined));
// Result: "/home/user/.ssh/id_ed25519"

Expand Home Directory:

char expanded[PATH_MAX];
expand_home_dir("~/.ssh/known_hosts", expanded, sizeof(expanded));
// Result: "/home/username/.ssh/known_hosts"

Get File Extension:

const char *ext = get_file_extension("document.pdf");
// Result: "pdf"

String Utilities

Trim Whitespace:

char text[] = " hello world ";
trim_whitespace(text);
// Result: "hello world"

String Matching:

if (str_starts_with(text, "hello")) {
log_info("Text starts with 'hello'");
}
if (str_ends_with(text, "world")) {
log_info("Text ends with 'world'");
}

Case-Insensitive Compare:

if (strcasecmp_safe(str1, str2) == 0) {
log_info("Strings are equal (case-insensitive)");
}

Parsing Utilities

Parse Integers:

int value;
if (parse_int("1234", &value)) {
log_info("Parsed: %d", value);
}
// Parse with range validation
if (parse_int_range("42", &value, 0, 100)) {
log_info("Valid value in range: %d", value);
}

Parse Floating Point:

double value;
if (parse_double("3.14159", &value)) {
log_info("Parsed: %f", value);
}

Parse Boolean:

bool value;
if (parse_bool("true", &value)) {
log_info("Boolean: %s", value ? "true" : "false");
}
// Accepts: "true", "false", "yes", "no", "1", "0" (case-insensitive)

Format Utilities

Format Byte Sizes:

char size_str[32];
format_bytes(1024 * 1024 * 5, size_str, sizeof(size_str));
// Result: "5.00 MB"

Format Duration:

char duration[64];
format_duration(3665, duration, sizeof(duration));
// Result: "1h 1m 5s"

Format Timestamp:

char timestamp[32];
format_timestamp(time(NULL), timestamp, sizeof(timestamp));
// Result: "2025-01-15 14:23:45"

Mathematical Utilities

Safe Integer Math:

// Clamp value to range
int clamped = clamp(value, 0, 100);
// Get minimum/maximum
int min_val = min(a, b);
int max_val = max(a, b);

Rounding:

// Round to nearest integer
int rounded = round_to_int(3.7); // Result: 4
// Round up/down
int ceil_val = round_up(3.2); // Result: 4
int floor_val = round_down(3.8); // Result: 3

Alignment:

// Align to power of 2
size_t aligned = align_up(123, 16); // Result: 128
size_t aligned_down = align_down(123, 16); // Result: 112

Performance Notes

CRC32 Hardware Acceleration:

  • ~10x faster on x86 with SSE4.2
  • ~8x faster on ARM with CRC32 instructions
  • Automatically detected at runtime

UTF-8 Validation:

  • Optimized for ASCII-common case
  • SIMD acceleration where available
  • Early exit on invalid sequences

Path Operations:

  • In-place modifications where possible
  • Minimal allocations
  • Platform-specific optimizations

Best Practices

DO:

  • Validate input strings before processing
  • Use UTF-8 functions for international text
  • Check return values from parsing functions
  • Use hardware-accelerated CRC32 when available
  • Normalize paths before comparison

DON'T:

  • Don't assume ASCII-only input
  • Don't ignore parse errors
  • Don't use strcpy/sprintf (use safe versions)
  • Don't hard-code path separators
  • Don't perform string operations without bounds checking
See also
lib/crc32.h
lib/util/aspect_ratio.h
lib/util/ip.h
lib/util/utf8.h
lib/util/path.h
lib/util/string.h
lib/util/parsing.h
lib/util/format.h
lib/util/math.h

Macro Definition Documentation

◆ asciichat_crc32

#define asciichat_crc32 (   data,
  len 
)    asciichat_crc32_hw((data), (len))

#include <crc32.h>

Main CRC32 dispatcher macro - use this in application code.

Parameters
dataData to compute checksum for
lenLength of data in bytes
Returns
CRC32 checksum value (32-bit unsigned integer)

Recommended macro for CRC32 computation. Automatically dispatches to hardware-accelerated implementation when available, otherwise uses software fallback.

Note
This macro expands to asciichat_crc32_hw() which performs automatic hardware detection and fallback.
Use this macro in all application code for optimal performance with automatic hardware acceleration.
Example:
uint32_t checksum = asciichat_crc32(packet_data, packet_size);
#define asciichat_crc32(data, len)
Main CRC32 dispatcher macro - use this in application code.
Definition crc32.h:144

Definition at line 144 of file crc32.h.

◆ FNV1A_32_HASH

#define FNV1A_32_HASH (   hash,
  byte 
)

#include <fnv1a.h>

Value:
do { \
(hash) = (((hash) ^ (uint64_t)(byte)) * FNV1A_32_PRIME) & FNV1A_32_MASK; \
} while (0)
unsigned long long uint64_t
Definition common.h:59
#define FNV1A_32_MASK
FNV-1a 32-bit mask.
Definition fnv1a.h:41
#define FNV1A_32_PRIME
FNV-1a 32-bit prime.
Definition fnv1a.h:38

FNV-1a 32-bit hash macro for a single byte.

Definition at line 51 of file fnv1a.h.

52 { \
53 (hash) = (((hash) ^ (uint64_t)(byte)) * FNV1A_32_PRIME) & FNV1A_32_MASK; \
54 } while (0)

◆ FNV1A_32_MASK

#define FNV1A_32_MASK   0xFFFFFFFFULL

#include <fnv1a.h>

FNV-1a 32-bit mask.

Definition at line 41 of file fnv1a.h.

◆ FNV1A_32_OFFSET_BASIS

#define FNV1A_32_OFFSET_BASIS   2166136261ULL

#include <fnv1a.h>

FNV-1a 32-bit offset basis.

Definition at line 35 of file fnv1a.h.

◆ FNV1A_32_PRIME

#define FNV1A_32_PRIME   16777619ULL

#include <fnv1a.h>

FNV-1a 32-bit prime.

Definition at line 38 of file fnv1a.h.

◆ FNV1A_INIT

#define FNV1A_INIT (   hash)    uint64_t hash = FNV1A_32_OFFSET_BASIS;

#include <fnv1a.h>

FNV-1a 32-bit hash macro.

Definition at line 48 of file fnv1a.h.

◆ MAX_PATH_BASES

#define MAX_PATH_BASES   16

#include <path.h>

Maximum number of path base directories.

Maximum number of base directories that can be checked in path validation.

Definition at line 85 of file path.h.

◆ PASSWORD_MAX_LEN

#define PASSWORD_MAX_LEN   260

#include <password.h>

Buffer size for password input.

Passwords are limited to 256 characters per validate_opt_password(). Adding space for null terminator and some margin.

Definition at line 30 of file password.h.

◆ PATH_COMPONENT_DOT

#define PATH_COMPONENT_DOT   '.'

#include <path.h>

Path component: current directory (single dot)

Character constant for the current directory component in paths.

Definition at line 59 of file path.h.

◆ PATH_COMPONENT_DOTDOT

#define PATH_COMPONENT_DOTDOT   ".."

#include <path.h>

Path component: parent directory (double dot)

String constant for the parent directory component in paths.

Definition at line 66 of file path.h.

◆ PATH_DRIVE_SEPARATOR

#define PATH_DRIVE_SEPARATOR   ':'

#include <path.h>

Path component: Windows drive separator (colon)

Definition at line 78 of file path.h.

◆ PATH_TILDE

#define PATH_TILDE   '~'

#include <path.h>

Path component: home directory tilde.

The home directory component in paths (e.g., "~/path").

Definition at line 73 of file path.h.

◆ ROUND

#define ROUND (   xfloat)    (int)(0.5f + (xfloat))

#include <math.h>

Round floating-point value to nearest integer.

Parameters
xfloatFloating-point value to round
Returns
Nearest integer value

Rounds a floating-point value to the nearest integer using standard rounding rules (0.5 rounds up). Uses simple addition of 0.5 and truncation.

Note
This is a macro for performance (inlined at compile time).
Uses standard rounding (banker's rounding not applied).
Example
int rounded = ROUND(3.7); // Returns 4
int rounded = ROUND(3.2); // Returns 3
int rounded = ROUND(-2.5); // Returns -2
#define ROUND(xfloat)
Round floating-point value to nearest integer.
Definition math.h:53

Definition at line 53 of file math.h.

◆ SIZE_ADD_SAFE

#define SIZE_ADD_SAFE (   a,
  b 
)    (size_add_would_overflow(a, b) ? SIZE_MAX : (a) + (b))

#include <overflow.h>

Add with overflow check inline (macro form for tight loops)

Parameters
aFirst operand
bSecond operand
Returns
Sum if no overflow, SIZE_MAX if would overflow

Macro version of checked addition that returns SIZE_MAX on overflow.

Warning
The result SIZE_MAX might be a valid sum. Use the function version (checked_size_add) for proper error handling in most code.

Definition at line 244 of file overflow.h.

◆ SIZE_MUL_SAFE

#define SIZE_MUL_SAFE (   a,
  b 
)    (size_mul_would_overflow(a, b) ? SIZE_MAX : (a) * (b))

#include <overflow.h>

Multiply with overflow check inline (macro form for tight loops)

Parameters
aFirst operand
bSecond operand
Returns
Product if no overflow, SIZE_MAX if would overflow

Macro version of checked multiplication that returns SIZE_MAX on overflow. Useful in performance-critical loops where you want to avoid function calls.

Warning
The result SIZE_MAX might be a valid product. Use the function version (checked_size_mul) for proper error handling in most code.

Definition at line 229 of file overflow.h.

Enumeration Type Documentation

◆ path_role_t

#include <path.h>

Classification for user-supplied filesystem paths.

Enumerator
PATH_ROLE_CONFIG_FILE 

Configuration files such as config.toml

PATH_ROLE_LOG_FILE 

Log file destinations

PATH_ROLE_KEY_PRIVATE 

Private key files (SSH/GPG)

PATH_ROLE_KEY_PUBLIC 

Public key files or expected server keys

PATH_ROLE_CLIENT_KEYS 

Client key whitelist files

Definition at line 316 of file path.h.

316 {
path_role_t
Classification for user-supplied filesystem paths.
Definition path.h:316
@ PATH_ROLE_CONFIG_FILE
Definition path.h:317
@ PATH_ROLE_CLIENT_KEYS
Definition path.h:321
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:320
@ PATH_ROLE_LOG_FILE
Definition path.h:318
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:319

Function Documentation

◆ __isoc23_scanf()

int __isoc23_scanf ( const char *  format,
  ... 
)

#include <musl_c23_compat.c>

Formatted input functions.

Read formatted input from stdin (C23 compatible)

Parameters
formatFormat string
...Variable arguments
Returns
Number of successfully read items

Definition at line 114 of file musl_c23_compat.c.

114 {
115 va_list args;
116 va_start(args, format);
117 int result = vscanf(format, args);
118 va_end(args);
119 return result;
120}
action_args_t args

References args.

◆ __isoc23_wcstol()

long __isoc23_wcstol ( const wchar_t *  str,
wchar_t **  endptr,
int  base 
)

#include <musl_c23_compat.c>

Wide string to integer conversion.

Convert wide string to long (C23 compatible)

Parameters
strWide string to convert
endptrPointer to first non-digit character (optional)
baseNumeric base (0-36)
Returns
Converted value

Definition at line 67 of file musl_c23_compat.c.

67 {
68 return wcstol(str, endptr, base);
69}

◆ __isoc23_wscanf()

int __isoc23_wscanf ( const wchar_t *  format,
  ... 
)

#include <musl_c23_compat.c>

Wide character formatted input functions.

Read formatted wide input from stdin (C23 compatible)

Parameters
formatFormat string
...Variable arguments
Returns
Number of successfully read items

Definition at line 194 of file musl_c23_compat.c.

194 {
195 va_list args;
196 va_start(args, format);
197 int result = vwscanf(format, args);
198 va_end(args);
199 return result;
200}

References args.

◆ arc4random_buf()

void arc4random_buf ( void *  buf,
size_t  nbytes 
)

#include <musl_c23_compat.c>

glibc-specific random functions for expat and fontconfig compatibility

Fill buffer with cryptographically secure random bytes

Used by expat for hash seeding and other security-critical operations. Attempts getrandom syscall first (Linux 3.17+), falls back to /dev/urandom.

Parameters
bufOutput buffer (must not be NULL if nbytes > 0)
nbytesNumber of random bytes to generate

Definition at line 283 of file musl_c23_compat.c.

283 {
284 if (nbytes == 0 || buf == NULL) {
285 return;
286 }
287
288/* Prefer getrandom syscall: available in Linux 3.17+, no file descriptor needed */
289#ifdef SYS_getrandom
290 long result = syscall(SYS_getrandom, buf, nbytes, 0);
291 if (result == (long)nbytes) {
292 return;
293 }
294#endif
295
296 /* Fallback to /dev/urandom device */
297 int fd = open("/dev/urandom", O_RDONLY);
298 if (fd >= 0) {
299 ssize_t n = read(fd, buf, nbytes);
300 close(fd);
301 if (n == (ssize_t)nbytes) {
302 return;
303 }
304 }
305
306 /* Fatal error: unable to obtain random bytes */
307 abort();
308}

Referenced by random_r().

◆ asciichat_crc32_hw()

uint32_t asciichat_crc32_hw ( const void *  data,
size_t  len 
)

#include <crc32.h>

Compute CRC32 checksum with hardware acceleration (if available)

Parameters
dataData to compute checksum for (must not be NULL)
lenLength of data in bytes
Returns
CRC32 checksum value (32-bit unsigned integer)

Computes CRC32 checksum using hardware acceleration when available. Automatically falls back to software implementation if hardware is not available on the current platform.

Note
This function performs runtime CPU feature detection to determine whether to use hardware or software implementation.
Hardware acceleration is available on:
  • x86/x64 processors with SSE4.2 (CRC32 instruction)
  • ARM processors with ARMv8 (CRC32 instructions)
For consistent performance testing or when hardware detection is not desired, use asciichat_crc32_sw() directly.
The CRC32 polynomial used is IEEE 802.3 (CRC-32), which is compatible with standard CRC32 implementations.

Definition at line 132 of file crc32.c.

132 {
133 check_crc32_hw_support();
134
135 if (!crc32_hw_available) {
136 // DEBUG: Log fallback to software
137 static bool logged_fallback = false;
138 if (!logged_fallback) {
139 log_debug("Using software CRC32 (no hardware acceleration)");
140 logged_fallback = true;
141 }
142 return asciichat_crc32_sw(data, len);
143 }
144
145#ifdef ARCH_ARM64
146 static bool logged_arm = false;
147 if (!logged_arm) {
148 log_debug("Using ARM64 hardware CRC32");
149 logged_arm = true;
150 }
151 return crc32_arm_hw(data, len);
152#elif defined(ARCH_X86_64)
153 static bool logged_intel = false;
154 if (!logged_intel) {
155 log_debug("Using Intel x86_64 hardware CRC32 (SSE4.2)");
156 logged_intel = true;
157 }
158 return crc32_intel_hw(data, len);
159#else
160 return asciichat_crc32_sw(data, len);
161#endif
162}
uint32_t asciichat_crc32_sw(const void *data, size_t len)
Compute CRC32 checksum using software implementation only.
Definition crc32.c:171

References asciichat_crc32_sw(), and log_debug.

◆ asciichat_crc32_sw()

uint32_t asciichat_crc32_sw ( const void *  data,
size_t  len 
)

#include <crc32.h>

Compute CRC32 checksum using software implementation only.

Parameters
dataData to compute checksum for (must not be NULL)
lenLength of data in bytes
Returns
CRC32 checksum value (32-bit unsigned integer)

Computes CRC32 checksum using a software lookup table implementation. Always uses software implementation regardless of hardware availability.

Note
This function provides consistent performance across all platforms, which is useful for testing and benchmarking.
For production code, prefer asciichat_crc32() macro which automatically uses hardware acceleration when available.
The CRC32 polynomial used is IEEE 802.3 (CRC-32), which is compatible with standard CRC32 implementations.

Definition at line 171 of file crc32.c.

171 {
172 const uint8_t *bytes = (const uint8_t *)data;
173 uint32_t crc = 0xFFFFFFFF;
174
175 // CRC32-C (Castagnoli) polynomial: 0x1EDC6F41
176 // Reversed (for LSB-first): 0x82F63B78
177 for (size_t i = 0; i < len; i++) {
178 crc ^= bytes[i];
179 for (int j = 0; j < 8; j++) {
180 if (crc & 1) {
181 crc = (crc >> 1) ^ 0x82F63B78; // CRC32-C polynomial (reversed)
182 } else {
183 crc >>= 1;
184 }
185 }
186 }
187
188 return ~crc;
189}
unsigned char uint8_t
Definition common.h:56

Referenced by asciichat_crc32_hw(), client_parse_packet(), and client_serialize_packet().

◆ aspect_ratio()

void aspect_ratio ( const ssize_t  img_width,
const ssize_t  img_height,
const ssize_t  width,
const ssize_t  height,
const bool  stretch,
ssize_t *  out_width,
ssize_t *  out_height 
)

#include <aspect_ratio.h>

Calculate aspect ratio with terminal character correction.

Parameters
img_widthInput image width in pixels (must be > 0)
img_heightInput image height in pixels (must be > 0)
widthTarget width in terminal characters (must be > 0)
heightTarget height in terminal lines (must be > 0)
stretchWhether to allow stretching (true) or maintain aspect ratio (false)
out_widthOutput parameter for calculated width (must not be NULL)
out_heightOutput parameter for calculated height (must not be NULL)

Calculates output dimensions for ASCII art rendering with terminal character aspect ratio correction. Terminal characters are typically taller than wide (e.g., 2:1 ratio), so this function corrects for that to maintain proper image proportions in ASCII art.

TERMINAL CORRECTION:

  • Applies character aspect ratio correction (accounts for terminal font)
  • Ensures ASCII art matches original image proportions
  • When stretch=false, maintains original image aspect ratio
  • When stretch=true, fills target dimensions exactly
Note
Output dimensions respect target size while maintaining aspect ratio.
Terminal character correction is essential for proper ASCII art rendering.
Output parameters are modified to contain calculated dimensions.
Example
ssize_t out_w, out_h;
aspect_ratio(1920, 1080, 80, 40, false, &out_w, &out_h);
// Calculates dimensions that fit within 80x40 terminal while maintaining 16:9 aspect
void aspect_ratio(const ssize_t img_w, const ssize_t img_h, const ssize_t width, const ssize_t height, const bool stretch, ssize_t *out_width, ssize_t *out_height)
Calculate aspect ratio with terminal character correction.

Definition at line 69 of file aspect_ratio.c.

70 {
71 // Input validation
72 if (!out_width || !out_height) {
73 return; // or log error
74 }
75
76 if (img_w <= 0 || img_h <= 0) {
77 // Handle degenerate image dimensions
78 *out_width = MIN_DIMENSION;
79 *out_height = MIN_DIMENSION;
80 return;
81 }
82
83 if (stretch) {
84 // If stretching is enabled, just use the full terminal dimensions
85 *out_width = width;
86 *out_height = height;
87 } else {
88 // Calculate aspect-correct dimensions to fit within the given width/height
89 calculate_fit_dimensions(img_w, img_h, width, height, out_width, out_height);
90 }
91}
@ MIN_DIMENSION

References MIN_DIMENSION.

Referenced by ascii_convert().

◆ aspect_ratio2()

void aspect_ratio2 ( const ssize_t  img_width,
const ssize_t  img_height,
const ssize_t  target_width,
const ssize_t  target_height,
ssize_t *  out_width,
ssize_t *  out_height 
)

#include <aspect_ratio.h>

Simple aspect ratio calculation without terminal character correction.

Parameters
img_widthInput image width in pixels (must be > 0)
img_heightInput image height in pixels (must be > 0)
target_widthTarget width (must be > 0)
target_heightTarget height (must be > 0)
out_widthOutput parameter for calculated width (must not be NULL)
out_heightOutput parameter for calculated height (must not be NULL)

Calculates output dimensions that maintain the input image's aspect ratio while fitting within target dimensions. This is a simple pixel-based calculation without terminal character correction.

Note
This function does NOT apply terminal character correction.
Use aspect_ratio() for ASCII art rendering (with terminal correction).
Output dimensions are calculated to fit within target while maintaining ratio.
Example
ssize_t out_w, out_h;
aspect_ratio2(1920, 1080, 800, 600, &out_w, &out_h);
// Calculates dimensions that fit within 800x600 while maintaining 16:9 aspect
void aspect_ratio2(const ssize_t img_w, const ssize_t img_h, const ssize_t target_w, const ssize_t target_h, ssize_t *out_width, ssize_t *out_height)
Simple aspect ratio calculation without terminal character correction.

Definition at line 95 of file aspect_ratio.c.

96 {
97 // Input validation
98 if (!out_width || !out_height) {
99 return;
100 }
101
102 if (img_w <= 0 || img_h <= 0 || target_w <= 0 || target_h <= 0) {
103 // Handle invalid dimensions
104 *out_width = MIN_DIMENSION;
105 *out_height = MIN_DIMENSION;
106 return;
107 }
108
109 // Calculate aspect ratio
110 float img_aspect = (float)img_w / (float)img_h;
111
112 // We want the image to fill at least one dimension of the target box
113 // while maintaining aspect ratio and fitting within the box
114
115 // Calculate dimensions if we scale to fill width
116 ssize_t width_if_fill_width = target_w;
117 ssize_t height_if_fill_width = (ssize_t)((float)target_w / img_aspect);
118
119 // Calculate dimensions if we scale to fill height
120 ssize_t width_if_fill_height = (ssize_t)((float)target_h * img_aspect);
121 ssize_t height_if_fill_height = target_h;
122
123 // Choose the scaling that maximizes usage of the target box
124 // We want to fill at least one dimension completely
125 if (height_if_fill_width <= target_h) {
126 // Filling width fits within height - use it
127 *out_width = width_if_fill_width;
128 *out_height = height_if_fill_width;
129 } else {
130 // Filling width would exceed height, so fill height instead
131 *out_width = width_if_fill_height;
132 *out_height = height_if_fill_height;
133 }
134
135 // Ensure minimum dimensions
136 if (*out_width <= 0) {
137 *out_width = MIN_DIMENSION;
138 }
139 if (*out_height <= 0) {
140 *out_height = MIN_DIMENSION;
141 }
142}

References MIN_DIMENSION.

◆ calculate_fit_dimensions_pixel()

void calculate_fit_dimensions_pixel ( int  img_width,
int  img_height,
int  max_width,
int  max_height,
int *  out_width,
int *  out_height 
)

#include <aspect_ratio.h>

Calculate fit dimensions for pixel-based images.

Parameters
img_widthInput image width in pixels (must be > 0)
img_heightInput image height in pixels (must be > 0)
max_widthMaximum allowed width in pixels (must be > 0)
max_heightMaximum allowed height in pixels (must be > 0)
out_widthOutput parameter for calculated width (must not be NULL)
out_heightOutput parameter for calculated height (must not be NULL)

Calculates dimensions that fit the input image within maximum bounds while maintaining the original aspect ratio. Useful for scaling images to fit within display constraints.

Note
Output dimensions never exceed max_width or max_height.
Aspect ratio is always maintained (image is not stretched).
Output dimensions are calculated to fit within maximum bounds.
Example
int out_w, out_h;
calculate_fit_dimensions_pixel(1920, 1080, 1280, 720, &out_w, &out_h);
// Calculates dimensions that fit within 1280x720 while maintaining 16:9 aspect
// Result: out_w=1280, out_h=720 (fits exactly)
void calculate_fit_dimensions_pixel(int img_width, int img_height, int max_width, int max_height, int *out_width, int *out_height)
Calculate fit dimensions for pixel-based images.

Definition at line 146 of file aspect_ratio.c.

147 {
148 if (!out_width || !out_height || img_width <= 0 || img_height <= 0) {
149 if (out_width)
150 *out_width = max_width;
151 if (out_height)
152 *out_height = max_height;
153 return;
154 }
155
156 float src_aspect = (float)img_width / (float)img_height;
157
158 // Try filling width
159 int width_if_fill_w = max_width;
160 int height_if_fill_w = (int)(((float)max_width / src_aspect) + 0.5f);
161
162 // Try filling height
163 int width_if_fill_h = (int)(((float)max_height * src_aspect) + 0.5f);
164 int height_if_fill_h = max_height;
165
166 // log_debug("calculate_fit_dimensions: img %dx%d (aspect %.3f), max %dx%d", img_width, img_height, src_aspect,
167 // max_width, max_height);
168 // log_debug(" Fill width: %dx%d, Fill height: %dx%d", width_if_fill_w, height_if_fill_w, width_if_fill_h,
169 // height_if_fill_h);
170
171 // Choose the option that fits
172 if (height_if_fill_w <= max_height) {
173 // Filling width fits
174 // log_debug(" Choosing fill width: %dx%d", width_if_fill_w, height_if_fill_w);
175 *out_width = width_if_fill_w;
176 *out_height = height_if_fill_w;
177 } else {
178 // Fill height instead
179 // log_debug(" Choosing fill height: %dx%d", width_if_fill_h, height_if_fill_h);
180 *out_width = width_if_fill_h;
181 *out_height = height_if_fill_h;
182 }
183
184 // Clamp to bounds
185 if (*out_width > max_width)
186 *out_width = max_width;
187 if (*out_height > max_height)
188 *out_height = max_height;
189 if (*out_width < 1)
190 *out_width = 1;
191 if (*out_height < 1)
192 *out_height = 1;
193
194 // log_debug(" Final output: %dx%d", *out_width, *out_height);
195}

◆ canonicalize_ipv6()

asciichat_error_t canonicalize_ipv6 ( const char *  ipv6,
char *  canonical_out,
size_t  out_size 
)

#include <ip.h>

Canonicalize IPv6 address to standard form.

Parameters
ipv6IPv6 address (must not be NULL)
canonical_outOutput buffer for canonical form (must not be NULL)
out_sizeSize of output buffer
Returns
ASCIICHAT_OK on success, error code on failure

Converts an IPv6 address to canonical form following RFC 5952:

  • Lowercase hexadecimal digits
  • Leading zeros removed from each group
  • Longest run of consecutive zero groups compressed to ::
  • Brackets removed

EXAMPLES:

  • "2001:0DB8:0000:0000:0000:0000:0000:0001" -> "2001:db8::1"
  • "[::1]" -> "::1"
  • "2001:DB8::1" -> "2001:db8::1"
Note
Output buffer should be at least 64 bytes.
Returns error if input is not a valid IPv6 address.
Example
char canonical[64];
if (canonicalize_ipv6("[2001:0DB8::1]", canonical, sizeof(canonical)) == ASCIICHAT_OK) {
// canonical = "2001:db8::1"
}
@ ASCIICHAT_OK
Definition error_codes.h:51
asciichat_error_t canonicalize_ipv6(const char *ipv6, char *canonical_out, size_t out_size)
Canonicalize IPv6 address to standard form.
Definition ip.c:969

Definition at line 969 of file ip.c.

969 {
970 if (!ipv6 || !canonical_out || out_size == 0) {
971 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments to canonicalize_ipv6");
972 }
973
974 // Normalize (remove brackets)
975 char normalized[INET6_ADDRSTRLEN];
976 if (parse_ipv6_address(ipv6, normalized, sizeof(normalized)) != 0) {
977 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to parse IPv6 address");
978 }
979
980 // Validate
981 if (!is_valid_ipv6(normalized)) {
982 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IPv6 address: %s", ipv6);
983 }
984
985 struct in6_addr addr;
986 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
987 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to convert IPv6 address");
988 }
989
990 // Use inet_ntop which produces canonical form
991 if (inet_ntop(AF_INET6, &addr, canonical_out, (socklen_t)out_size) == NULL) {
992 return SET_ERRNO(ERROR_NETWORK, "Failed to format IPv6 address");
993 }
994
995 return ASCIICHAT_OK;
996}
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_NETWORK
Definition error_codes.h:77
@ ERROR_INVALID_PARAM
int parse_ipv6_address(const char *input, char *output, size_t output_size)
Parse IPv6 address, removing brackets if present.
Definition ip.c:158

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NETWORK, is_valid_ipv6(), parse_ipv6_address(), and SET_ERRNO.

Referenced by compact_ipv6(), and compare_ip_port_strings().

◆ colored_string()

const char * colored_string ( log_color_t  color,
const char *  text 
)

#include <string.h>

Build a colored string for terminal output.

Wraps text with ANSI color codes based on terminal capabilities. Uses a rotating buffer to handle multiple colored strings in the same scope. Checks if stdout is a TTY and CLAUDECODE environment variable.

BEHAVIOR:

  • If stdout is a TTY and CLAUDECODE is not set: Returns colored text with ANSI codes
  • If stdout is not a TTY or CLAUDECODE is set: Returns plain text without colors
  • Detects terminal color capabilities (respects NO_COLOR and terminal restrictions)

USAGE:

// Single colored string
fprintf(stderr, "%s\n", colored_string(LOG_COLOR_FATAL, "Error"));
// Multiple colored strings in same scope (uses rotating buffers)
fprintf(stderr, "%s %s\n",
colored_string(LOG_COLOR_WARN, "warning message"));
@ LOG_COLOR_FATAL
Definition log/log.h:136
@ LOG_COLOR_WARN
Definition log/log.h:134
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
Parameters
colorThe log color to apply (LOG_COLOR_DEBUG, LOG_COLOR_FATAL, LOG_COLOR_WARN, etc.)
textThe text to color
Returns
Colored string with ANSI codes, or plain text if colors disabled
Note
The returned pointer points to a static rotating buffer. Use or copy the result before calling colored_string() again in tight loops.
Color codes are applied based on log_level_color() from logging subsystem.

Definition at line 274 of file util/string.c.

274 {
275#define COLORED_BUFFERS 4
276#define COLORED_BUFFER_SIZE 256
277 static char buffers[COLORED_BUFFERS][COLORED_BUFFER_SIZE];
278 static int buffer_idx = 0;
279
280 if (!text) {
281 return "";
282 }
283
284 // Check if we should use colors
285 // Use global flag that persists even after options cleanup
286 bool use_colors = false;
287
288 // Priority 1: If --color was explicitly passed, force colors
290 use_colors = true;
291 }
292 // Priority 2: If --color NOT explicitly passed, use terminal detection
293 else if (!g_color_flag_passed) {
294 use_colors = terminal_should_color_output(STDOUT_FILENO);
295 }
296 // Priority 3: If --color=false was explicitly passed, disable colors
297 // (use_colors stays false)
298
299 if (!use_colors) {
300 // No colors, just return the text directly
301 return text;
302 }
303
304 // Use rotating buffer to handle multiple calls in same fprintf
305 char *current_buf = buffers[buffer_idx];
306 buffer_idx = (buffer_idx + 1) % COLORED_BUFFERS;
307
308 const char *color_code = log_level_color(color);
309 const char *reset_code = log_level_color(LOG_COLOR_RESET);
310
311 // Ensure we never pass NULL to snprintf (handle uninitialized color system)
312 if (!color_code)
313 color_code = "";
314 if (!reset_code)
315 reset_code = "";
316
317 // Format into rotating static buffer: color_code + text + reset_code
318 safe_snprintf(current_buf, COLORED_BUFFER_SIZE, "%s%s%s", color_code, text, reset_code);
319 return current_buf;
320}
bool g_color_flag_value
Value of –color flag (true if –color was in argv) Set by options_init() before RCU is initialized,...
Definition common.c:58
bool g_color_flag_passed
Was –color explicitly passed in command-line arguments? Set by options_init() before RCU is initializ...
Definition common.c:57
const char * log_level_color(log_color_t color)
Get color string for a given color enum.
Definition log/log.c:1656
@ LOG_COLOR_RESET
Definition log/log.h:138
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
bool terminal_should_color_output(int fd)
Determine if color output should be used.
#define COLORED_BUFFERS
#define COLORED_BUFFER_SIZE

References COLORED_BUFFER_SIZE, COLORED_BUFFERS, g_color_flag_passed, g_color_flag_value, LOG_COLOR_RESET, log_level_color(), safe_snprintf(), and terminal_should_color_output().

Referenced by action_show_capabilities_immediate(), action_show_version(), asciichat_print_error_context(), backtrace_print(), colorize_log_message(), colorize_named_string(), log_labeled(), log_template_apply(), mutex_stack_detect_deadlocks(), options_config_calculate_max_col_width(), options_config_print_options_sections_with_width(), options_config_print_usage(), options_print_help_for_mode(), platform_is_binary_in_path(), print_project_links(), and ui_mdns_select().

◆ compact_ipv6()

asciichat_error_t compact_ipv6 ( const char *  ipv6,
char *  compact_out,
size_t  out_size 
)

#include <ip.h>

Compress IPv6 address using :: notation.

Parameters
ipv6IPv6 address (must not be NULL)
compact_outOutput buffer for compressed form (must not be NULL)
out_sizeSize of output buffer
Returns
ASCIICHAT_OK on success, error code on failure

Compresses an IPv6 address by replacing the longest run of consecutive zero groups with ::. Follows RFC 5952 compression rules.

EXAMPLES:

  • "2001:0db8:0000:0000:0000:0000:0000:0001" -> "2001:db8::1"
  • "fe80:0000:0000:0000:0000:0000:0000:0001" -> "fe80::1"
  • "2001:db8:0:0:1:0:0:1" -> "2001:db8::1:0:0:1"
Note
Output buffer should be at least 64 bytes.
Returns error if input is not a valid IPv6 address.
Alias for canonicalize_ipv6() - both produce the same output.
Example
char compact[64];
if (compact_ipv6("2001:0db8:0000:0000:0000:0000:0000:0001", compact, sizeof(compact)) == ASCIICHAT_OK) {
// compact = "2001:db8::1"
}
asciichat_error_t compact_ipv6(const char *ipv6, char *compact_out, size_t out_size)
Compress IPv6 address using :: notation.
Definition ip.c:999

Definition at line 999 of file ip.c.

999 {
1000 return canonicalize_ipv6(ipv6, compact_out, out_size);
1001}

References canonicalize_ipv6().

◆ compare_ip_port_strings()

int compare_ip_port_strings ( const char *  ip_port1,
const char *  ip_port2 
)

#include <ip.h>

Compare two "IP:port" strings with IPv6 normalization.

Compares two formatted address strings (e.g., "192.168.1.1:8080" or "[::1]:8080") with proper handling of different IPv6 representations.

Handles IPv6 normalization so different representations are treated as equal:

  • "[::1]:8080" equals "[0:0:0:0:0:0:0:1]:8080"
  • "[2001:db8::1]:80" equals "[2001:0db8:0000:0000:0000:0000:0000:0001]:80"

For IPv4 addresses, uses robust comparison via ip_equals() that handles different binary representations correctly.

Parameters
ip_port1First "IP:port" string (IPv4: "192.168.1.1:8080", IPv6: "[::1]:8080")
ip_port2Second "IP:port" string to compare
Returns
1 if addresses match (after normalization), 0 otherwise
Example
// IPv6 normalization
int match1 = compare_ip_port_strings("[::1]:8080", "[0:0:0:0:0:0:0:1]:8080");
// Returns 1 (equal after normalization)
// IPv4 comparison
int match2 = compare_ip_port_strings("192.168.1.1:80", "192.168.1.1:80");
// Returns 1 (equal)
// Different ports
int match3 = compare_ip_port_strings("192.168.1.1:80", "192.168.1.1:8080");
// Returns 0 (different ports)
int compare_ip_port_strings(const char *ip_port1, const char *ip_port2)
Compare two "IP:port" strings with IPv6 normalization.
Definition ip.c:1133

Definition at line 1133 of file ip.c.

1133 {
1134 if (!ip_port1 || !ip_port2) {
1135 return 0; // Not equal
1136 }
1137
1138 // Fast path: if strings are identical, no parsing needed
1139 if (strcmp(ip_port1, ip_port2) == 0) {
1140 return 1; // Equal
1141 }
1142
1143 // Parse both IP:port strings
1144 char ip1[64], ip2[64];
1145 uint16_t port1 = 0, port2 = 0;
1146
1147 if (parse_ip_with_port(ip_port1, ip1, sizeof(ip1), &port1) != 0) {
1148 return 0; // Invalid format - not equal
1149 }
1150 if (parse_ip_with_port(ip_port2, ip2, sizeof(ip2), &port2) != 0) {
1151 return 0; // Invalid format - not equal
1152 }
1153
1154 // Ports must match exactly
1155 if (port1 != port2) {
1156 return 0; // Not equal
1157 }
1158
1159 // For IPv6, normalize both addresses before comparison
1160 int version1 = get_ip_version(ip1);
1161 int version2 = get_ip_version(ip2);
1162
1163 // IP versions must match
1164 if (version1 != version2 || version1 == 0) {
1165 return 0; // Not equal
1166 }
1167
1168 if (version1 == 6) {
1169 // Normalize both IPv6 addresses
1170 char canonical1[INET6_ADDRSTRLEN], canonical2[INET6_ADDRSTRLEN];
1171 if (canonicalize_ipv6(ip1, canonical1, sizeof(canonical1)) != ASCIICHAT_OK) {
1172 return 0; // Failed to normalize - not equal
1173 }
1174 if (canonicalize_ipv6(ip2, canonical2, sizeof(canonical2)) != ASCIICHAT_OK) {
1175 return 0; // Failed to normalize - not equal
1176 }
1177 return (strcmp(canonical1, canonical2) == 0) ? 1 : 0;
1178 } else {
1179 // IPv4: use ip_equals for robust comparison
1180 return ip_equals(ip1, ip2) ? 1 : 0;
1181 }
1182}
unsigned short uint16_t
Definition common.h:57
int parse_ip_with_port(const char *input, char *ip_output, size_t ip_output_size, uint16_t *port_output)
Parse IP address and port from string.
Definition ip.c:296
int ip_equals(const char *ip1, const char *ip2)
Compare two IP addresses for equality.
Definition ip.c:557
int get_ip_version(const char *ip)
Get IP address version.
Definition ip.c:510

References ASCIICHAT_OK, canonicalize_ipv6(), get_ip_version(), ip_equals(), and parse_ip_with_port().

Referenced by check_known_host(), and check_known_host_no_identity().

◆ crc32_hw_is_available()

bool crc32_hw_is_available ( void  )

#include <crc32.h>

Check if hardware CRC32 acceleration is available at runtime.

Returns
true if hardware acceleration is available, false otherwise

Performs runtime CPU feature detection to determine if hardware CRC32 acceleration can be used on the current platform.

Note
Detection is performed once per program execution and cached. Subsequent calls return the cached result.
Hardware acceleration is available on:
  • x86/x64: Processors with SSE4.2 feature (CRC32 instruction)
  • ARM: Processors with ARMv8 architecture (CRC32 instructions)
Use this function to determine if hardware acceleration will be used before calling asciichat_crc32_hw().

Definition at line 164 of file crc32.c.

164 {
165 check_crc32_hw_support();
166 return crc32_hw_available;
167}

◆ display_center_horizontal()

int display_center_horizontal ( const char *  text,
int  terminal_width 
)

#include <display.h>

Calculate left padding needed to center text horizontally.

Calculates how many spaces are needed before text to center it within a given terminal width. Properly handles ANSI escape codes and UTF-8.

Parameters
textText to center (may contain ANSI codes, must not be NULL)
terminal_widthTotal width available for centering (e.g., 80)
Returns
Number of spaces to print before text, or 0 if text is wider than terminal
Example
// Center "Hello" in 80-char line
int padding = display_center_horizontal("\033[1;36mHello\033[0m", 80);
// Returns 38 ((80-5)/2 = 37.5, rounds to 38)
int display_center_horizontal(const char *text, int terminal_width)
Calculate left padding needed to center text horizontally.

Definition at line 32 of file lib/util/display.c.

32 {
33 if (!text || terminal_width <= 0) {
34 return 0;
35 }
36
37 int text_width = display_width(text);
38
39 // If text is empty or wider than terminal, don't add padding
40 if (text_width == 0 || text_width >= terminal_width) {
41 return 0;
42 }
43
44 // Calculate left padding to center
45 return (terminal_width - text_width) / 2;
46}

References display_width().

◆ display_center_vertical()

int display_center_vertical ( int  content_height,
int  terminal_height 
)

#include <display.h>

Calculate top padding needed to vertically center content.

Calculates how many newlines are needed before content to center it vertically within a terminal of given height.

Parameters
content_heightHeight of content in lines
terminal_heightTotal height available (typical: 24-30 lines)
Returns
Number of newlines to print before content
Example
// Center 8-line status screen in 24-line terminal
int padding = display_center_vertical(8, 24);
// Returns 8 ((24-8)/2 = 8 lines)
int display_center_vertical(int content_height, int terminal_height)
Calculate top padding needed to vertically center content.

Definition at line 48 of file lib/util/display.c.

48 {
49 if (content_height <= 0 || terminal_height <= 0) {
50 return 0;
51 }
52
53 if (content_height >= terminal_height) {
54 return 0;
55 }
56
57 return (terminal_height - content_height) / 2;
58}

◆ display_height()

int display_height ( const char *  text,
int  terminal_width 
)

#include <display.h>

Calculate how many display lines text will consume when wrapped.

Splits text by newlines and calculates how many terminal rows it will occupy when wrapped to the given terminal width. Properly handles ANSI escape codes (ignored in width calculation) and UTF-8 multi-byte characters.

Parameters
textInput text which may contain ANSI codes and newlines (NULL-safe)
terminal_widthWidth available for wrapping (e.g., 80)
Returns
Number of display lines consumed, or 0 if text is empty
Example
// "Hello\nWorld" in 80-char terminal = 2 lines
int height = display_height("Hello\nWorld", 80); // Returns 2
// "This is a very long line..." wrapped to 40 chars
int height = display_height("This is a very long line that exceeds...", 40);
// Returns 2-3 depending on exact length
// With ANSI codes: "\033[1;36m" is not counted in width
int height = display_height("\033[1;36m50-char line\033[0m", 40);
// Returns 2 (50 visible chars / 40 width)

Definition at line 60 of file lib/util/display.c.

60 {
61 if (!text || terminal_width <= 0) {
62 return 0;
63 }
64
65 int total_lines = 0;
66 const char *p = text;
67
68 // Count lines by processing the text and counting newlines + final segment
69 while (*p) {
70 // Find the end of this line (next newline or end of string)
71 const char *line_end = p;
72 while (*line_end && *line_end != '\n') {
73 line_end++;
74 }
75
76 // Calculate display width of this line
77 size_t line_len = line_end - p;
78 if (line_len > 0) {
79 // Strip ANSI codes and calculate display width
80 char *stripped = ansi_strip_escapes(p, line_len);
81 if (stripped) {
82 int line_width = utf8_display_width(stripped);
83 SAFE_FREE(stripped);
84
85 if (line_width > 0) {
86 // Calculate how many display lines this wraps to
87 int display_lines = (line_width + terminal_width - 1) / terminal_width; // Ceiling division
88 total_lines += display_lines;
89 } else {
90 total_lines += 1;
91 }
92 } else {
93 // If stripping fails, count as 1 line
94 total_lines += 1;
95 }
96 } else {
97 // Empty line still counts as 1 line
98 total_lines += 1;
99 }
100
101 // Move to next line
102 if (*line_end == '\n') {
103 p = line_end + 1;
104 } else {
105 break;
106 }
107 }
108
109 return total_lines;
110}
char * ansi_strip_escapes(const char *input, size_t input_len)
Strip all ANSI escape sequences from a string.
Definition ansi.c:23
#define SAFE_FREE(ptr)
Definition common.h:376
int utf8_display_width(const char *str)
Calculate terminal display width of a UTF-8 string.
Definition utf8.c:46

References ansi_strip_escapes(), SAFE_FREE, and utf8_display_width().

Referenced by terminal_screen_render().

◆ display_width()

int display_width ( const char *  text)

#include <display.h>

Calculate the visible display width of text with ANSI escape codes.

Strips ANSI escape sequences and calculates the actual terminal display width. Properly handles UTF-8 multi-byte characters and wide characters.

Parameters
textInput text which may contain ANSI escape codes (must not be NULL)
Returns
Display width in terminal columns, or -1 on error
Example
int width = display_width("\033[1;36mHello\033[0m"); // Returns 5

Definition at line 13 of file lib/util/display.c.

13 {
14 if (!text) {
15 return 0;
16 }
17
18 // Strip ANSI escape codes
19 char *stripped = ansi_strip_escapes(text, strlen(text));
20
21 // Use stripped version if available, otherwise use original text
22 const char *to_measure = stripped ? stripped : text;
23
24 // Calculate display width using UTF-8 aware calculation
25 int width = utf8_display_width(to_measure);
26 SAFE_FREE(stripped);
27
28 // Return 0 for empty or invalid input, otherwise return calculated width
29 return width < 0 ? 0 : width;
30}

References ansi_strip_escapes(), SAFE_FREE, and utf8_display_width().

Referenced by display_center_horizontal(), frame_buffer_render_centered(), layout_print_two_column_row(), terminal_fd_reader_next(), terminal_screen_render(), and truncate_with_ellipsis().

◆ escape_ascii()

void escape_ascii ( const char *  str,
const char *  escape_char,
char *  out_buffer,
size_t  out_buffer_size 
)

#include <string.h>

Escape ASCII characters in a string.

Parameters
strInput string to escape (must not be NULL)
escape_charCharacter to escape (must not be NULL)
out_bufferOutput buffer for escaped string (must not be NULL)
out_buffer_sizeSize of output buffer (must be > 0)

Escapes a specific ASCII character in a string by adding an escape prefix. Useful for terminal output or string formatting where certain characters need to be escaped.

Note
Output buffer must be large enough to hold the escaped string.
Function truncates output if buffer is too small.
Does not validate input parameters for NULL (caller's responsibility).

Definition at line 20 of file util/string.c.

20 {
21 if (escape_char == NULL || str == NULL || !out_buffer || out_buffer_size == 0) {
22 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid string or escape character or output buffer");
23 return;
24 }
25
26 // Count how many characters need escaping
27 size_t escape_count = 0;
28 for (size_t i = 0; i < strlen(str); i++) {
29 for (size_t j = 0; j < strlen(escape_char); j++) {
30 if (str[i] == escape_char[j]) {
31 escape_count++;
32 break;
33 }
34 }
35 }
36
37 size_t total_len = strlen(str) + escape_count;
38 if (out_buffer_size < total_len) {
39 SET_ERRNO(ERROR_INVALID_PARAM, "Output buffer is too small");
40 return;
41 }
42
43 size_t out_pos = 0;
44 for (size_t i = 0; i < strlen(str); i++) {
45 bool needs_escape = false;
46 for (size_t j = 0; j < strlen(escape_char); j++) {
47 if (str[i] == escape_char[j]) {
48 needs_escape = true;
49 break;
50 }
51 }
52
53 if (needs_escape) {
54 out_buffer[out_pos++] = '\\';
55 }
56 out_buffer[out_pos++] = str[i];
57 }
58
59 out_buffer[out_pos] = '\0';
60}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by display_mitm_warning().

◆ escape_path_for_shell()

bool escape_path_for_shell ( const char *  path,
char *  out_buffer,
size_t  out_buffer_size 
)

#include <string.h>

Escape a path for safe use in shell commands (auto-platform)

Parameters
pathPath string to escape (must not be NULL)
out_bufferOutput buffer for escaped path (must not be NULL)
out_buffer_sizeSize of output buffer (must be > 0)
Returns
true on success, false on failure

Automatically escapes a path for safe use in shell commands. If the path contains special characters or whitespace, it applies the appropriate platform-specific escaping (single quotes on Unix, double quotes on Windows). If no escaping is needed, the path is copied as-is.

PLATFORM BEHAVIOR:

  • Unix/Linux/macOS: Uses single quote escaping (escape_shell_single_quotes)
  • Windows: Uses double quote escaping (escape_shell_double_quotes)
Note
Output buffer must be large enough (worst case: 4x input size for Unix).
Returns false if buffer is too small or escaping fails.
Automatically detects if quoting is needed (no unnecessary escaping).
Example
char escaped[1024];
if (escape_path_for_shell("/usr/local/bin/tool", escaped, sizeof(escaped))) {
// escaped contains the path, properly escaped if needed
}
bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size)
Escape a path for safe use in shell commands (auto-platform)

Definition at line 244 of file util/string.c.

244 {
245 if (!path || !out_buffer || out_buffer_size == 0) {
246 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid path or output buffer");
247 return false;
248 }
249
250 // Check if quoting is needed
251 if (!string_needs_shell_quoting(path)) {
252 // No quoting needed, just copy the path
253 size_t path_len = strlen(path);
254 if (out_buffer_size <= path_len) {
255 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for path");
256 return false;
257 }
258 SAFE_STRNCPY(out_buffer, path, out_buffer_size);
259 return true;
260 }
261
262 // Quoting is needed, use platform-specific escaping
263#ifdef _WIN32
264 return escape_shell_double_quotes(path, out_buffer, out_buffer_size);
265#else
266 return escape_shell_single_quotes(path, out_buffer, out_buffer_size);
267#endif
268}
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
@ ERROR_BUFFER_OVERFLOW
bool escape_shell_double_quotes(const char *str, char *out_buffer, size_t out_buffer_size)
Escape a string for safe use in shell commands (double quotes, Windows-compatible)
bool escape_shell_single_quotes(const char *str, char *out_buffer, size_t out_buffer_size)
Escape a string for safe use in shell commands (single quotes)
bool string_needs_shell_quoting(const char *str)
Check if a string needs shell quoting (contains spaces or special chars)

References ERROR_BUFFER_OVERFLOW, ERROR_INVALID_PARAM, escape_shell_double_quotes(), escape_shell_single_quotes(), SAFE_STRNCPY, SET_ERRNO, and string_needs_shell_quoting().

Referenced by gpg_sign_with_key().

◆ escape_shell_double_quotes()

bool escape_shell_double_quotes ( const char *  str,
char *  out_buffer,
size_t  out_buffer_size 
)

#include <string.h>

Escape a string for safe use in shell commands (double quotes, Windows-compatible)

Parameters
strString to escape (must not be NULL)
out_bufferOutput buffer for escaped string (must not be NULL)
out_buffer_sizeSize of output buffer (must be > 0)
Returns
true on success, false on failure (buffer too small)

Escapes a string for safe use in shell commands using double quotes, with Windows cmd.exe compatibility. Escapes double quotes and backslashes as required by Windows command-line parsing rules.

ESCAPING STRATEGY:

  • Input: "C:\Program Files"
  • Output: "C:\\Program Files"
  • Double quotes and backslashes are escaped: " -> \", \ -> \
Note
Output buffer must be large enough (worst case: 2x input size + quotes).
Returns false if buffer is too small to hold escaped string.
This escaping method is compatible with Windows cmd.exe and PowerShell.
Warning
Buffer overflow protection is enforced - function fails safely.

Definition at line 174 of file util/string.c.

174 {
175 if (!str || !out_buffer || out_buffer_size == 0) {
176 return false;
177 }
178
179 // Calculate required size: each " becomes \" (2 chars) instead of 1
180 // Each \ also needs escaping, so \\ becomes \\\\
181 // Worst case: all chars are quotes/backslashes, so we need strlen(str) * 2 + 2 (for surrounding quotes)
182 size_t required_size = 2; // Start and end quotes
183 for (size_t i = 0; str[i] != '\0'; i++) {
184 if (str[i] == '"' || str[i] == '\\') {
185 required_size += 2; // \" or '\\'
186 } else {
187 required_size += 1;
188 }
189 }
190
191 if (out_buffer_size < required_size) {
192 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for shell escaping");
193 return false;
194 }
195
196 size_t out_pos = 0;
197 out_buffer[out_pos++] = '"'; // Opening quote
198
199 for (size_t i = 0; str[i] != '\0'; i++) {
200 if (str[i] == '"') {
201 // Escape double quote: " becomes \"
202 out_buffer[out_pos++] = '\\';
203 out_buffer[out_pos++] = '"';
204 } else if (str[i] == '\\') {
205 // Escape backslash: \ becomes '\\'
206 out_buffer[out_pos++] = '\\';
207 out_buffer[out_pos++] = '\\';
208 } else {
209 out_buffer[out_pos++] = str[i];
210 }
211 }
212
213 out_buffer[out_pos++] = '"'; // Closing quote
214 out_buffer[out_pos] = '\0';
215
216 return true;
217}

References ERROR_BUFFER_OVERFLOW, and SET_ERRNO.

Referenced by escape_path_for_shell().

◆ escape_shell_single_quotes()

bool escape_shell_single_quotes ( const char *  str,
char *  out_buffer,
size_t  out_buffer_size 
)

#include <string.h>

Escape a string for safe use in shell commands (single quotes)

Parameters
strString to escape (must not be NULL)
out_bufferOutput buffer for escaped string (must not be NULL)
out_buffer_sizeSize of output buffer (must be > 0)
Returns
true on success, false on failure (buffer too small)

Escapes a string for safe use in Unix shell commands using single quotes. Single quotes in the input are escaped by replacing ' with '\'' (single quote, escaped quote, single quote). This ensures the string is safe for shell evaluation when wrapped in single quotes.

ESCAPING STRATEGY:

  • Input: "don't"
  • Output: "don'\''t"
  • Shell command: echo 'don'\''t' -> prints: don't
Note
Output buffer must be large enough (worst case: 4x input size).
Returns false if buffer is too small to hold escaped string.
This escaping method is compatible with sh, bash, zsh, and other Unix shells.
Warning
Buffer overflow protection is enforced - function fails safely.

Definition at line 132 of file util/string.c.

132 {
133 if (!str || !out_buffer || out_buffer_size == 0) {
134 return false;
135 }
136
137 // Calculate required size: each ' becomes '\'' (4 chars) instead of 1
138 // Worst case: all chars are quotes, so we need strlen(str) * 4 + 2 (for surrounding quotes)
139 size_t required_size = 2; // Start and end quotes
140 for (size_t i = 0; str[i] != '\0'; i++) {
141 if (str[i] == '\'') {
142 required_size += 4; // '\''
143 } else {
144 required_size += 1;
145 }
146 }
147
148 if (out_buffer_size < required_size) {
149 SET_ERRNO(ERROR_BUFFER_OVERFLOW, "Output buffer too small for shell escaping");
150 return false;
151 }
152
153 size_t out_pos = 0;
154 out_buffer[out_pos++] = '\''; // Opening quote
155
156 for (size_t i = 0; str[i] != '\0'; i++) {
157 if (str[i] == '\'') {
158 // Replace ' with '\'' (close quote, escaped quote, open quote)
159 out_buffer[out_pos++] = '\'';
160 out_buffer[out_pos++] = '\\';
161 out_buffer[out_pos++] = '\'';
162 out_buffer[out_pos++] = '\'';
163 } else {
164 out_buffer[out_pos++] = str[i];
165 }
166 }
167
168 out_buffer[out_pos++] = '\''; // Closing quote
169 out_buffer[out_pos] = '\0';
170
171 return true;
172}

References ERROR_BUFFER_OVERFLOW, and SET_ERRNO.

Referenced by escape_path_for_shell(), and gpg_get_public_key().

◆ expand_ipv6()

asciichat_error_t expand_ipv6 ( const char *  ipv6,
char *  expanded_out,
size_t  out_size 
)

#include <ip.h>

Expand IPv6 address to full form.

Parameters
ipv6IPv6 address (must not be NULL)
expanded_outOutput buffer for expanded form (must not be NULL)
out_sizeSize of output buffer
Returns
ASCIICHAT_OK on success, error code on failure

Expands an IPv6 address to its full uncompressed form with all 8 groups of 4 hexadecimal digits each, separated by colons.

EXAMPLES:

  • "::1" -> "0000:0000:0000:0000:0000:0000:0000:0001"
  • "2001:db8::1" -> "2001:0db8:0000:0000:0000:0000:0000:0001"
  • "fe80::1" -> "fe80:0000:0000:0000:0000:0000:0000:0001"
Note
Output buffer should be at least 64 bytes.
Returns error if input is not a valid IPv6 address.
Example
char expanded[64];
if (expand_ipv6("::1", expanded, sizeof(expanded)) == ASCIICHAT_OK) {
// expanded = "0000:0000:0000:0000:0000:0000:0000:0001"
}
asciichat_error_t expand_ipv6(const char *ipv6, char *expanded_out, size_t out_size)
Expand IPv6 address to full form.
Definition ip.c:932

Definition at line 932 of file ip.c.

932 {
933 if (!ipv6 || !expanded_out || out_size == 0) {
934 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments to expand_ipv6");
935 }
936
937 // Normalize (remove brackets)
938 char normalized[INET6_ADDRSTRLEN];
939 if (parse_ipv6_address(ipv6, normalized, sizeof(normalized)) != 0) {
940 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to parse IPv6 address");
941 }
942
943 // Validate
944 if (!is_valid_ipv6(normalized)) {
945 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IPv6 address: %s", ipv6);
946 }
947
948 struct in6_addr addr;
949 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
950 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to convert IPv6 address");
951 }
952
953 // Format as full expanded form: xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
954 size_t written =
955 SAFE_SNPRINTF(expanded_out, out_size, "%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x",
956 (addr.s6_addr[0] << 8) | addr.s6_addr[1], (addr.s6_addr[2] << 8) | addr.s6_addr[3],
957 (addr.s6_addr[4] << 8) | addr.s6_addr[5], (addr.s6_addr[6] << 8) | addr.s6_addr[7],
958 (addr.s6_addr[8] << 8) | addr.s6_addr[9], (addr.s6_addr[10] << 8) | addr.s6_addr[11],
959 (addr.s6_addr[12] << 8) | addr.s6_addr[13], (addr.s6_addr[14] << 8) | addr.s6_addr[15]);
960
961 if (written >= out_size) {
962 return SET_ERRNO(ERROR_INVALID_PARAM, "Output buffer too small");
963 }
964
965 return ASCIICHAT_OK;
966}
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492

References ASCIICHAT_OK, ERROR_INVALID_PARAM, is_valid_ipv6(), parse_ipv6_address(), SAFE_SNPRINTF, and SET_ERRNO.

◆ expand_path()

char * expand_path ( const char *  path)

#include <path.h>

Expand path with tilde (~) support.

Parameters
pathPath to expand (may contain ~, must not be NULL)
Returns
Expanded path (must be freed by caller), or NULL on failure

Expands a file path, replacing ~ with the user's home directory. On Unix, uses $HOME environment variable. On Windows, uses USERPROFILE% environment variable.

Note
Returned path is allocated with malloc() and must be freed by caller.
Returns NULL on failure (invalid path, memory allocation error, etc.).
Does not expand environment variables other than ~.
Example
char *expanded = expand_path("~/.config/ascii-chat");
// Returns: "/home/user/.config/ascii-chat" (on Unix)
free(expanded);
char * expand_path(const char *path)
Expand path with tilde (~) support.
Definition path.c:505

Definition at line 505 of file path.c.

505 {
506 if (path[0] == PATH_TILDE) {
507 const char *home = platform_get_home_dir();
508 if (!home) {
509 return NULL;
510 }
511
512 char *expanded;
513 size_t total_len = strlen(home) + strlen(path) + 1; // path includes the tilde
514 expanded = SAFE_MALLOC(total_len, char *);
515 if (!expanded) {
516 return NULL;
517 }
518 safe_snprintf(expanded, total_len, "%s%s", home, path + 1);
519
521
522 return expanded;
523 }
524 return platform_strdup(path);
525}
#define SAFE_MALLOC(size, cast)
Definition common.h:264
void platform_normalize_path_separators(char *path)
Normalize path separators for the current platform.
Definition util.c:98
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
const char * platform_get_home_dir(void)
Get the user's home directory.
Definition util.c:94
#define PATH_TILDE
Path component: home directory tilde.
Definition path.h:73

References PATH_TILDE, platform_get_home_dir(), platform_normalize_path_separators(), platform_strdup(), SAFE_MALLOC, and safe_snprintf().

Referenced by config_create_default(), config_load_and_apply(), detect_default_ssh_key(), and path_validate_user_path().

◆ extract_ip_from_address()

int extract_ip_from_address ( const char *  addr_with_port,
char *  ip_out,
size_t  ip_out_size 
)

#include <ip.h>

Extract IP address (without port) from formatted address string.

Extracts the IP address portion from a formatted address that includes a port. Handles both IPv4 (e.g., "192.168.1.1:27224") and IPv6 with brackets (e.g., "[2001:db8::1]:27224").

Parameters
addr_with_portFormatted address string with port
ip_outOutput buffer for IP address (without port)
ip_out_sizeSize of output buffer
Returns
0 on success, -1 on error
Example
char ip[64];
extract_ip_from_address("192.168.1.1:27224", ip, sizeof(ip));
// ip now contains "192.168.1.1"
extract_ip_from_address("[::1]:27224", ip, sizeof(ip));
// ip now contains "::1"
int extract_ip_from_address(const char *addr_with_port, char *ip_out, size_t ip_out_size)
Extract IP address (without port) from formatted address string.
Definition ip.c:1053

Definition at line 1053 of file ip.c.

1053 {
1054 if (!addr_with_port || !ip_out || ip_out_size == 0) {
1055 return -1;
1056 }
1057
1058 // Check for IPv6 bracket notation: [2001:db8::1]:27224
1059 if (addr_with_port[0] == '[') {
1060 const char *bracket_end = strchr(addr_with_port, ']');
1061 if (!bracket_end) {
1062 return -1;
1063 }
1064 size_t ip_len = (size_t)(bracket_end - addr_with_port - 1);
1065 if (ip_len >= ip_out_size) {
1066 return -1;
1067 }
1068 memcpy(ip_out, addr_with_port + 1, ip_len);
1069 ip_out[ip_len] = '\0';
1070 return 0;
1071 }
1072
1073 // IPv4: Find last colon and extract IP part
1074 const char *colon = strrchr(addr_with_port, ':');
1075 if (!colon) {
1076 // No port, just copy the whole thing
1077 SAFE_STRNCPY(ip_out, addr_with_port, ip_out_size);
1078 return 0;
1079 }
1080
1081 size_t ip_len = (size_t)(colon - addr_with_port);
1082 if (ip_len >= ip_out_size) {
1083 return -1;
1084 }
1085 memcpy(ip_out, addr_with_port, ip_len);
1086 ip_out[ip_len] = '\0';
1087 return 0;
1088}

References SAFE_STRNCPY.

◆ extract_ipv4_from_mapped_ipv6()

asciichat_error_t extract_ipv4_from_mapped_ipv6 ( const char *  ipv6,
char *  ipv4_out,
size_t  out_size 
)

#include <ip.h>

Extract IPv4 address from IPv6-mapped format.

Parameters
ipv6IPv6-mapped address (must not be NULL)
ipv4_outOutput buffer for IPv4 address (must not be NULL)
out_sizeSize of output buffer
Returns
ASCIICHAT_OK on success, error code on failure

Extracts the IPv4 address from an IPv6-mapped address (::ffff:x.x.x.x).

INPUT FORMATS:

  • "::ffff:192.168.1.1" -> "192.168.1.1"
  • "::ffff:c0a8:0101" (hex notation) -> "192.168.1.1"
Note
Returns error if input is not a valid IPv6-mapped IPv4 address.
Output buffer should be at least 16 bytes.
Example
char ipv4[16];
if (extract_ipv4_from_mapped_ipv6("::ffff:192.168.1.1", ipv4, sizeof(ipv4)) == ASCIICHAT_OK) {
// ipv4 = "192.168.1.1"
}
asciichat_error_t extract_ipv4_from_mapped_ipv6(const char *ipv6, char *ipv4_out, size_t out_size)
Extract IPv4 address from IPv6-mapped format.
Definition ip.c:896

Definition at line 896 of file ip.c.

896 {
897 if (!ipv6 || !ipv4_out || out_size == 0) {
898 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments to extract_ipv4_from_mapped_ipv6");
899 }
900
901 if (!is_ipv4_mapped_ipv6(ipv6)) {
902 return SET_ERRNO(ERROR_INVALID_PARAM, "Not an IPv4-mapped IPv6 address: %s", ipv6);
903 }
904
905 // Normalize
906 char normalized[INET6_ADDRSTRLEN];
907 if (parse_ipv6_address(ipv6, normalized, sizeof(normalized)) != 0) {
908 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to parse IPv6 address");
909 }
910
911 struct in6_addr addr;
912 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
913 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to convert IPv6 address");
914 }
915
916 // Extract last 4 bytes and format as IPv4
917 struct in_addr ipv4_addr;
918 memcpy(&ipv4_addr.s_addr, &addr.s6_addr[12], 4);
919
920 if (inet_ntop(AF_INET, &ipv4_addr, ipv4_out, (socklen_t)out_size) == NULL) {
921 return SET_ERRNO(ERROR_NETWORK, "Failed to format IPv4 address");
922 }
923
924 return ASCIICHAT_OK;
925}
int is_ipv4_mapped_ipv6(const char *ipv6)
Check if IPv6 address is IPv4-mapped.
Definition ip.c:860

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NETWORK, is_ipv4_mapped_ipv6(), parse_ipv6_address(), and SET_ERRNO.

◆ extract_project_relative_path()

const char * extract_project_relative_path ( const char *  file)

#include <path.h>

Extract relative path from an absolute path.

Parameters
fileAbsolute file path (typically from FILE, must not be NULL)
Returns
Relative path from project directory (e.g., lib/platform/symbols.c), or filename if no project directory found

Extracts a project-relative path from an absolute file path. Searches for common project directories (lib/, src/, tests/, include/) and returns the path relative from that directory. Useful for logging and error reporting where full paths are too verbose.

Note
Handles both Unix (/) and Windows () path separators.
Falls back to just the filename if no project directory found.
Returns a pointer into the input string (does not allocate memory).
Input string must remain valid for the lifetime of the returned pointer.
Example
const char *rel = extract_project_relative_path("/home/user/src/lib/path.c");
// Returns: "lib/path.c"
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
Definition path.c:456

Definition at line 456 of file path.c.

456 {
457 if (!file) {
458 SET_ERRNO(ERROR_INVALID_PARAM, "file is null");
459 return "unknown";
460 }
461
462#ifdef __EMSCRIPTEN__
463 // WASM builds: Return raw path to avoid recursion in logging system
464 // extract_project_relative_path is called from format_log_header, which is part of logging.
465 // If we call normalize_path or other functions that might use SET_ERRNO or log functions,
466 // we create infinite recursion: log_msg -> format_log_header -> extract_project_relative_path -> SET_ERRNO ->
467 // log_error -> log_msg For WASM, just return the filename without path processing.
468 const char *sep = strrchr(file, '/');
469 if (!sep) {
470 sep = strrchr(file, '\\');
471 }
472 return sep ? sep + 1 : file;
473#endif
474
475 /* First normalize the path to resolve .. and . components */
476 const char *normalized = normalize_path(file);
477
478 /* Try to find and strip the project root from the absolute path */
479 char *project_root = find_project_root();
480 if (project_root) {
481 size_t root_len = strlen(project_root);
482 size_t norm_len = strlen(normalized);
483
484 /* Check if normalized path starts with project root */
485 if (norm_len > root_len && strncmp(normalized, project_root, root_len) == 0) {
486 const char *remainder = normalized + root_len;
487
488 /* Skip the separator if present */
489 if (*remainder == PATH_DELIM || *remainder == '/' || *remainder == '\\') {
490 remainder++;
491 }
492
493 free(project_root);
494 return remainder;
495 }
496 free(project_root);
497 }
498
499 /* Fallback: Project root detection failed, but we should still try to return
500 * a meaningful relative path, not just the filename. Return the normalized path
501 * which should contain the full directory structure. */
502 return normalized;
503}
#define PATH_DELIM
Platform-specific path separator character.
Definition filesystem.h:112

References ERROR_INVALID_PARAM, PATH_DELIM, and SET_ERRNO.

Referenced by asciichat_fatal_with_context(), asciichat_print_error_context(), log_json_async_safe(), log_json_write(), and log_template_apply().

◆ format_bytes_pretty()

void format_bytes_pretty ( size_t  bytes,
char *  out,
size_t  out_capacity 
)

#include <format.h>

Format byte count into human-readable string.

Parameters
bytesNumber of bytes to format
outOutput buffer for formatted string (must not be NULL)
out_capacitySize of output buffer (must be > 0)

Formats a byte count into a human-readable string with appropriate units. Automatically selects the appropriate unit (B, KB, MB, GB, TB, PB, EB) based on the value. Units step up at 80% of the next tier to avoid awkward transitions. Uses binary units (1024-based) for calculations.

FORMATTING RULES (threshold at 80% of next unit):

  • 0-819 bytes: "N B" (e.g., "512 B")
  • 819-839,339 bytes: "N.NN KB" (e.g., "0.80 KB")
  • 839,339-858,993,459 bytes: "N.NN MB" (e.g., "0.80 MB")
  • 858,993,459-879,609,302,015 bytes: "N.NN GB" (e.g., "0.80 GB")
  • 879,609,302,015-900,719,925,374,591 bytes: "N.NN TB" (e.g., "0.80 TB")
  • 900,719,925,374,591-921,737,309,184,883,711 bytes: "N.NN PB" (e.g., "0.80 PB")
  • 921,737,309,184,883,711 and above: "N.NN EB" (e.g., "0.80 EB")
Note
Output buffer should be at least 32 bytes to accommodate all formats.
Function truncates output if buffer is too small.
Decimal precision is 2 places for units >= KB.
Uses binary units (1024-based), not decimal (1000-based).
Example
char buf[64];
format_bytes_pretty(512, buf, sizeof(buf)); // "512 B"
format_bytes_pretty(819, buf, sizeof(buf)); // "819 B" (just below KB threshold)
format_bytes_pretty(820, buf, sizeof(buf)); // "0.80 KB" (at 80% of 1 KB)
format_bytes_pretty(1048576, buf, sizeof(buf)); // "1.00 MB" (at 100% of 1 MB)
format_bytes_pretty(838860, buf, sizeof(buf)); // "0.80 MB" (at 80% of 1 MB)
format_bytes_pretty(1125899906842624, buf, sizeof(buf)); // "1.00 PB"
format_bytes_pretty(1152921504606846976ULL, buf, sizeof(buf)); // "1.00 EB"
void format_bytes_pretty(size_t bytes, char *out, size_t out_capacity)
Format byte count into human-readable string.
Definition util/format.c:10

Definition at line 10 of file util/format.c.

10 {
11 const double KB = 1024.0;
12 const double MB = KB * 1024.0;
13 const double GB = MB * 1024.0;
14 const double TB = GB * 1024.0;
15 const double PB = TB * 1024.0;
16 const double EB = PB * 1024.0;
17
18 const double THRESHOLD = 0.8;
19
20 double byte_val = (double)bytes;
21
22 if (byte_val < THRESHOLD * KB) {
23 SAFE_IGNORE_PRINTF_RESULT(safe_snprintf(out, out_capacity, "%zu B", bytes));
24 } else if (byte_val < THRESHOLD * MB) {
25 double value = byte_val / KB;
26 SAFE_IGNORE_PRINTF_RESULT(safe_snprintf(out, out_capacity, "%.2f KB", value));
27 } else if (byte_val < THRESHOLD * GB) {
28 double value = byte_val / MB;
29 SAFE_IGNORE_PRINTF_RESULT(safe_snprintf(out, out_capacity, "%.2f MB", value));
30 } else if (byte_val < THRESHOLD * TB) {
31 double value = byte_val / GB;
32 SAFE_IGNORE_PRINTF_RESULT(safe_snprintf(out, out_capacity, "%.2f GB", value));
33 } else if (byte_val < THRESHOLD * PB) {
34 double value = byte_val / TB;
35 SAFE_IGNORE_PRINTF_RESULT(safe_snprintf(out, out_capacity, "%.2f TB", value));
36 } else if (byte_val < THRESHOLD * EB) {
37 double value = byte_val / PB;
38 SAFE_IGNORE_PRINTF_RESULT(safe_snprintf(out, out_capacity, "%.2f PB", value));
39 } else {
40 double value = byte_val / EB;
41 SAFE_IGNORE_PRINTF_RESULT(safe_snprintf(out, out_capacity, "%.2f EB", value));
42 }
43}
#define SAFE_IGNORE_PRINTF_RESULT(expr)
Definition system.h:525

References SAFE_IGNORE_PRINTF_RESULT, and safe_snprintf().

Referenced by buffer_pool_create(), buffer_pool_log_stats(), crypto_destroy(), frame_check_size_overflow(), frame_validate_new(), handle_image_frame_packet(), packet_decode_frame_data_malloc(), and packet_validate_frame_dimensions().

◆ format_ip_address()

asciichat_error_t format_ip_address ( int  family,
const struct sockaddr *  addr,
char *  output,
size_t  output_size 
)

#include <ip.h>

Format IP address from socket address structure.

Parameters
familyAddress family (AF_INET for IPv4 or AF_INET6 for IPv6)
addrSocket address pointer (must not be NULL)
outputOutput buffer for formatted IP address (must not be NULL)
output_sizeSize of output buffer (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Formats an IP address from a socket address structure. Automatically handles both IPv4 and IPv6 addresses by extracting the address from the appropriate socket structure fields.

OUTPUT FORMATS:

  • IPv4: "192.0.2.1"
  • IPv6: "2001:db8::1"
Note
Output buffer should be at least 64 bytes to accommodate any IPv6 address.
For IPv4: addr should point to sockaddr_in, for IPv6: should point to sockaddr_in6.
Returns error code on failure (invalid family, buffer too small, etc.).
Example
char ip_str[64];
struct sockaddr_in addr;
// ... initialize addr ...
if (format_ip_address(AF_INET, &addr, ip_str, sizeof(ip_str)) == ASCIICHAT_OK) {
// ip_str contains the formatted IP address
}

Definition at line 196 of file ip.c.

196 {
197 if (!addr || !output || output_size == 0) {
198 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments to format_ip_address");
199 }
200
201 const void *ip_ptr = NULL;
202
203 if (family == AF_INET) {
204 const struct sockaddr_in *addr_in = (const struct sockaddr_in *)addr;
205 ip_ptr = &addr_in->sin_addr;
206 } else if (family == AF_INET6) {
207 const struct sockaddr_in6 *addr_in6 = (const struct sockaddr_in6 *)addr;
208 ip_ptr = &addr_in6->sin6_addr;
209 } else {
210 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported address family: %d", family);
211 }
212
213 if (inet_ntop(family, ip_ptr, output, (socklen_t)output_size) == NULL) {
214 return SET_ERRNO(ERROR_NETWORK, "Failed to format IP address");
215 }
216
217 return ASCIICHAT_OK;
218}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NETWORK, and SET_ERRNO.

Referenced by tcp_client_connect(), tcp_client_context_get_ip(), and tcp_server_run().

◆ format_ip_with_port()

asciichat_error_t format_ip_with_port ( const char *  ip,
uint16_t  port,
char *  output,
size_t  output_size 
)

#include <ip.h>

Format IP address with port number.

Parameters
ipIP address (without brackets, must not be NULL)
portPort number (0-65535)
outputOutput buffer (must not be NULL)
output_sizeSize of output buffer (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Formats an IP address with port number. Automatically adds brackets for IPv6 addresses to separate address from port. IPv4 addresses are formatted without brackets.

OUTPUT FORMATS:

  • IPv4: "192.0.2.1:8080"
  • IPv6: "[2001:db8::1]:8080"
Note
Output buffer must be large enough (at least 64 bytes recommended).
IPv6 addresses automatically get brackets in output.
Returns error code on failure (buffer too small, invalid address, etc.).
Example
char output[64];
if (format_ip_with_port("::1", 8080, output, sizeof(output)) == ASCIICHAT_OK) {
// output = "[::1]:8080"
}
asciichat_error_t format_ip_with_port(const char *ip, uint16_t port, char *output, size_t output_size)
Format IP address with port number.
Definition ip.c:221

Definition at line 221 of file ip.c.

221 {
222 if (!ip || !output || output_size == 0) {
223 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", ip);
224 }
225
226 if (strlen(ip) == 0) {
227 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", ip);
228 }
229
230 if (strlen(ip) > 256) {
231 char ip_buffer[BUFFER_SIZE_SMALL];
232 SAFE_STRNCPY(ip_buffer, ip, sizeof(ip_buffer));
233 return SET_ERRNO(ERROR_INVALID_PARAM, "Suspiciously long ip: %s", ip_buffer);
234 }
235
236 // Check if it's IPv6 (contains ':')
237 if (strchr(ip, ':') != NULL) {
238 // IPv6 - use bracket notation
239 size_t written = SAFE_SNPRINTF(output, output_size, "[%s]:%u", ip, port);
240 if (written >= output_size) {
241 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ip=%s or port=%u", ip, port);
242 }
243 } else {
244 // IPv4 - no brackets
245 size_t written = SAFE_SNPRINTF(output, output_size, "%s:%u", ip, port);
246 if (written >= output_size) {
247 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ip=%s or port=%u", ip, port);
248 }
249 }
250
251 return ASCIICHAT_OK;
252}
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, ERROR_INVALID_PARAM, SAFE_SNPRINTF, SAFE_STRNCPY, and SET_ERRNO.

Referenced by add_known_host(), check_known_host(), check_known_host_no_identity(), display_mitm_warning(), prompt_unknown_host(), prompt_unknown_host_no_identity(), and remove_known_host().

◆ get_config_dir()

char * get_config_dir ( void  )

#include <path.h>

Get configuration directory path with XDG_CONFIG_HOME support.

Returns
Path to configuration directory (must be freed by caller), or NULL on failure

Returns the appropriate configuration directory path according to platform conventions and environment variables. The path includes the directory separator at the end for convenience.

All ascii-chat data files use this single directory:

  • config.toml (configuration)
  • known_hosts (server key verification)

Configuration path resolution:

  • Unix: $XDG_CONFIG_HOME/ascii-chat/ if XDG_CONFIG_HOME is set, otherwise ~/.ascii-chat/
  • Windows: APPDATA%\ascii-chat\ if APPDATA is set, with fallback to USERPROFILE%\AppData\Roaming\ascii-chat\, HOME%.config\ascii-chat\, or C:\ProgramData\ascii-chat\
Note
Returned path is allocated with malloc() and must be freed by caller.
Path includes directory separator at the end (e.g., "/home/user/.config/ascii-chat/").
Returns NULL on failure (memory allocation error, etc.).
Example
char *config_dir = get_config_dir();
// Returns: "/home/user/.config/ascii-chat/" (on Unix with XDG_CONFIG_HOME unset)
// or: "C:\Users\user\AppData\Roaming\ascii-chat\" (on Windows)
free(config_dir);
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:527

Definition at line 527 of file path.c.

527 {
528 /* Delegate to platform abstraction layer */
530}
char * platform_get_config_dir(void)
Get the application configuration directory.
Definition util.c:104

References platform_get_config_dir().

Referenced by config_load_and_apply(), discovery_keys_get_cache_path(), get_known_hosts_path(), options_init(), and path_validate_user_path().

◆ get_data_dir()

char * get_data_dir ( void  )

#include <path.h>

Get application data directory path.

Returns
Path to data directory (must be freed by caller), or NULL on failure

Returns XDG_DATA_HOME/ascii-chat/ on Unix (~/.local/share/ascii-chat/ by default) or AppData\ascii-chat\ on Windows.

Use this for application data files like databases, not configuration files.

Example:
char *data_dir = get_data_dir();
// Returns: "/home/user/.local/share/ascii-chat/" (on Unix)
// or: "C:\Users\user\AppData\Roaming\ascii-chat\" (on Windows)
free(data_dir);
char * get_data_dir(void)
Get application data directory path.
Definition path.c:532

Definition at line 532 of file path.c.

532 {
533 /* Delegate to platform abstraction layer */
534 return platform_get_data_dir();
535}
char * platform_get_data_dir(void)
Get the application data directory.

References platform_get_data_dir().

Referenced by get_discovery_database_dir().

◆ get_discovery_database_dir()

char * get_discovery_database_dir ( void  )

#include <path.h>

Get discovery service database directory with Homebrew-aware system-wide fallback.

Returns
Path to database directory (must be freed by caller), or NULL on failure

Returns the appropriate directory for the discovery service database, with Homebrew-aware system-wide installation support. The path includes a trailing directory separator.

Directory resolution (in order of preference):

  1. System-wide location using baked-in install prefix:
    • Unix: ${ASCIICHAT_INSTALL_PREFIX}/var/ascii-chat/ (e.g., /opt/homebrew/var/ascii-chat/ or /usr/local/var/ascii-chat/)
    • Windows: PROGRAMDATA%\ascii-chat\
  2. macOS only: /usr/local/var/ascii-chat/ (if different from install prefix, typically user-writable)
  3. User data directory: $XDG_DATA_HOME/ascii-chat/ or ~/.local/share/ascii-chat/

The install prefix is determined at compile time from CMake's CMAKE_INSTALL_PREFIX, which Homebrew sets appropriately:

  • macOS Apple Silicon: /opt/homebrew
  • macOS Intel: /usr/local
  • Linux: /usr/local (default)

The function tries each location and automatically creates directories with appropriate permissions (0755 for system directories, 0700 for user directories). If a location is not writable, it falls back to the next option.

Note
Returned path is allocated with malloc() and must be freed by caller.
Path includes directory separator at the end (e.g., "/opt/homebrew/var/ascii-chat/").
Returns NULL if both system and user data directories are inaccessible.
Creates directories recursively if they don't exist and parent is writable.
Example
char *db_dir = get_discovery_database_dir();
// macOS (Apple Silicon): "/opt/homebrew/var/ascii-chat/", then "/usr/local/var/ascii-chat/"
// macOS (Intel): "/usr/local/var/ascii-chat/"
// Linux: "/usr/local/var/ascii-chat/"
// User fallback: "~/.local/share/ascii-chat/"
free(db_dir);
char * get_discovery_database_dir(void)
Get discovery service database directory with Homebrew-aware system-wide fallback.
Definition path.c:624

Definition at line 624 of file path.c.

624 {
625#ifdef _WIN32
626 // Windows: Try %PROGRAMDATA%\ascii-chat\ first, then fall back to user directories
627 const char *program_data = platform_getenv("PROGRAMDATA");
628 if (program_data && program_data[0] != '\0') {
629 size_t len = strlen(program_data) + strlen("\\ascii-chat\\") + 1;
630 char *system_dir = SAFE_MALLOC(len, char *);
631 if (system_dir) {
632 safe_snprintf(system_dir, len, "%s\\ascii-chat\\", program_data);
633
634 // Try to create directory recursively with public permissions (755 equivalent)
635 asciichat_error_t mkdir_result = platform_mkdir_recursive(system_dir, 0755);
636 if (mkdir_result == ASCIICHAT_OK) {
637 // Directory exists or was created - check if writable
638 if (platform_access(system_dir, PLATFORM_ACCESS_WRITE) == 0) {
639 return system_dir; // System-wide location is writable
640 }
641 }
642 SAFE_FREE(system_dir);
643 }
644 }
645
646 // Fall back to user data directory
647 char *data_dir = get_data_dir();
648 if (data_dir) {
649 // Try to create the directory recursively
651 if (mkdir_result == ASCIICHAT_OK) {
652 if (platform_access(data_dir, PLATFORM_ACCESS_WRITE) == 0) {
653 return data_dir;
654 }
655 }
656 SAFE_FREE(data_dir);
657 }
658
659 return NULL;
660#else
661 // Unix: Try ${INSTALL_PREFIX}/var/ascii-chat/ first (system-wide, Homebrew-aware)
662 // This uses the baked-in install prefix from paths.h (e.g., /opt/homebrew or /usr/local)
663 const char *prefix = ASCIICHAT_INSTALL_PREFIX;
664 size_t system_len = strlen(prefix) + strlen("/var/ascii-chat/") + 1;
665 char *system_dir = SAFE_MALLOC(system_len, char *);
666 if (system_dir) {
667 safe_snprintf(system_dir, system_len, "%s/var/ascii-chat/", prefix);
668
669 // Try to create directory recursively with public permissions (755) for system-wide use
670 // platform_mkdir_recursive creates parent directories as needed
671 asciichat_error_t mkdir_result = platform_mkdir_recursive(system_dir, 0755);
672 if (mkdir_result == ASCIICHAT_OK) {
673 // Directory exists or was created - check if writable
674 if (platform_access(system_dir, PLATFORM_ACCESS_WRITE) == 0) {
675 return system_dir; // System-wide location is writable
676 }
677 }
678 SAFE_FREE(system_dir);
679 }
680
681#ifdef __APPLE__
682 // On macOS, /usr/local is typically user-writable (set up by Homebrew)
683 // Try it if the install prefix is different (e.g., /opt/homebrew on Apple Silicon)
684 if (strcmp(prefix, "/usr/local") != 0) {
685 const char *usr_local_path = "/usr/local/var/ascii-chat/";
686 size_t usr_local_len = strlen(usr_local_path) + 1;
687 char *usr_local_dir = SAFE_MALLOC(usr_local_len, char *);
688 if (usr_local_dir) {
689 safe_snprintf(usr_local_dir, usr_local_len, "%s", usr_local_path);
690
691 asciichat_error_t mkdir_result = platform_mkdir_recursive(usr_local_dir, 0755);
692 if (mkdir_result == ASCIICHAT_OK) {
693 if (platform_access(usr_local_dir, PLATFORM_ACCESS_WRITE) == 0) {
694 return usr_local_dir; // /usr/local is writable
695 }
696 }
697 SAFE_FREE(usr_local_dir);
698 }
699 }
700#endif
701
702 // Fall back to user data directory (XDG_DATA_HOME or ~/.local/share/ascii-chat/)
703 char *data_dir = get_data_dir();
704 if (data_dir) {
705 // Try to create the directory recursively
707 if (mkdir_result == ASCIICHAT_OK) {
708 if (platform_access(data_dir, PLATFORM_ACCESS_WRITE) == 0) {
709 return data_dir;
710 }
711 }
712 SAFE_FREE(data_dir);
713 }
714
715 return NULL;
716#endif
717}
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
#define DIR_PERM_PRIVATE
Directory permission: Private (owner read/write/execute only)
Definition filesystem.h:199
int platform_access(const char *pathname, int mode)
Check file/directory access permissions.
asciichat_error_t platform_mkdir_recursive(const char *path, int mode)
Create directories recursively (mkdir -p equivalent)
const char * platform_getenv(const char *name)
Get an environment variable value.
Definition wasm/system.c:39
#define PLATFORM_ACCESS_WRITE
Check if file/directory is writable.
#define ASCIICHAT_INSTALL_PREFIX
Installation prefix.
Definition paths.h:30

References ASCIICHAT_INSTALL_PREFIX, ASCIICHAT_OK, DIR_PERM_PRIVATE, get_data_dir(), platform_access(), PLATFORM_ACCESS_WRITE, platform_getenv(), platform_mkdir_recursive(), SAFE_FREE, SAFE_MALLOC, and safe_snprintf().

Referenced by options_init().

◆ get_ip_type_string()

const char * get_ip_type_string ( const char *  ip)

#include <ip.h>

Get human-readable string describing IP address type.

Classifies an IP address and returns a descriptive string. Possible return values:

  • "All Interfaces" - Wildcard bind address (0.0.0.0 or ::)
  • "Localhost" - Loopback address (127.0.0.0/8 or ::1)
  • "LAN" - Private network address (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, or fc00::/7)
  • "Internet" - Public internet address
  • "Unknown" - Invalid or unrecognized address
  • "" - Empty/null input
Parameters
ipIP address to classify (without port)
Returns
Constant string describing the IP type
Example
const char *type1 = get_ip_type_string("0.0.0.0"); // Returns "All Interfaces"
const char *type2 = get_ip_type_string("127.0.0.1"); // Returns "Localhost"
const char *type3 = get_ip_type_string("192.168.1.1"); // Returns "LAN"
const char *type4 = get_ip_type_string("8.8.8.8"); // Returns "Internet"
const char *type5 = get_ip_type_string("::1"); // Returns "Localhost"
const char * get_ip_type_string(const char *ip)
Get human-readable string describing IP address type.
Definition ip.c:1091

Definition at line 1091 of file ip.c.

1091 {
1092 if (!ip || ip[0] == '\0') {
1093 return "";
1094 }
1095
1096 // Check for wildcard "bind to all interfaces" addresses
1097 // IPv4: 0.0.0.0
1098 if (strcmp(ip, "0.0.0.0") == 0) {
1099 return "All Interfaces";
1100 }
1101
1102 // IPv6: :: (unspecified address)
1103 if (strcmp(ip, "::") == 0) {
1104 return "All Interfaces";
1105 }
1106
1107 // Check IPv4
1108 if (is_localhost_ipv4(ip)) {
1109 return "Localhost";
1110 }
1111 if (is_lan_ipv4(ip)) {
1112 return "LAN";
1113 }
1114 if (is_internet_ipv4(ip)) {
1115 return "Internet";
1116 }
1117
1118 // Check IPv6
1119 if (is_localhost_ipv6(ip)) {
1120 return "Localhost";
1121 }
1122 if (is_lan_ipv6(ip)) {
1123 return "LAN";
1124 }
1125 if (is_internet_ipv6(ip)) {
1126 return "Internet";
1127 }
1128
1129 return "Unknown";
1130}
int is_lan_ipv6(const char *ip)
Check if IPv6 address is a private/LAN address.
Definition ip.c:1216
int is_localhost_ipv6(const char *ip)
Check if IPv6 address is localhost.
Definition ip.c:1320
int is_internet_ipv4(const char *ip)
Check if IPv4 address is a public internet address.
Definition ip.c:1408
int is_lan_ipv4(const char *ip)
Check if IPv4 address is a private/LAN address.
Definition ip.c:1185
int is_localhost_ipv4(const char *ip)
Check if IPv4 address is localhost.
Definition ip.c:1299
int is_internet_ipv6(const char *ip)
Check if IPv6 address is a public internet address.
Definition ip.c:1490

References is_internet_ipv4(), is_internet_ipv6(), is_lan_ipv4(), is_lan_ipv6(), is_localhost_ipv4(), and is_localhost_ipv6().

◆ get_ip_version()

int get_ip_version ( const char *  ip)

#include <ip.h>

Get IP address version.

Parameters
ipIP address string (must not be NULL)
Returns
4 for IPv4, 6 for IPv6, 0 for invalid

Detects the version of an IP address without validating its full correctness. Useful for protocol selection and dual-stack handling.

Note
Returns 0 for NULL, empty strings, or invalid addresses.
Does not validate address correctness beyond basic format.
Example
int version = get_ip_version("192.168.1.1"); // Returns 4
int version = get_ip_version("::1"); // Returns 6
int version = get_ip_version("invalid"); // Returns 0

Definition at line 510 of file ip.c.

510 {
511 if (!ip || strlen(ip) == 0) {
512 return 0;
513 }
514
515 // Quick heuristic: if it contains a colon, it's likely IPv6
516 // (unless it's a port separator, but we're just checking the IP part)
517 if (strchr(ip, ':') != NULL) {
518 // Could be IPv6 or bracketed IPv6
519 if (is_valid_ipv6(ip)) {
520 return 6;
521 }
522 return 0; // Has colon but not valid IPv6
523 }
524
525 // No colon, check if it's IPv4
526 if (is_valid_ipv4(ip)) {
527 return 4;
528 }
529
530 return 0; // Not valid IPv4 or IPv6
531}

References is_valid_ipv4(), and is_valid_ipv6().

Referenced by compare_ip_port_strings(), ip_compare(), ip_equals(), ip_in_cidr_parsed(), and parse_cidr().

◆ get_log_dir()

char * get_log_dir ( void  )

#include <path.h>

Get log directory path appropriate for current build type.

Returns
Path to log directory (must be freed by caller), or NULL on failure

Returns the appropriate log directory path based on build type. The directory is created if it doesn't exist (for release builds). The path does NOT include a trailing directory separator.

Log directory resolution:

  • Debug builds: Current working directory (for fast iteration)
  • Release builds: $TMPDIR/ascii-chat/ (Unix: /tmp/ascii-chat/, Windows: TEMP%\ascii-chat)

For release builds, this function automatically creates the ascii-chat subdirectory if it doesn't exist and verifies it's writable.

Note
Returned path is allocated with malloc() and must be freed by caller.
Path does NOT include directory separator at the end.
Returns NULL on failure (memory allocation error, directory creation failed, etc.).
For release builds, the directory is created with 0700 permissions (owner-only).
Example (Debug Build)
char *log_dir = get_log_dir();
// Returns: "/home/user/myproject" (current working directory)
free(log_dir);
char * get_log_dir(void)
Get log directory path appropriate for current build type.
Definition path.c:537
Example (Release Build)
char *log_dir = get_log_dir();
// Returns: "/tmp/ascii-chat" (on Unix)
// or: "C:\Users\user\AppData\Local\Temp\ascii-chat" (on Windows)
free(log_dir);

Definition at line 537 of file path.c.

537 {
538#ifdef __EMSCRIPTEN__
539 // WASM builds: Return NULL to skip SAFE_MALLOC before memory tracking is initialized
540 // The caller will use a fallback path (temp dir + filename)
541 return NULL;
542#endif
543
544#ifdef NDEBUG
545 // Release builds: Use $TMPDIR/ascii-chat/
546 // Get system temp directory
547 char temp_dir[PLATFORM_MAX_PATH_LENGTH];
548 if (!platform_get_temp_dir(temp_dir, sizeof(temp_dir))) {
549 // Fallback: Use current working directory if temp dir unavailable
550 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
551 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
552 return NULL;
553 }
554 char *result = SAFE_MALLOC(strlen(cwd_buf) + 1, char *);
555 if (!result) {
556 return NULL;
557 }
558 safe_snprintf(result, strlen(cwd_buf) + 1, "%s", cwd_buf);
559 return result;
560 }
561
562 // Build path to ascii-chat subdirectory
563 size_t log_dir_len = strlen(temp_dir) + strlen(PATH_SEPARATOR_STR) + strlen("ascii-chat") + 1;
564 char *log_dir = SAFE_MALLOC(log_dir_len, char *);
565 if (!log_dir) {
566 return NULL;
567 }
568 safe_snprintf(log_dir, log_dir_len, "%s%sascii-chat", temp_dir, PATH_SEPARATOR_STR);
569
570 // Create the directory if it doesn't exist (with owner-only permissions)
571 asciichat_error_t mkdir_result = platform_mkdir(log_dir, DIR_PERM_PRIVATE);
572 if (mkdir_result != ASCIICHAT_OK) {
573 // Directory creation failed - fall back to temp_dir without subdirectory
574 SAFE_FREE(log_dir);
575 char *result = SAFE_MALLOC(strlen(temp_dir) + 1, char *);
576 if (!result) {
577 return NULL;
578 }
579 safe_snprintf(result, strlen(temp_dir) + 1, "%s", temp_dir);
580 return result;
581 }
582
583 // Verify the directory is writable
584 if (platform_access(log_dir, PLATFORM_ACCESS_WRITE) != 0) {
585 // Directory not writable - fall back to temp_dir
586 SAFE_FREE(log_dir);
587 char *result = SAFE_MALLOC(strlen(temp_dir) + 1, char *);
588 if (!result) {
589 return NULL;
590 }
591 safe_snprintf(result, strlen(temp_dir) + 1, "%s", temp_dir);
592 return result;
593 }
594
595 return log_dir;
596#else
597 // Debug builds: Use repository root for logs.
598 // find_project_root() returns a raw malloc() allocation (to avoid debug_malloc
599 // recursion), but callers free with SAFE_FREE. Re-allocate with SAFE_MALLOC
600 // so the allocator matches.
601 char *repo_root = find_project_root();
602 if (repo_root) {
603 size_t len = strlen(repo_root) + 1;
604 char *result = SAFE_MALLOC(len, char *);
605 if (result) {
606 memcpy(result, repo_root, len);
607 }
608 free(repo_root);
609 return result;
610 }
611
612 // Fallback to current working directory if repo root not found
613 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
614 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
615 return NULL;
616 }
617
618 char *result = SAFE_MALLOC(strlen(cwd_buf) + 1, char *);
619 safe_snprintf(result, strlen(cwd_buf) + 1, "%s", cwd_buf);
620 return result;
621#endif
622}
asciichat_error_t platform_mkdir(const char *path, int mode)
Create a directory.
bool platform_get_cwd(char *cwd, size_t path_size)
Get the current working directory of the process.
Definition util.c:110
#define PATH_SEPARATOR_STR
Definition filesystem.h:113
bool platform_get_temp_dir(char *temp_dir, size_t path_size)
Get the system temporary directory path.
Definition util.c:74
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69

References ASCIICHAT_OK, DIR_PERM_PRIVATE, PATH_SEPARATOR_STR, platform_access(), PLATFORM_ACCESS_WRITE, platform_get_cwd(), platform_get_temp_dir(), PLATFORM_MAX_PATH_LENGTH, platform_mkdir(), SAFE_FREE, SAFE_MALLOC, and safe_snprintf().

◆ ip_compare()

int ip_compare ( const char *  ip1,
const char *  ip2 
)

#include <ip.h>

Compare two IP addresses for sorting.

Parameters
ip1First IP address (must not be NULL)
ip2Second IP address (must not be NULL)
Returns
-1 if ip1 < ip2, 0 if equal, 1 if ip1 > ip2, -2 on error

Compares two IP addresses for sorting. IPv4 addresses sort before IPv6. Within the same version, compares numerically (not lexicographically).

SORT ORDER:

  • Invalid addresses sort last
  • IPv4 addresses sort before IPv6
  • Within IPv4: numeric comparison (10.0.0.1 < 192.168.1.1)
  • Within IPv6: numeric comparison (::1 < 2001::1)
Note
Returns -2 if either address is invalid.
Useful for qsort() callbacks.
Example
int result = ip_compare("10.0.0.1", "192.168.1.1"); // Returns -1
int result = ip_compare("::1", "192.168.1.1"); // Returns 1 (IPv6 > IPv4)
int ip_compare(const char *ip1, const char *ip2)
Compare two IP addresses for sorting.
Definition ip.c:601

Definition at line 601 of file ip.c.

601 {
602 if (!ip1 || !ip2) {
603 return -2; // Error
604 }
605
606 int v1 = get_ip_version(ip1);
607 int v2 = get_ip_version(ip2);
608
609 // Invalid addresses sort last
610 if (v1 == 0 && v2 == 0) {
611 return 0; // Both invalid, consider equal
612 }
613 if (v1 == 0) {
614 return 1; // ip1 invalid, sorts after ip2
615 }
616 if (v2 == 0) {
617 return -1; // ip2 invalid, sorts after ip1
618 }
619
620 // IPv4 sorts before IPv6
621 if (v1 != v2) {
622 return (v1 < v2) ? -1 : 1;
623 }
624
625 if (v1 == 4) {
626 // IPv4: compare numerically
627 struct in_addr addr1, addr2;
628 if (inet_pton(AF_INET, ip1, &addr1) != 1 || inet_pton(AF_INET, ip2, &addr2) != 1) {
629 return -2; // Error
630 }
631
632 uint32_t val1 = ntohl(addr1.s_addr);
633 uint32_t val2 = ntohl(addr2.s_addr);
634
635 if (val1 < val2)
636 return -1;
637 if (val1 > val2)
638 return 1;
639 return 0;
640 } else {
641 // IPv6: compare byte by byte
642 char norm1[INET6_ADDRSTRLEN], norm2[INET6_ADDRSTRLEN];
643
644 if (parse_ipv6_address(ip1, norm1, sizeof(norm1)) != 0) {
645 return -2;
646 }
647 if (parse_ipv6_address(ip2, norm2, sizeof(norm2)) != 0) {
648 return -2;
649 }
650
651 struct in6_addr addr1, addr2;
652 if (inet_pton(AF_INET6, norm1, &addr1) != 1 || inet_pton(AF_INET6, norm2, &addr2) != 1) {
653 return -2;
654 }
655
656 return memcmp(&addr1, &addr2, sizeof(addr1));
657 }
658}

References get_ip_version(), and parse_ipv6_address().

◆ ip_equals()

int ip_equals ( const char *  ip1,
const char *  ip2 
)

#include <ip.h>

Compare two IP addresses for equality.

Parameters
ip1First IP address (must not be NULL)
ip2Second IP address (must not be NULL)
Returns
1 if equal, 0 if not equal or invalid

Compares two IP addresses for equality, handling normalization automatically. Works with IPv4 and IPv6, including bracketed IPv6 notation.

NORMALIZATION RULES:

  • IPv6 addresses are normalized before comparison
  • Brackets are ignored: "[::1]" equals "::1"
  • IPv6 zero compression is normalized: "::1" equals "0:0:0:0:0:0:0:1"
  • IPv4 and IPv6 are never equal
Note
Returns 0 if either address is invalid.
Case-insensitive for IPv6 (A-F vs a-f).
Example
if (ip_equals("::1", "[::1]")) {
// True - brackets ignored
}
if (ip_equals("192.168.1.1", "192.168.1.2")) {
// False - different addresses
}

Definition at line 557 of file ip.c.

557 {
558 if (!ip1 || !ip2) {
559 return 0;
560 }
561
562 // Get versions
563 int v1 = get_ip_version(ip1);
564 int v2 = get_ip_version(ip2);
565
566 // If different versions or either invalid, not equal
567 if (v1 != v2 || v1 == 0) {
568 return 0;
569 }
570
571 if (v1 == 4) {
572 // IPv4: direct string comparison after validation
573 struct in_addr addr1, addr2;
574 if (inet_pton(AF_INET, ip1, &addr1) != 1 || inet_pton(AF_INET, ip2, &addr2) != 1) {
575 return 0;
576 }
577 return addr1.s_addr == addr2.s_addr;
578 } else {
579 // IPv6: normalize then compare
580 char norm1[INET6_ADDRSTRLEN], norm2[INET6_ADDRSTRLEN];
581
582 // Remove brackets if present
583 if (parse_ipv6_address(ip1, norm1, sizeof(norm1)) != 0) {
584 return 0;
585 }
586 if (parse_ipv6_address(ip2, norm2, sizeof(norm2)) != 0) {
587 return 0;
588 }
589
590 // Parse to binary and compare
591 struct in6_addr addr1, addr2;
592 if (inet_pton(AF_INET6, norm1, &addr1) != 1 || inet_pton(AF_INET6, norm2, &addr2) != 1) {
593 return 0;
594 }
595
596 return memcmp(&addr1, &addr2, sizeof(addr1)) == 0;
597 }
598}

References get_ip_version(), and parse_ipv6_address().

Referenced by compare_ip_port_strings().

◆ ip_in_cidr()

int ip_in_cidr ( const char *  ip,
const char *  cidr 
)

#include <ip.h>

Check if IP address is within CIDR range.

Parameters
ipIP address to check (must not be NULL)
cidrCIDR range (e.g., "192.168.1.0/24", must not be NULL)
Returns
1 if IP is in range, 0 if not or on error

Checks if an IP address falls within a CIDR range. Automatically handles both IPv4 and IPv6. IP and CIDR must be the same version.

EXAMPLES:

  • "192.168.1.10" in "192.168.1.0/24" -> true
  • "192.168.2.10" in "192.168.1.0/24" -> false
  • "2001:db8::1" in "2001:db8::/32" -> true
Note
Returns 0 if IP and CIDR are different versions (IPv4 vs IPv6).
Returns 0 on error (invalid IP, invalid CIDR).
Example
if (ip_in_cidr("192.168.1.10", "192.168.1.0/24")) {
// IP is in the 192.168.1.0/24 subnet
}
int ip_in_cidr(const char *ip, const char *cidr)
Check if IP address is within CIDR range.
Definition ip.c:820

Definition at line 820 of file ip.c.

820 {
821 if (!ip || !cidr) {
822 return 0;
823 }
824
825 char network[64];
826 int prefix;
827
828 if (parse_cidr(cidr, network, sizeof(network), &prefix) != 0) {
829 return 0; // Invalid CIDR
830 }
831
832 return ip_in_cidr_parsed(ip, network, prefix);
833}
int parse_cidr(const char *cidr, char *ip_out, size_t ip_out_size, int *prefix_out)
Parse CIDR notation into IP and prefix length.
Definition ip.c:665
int ip_in_cidr_parsed(const char *ip, const char *network, int prefix_len)
Check if IP is within CIDR range (parsed form)
Definition ip.c:766

References ip_in_cidr_parsed(), and parse_cidr().

◆ ip_in_cidr_parsed()

int ip_in_cidr_parsed ( const char *  ip,
const char *  network,
int  prefix_len 
)

#include <ip.h>

Check if IP is within CIDR range (parsed form)

Parameters
ipIP address to check (must not be NULL)
networkNetwork address (must not be NULL)
prefix_lenPrefix length (0-32 for IPv4, 0-128 for IPv6)
Returns
1 if IP is in range, 0 if not or on error

Checks if an IP address falls within a CIDR range using pre-parsed network address and prefix length. More efficient than ip_in_cidr() when checking multiple IPs against the same range.

Note
IP and network must be the same version.
Returns 0 on error (invalid IP, invalid network, out of range prefix).
Example
if (ip_in_cidr_parsed("192.168.1.10", "192.168.1.0", 24)) {
// IP is in the 192.168.1.0/24 subnet
}

Definition at line 766 of file ip.c.

766 {
767 if (!ip || !network) {
768 return 0;
769 }
770
771 int ip_version = get_ip_version(ip);
772 int net_version = get_ip_version(network);
773
774 // Must be same version
775 if (ip_version != net_version || ip_version == 0) {
776 return 0;
777 }
778
779 if (ip_version == 4) {
780 // IPv4 comparison
781 struct in_addr ip_addr, net_addr;
782 if (inet_pton(AF_INET, ip, &ip_addr) != 1 || inet_pton(AF_INET, network, &net_addr) != 1) {
783 return 0;
784 }
785
786 uint32_t ip_val = ntohl(ip_addr.s_addr);
787 uint32_t net_val = ntohl(net_addr.s_addr);
788
789 uint32_t ip_masked = apply_ipv4_mask(ip_val, prefix_len);
790 uint32_t net_masked = apply_ipv4_mask(net_val, prefix_len);
791
792 return ip_masked == net_masked;
793 } else {
794 // IPv6 comparison
795 char ip_norm[INET6_ADDRSTRLEN], net_norm[INET6_ADDRSTRLEN];
796
797 if (parse_ipv6_address(ip, ip_norm, sizeof(ip_norm)) != 0) {
798 return 0;
799 }
800 if (parse_ipv6_address(network, net_norm, sizeof(net_norm)) != 0) {
801 return 0;
802 }
803
804 struct in6_addr ip_addr, net_addr;
805 if (inet_pton(AF_INET6, ip_norm, &ip_addr) != 1 || inet_pton(AF_INET6, net_norm, &net_addr) != 1) {
806 return 0;
807 }
808
809 // Apply mask to both addresses
810 struct in6_addr ip_masked = ip_addr;
811 struct in6_addr net_masked = net_addr;
812 apply_ipv6_mask(&ip_masked, prefix_len);
813 apply_ipv6_mask(&net_masked, prefix_len);
814
815 return memcmp(&ip_masked, &net_masked, sizeof(ip_masked)) == 0;
816 }
817}

References get_ip_version(), and parse_ipv6_address().

Referenced by ip_in_cidr().

◆ ipv4_to_ipv6_mapped()

asciichat_error_t ipv4_to_ipv6_mapped ( const char *  ipv4,
char *  ipv6_out,
size_t  out_size 
)

#include <ip.h>

Convert IPv4 address to IPv6-mapped format.

Parameters
ipv4IPv4 address (must not be NULL)
ipv6_outOutput buffer for IPv6-mapped address (must not be NULL)
out_sizeSize of output buffer
Returns
ASCIICHAT_OK on success, error code on failure

Converts an IPv4 address to IPv6-mapped format (::ffff:x.x.x.x). This format allows IPv4 addresses to be represented in IPv6.

OUTPUT FORMAT: "::ffff:192.168.1.1"

Note
Output buffer should be at least 64 bytes.
Returns error if input is not a valid IPv4 address.
Example
char ipv6[64];
if (ipv4_to_ipv6_mapped("192.168.1.1", ipv6, sizeof(ipv6)) == ASCIICHAT_OK) {
// ipv6 = "::ffff:192.168.1.1"
}
asciichat_error_t ipv4_to_ipv6_mapped(const char *ipv4, char *ipv6_out, size_t out_size)
Convert IPv4 address to IPv6-mapped format.
Definition ip.c:840

Definition at line 840 of file ip.c.

840 {
841 if (!ipv4 || !ipv6_out || out_size == 0) {
842 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments to ipv4_to_ipv6_mapped");
843 }
844
845 // Validate IPv4
846 if (!is_valid_ipv4(ipv4)) {
847 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IPv4 address: %s", ipv4);
848 }
849
850 // Format as ::ffff:x.x.x.x
851 size_t written = SAFE_SNPRINTF(ipv6_out, out_size, "::ffff:%s", ipv4);
852 if (written >= out_size) {
853 return SET_ERRNO(ERROR_INVALID_PARAM, "Output buffer too small");
854 }
855
856 return ASCIICHAT_OK;
857}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, is_valid_ipv4(), SAFE_SNPRINTF, and SET_ERRNO.

◆ is_anycast_ipv6()

int is_anycast_ipv6 ( const char *  ipv6)

#include <ip.h>

Check if IPv6 address is anycast.

Parameters
ipv6IPv6 address (must not be NULL)
Returns
1 if anycast, 0 otherwise

Checks if an IPv6 address is a known anycast address. Anycast addresses are typically indistinguishable from unicast, but some well-known ranges exist:

  • Subnet-router anycast: last address in each subnet with all host bits zero
  • 6to4 relay anycast: 192.88.99.0/24 (mapped to 2002:c058:6301::)
Note
This function only detects known anycast ranges.
Most anycast addresses cannot be detected without network context.
Returns 0 for invalid IPv6 addresses.
Example
if (is_anycast_ipv6("2002:c058:6301::")) {
// This is a 6to4 relay anycast address
}
int is_anycast_ipv6(const char *ipv6)
Check if IPv6 address is anycast.
Definition ip.c:1004

Definition at line 1004 of file ip.c.

1004 {
1005 if (!ipv6) {
1006 return 0;
1007 }
1008
1009 // Normalize
1010 char normalized[INET6_ADDRSTRLEN];
1011 if (parse_ipv6_address(ipv6, normalized, sizeof(normalized)) != 0) {
1012 return 0;
1013 }
1014
1015 // Validate
1016 if (!is_valid_ipv6(normalized)) {
1017 return 0;
1018 }
1019
1020 struct in6_addr addr;
1021 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
1022 return 0;
1023 }
1024
1025 // 6to4 relay anycast: 192.88.99.0/24 (mapped to 2002:c058:6301::)
1026 // Check if it matches 2002:c058:6301::
1027 if (addr.s6_addr[0] == 0x20 && addr.s6_addr[1] == 0x02 && addr.s6_addr[2] == 0xc0 && addr.s6_addr[3] == 0x58 &&
1028 addr.s6_addr[4] == 0x63 && addr.s6_addr[5] == 0x01) {
1029 // Check if remaining bytes are zero (anycast)
1030 int is_zero = 1;
1031 for (int i = 6; i < 16; i++) {
1032 if (addr.s6_addr[i] != 0) {
1033 is_zero = 0;
1034 break;
1035 }
1036 }
1037 if (is_zero) {
1038 return 1;
1039 }
1040 }
1041
1042 // Subnet-router anycast: would need subnet context to determine
1043 // We can't detect this without knowing the subnet
1044
1045 return 0;
1046}

References is_valid_ipv6(), and parse_ipv6_address().

◆ is_broadcast_ipv4()

int is_broadcast_ipv4 ( const char *  ip)

#include <ip.h>

Check if IPv4 address is a broadcast address.

Parameters
ipIPv4 address string (must not be NULL)
Returns
1 if broadcast address, 0 otherwise

Checks if an IPv4 address is the limited broadcast address:

  • 255.255.255.255
Note
Subnet-specific broadcast addresses (e.g., 192.168.1.255 for 192.168.1.0/24) cannot be determined without network mask information.
Returns 0 for invalid IPv4 addresses.
Example
if (is_broadcast_ipv4("255.255.255.255")) {
// This is the limited broadcast address
}
int is_broadcast_ipv4(const char *ip)
Check if IPv4 address is a broadcast address.
Definition ip.c:1247

Definition at line 1247 of file ip.c.

1247 {
1248 if (!is_valid_ipv4(ip)) {
1249 return 0;
1250 }
1251
1252 struct in_addr addr;
1253 if (inet_pton(AF_INET, ip, &addr) != 1) {
1254 return 0;
1255 }
1256
1257 uint32_t ip_val = ntohl(addr.s_addr);
1258
1259 // Limited broadcast: 255.255.255.255
1260 if (ip_val == 0xFFFFFFFF) {
1261 return 1;
1262 }
1263
1264 return 0;
1265}

References is_valid_ipv4().

Referenced by is_internet_ipv4().

◆ is_broadcast_ipv6()

int is_broadcast_ipv6 ( const char *  ip)

#include <ip.h>

Check if IPv6 address is a multicast address.

Parameters
ipIPv6 address string (must not be NULL)
Returns
1 if multicast address, 0 otherwise

Checks if an IPv6 address is in the multicast range:

  • ff00::/8 (ff00:: - ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff)
Note
IPv6 doesn't have broadcast; multicast is used instead.
Returns 0 for invalid IPv6 addresses.
Example
if (is_broadcast_ipv6("ff02::1")) {
// This is a multicast address (all-nodes link-local)
}
int is_broadcast_ipv6(const char *ip)
Check if IPv6 address is a multicast address.
Definition ip.c:1268

Definition at line 1268 of file ip.c.

1268 {
1269 if (!ip) {
1270 return 0;
1271 }
1272
1273 // Remove brackets if present
1274 char normalized[INET6_ADDRSTRLEN];
1275 if (parse_ipv6_address(ip, normalized, sizeof(normalized)) != 0) {
1276 return 0;
1277 }
1278
1279 // Validate the normalized address
1280 if (!is_valid_ipv6(normalized)) {
1281 return 0;
1282 }
1283
1284 struct in6_addr addr;
1285 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
1286 return 0;
1287 }
1288
1289 // ff00::/8: Multicast addresses
1290 // First byte should be 0xff
1291 if (addr.s6_addr[0] == 0xFF) {
1292 return 1;
1293 }
1294
1295 return 0;
1296}

References is_valid_ipv6(), and parse_ipv6_address().

Referenced by is_internet_ipv6().

◆ is_internet_ipv4()

int is_internet_ipv4 ( const char *  ip)

#include <ip.h>

Check if IPv4 address is a public internet address.

Parameters
ipIPv4 address string (must not be NULL)
Returns
1 if public internet address, 0 otherwise

Checks if an IPv4 address is a globally routable public address. Returns 0 for:

  • Private/LAN addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Loopback addresses (127.0.0.0/8)
  • Link-local addresses (169.254.0.0/16)
  • Broadcast addresses (255.255.255.255)
  • Multicast addresses (224.0.0.0/4)
  • Reserved/special use addresses
  • Invalid addresses
Note
Returns 0 for invalid IPv4 addresses.
Example
if (is_internet_ipv4("8.8.8.8")) {
// This is a public internet address
}

Definition at line 1408 of file ip.c.

1408 {
1409 if (!is_valid_ipv4(ip)) {
1410 return 0;
1411 }
1412
1413 struct in_addr addr;
1414 if (inet_pton(AF_INET, ip, &addr) != 1) {
1415 return 0;
1416 }
1417
1418 uint32_t ip_val = ntohl(addr.s_addr);
1419
1420 // Exclude private/LAN addresses
1421 if (is_lan_ipv4(ip)) {
1422 return 0;
1423 }
1424
1425 // Exclude loopback
1426 if (is_localhost_ipv4(ip)) {
1427 return 0;
1428 }
1429
1430 // Exclude link-local
1431 if (is_link_local_ipv4(ip)) {
1432 return 0;
1433 }
1434
1435 // Exclude broadcast
1436 if (is_broadcast_ipv4(ip)) {
1437 return 0;
1438 }
1439
1440 // Exclude multicast: 224.0.0.0/4
1441 if ((ip_val & 0xF0000000) == 0xE0000000) {
1442 return 0;
1443 }
1444
1445 // Exclude 0.0.0.0/8: "This network"
1446 if ((ip_val & 0xFF000000) == 0x00000000) {
1447 return 0;
1448 }
1449
1450 // Exclude 100.64.0.0/10: Shared Address Space (RFC 6598)
1451 if ((ip_val & 0xFFC00000) == 0x64400000) {
1452 return 0;
1453 }
1454
1455 // Exclude 192.0.0.0/24: IETF Protocol Assignments
1456 if ((ip_val & 0xFFFFFF00) == 0xC0000000) {
1457 return 0;
1458 }
1459
1460 // Exclude 192.0.2.0/24: TEST-NET-1
1461 if ((ip_val & 0xFFFFFF00) == 0xC0000200) {
1462 return 0;
1463 }
1464
1465 // Exclude 198.18.0.0/15: Benchmarking
1466 if ((ip_val & 0xFFFE0000) == 0xC6120000) {
1467 return 0;
1468 }
1469
1470 // Exclude 198.51.100.0/24: TEST-NET-2
1471 if ((ip_val & 0xFFFFFF00) == 0xC6336400) {
1472 return 0;
1473 }
1474
1475 // Exclude 203.0.113.0/24: TEST-NET-3
1476 if ((ip_val & 0xFFFFFF00) == 0xCB007100) {
1477 return 0;
1478 }
1479
1480 // Exclude 240.0.0.0/4: Reserved for future use
1481 if ((ip_val & 0xF0000000) == 0xF0000000) {
1482 return 0;
1483 }
1484
1485 // If none of the exclusions matched, it's a public internet address
1486 return 1;
1487}
int is_link_local_ipv4(const char *ip)
Check if IPv4 address is a link-local address.
Definition ip.c:1356

References is_broadcast_ipv4(), is_lan_ipv4(), is_link_local_ipv4(), is_localhost_ipv4(), and is_valid_ipv4().

Referenced by get_ip_type_string().

◆ is_internet_ipv6()

int is_internet_ipv6 ( const char *  ip)

#include <ip.h>

Check if IPv6 address is a public internet address.

Parameters
ipIPv6 address string (must not be NULL)
Returns
1 if public internet address, 0 otherwise

Checks if an IPv6 address is a globally routable public address. Returns 0 for:

  • Loopback address (::1)
  • Link-local addresses (fe80::/10)
  • Unique local addresses (fc00::/7)
  • Multicast addresses (ff00::/8)
  • Special use/reserved addresses
  • Invalid addresses
Note
Returns 0 for invalid IPv6 addresses.
Example
if (is_internet_ipv6("2001:4860:4860::8888")) {
// This is a public internet address
}

Definition at line 1490 of file ip.c.

1490 {
1491 if (!ip) {
1492 return 0;
1493 }
1494
1495 // Remove brackets if present
1496 char normalized[INET6_ADDRSTRLEN];
1497 if (parse_ipv6_address(ip, normalized, sizeof(normalized)) != 0) {
1498 return 0;
1499 }
1500
1501 // Validate the normalized address
1502 if (!is_valid_ipv6(normalized)) {
1503 return 0;
1504 }
1505
1506 struct in6_addr addr;
1507 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
1508 return 0;
1509 }
1510
1511 // Exclude loopback
1512 if (is_localhost_ipv6(ip)) {
1513 return 0;
1514 }
1515
1516 // Exclude link-local
1517 if (is_link_local_ipv6(ip)) {
1518 return 0;
1519 }
1520
1521 // Exclude ULA
1522 if (is_lan_ipv6(ip)) {
1523 return 0;
1524 }
1525
1526 // Exclude multicast
1527 if (is_broadcast_ipv6(ip)) {
1528 return 0;
1529 }
1530
1531 // Exclude unspecified address: ::
1532 int is_unspecified = 1;
1533 for (int i = 0; i < 16; i++) {
1534 if (addr.s6_addr[i] != 0) {
1535 is_unspecified = 0;
1536 break;
1537 }
1538 }
1539 if (is_unspecified) {
1540 return 0;
1541 }
1542
1543 // Exclude IPv4-mapped IPv6: ::ffff:0:0/96
1544 if (addr.s6_addr[0] == 0 && addr.s6_addr[1] == 0 && addr.s6_addr[2] == 0 && addr.s6_addr[3] == 0 &&
1545 addr.s6_addr[4] == 0 && addr.s6_addr[5] == 0 && addr.s6_addr[6] == 0 && addr.s6_addr[7] == 0 &&
1546 addr.s6_addr[8] == 0 && addr.s6_addr[9] == 0 && addr.s6_addr[10] == 0xFF && addr.s6_addr[11] == 0xFF) {
1547 return 0;
1548 }
1549
1550 // Exclude IPv4-compatible IPv6: ::/96 (deprecated)
1551 if (addr.s6_addr[0] == 0 && addr.s6_addr[1] == 0 && addr.s6_addr[2] == 0 && addr.s6_addr[3] == 0 &&
1552 addr.s6_addr[4] == 0 && addr.s6_addr[5] == 0 && addr.s6_addr[6] == 0 && addr.s6_addr[7] == 0 &&
1553 addr.s6_addr[8] == 0 && addr.s6_addr[9] == 0 && addr.s6_addr[10] == 0 && addr.s6_addr[11] == 0) {
1554 // But not ::, which we already excluded above
1555 if (addr.s6_addr[12] != 0 || addr.s6_addr[13] != 0 || addr.s6_addr[14] != 0 || addr.s6_addr[15] != 0) {
1556 return 0;
1557 }
1558 }
1559
1560 // Exclude documentation prefix: 2001:db8::/32
1561 if (addr.s6_addr[0] == 0x20 && addr.s6_addr[1] == 0x01 && addr.s6_addr[2] == 0x0D && addr.s6_addr[3] == 0xB8) {
1562 return 0;
1563 }
1564
1565 // Exclude 6to4: 2002::/16
1566 if (addr.s6_addr[0] == 0x20 && addr.s6_addr[1] == 0x02) {
1567 return 0;
1568 }
1569
1570 // If none of the exclusions matched, it's likely a global unicast address
1571 // Global unicast is typically 2000::/3, but let's accept anything that's not excluded
1572 return 1;
1573}
int is_link_local_ipv6(const char *ip)
Check if IPv6 address is a link-local address.
Definition ip.c:1377

References is_broadcast_ipv6(), is_lan_ipv6(), is_link_local_ipv6(), is_localhost_ipv6(), is_valid_ipv6(), and parse_ipv6_address().

Referenced by get_ip_type_string().

◆ is_ipv4_mapped_ipv6()

int is_ipv4_mapped_ipv6 ( const char *  ipv6)

#include <ip.h>

Check if IPv6 address is IPv4-mapped.

Parameters
ipv6IPv6 address (must not be NULL)
Returns
1 if IPv4-mapped, 0 otherwise

Checks if an IPv6 address is in IPv4-mapped format (::ffff:x.x.x.x/96).

Note
Returns 0 for invalid IPv6 addresses.
Example
if (is_ipv4_mapped_ipv6("::ffff:192.168.1.1")) {
// This is an IPv4-mapped IPv6 address
}

Definition at line 860 of file ip.c.

860 {
861 if (!ipv6) {
862 return 0;
863 }
864
865 // Normalize (remove brackets)
866 char normalized[INET6_ADDRSTRLEN];
867 if (parse_ipv6_address(ipv6, normalized, sizeof(normalized)) != 0) {
868 return 0;
869 }
870
871 // Validate IPv6
872 if (!is_valid_ipv6(normalized)) {
873 return 0;
874 }
875
876 struct in6_addr addr;
877 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
878 return 0;
879 }
880
881 // Check for ::ffff:0:0/96 prefix
882 // First 10 bytes should be 0, next 2 should be 0xff
883 for (int i = 0; i < 10; i++) {
884 if (addr.s6_addr[i] != 0) {
885 return 0;
886 }
887 }
888 if (addr.s6_addr[10] != 0xFF || addr.s6_addr[11] != 0xFF) {
889 return 0;
890 }
891
892 return 1;
893}

References is_valid_ipv6(), and parse_ipv6_address().

Referenced by extract_ipv4_from_mapped_ipv6().

◆ is_lan_ipv4()

int is_lan_ipv4 ( const char *  ip)

#include <ip.h>

Check if IPv4 address is a private/LAN address.

Parameters
ipIPv4 address string (must not be NULL)
Returns
1 if LAN address, 0 otherwise

Checks if an IPv4 address is in private address ranges:

  • 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
  • 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
  • 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
Note
Returns 0 for invalid IPv4 addresses.
Example
if (is_lan_ipv4("192.168.1.1")) {
// This is a private LAN address
}

Definition at line 1185 of file ip.c.

1185 {
1186 if (!is_valid_ipv4(ip)) {
1187 return 0;
1188 }
1189
1190 struct in_addr addr;
1191 if (inet_pton(AF_INET, ip, &addr) != 1) {
1192 return 0;
1193 }
1194
1195 uint32_t ip_val = ntohl(addr.s_addr);
1196
1197 // 10.0.0.0/8: 10.0.0.0 - 10.255.255.255
1198 if ((ip_val & 0xFF000000) == 0x0A000000) {
1199 return 1;
1200 }
1201
1202 // 172.16.0.0/12: 172.16.0.0 - 172.31.255.255
1203 if ((ip_val & 0xFFF00000) == 0xAC100000) {
1204 return 1;
1205 }
1206
1207 // 192.168.0.0/16: 192.168.0.0 - 192.168.255.255
1208 if ((ip_val & 0xFFFF0000) == 0xC0A80000) {
1209 return 1;
1210 }
1211
1212 return 0;
1213}

References is_valid_ipv4().

Referenced by get_ip_type_string(), and is_internet_ipv4().

◆ is_lan_ipv6()

int is_lan_ipv6 ( const char *  ip)

#include <ip.h>

Check if IPv6 address is a private/LAN address.

Parameters
ipIPv6 address string (must not be NULL)
Returns
1 if LAN address, 0 otherwise

Checks if an IPv6 address is a Unique Local Address (ULA):

  • fc00::/7 (fc00:: - fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff)
Note
Returns 0 for invalid IPv6 addresses.
Example
if (is_lan_ipv6("fc00::1")) {
// This is a private ULA address
}

Definition at line 1216 of file ip.c.

1216 {
1217 if (!ip) {
1218 return 0;
1219 }
1220
1221 // Remove brackets if present
1222 char normalized[INET6_ADDRSTRLEN];
1223 if (parse_ipv6_address(ip, normalized, sizeof(normalized)) != 0) {
1224 return 0;
1225 }
1226
1227 // Validate the normalized address
1228 if (!is_valid_ipv6(normalized)) {
1229 return 0;
1230 }
1231
1232 struct in6_addr addr;
1233 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
1234 return 0;
1235 }
1236
1237 // fc00::/7: Unique Local Addresses (ULA)
1238 // First byte should be 0xfc or 0xfd
1239 if ((addr.s6_addr[0] & 0xFE) == 0xFC) {
1240 return 1;
1241 }
1242
1243 return 0;
1244}

References is_valid_ipv6(), and parse_ipv6_address().

Referenced by get_ip_type_string(), and is_internet_ipv6().

◆ is_link_local_ipv4()

int is_link_local_ipv4 ( const char *  ip)

#include <ip.h>

Check if IPv4 address is a link-local address.

Parameters
ipIPv4 address string (must not be NULL)
Returns
1 if link-local, 0 otherwise

Checks if an IPv4 address is in the link-local range:

  • 169.254.0.0/16 (169.254.0.0 - 169.254.255.255)

Link-local addresses are auto-configured when DHCP is unavailable (APIPA).

Note
Returns 0 for invalid IPv4 addresses.
Example
if (is_link_local_ipv4("169.254.1.1")) {
// This is an auto-configured link-local address
}

Definition at line 1356 of file ip.c.

1356 {
1357 if (!is_valid_ipv4(ip)) {
1358 return 0;
1359 }
1360
1361 struct in_addr addr;
1362 if (inet_pton(AF_INET, ip, &addr) != 1) {
1363 return 0;
1364 }
1365
1366 uint32_t ip_val = ntohl(addr.s_addr);
1367
1368 // 169.254.0.0/16: Link-local addresses (APIPA)
1369 if ((ip_val & 0xFFFF0000) == 0xA9FE0000) {
1370 return 1;
1371 }
1372
1373 return 0;
1374}

References is_valid_ipv4().

Referenced by is_internet_ipv4().

◆ is_link_local_ipv6()

int is_link_local_ipv6 ( const char *  ip)

#include <ip.h>

Check if IPv6 address is a link-local address.

Parameters
ipIPv6 address string (must not be NULL)
Returns
1 if link-local, 0 otherwise

Checks if an IPv6 address is in the link-local range:

  • fe80::/10 (fe80:: - febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff)

Link-local addresses are auto-configured and only valid on a single link.

Note
Returns 0 for invalid IPv6 addresses.
Example
if (is_link_local_ipv6("fe80::1")) {
// This is a link-local address
}

Definition at line 1377 of file ip.c.

1377 {
1378 if (!ip) {
1379 return 0;
1380 }
1381
1382 // Remove brackets if present
1383 char normalized[INET6_ADDRSTRLEN];
1384 if (parse_ipv6_address(ip, normalized, sizeof(normalized)) != 0) {
1385 return 0;
1386 }
1387
1388 // Validate the normalized address
1389 if (!is_valid_ipv6(normalized)) {
1390 return 0;
1391 }
1392
1393 struct in6_addr addr;
1394 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
1395 return 0;
1396 }
1397
1398 // fe80::/10: Link-local addresses
1399 // First byte should be 0xfe, second byte & 0xc0 should be 0x80
1400 if (addr.s6_addr[0] == 0xFE && (addr.s6_addr[1] & 0xC0) == 0x80) {
1401 return 1;
1402 }
1403
1404 return 0;
1405}

References is_valid_ipv6(), and parse_ipv6_address().

Referenced by is_internet_ipv6().

◆ is_localhost_ipv4()

int is_localhost_ipv4 ( const char *  ip)

#include <ip.h>

Check if IPv4 address is localhost.

Parameters
ipIPv4 address string (must not be NULL)
Returns
1 if localhost, 0 otherwise

Checks if an IPv4 address is in the loopback range:

  • 127.0.0.0/8 (127.0.0.0 - 127.255.255.255)
Note
Returns 0 for invalid IPv4 addresses.
Example
if (is_localhost_ipv4("127.0.0.1")) {
// This is a loopback address
}

Definition at line 1299 of file ip.c.

1299 {
1300 if (!is_valid_ipv4(ip)) {
1301 return 0;
1302 }
1303
1304 struct in_addr addr;
1305 if (inet_pton(AF_INET, ip, &addr) != 1) {
1306 return 0;
1307 }
1308
1309 uint32_t ip_val = ntohl(addr.s_addr);
1310
1311 // 127.0.0.0/8: Loopback addresses
1312 if ((ip_val & 0xFF000000) == 0x7F000000) {
1313 return 1;
1314 }
1315
1316 return 0;
1317}

References is_valid_ipv4().

Referenced by client_main(), get_ip_type_string(), is_internet_ipv4(), parse_server_bind_address(), and tcp_client_connect().

◆ is_localhost_ipv6()

int is_localhost_ipv6 ( const char *  ip)

#include <ip.h>

Check if IPv6 address is localhost.

Parameters
ipIPv6 address string (must not be NULL)
Returns
1 if localhost, 0 otherwise

Checks if an IPv6 address is the loopback address:

  • ::1
Note
Returns 0 for invalid IPv6 addresses.
Example
if (is_localhost_ipv6("::1")) {
// This is the IPv6 loopback address
}

Definition at line 1320 of file ip.c.

1320 {
1321 if (!ip) {
1322 return 0;
1323 }
1324
1325 // Remove brackets if present
1326 char normalized[INET6_ADDRSTRLEN];
1327 if (parse_ipv6_address(ip, normalized, sizeof(normalized)) != 0) {
1328 return 0;
1329 }
1330
1331 // Validate the normalized address
1332 if (!is_valid_ipv6(normalized)) {
1333 return 0;
1334 }
1335
1336 struct in6_addr addr;
1337 if (inet_pton(AF_INET6, normalized, &addr) != 1) {
1338 return 0;
1339 }
1340
1341 // ::1: Loopback address
1342 // All bytes should be 0 except the last byte which should be 1
1343 for (int i = 0; i < 15; i++) {
1344 if (addr.s6_addr[i] != 0) {
1345 return 0;
1346 }
1347 }
1348 if (addr.s6_addr[15] != 1) {
1349 return 0;
1350 }
1351
1352 return 1;
1353}

References is_valid_ipv6(), and parse_ipv6_address().

Referenced by get_ip_type_string(), is_internet_ipv6(), parse_server_bind_address(), and tcp_client_connect().

◆ is_valid_ip()

int is_valid_ip ( const char *  ip)

#include <ip.h>

Check if string is a valid IP address (IPv4 or IPv6)

Parameters
ipString to validate (must not be NULL)
Returns
1 if valid IPv4 or IPv6, 0 otherwise

Unified validation function that accepts either IPv4 or IPv6 addresses. Use this when you don't care about the IP version, just validity.

Note
IPv6 addresses can be with or without brackets.
Returns 0 for NULL, empty strings, or invalid addresses.
Example
if (is_valid_ip("192.168.1.1")) {
// Valid IPv4
}
if (is_valid_ip("::1")) {
// Valid IPv6
}
int is_valid_ip(const char *ip)
Check if string is a valid IP address (IPv4 or IPv6)
Definition ip.c:534

Definition at line 534 of file ip.c.

534 {
535 if (!ip) {
536 return 0;
537 }
538
539 // Try IPv4 first (faster check)
540 if (is_valid_ipv4(ip)) {
541 return 1;
542 }
543
544 // Try IPv6
545 if (is_valid_ipv6(ip)) {
546 return 1;
547 }
548
549 return 0;
550}

References is_valid_ipv4(), and is_valid_ipv6().

◆ is_valid_ipv4()

int is_valid_ipv4 ( const char *  ip)

#include <ip.h>

Check if a string is a valid IPv4 address.

Parameters
ipString to validate (must not be NULL)
Returns
1 if valid IPv4, 0 otherwise

Validates that a string is a valid IPv4 address in dotted decimal notation (e.g., "192.0.2.1"). Checks format correctness and numeric value ranges for each octet (0-255).

Note
Returns 0 for invalid format, out-of-range values, or non-IPv4 strings.
Does not validate that address is routable or assigned.
Example
if (is_valid_ipv4("192.0.2.1")) {
// Valid IPv4 address
}

Definition at line 58 of file ip.c.

58 {
59 if (!ip) {
60 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: NULL");
61 return 0; // Invalid
62 }
63
64 size_t ip_len = strlen(ip);
65 if (ip_len == 0) {
66 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: empty string");
67 return 0; // Invalid
68 }
69
70 if (ip_len > 256) {
71 char ip_buffer[BUFFER_SIZE_SMALL];
72 SAFE_STRNCPY(ip_buffer, ip, sizeof(ip_buffer));
73 SET_ERRNO(ERROR_INVALID_PARAM, "Suspiciously long ip: %s", ip_buffer);
74 return 0; // Invalid
75 }
76
77 // Get compiled IPv4 regex
78 pcre2_code *regex = ipv4_regex_get();
79 if (!regex) {
80 log_error("IPv4 validator not initialized");
81 return 0;
82 }
83
84 // Create match data for regex matching
85 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
86 if (!match_data) {
87 log_error("Failed to allocate match data for IPv4 regex");
88 return 0;
89 }
90
91 // Validate format using PCRE2 regex
92 int match_result = pcre2_match(regex, (PCRE2_SPTR8)ip, ip_len, 0, 0, match_data, NULL);
93
94 pcre2_match_data_free(match_data);
95
96 if (match_result < 0) {
97 return 0; // Format validation failed
98 }
99
100 return 1; // Valid IPv4 format
101}
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587

References BUFFER_SIZE_SMALL, ERROR_INVALID_PARAM, log_error, SAFE_STRNCPY, and SET_ERRNO.

Referenced by get_ip_version(), ipv4_to_ipv6_mapped(), is_broadcast_ipv4(), is_internet_ipv4(), is_lan_ipv4(), is_link_local_ipv4(), is_localhost_ipv4(), is_tailscale_host(), is_valid_ip(), parse_address_with_optional_port(), parse_client_address(), parse_server_bind_address(), and validate_opt_ip_address().

◆ is_valid_ipv6()

int is_valid_ipv6 ( const char *  ip)

#include <ip.h>

Check if a string is a valid IPv6 address.

Parameters
ipString to validate (with or without brackets, must not be NULL)
Returns
1 if valid IPv6, 0 otherwise

Validates that a string is a valid IPv6 address. Accepts addresses with or without brackets (e.g., "::1" or "[::1]"). Checks format correctness including compressed zero notation (::) and valid hexadecimal groups.

Note
Returns 0 for invalid format, malformed addresses, or non-IPv6 strings.
Handles both bracketed and unbracketed IPv6 addresses.
Does not validate that address is routable or assigned.
Example
if (is_valid_ipv6("[::1]")) {
// Valid IPv6 address
}

Definition at line 105 of file ip.c.

105 {
106 if (!ip) {
107 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: NULL");
108 return 0; // Invalid
109 }
110
111 size_t ip_len = strlen(ip);
112 if (ip_len == 0) {
113 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: empty string");
114 return 0; // Invalid
115 }
116
117 if (ip_len > 256) {
118 char ip_buffer[BUFFER_SIZE_SMALL];
119 SAFE_STRNCPY(ip_buffer, ip, sizeof(ip_buffer));
120 SET_ERRNO(ERROR_INVALID_PARAM, "Suspiciously long ip: %s", ip_buffer);
121 return 0; // Invalid
122 }
123
124 // Remove brackets if present (IPv6 addresses can be bracketed)
125 char normalized[INET6_ADDRSTRLEN];
126 const char *ip_to_validate = ip;
127
128 if (ip[0] == '[') {
129 // Has brackets, need to remove them
130 if (parse_ipv6_address(ip, normalized, sizeof(normalized)) != 0) {
131 return 0; // Failed to parse (malformed brackets)
132 }
133 ip_to_validate = normalized;
134 }
135
136 // Use POSIX inet_pton() for robust IPv6 validation
137 // It handles all RFC 5952 compliant formats:
138 // - Full format: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
139 // - Compressed: 2001:db8:85a3::8a2e:370:7334
140 // - Loopback: ::1
141 // - All zeros: ::
142 // - IPv4-mapped: ::ffff:192.0.2.1
143 struct in6_addr addr;
144 int result = inet_pton(AF_INET6, ip_to_validate, &addr);
145
146 if (result == 1) {
147 return 1; // Valid IPv6
148 } else if (result == 0) {
149 return 0; // Not a valid IPv6 address
150 } else {
151 // result < 0 means system error (e.g., unsupported address family)
152 SET_ERRNO(ERROR_INVALID_PARAM, "Failed to validate IPv6 address");
153 return 0;
154 }
155}

References BUFFER_SIZE_SMALL, ERROR_INVALID_PARAM, parse_ipv6_address(), SAFE_STRNCPY, and SET_ERRNO.

Referenced by canonicalize_ipv6(), expand_ipv6(), get_ip_version(), is_anycast_ipv6(), is_broadcast_ipv6(), is_internet_ipv6(), is_ipv4_mapped_ipv6(), is_lan_ipv6(), is_link_local_ipv6(), is_localhost_ipv6(), is_tailscale_host(), is_valid_ip(), parse_address_with_optional_port(), parse_client_address(), parse_server_bind_address(), and validate_opt_ip_address().

◆ parse_address_with_optional_port()

int parse_address_with_optional_port ( const char *  input,
char *  address_output,
size_t  address_output_size,
uint16_t *  port_output,
uint16_t  default_port 
)

#include <ip.h>

Parse address with optional port from string.

Parameters
inputInput string (address or address:port, must not be NULL)
address_outputOutput buffer for address (without brackets, must not be NULL)
address_output_sizeSize of address output buffer (must be > 0)
port_outputPointer to store port number (must not be NULL)
default_portDefault port to use if not specified in input
Returns
0 on success, -1 on error

Parses an address (IPv4, IPv6, or hostname) with an optional port number. If no port is specified, uses the default_port value. Handles various formats including bracketed IPv6 addresses.

SUPPORTED FORMATS (without port - uses default_port):

  • IPv4: "192.0.2.1"
  • IPv6: "::1" or "[::1]"
  • Hostname: "example.com" or "localhost"

SUPPORTED FORMATS (with port):

  • IPv4: "192.0.2.1:8080"
  • IPv6: "[2001:db8::1]:8080" (brackets required with port)
  • Hostname: "example.com:8080"
Note
IPv6 addresses with port MUST use brackets: "[::1]:8080"
IPv6 addresses without port can omit brackets: "::1" or "[::1]"
Port numbers are validated (1-65535 range).
Output address is normalized (brackets removed for IPv6).
Returns -1 on error (invalid format, buffer too small, invalid port, etc.).
Example
char addr[64];
uint16_t port;
// Without port - uses default
parse_address_with_optional_port("localhost", addr, sizeof(addr), &port, 27224);
// addr = "localhost", port = 27224
// With port
parse_address_with_optional_port("192.168.1.1:8080", addr, sizeof(addr), &port, 27224);
// addr = "192.168.1.1", port = 8080
// IPv6 without port
parse_address_with_optional_port("::1", addr, sizeof(addr), &port, 27224);
// addr = "::1", port = 27224
// IPv6 with port
parse_address_with_optional_port("[::1]:8080", addr, sizeof(addr), &port, 27224);
// addr = "::1", port = 8080
int parse_address_with_optional_port(const char *input, char *address_output, size_t address_output_size, uint16_t *port_output, uint16_t default_port)
Parse address with optional port from string.
Definition ip.c:370

Definition at line 370 of file ip.c.

371 {
372 if (!input || !address_output || !port_output || address_output_size == 0) {
373 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid arguments to parse_address_with_optional_port");
374 return -1;
375 }
376
377 size_t input_len = strlen(input);
378 if (input_len == 0) {
379 SET_ERRNO(ERROR_INVALID_PARAM, "Empty address string");
380 return -1;
381 }
382
383 if (input_len > 256) {
384 SET_ERRNO(ERROR_INVALID_PARAM, "Address string too long");
385 return -1;
386 }
387
388 // Case 1: Bracketed IPv6 address with or without port
389 // Examples: "[::1]" or "[::1]:8080" or "[2001:db8::1]:443"
390 if (input[0] == '[') {
391 const char *bracket_end = strchr(input, ']');
392 if (!bracket_end) {
393 SET_ERRNO(ERROR_INVALID_PARAM, "Malformed bracketed address (missing ']'): %s", input);
394 return -1;
395 }
396
397 // Extract address without brackets
398 size_t addr_len = (size_t)(bracket_end - input - 1);
399 if (addr_len == 0) {
400 SET_ERRNO(ERROR_INVALID_PARAM, "Empty bracketed address: %s", input);
401 return -1;
402 }
403 if (addr_len >= address_output_size) {
404 SET_ERRNO(ERROR_INVALID_PARAM, "Address too long for buffer");
405 return -1;
406 }
407
408 memcpy(address_output, input + 1, addr_len);
409 address_output[addr_len] = '\0';
410
411 // Validate it's a valid IPv6 address
412 if (!is_valid_ipv6(address_output)) {
413 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IPv6 address: %s", address_output);
414 return -1;
415 }
416
417 // Check for port after closing bracket
418 if (bracket_end[1] == '\0') {
419 // No port specified, use default
420 *port_output = default_port;
421 } else if (bracket_end[1] == ':') {
422 // Port specified
423 if (parse_port(bracket_end + 2, port_output) != ASCIICHAT_OK) {
424 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid port in: %s", input);
425 return -1;
426 }
427 } else {
428 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid format after ']' (expected ':' or end): %s", input);
429 return -1;
430 }
431 return 0;
432 }
433
434 // Case 2: Check if it's a valid IPv6 address without brackets (no port allowed in this case)
435 // Examples: "::1" or "2001:db8::1" or "fe80::1"
436 // Note: IPv6 addresses contain colons, so we can't use ':' as port separator without brackets
437 if (is_valid_ipv6(input)) {
438 if (strlen(input) >= address_output_size) {
439 SET_ERRNO(ERROR_INVALID_PARAM, "Address too long for buffer");
440 return -1;
441 }
442 SAFE_STRNCPY(address_output, input, address_output_size);
443 *port_output = default_port;
444 return 0;
445 }
446
447 // Case 3: IPv4 or hostname, possibly with port
448 // Examples: "192.168.1.1" or "192.168.1.1:8080" or "localhost" or "example.com:443"
449 const char *last_colon = strrchr(input, ':');
450
451 if (last_colon == NULL) {
452 // No colon - just address/hostname, no port
453 if (strlen(input) >= address_output_size) {
454 SET_ERRNO(ERROR_INVALID_PARAM, "Address too long for buffer");
455 return -1;
456 }
457
458 // Validate it's a valid IPv4 or hostname
459 if (!is_valid_ipv4(input) && !is_valid_hostname(input)) {
460 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid address format: %s", input);
461 return -1;
462 }
463
464 SAFE_STRNCPY(address_output, input, address_output_size);
465 *port_output = default_port;
466 return 0;
467 }
468
469 // Has colon - extract address and port
470 size_t addr_len = (size_t)(last_colon - input);
471 if (addr_len == 0) {
472 SET_ERRNO(ERROR_INVALID_PARAM, "Empty address before port: %s", input);
473 return -1;
474 }
475 if (addr_len >= address_output_size) {
476 SET_ERRNO(ERROR_INVALID_PARAM, "Address too long for buffer");
477 return -1;
478 }
479
480 memcpy(address_output, input, addr_len);
481 address_output[addr_len] = '\0';
482
483 // Check if extracted address contains ':' - that would be IPv6 without brackets (invalid with port)
484 if (strchr(address_output, ':') != NULL) {
485 SET_ERRNO(ERROR_INVALID_PARAM, "IPv6 addresses must use brackets when specifying port: [%s]:%s instead of %s",
486 address_output, last_colon + 1, input);
487 return -1;
488 }
489
490 // Validate address part is valid IPv4 or hostname
491 if (!is_valid_ipv4(address_output) && !is_valid_hostname(address_output)) {
492 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid address format: %s", address_output);
493 return -1;
494 }
495
496 // Parse port
497 if (parse_port(last_colon + 1, port_output) != ASCIICHAT_OK) {
498 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid port in: %s", input);
499 return -1;
500 }
501
502 return 0;
503}
asciichat_error_t parse_port(const char *str, uint16_t *out_port)
Parse port number (1-65535) from string.

References ASCIICHAT_OK, ERROR_INVALID_PARAM, is_valid_ipv4(), is_valid_ipv6(), parse_port(), SAFE_STRNCPY, and SET_ERRNO.

◆ parse_cidr()

int parse_cidr ( const char *  cidr,
char *  ip_out,
size_t  ip_out_size,
int *  prefix_out 
)

#include <ip.h>

Parse CIDR notation into IP and prefix length.

Parameters
cidrCIDR string (e.g., "192.168.1.0/24", must not be NULL)
ip_outOutput buffer for IP address (without prefix, must not be NULL)
ip_out_sizeSize of IP output buffer
prefix_outPointer to store prefix length (must not be NULL)
Returns
0 on success, -1 on error

Parses CIDR notation into separate IP address and prefix length components. Supports both IPv4 and IPv6 CIDR notation.

SUPPORTED FORMATS:

  • IPv4: "192.168.1.0/24" -> ip="192.168.1.0", prefix=24
  • IPv6: "2001:db8::/32" -> ip="2001:db8::", prefix=32
  • IPv6 with brackets: "[::1]/128" -> ip="::1", prefix=128
Note
Validates that prefix length is in valid range (0-32 for IPv4, 0-128 for IPv6).
Output IP has brackets removed for IPv6.
Returns -1 on error (invalid format, out of range prefix, buffer too small).
Example
char ip[64];
int prefix;
if (parse_cidr("192.168.1.0/24", ip, sizeof(ip), &prefix) == 0) {
// ip = "192.168.1.0", prefix = 24
}

Definition at line 665 of file ip.c.

665 {
666 if (!cidr || !ip_out || !prefix_out || ip_out_size == 0) {
667 return -1;
668 }
669
670 // Find the '/' separator
671 const char *slash = strchr(cidr, '/');
672 if (!slash) {
673 return -1; // No slash found
674 }
675
676 // Extract IP part
677 size_t ip_len = (size_t)(slash - cidr);
678 if (ip_len == 0 || ip_len >= ip_out_size) {
679 return -1;
680 }
681
682 char ip_temp[256];
683 if (ip_len >= sizeof(ip_temp)) {
684 return -1;
685 }
686
687 memcpy(ip_temp, cidr, ip_len);
688 ip_temp[ip_len] = '\0';
689
690 // Remove brackets if IPv6
691 if (ip_temp[0] == '[') {
692 if (parse_ipv6_address(ip_temp, ip_out, ip_out_size) != 0) {
693 return -1;
694 }
695 } else {
696 if (ip_len >= ip_out_size) {
697 return -1;
698 }
699 memcpy(ip_out, ip_temp, ip_len + 1);
700 }
701
702 // Validate IP
703 int version = get_ip_version(ip_out);
704 if (version == 0) {
705 return -1; // Invalid IP
706 }
707
708 // Parse prefix length
709 const char *prefix_str = slash + 1;
710 if (strlen(prefix_str) == 0) {
711 return -1;
712 }
713
714 char *endptr;
715 long prefix = strtol(prefix_str, &endptr, 10);
716 if (*endptr != '\0' || prefix < 0) {
717 return -1; // Invalid prefix
718 }
719
720 // Validate prefix range based on IP version
721 if (version == 4 && (prefix > 32)) {
722 return -1;
723 }
724 if (version == 6 && (prefix > 128)) {
725 return -1;
726 }
727
728 *prefix_out = (int)prefix;
729 return 0;
730}

References get_ip_version(), and parse_ipv6_address().

Referenced by ip_in_cidr().

◆ parse_ip_with_port()

int parse_ip_with_port ( const char *  input,
char *  ip_output,
size_t  ip_output_size,
uint16_t *  port_output 
)

#include <ip.h>

Parse IP address and port from string.

Parameters
inputInput string with port (must not be NULL, format: "ip:port" or "[ip]:port")
ip_outputOutput buffer for IP address (without brackets, must not be NULL)
ip_output_sizeSize of IP output buffer (must be > 0)
port_outputPointer to store port number (must not be NULL)
Returns
0 on success, -1 on error

Parses an IP address (IPv4 or IPv6) with port number from a string. Handles various formats including bracketed IPv6 addresses. Also accepts hostnames (non-IP addresses).

SUPPORTED FORMATS:

  • IPv4: "192.0.2.1:8080"
  • IPv6: "[2001:db8::1]:8080"
  • Hostname: "example.com:8080"
Note
IPv6 addresses must use brackets in input format.
Port numbers are validated (0-65535 range).
Output IP address is normalized (brackets removed).
Returns -1 on error (invalid format, buffer too small, invalid port, etc.).
Example
char ip[64];
uint16_t port;
if (parse_ip_with_port("[::1]:8080", ip, sizeof(ip), &port) == 0) {
// ip = "::1", port = 8080
}

Definition at line 296 of file ip.c.

296 {
297 if (!input || !ip_output || !port_output || ip_output_size == 0) {
298 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", input);
299 return -1;
300 }
301
302 if (strlen(input) == 0) {
303 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", input);
304 return -1;
305 }
306
307 if (strlen(input) > 256) {
308 char input_buffer[BUFFER_SIZE_SMALL];
309 SAFE_STRNCPY(input_buffer, input, sizeof(input_buffer));
310 SET_ERRNO(ERROR_INVALID_PARAM, "Suspiciously long ip: %s", input_buffer);
311 return -1;
312 }
313
314 // Check for IPv6 bracket notation: [2001:db8::1]:8080
315 if (input[0] == '[') {
316 // Find closing bracket
317 const char *bracket_end = strchr(input, ']');
318 if (!bracket_end)
319 return -1; // Malformed - opening bracket but no closing bracket
320
321 // Extract IP address (without brackets)
322 size_t ip_len = (size_t)(bracket_end - input - 1);
323 if (ip_len >= ip_output_size)
324 return -1; // IP too long
325
326 memcpy(ip_output, input + 1, ip_len);
327 ip_output[ip_len] = '\0';
328
329 // Check for port after closing bracket
330 if (bracket_end[1] == ':') {
331 // Parse port using safe integer parsing
332 if (parse_port(bracket_end + 2, port_output) != ASCIICHAT_OK) {
333 return -1; // Invalid port
334 }
335 } else {
336 return -1; // Expected ':' after closing bracket
337 }
338 } else {
339 // IPv4 or hostname: "192.0.2.1:8080" or "example.com:8080"
340 // Find last ':' for port separation
341 const char *colon = strrchr(input, ':');
342 if (!colon)
343 return -1; // No port separator
344
345 // Extract IP/hostname
346 size_t ip_len = (size_t)(colon - input);
347 if (ip_len >= ip_output_size)
348 return -1; // IP too long
349
350 memcpy(ip_output, input, ip_len);
351 ip_output[ip_len] = '\0';
352
353 // Reject IPv6 without brackets (check if extracted IP contains ':')
354 // This catches cases like "::1:8080" which should be "[::1]:8080"
355 if (strchr(ip_output, ':') != NULL)
356 return -1; // IPv6 address without brackets - invalid format
357
358 // Parse port using safe integer parsing
359 if (parse_port(colon + 1, port_output) != ASCIICHAT_OK) {
360 return -1; // Invalid port
361 }
362 }
363
364 return 0;
365}

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, ERROR_INVALID_PARAM, parse_port(), SAFE_STRNCPY, and SET_ERRNO.

Referenced by compare_ip_port_strings().

◆ parse_ipv6_address()

int parse_ipv6_address ( const char *  input,
char *  output,
size_t  output_size 
)

#include <ip.h>

Parse IPv6 address, removing brackets if present.

Parameters
inputInput address (may have brackets, must not be NULL)
outputOutput buffer for parsed address (without brackets, must not be NULL)
output_sizeSize of output buffer (must be > 0)
Returns
0 on success, -1 on error

Parses an IPv6 address and removes brackets if present. Handles both bracketed and unbracketed formats. Validates the address and normalizes it to unbracketed form for internal use.

SUPPORTED FORMATS:

  • "::1" -> "::1"
  • "[::1]" -> "::1"
  • "2001:db8::1" -> "2001:db8::1"
  • "[2001:db8::1]" -> "2001:db8::1"
Note
Output buffer must be large enough to hold IPv6 address (at least 39 bytes).
Returns -1 on error (invalid address, buffer too small, etc.).
Brackets are removed from output (address is normalized).

Definition at line 158 of file ip.c.

158 {
159 if (!input || !output || output_size == 0)
160 return -1;
161
162 size_t input_len = strlen(input);
163 if (input_len == 0)
164 return -1; // Empty string
165
166 const char *start = input;
167 const char *end = input + input_len;
168
169 // Remove brackets if present - but brackets must be matched
170 if (*start == '[') {
171 if (input_len < 2 || *(end - 1) != ']')
172 return -1; // Malformed brackets
173 start++;
174 end--;
175
176 // Check for double brackets [[...]]
177 if (start < end && *start == '[')
178 return -1; // Double opening bracket
179 } else if (*(end - 1) == ']') {
180 // Closing bracket without opening bracket
181 return -1;
182 }
183
184 size_t len = (size_t)(end - start);
185 if (len == 0)
186 return -1; // Empty content after removing brackets
187 if (len >= output_size)
188 return -1; // Buffer too small
189
190 memcpy(output, start, len);
191 output[len] = '\0';
192 return 0;
193}

Referenced by canonicalize_ipv6(), expand_ipv6(), extract_ipv4_from_mapped_ipv6(), ip_compare(), ip_equals(), ip_in_cidr_parsed(), is_anycast_ipv6(), is_broadcast_ipv6(), is_internet_ipv6(), is_ipv4_mapped_ipv6(), is_lan_ipv6(), is_link_local_ipv6(), is_localhost_ipv6(), is_valid_ipv6(), parse_cidr(), parse_server_bind_address(), and validate_opt_ip_address().

◆ path_cleanup_thread_locals()

void path_cleanup_thread_locals ( void  )

#include <path.h>

Cleanup thread-local path resources.

Frees PCRE2 match data allocated for thread-local path normalization. Called automatically on thread exit. Safe to call multiple times.

Note
This is called automatically by the threading system - users should not need to call this directly.

Cleanup thread-local PCRE2 resources (now a no-op, kept for compatibility)

Previously this freed cached thread-local match data, but we now allocate fresh match data for each operation and free it immediately, so there's nothing to clean up. This function is kept for API compatibility.

Definition at line 85 of file path.c.

85 {
86 /* No-op: match data is now allocated and freed immediately per operation */
87}

Referenced by main().

◆ path_is_absolute()

bool path_is_absolute ( const char *  path)

#include <path.h>

Determine whether a path is absolute on the current platform.

Parameters
pathPath string to test (may be NULL)
Returns
true if the path is absolute, false otherwise

Definition at line 739 of file path.c.

739 {
740 if (!path || !*path) {
741 return false;
742 }
743
744#ifdef _WIN32
745 if ((path[0] == '\\' && path[1] == '\\')) {
746 return true; // UNC path
747 }
748 if (isalpha((unsigned char)path[0]) && path[1] == PATH_DRIVE_SEPARATOR && path[2] == PATH_DELIM) {
749 return true;
750 }
751 return false;
752#else
753 return path[0] == PATH_DELIM;
754#endif
755}
#define PATH_DRIVE_SEPARATOR
Path component: Windows drive separator (colon)
Definition path.h:78

References PATH_DELIM, and PATH_DRIVE_SEPARATOR.

Referenced by path_is_within_base(), and path_validate_user_path().

◆ path_is_within_any_base()

bool path_is_within_any_base ( const char *  path,
const char *const *  bases,
size_t  base_count 
)

#include <path.h>

Check whether a path resides within any of several base directories.

Parameters
pathCandidate path to validate (must be absolute)
basesArray of base directory strings
base_countNumber of entries in bases
Returns
true if path is inside any allowed base, false otherwise

Definition at line 791 of file path.c.

791 {
792 if (!path || !bases || base_count == 0) {
793 return false;
794 }
795
796 for (size_t i = 0; i < base_count; ++i) {
797 const char *base = bases[i];
798 if (!base) {
799 continue;
800 }
801 if (path_is_within_base(path, base)) {
802 return true;
803 }
804 }
805
806 return false;
807}
bool path_is_within_base(const char *path, const char *base)
Check whether a path resides within a specified base directory.
Definition path.c:757

References path_is_within_base().

Referenced by path_validate_user_path().

◆ path_is_within_base()

bool path_is_within_base ( const char *  path,
const char *  base 
)

#include <path.h>

Check whether a path resides within a specified base directory.

Both the candidate path and the base directory are normalized before comparison. The base directory must be absolute. The comparison honours the platform's case sensitivity rules (case-insensitive on Windows).

Parameters
pathCandidate path to validate (must be absolute)
baseAbsolute base directory to compare against
Returns
true if path is inside base (or exactly equal), false otherwise

Definition at line 757 of file path.c.

757 {
758 if (!path || !base) {
759 return false;
760 }
761
762 if (!path_is_absolute(path) || !path_is_absolute(base)) {
763 return false;
764 }
765
766 char normalized_path[PLATFORM_MAX_PATH_LENGTH];
767 char normalized_base[PLATFORM_MAX_PATH_LENGTH];
768
769 if (!path_normalize_copy(path, normalized_path, sizeof(normalized_path))) {
770 return false;
771 }
772 if (!path_normalize_copy(base, normalized_base, sizeof(normalized_base))) {
773 return false;
774 }
775
776 size_t base_len = strlen(normalized_base);
777 if (base_len == 0) {
778 return false;
779 }
780
781 if (platform_path_strcasecmp(normalized_path, normalized_base, base_len) != 0) {
782 return false;
783 }
784 char next = normalized_path[base_len];
785 if (next == '\0') {
786 return true;
787 }
788 return next == PATH_DELIM;
789}
int platform_path_strcasecmp(const char *a, const char *b, size_t n)
Platform-aware path string comparison.
Definition util.c:119
bool path_is_absolute(const char *path)
Determine whether a path is absolute on the current platform.
Definition path.c:739
bool path_normalize_copy(const char *path, char *out, size_t out_len)
Normalize a path and copy it into the provided buffer.
Definition path.c:719

References PATH_DELIM, path_is_absolute(), path_normalize_copy(), PLATFORM_MAX_PATH_LENGTH, and platform_path_strcasecmp().

Referenced by path_is_within_any_base().

◆ path_looks_like_path()

bool path_looks_like_path ( const char *  value)

#include <path.h>

Determine if a string is likely intended to reference the filesystem.

Heuristics include presence of path separators, leading ~, relative prefixes, or Windows drive designators. Used to avoid treating tokens like "github:user" or raw hex keys as file paths.

Parameters
valueString to analyze
Returns
true if the string looks like a filesystem path

Definition at line 809 of file path.c.

809 {
810 if (!value || *value == '\0') {
811 return false;
812 }
813
814 if (value[0] == PATH_DELIM || value[0] == PATH_COMPONENT_DOT || value[0] == PATH_TILDE) {
815 return true;
816 }
817
818 if (strchr(value, PATH_DELIM)) {
819 return true;
820 }
821
822#ifdef _WIN32
823 if (isalpha((unsigned char)value[0]) && value[1] == ':' && value[2] == PATH_DELIM) {
824 return true;
825 }
826#endif
827
828 return false;
829}
#define PATH_COMPONENT_DOT
Path component: current directory (single dot)
Definition path.h:59

References PATH_COMPONENT_DOT, PATH_DELIM, and PATH_TILDE.

Referenced by options_init(), parse_keys_from_file(), parse_public_key(), parse_public_keys(), path_validate_user_path(), and validate_ssh_key_file().

◆ path_normalize_copy()

bool path_normalize_copy ( const char *  path,
char *  out,
size_t  out_len 
)

#include <path.h>

Normalize a path and copy it into the provided buffer.

Resolves '.' and '..' components without requiring the path to exist on disk. The output buffer receives a canonicalized representation using platform preferred separators.

Parameters
pathInput path string (must not be NULL)
outDestination buffer for normalized path
out_lenSize of destination buffer in bytes
Returns
true on success, false on failure (invalid arguments or buffer too small)

Definition at line 719 of file path.c.

719 {
720 if (!path || !out || out_len == 0) {
721 SET_ERRNO(ERROR_INVALID_PARAM, "null path or out or out_len is 0");
722 return false;
723 }
724
725 const char *normalized = normalize_path(path);
726 if (!normalized) {
727 return false;
728 }
729
730 size_t len = strlen(normalized);
731 if (len + 1 > out_len) {
732 return false;
733 }
734
735 memcpy(out, normalized, len + 1);
736 return true;
737}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by path_is_within_base(), and path_validate_user_path().

◆ path_validate_user_path()

asciichat_error_t path_validate_user_path ( const char *  input,
path_role_t  role,
char **  normalized_out 
)

#include <path.h>

Validate and canonicalize a user-supplied filesystem path.

Resolves ~, relative segments, and enforces that the resulting absolute path resides within the trusted base directories for the supplied role.

Trusted base directories include:

  • Current working directory
  • System temp directory
  • User config directory (~/.ascii-chat/ or APPDATA%\ascii-chat)
  • User home directory
  • User SSH directory (~/.ssh/)
  • System config directories (Unix: /etc/ascii-chat/, /usr/local/etc/ascii-chat/)
  • System log directories (Unix: /var/log/, /var/tmp/)
  • System-wide app data (Windows: PROGRAMDATA%\ascii-chat)
Note
For PATH_ROLE_LOG_FILE, the base directory check is skipped to allow logging to any writable location.
Parameters
inputOriginal user-provided path (must not be NULL)
roleIntended usage category
normalized_outOutput pointer receiving SAFE_MALLOC'd canonical path
Returns
ASCIICHAT_OK on success, error code on failure (and normalized_out is NULL)

Definition at line 1020 of file path.c.

1020 {
1021 if (!normalized_out) {
1022 return SET_ERRNO(map_role_to_error(role), "path_validate_user_path requires output pointer");
1023 }
1024 *normalized_out = NULL;
1025
1026 if (!input || *input == '\0') {
1027 return SET_ERRNO(map_role_to_error(role), "Path is empty for role %d", role);
1028 }
1029
1030 // SECURITY: For log files, if input is a simple filename (no separators or ..), constrain it to a safe directory
1031 if (role == PATH_ROLE_LOG_FILE) {
1032 // Check if input contains path separators or parent directory references
1033 bool is_simple_filename = true;
1034 for (const char *p = input; *p; p++) {
1035 if (*p == PATH_DELIM || *p == '/' || *p == '\\') {
1036 is_simple_filename = false;
1037 break;
1038 }
1039 }
1040 // Also reject ".." components (even without separators like "..something")
1041 if (strstr(input, "..") != NULL) {
1042 is_simple_filename = false;
1043 }
1044
1045 // If it's a simple filename, resolve it to a safe base directory
1046 if (is_simple_filename) {
1047 // Always prefer current working directory for simple log filenames
1048 // This ensures logs go to where the user is running the command from
1049 char safe_base[PLATFORM_MAX_PATH_LENGTH];
1050
1051 if (!platform_get_cwd(safe_base, sizeof(safe_base))) {
1052 // If cwd fails, try config dir as fallback
1053 char *config_dir = get_config_dir();
1054 if (config_dir) {
1055 SAFE_STRNCPY(safe_base, config_dir, sizeof(safe_base));
1056 SAFE_FREE(config_dir);
1057 } else {
1058 return SET_ERRNO(ERROR_LOGGING_INIT, "Failed to determine safe directory for log file");
1059 }
1060 }
1061
1062 // Build the full path: safe_base + separator + input
1063 size_t base_len = strlen(safe_base);
1064 size_t input_len = strlen(input);
1065 bool needs_sep = base_len > 0 && safe_base[base_len - 1] != PATH_DELIM;
1066 size_t total_len = base_len + (needs_sep ? 1 : 0) + input_len + 1;
1067
1068 if (total_len > PLATFORM_MAX_PATH_LENGTH) {
1069 return SET_ERRNO(ERROR_LOGGING_INIT, "Log file path too long: %s/%s", safe_base, input);
1070 }
1071
1072 char resolved_buf[PLATFORM_MAX_PATH_LENGTH];
1073 safe_snprintf(resolved_buf, sizeof(resolved_buf), "%s%s%s", safe_base, needs_sep ? PATH_SEPARATOR_STR : "",
1074 input);
1075
1076 // Normalize the resolved path
1077 char normalized_buf[PLATFORM_MAX_PATH_LENGTH];
1078 if (!path_normalize_copy(resolved_buf, normalized_buf, sizeof(normalized_buf))) {
1079 return SET_ERRNO(ERROR_LOGGING_INIT, "Failed to normalize log file path: %s", resolved_buf);
1080 }
1081
1082 // Allocate and return the result
1083 char *result = SAFE_MALLOC(strlen(normalized_buf) + 1, char *);
1084 if (!result) {
1085 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate normalized path");
1086 }
1087 safe_snprintf(result, strlen(normalized_buf) + 1, "%s", normalized_buf);
1088 *normalized_out = result;
1089 return ASCIICHAT_OK;
1090 }
1091 // If not a simple filename (contains separators), continue with normal validation below
1092 }
1093
1094 // For non-log-files or log files with path separators, validate as usual
1095 if (role != PATH_ROLE_LOG_FILE && !path_looks_like_path(input)) {
1096 return SET_ERRNO(map_role_to_error(role), "Value does not look like a filesystem path: %s", input);
1097 }
1098
1099 char *expanded = expand_path(input);
1100 if (!expanded) {
1101 return SET_ERRNO(map_role_to_error(role), "Failed to expand path: %s", input);
1102 }
1103
1104 char candidate_buf[PLATFORM_MAX_PATH_LENGTH];
1105 const char *candidate_path = expanded;
1106
1107 if (!path_is_absolute(candidate_path)) {
1108 char cwd_buf[PLATFORM_MAX_PATH_LENGTH];
1109 if (!platform_get_cwd(cwd_buf, sizeof(cwd_buf))) {
1110 SAFE_FREE(expanded);
1111 return SET_ERRNO(map_role_to_error(role), "Failed to determine current working directory");
1112 }
1113
1114 size_t total_len = strlen(cwd_buf) + 1 + strlen(candidate_path) + 1;
1115 if (total_len >= sizeof(candidate_buf)) {
1116 SAFE_FREE(expanded);
1117 return SET_ERRNO(map_role_to_error(role), "Resolved path is too long: %s/%s", cwd_buf, candidate_path);
1118 }
1119 if (strlen(candidate_path) > 0 && candidate_path[0] == PATH_DELIM) {
1120 safe_snprintf(candidate_buf, sizeof(candidate_buf), "%s%s", cwd_buf, candidate_path);
1121 } else {
1122 safe_snprintf(candidate_buf, sizeof(candidate_buf), "%s%c%s", cwd_buf, PATH_DELIM, candidate_path);
1123 }
1124 candidate_path = candidate_buf;
1125 }
1126
1127 char normalized_buf[PLATFORM_MAX_PATH_LENGTH];
1128 if (!path_normalize_copy(candidate_path, normalized_buf, sizeof(normalized_buf))) {
1129 SAFE_FREE(expanded);
1130 return SET_ERRNO(map_role_to_error(role), "Failed to normalize path: %s", candidate_path);
1131 }
1132
1133 if (!path_is_absolute(normalized_buf)) {
1134 SAFE_FREE(expanded);
1135 return SET_ERRNO(map_role_to_error(role), "Normalized path is not absolute: %s", normalized_buf);
1136 }
1137
1138 const char *bases[MAX_PATH_BASES] = {0};
1139 size_t base_count = 0;
1140
1141 // Always add current working directory as an allowed base
1142 // This is critical for log files and other paths relative to where the user runs the command
1143 char cwd_base[PLATFORM_MAX_PATH_LENGTH];
1144 if (platform_get_cwd(cwd_base, sizeof(cwd_base))) {
1145 append_base_if_valid(cwd_base, bases, &base_count);
1146 }
1147
1148 char temp_base[PLATFORM_MAX_PATH_LENGTH];
1149 if (platform_get_temp_dir(temp_base, sizeof(temp_base))) {
1150 append_base_if_valid(temp_base, bases, &base_count);
1151 }
1152
1153 char *config_dir = get_config_dir();
1154 if (config_dir) {
1155 append_base_if_valid(config_dir, bases, &base_count);
1156 }
1157
1158 const char *home_env = platform_get_home_dir();
1159 if (home_env) {
1160 append_base_if_valid(home_env, bases, &base_count);
1161 }
1162
1163 char ascii_chat_home[PLATFORM_MAX_PATH_LENGTH];
1164 if (home_env) {
1165 build_ascii_chat_path(home_env, ".ascii-chat", ascii_chat_home, sizeof(ascii_chat_home));
1166 append_base_if_valid(ascii_chat_home, bases, &base_count);
1167 }
1168
1169#ifndef _WIN32
1170 char ascii_chat_home_tmp[PLATFORM_MAX_PATH_LENGTH];
1171 build_ascii_chat_path("/tmp", ".ascii-chat", ascii_chat_home_tmp, sizeof(ascii_chat_home_tmp));
1172 append_base_if_valid(ascii_chat_home_tmp, bases, &base_count);
1173#endif
1174
1175 char ssh_home[PLATFORM_MAX_PATH_LENGTH];
1176 if (home_env) {
1177 build_ascii_chat_path(home_env, ".ssh", ssh_home, sizeof(ssh_home));
1178 append_base_if_valid(ssh_home, bases, &base_count);
1179 }
1180
1181#ifdef _WIN32
1182 char program_data_logs[PLATFORM_MAX_PATH_LENGTH];
1183 const char *program_data = platform_getenv("PROGRAMDATA");
1184 if (program_data) {
1185 build_ascii_chat_path(program_data, "ascii-chat", program_data_logs, sizeof(program_data_logs));
1186 append_base_if_valid(program_data_logs, bases, &base_count);
1187 }
1188#else
1189 // System-wide config directories (for server deployments)
1190 append_base_if_valid("/etc/ascii-chat", bases, &base_count);
1191 append_base_if_valid("/usr/local/etc/ascii-chat", bases, &base_count);
1192 append_base_if_valid("/var/log", bases, &base_count);
1193 append_base_if_valid("/var/tmp", bases, &base_count);
1194 append_base_if_valid("/tmp", bases, &base_count);
1195#ifdef __APPLE__
1196 // On macOS, /tmp is a symlink to /private/tmp
1197 append_base_if_valid("/private/tmp", bases, &base_count);
1198 // On macOS, all user home directories are under /Users
1199 append_base_if_valid("/Users", bases, &base_count);
1200#endif
1201#endif
1202
1203 // Security check: Reject paths that point to sensitive system files
1204 // This applies to all path roles, not just logs
1205 if (is_sensitive_system_path(normalized_buf)) {
1206 SAFE_FREE(expanded);
1207 if (config_dir) {
1208 SAFE_FREE(config_dir);
1209 }
1210 return SET_ERRNO(map_role_to_error(role), "Cannot write to protected system path: %s", normalized_buf);
1211 }
1212
1213 // For log files, apply special validation rules
1214 if (role == PATH_ROLE_LOG_FILE) {
1215 // Check if path is a regular file (not a directory, not non-existent)
1216 // On macOS, fopen() can succeed on directories, so we must use platform_is_regular_file()
1217 bool is_regular_file = platform_is_regular_file(normalized_buf);
1218
1219 // If a regular file exists, it MUST be an ascii-chat log or empty file to be overwritten
1220 if (is_regular_file && !is_existing_ascii_chat_log(normalized_buf) && !is_file_empty(normalized_buf)) {
1221 SAFE_FREE(expanded);
1222 if (config_dir) {
1223 SAFE_FREE(config_dir);
1224 }
1226 "Cannot overwrite existing non-ascii-chat file: %s\n"
1227 "For safety, ascii-chat will only overwrite its own log files or empty files",
1228 normalized_buf);
1229 }
1230
1231 // If file doesn't exist (or is a directory), check that path is in safe locations
1232 if (!is_regular_file) {
1233 bool allowed = base_count == 0 ? true : path_is_within_any_base(normalized_buf, bases, base_count);
1234 if (!allowed) {
1235 SAFE_FREE(expanded);
1236 if (config_dir) {
1237 SAFE_FREE(config_dir);
1238 }
1240 "Log path %s is outside allowed directories (use -L /tmp/file.log, ~/file.log, or "
1241 "relative/absolute paths in safe locations)",
1242 normalized_buf);
1243 }
1244 }
1245 } else {
1246 // For non-log-file paths, apply standard whitelist validation
1247 bool allowed = base_count == 0 ? true : path_is_within_any_base(normalized_buf, bases, base_count);
1248 if (!allowed) {
1249 SAFE_FREE(expanded);
1250 if (config_dir) {
1251 SAFE_FREE(config_dir);
1252 }
1253 return SET_ERRNO(map_role_to_error(role), "Path %s is outside allowed directories", normalized_buf);
1254 }
1255 }
1256
1257 char *result = SAFE_MALLOC(strlen(normalized_buf) + 1, char *);
1258 if (!result) {
1259 SAFE_FREE(expanded);
1260 if (config_dir) {
1261 SAFE_FREE(config_dir);
1262 }
1263 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate normalized path");
1264 }
1265 safe_snprintf(result, strlen(normalized_buf) + 1, "%s", normalized_buf);
1266 *normalized_out = result;
1267
1268 SAFE_FREE(expanded);
1269 if (config_dir) {
1270 SAFE_FREE(config_dir);
1271 }
1272 return ASCIICHAT_OK;
1273}
@ ERROR_LOGGING_INIT
Definition error_codes.h:59
@ ERROR_MEMORY
Definition error_codes.h:56
int platform_is_regular_file(const char *path)
Check if a path is a regular file.
Definition util.c:124
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
Definition path.c:809
#define MAX_PATH_BASES
Maximum number of path base directories.
Definition path.h:85
bool path_is_within_any_base(const char *path, const char *const *bases, size_t base_count)
Check whether a path resides within any of several base directories.
Definition path.c:791
#define true
Definition stdbool.h:62

References ASCIICHAT_OK, ERROR_LOGGING_INIT, ERROR_MEMORY, expand_path(), get_config_dir(), MAX_PATH_BASES, PATH_DELIM, path_is_absolute(), path_is_within_any_base(), path_looks_like_path(), path_normalize_copy(), PATH_ROLE_LOG_FILE, PATH_SEPARATOR_STR, platform_get_cwd(), platform_get_home_dir(), platform_get_temp_dir(), platform_getenv(), platform_is_regular_file(), PLATFORM_MAX_PATH_LENGTH, SAFE_FREE, SAFE_MALLOC, safe_snprintf(), SAFE_STRNCPY, SET_ERRNO, and true.

Referenced by config_create_default(), config_load_and_apply(), parse_keys_from_file(), parse_log_file(), parse_private_key(), parse_public_key(), and validate_ssh_key_file().

◆ prompt_password()

int prompt_password ( const char *  prompt,
char *  password,
size_t  max_len 
)

#include <password.h>

Prompt the user for a password with secure input.

Parameters
promptThe prompt message to display
passwordBuffer to store the entered password
max_lenMaximum length of the password buffer (including null terminator)
Returns
0 on success, -1 on failure or user cancellation

Displays a formatted password prompt with visual separators:

========================================
<prompt>
========================================
> ********
========================================

Input is masked with asterisks and echo is disabled for security.

Note
Returns -1 if stdin is not a TTY (non-interactive mode).
Acquires terminal lock during prompting.

Definition at line 72 of file lib/platform/wasm/stubs/crypto.c.

72 {
73 // Password prompts not supported in WASM
74 return SET_ERRNO(ERROR_NOT_SUPPORTED, "Password prompts not supported in WASM");
75}
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:74

References BUFFER_SIZE_SMALL, ERROR_NOT_SUPPORTED, log_lock_terminal(), log_plain, log_unlock_terminal(), log_warn, platform_is_interactive(), platform_prompt_question(), PROMPT_OPTS_PASSWORD, SET_ERRNO, utf8_display_width(), and utf8_is_valid().

Referenced by crypto_handshake_client_auth_response().

◆ prompt_password_simple()

int prompt_password_simple ( const char *  prompt,
char *  password,
size_t  max_len 
)

#include <password.h>

Prompt the user for a password with simple formatting.

Parameters
promptThe prompt message to display (shown inline)
passwordBuffer to store the entered password
max_lenMaximum length of the password buffer (including null terminator)
Returns
0 on success, -1 on failure or user cancellation

Displays a simple inline password prompt:

<prompt>: ********

Use this for simpler prompts like SSH key passphrases.

Note
Returns -1 if stdin is not a TTY (non-interactive mode).
Acquires terminal lock during prompting.

Definition at line 70 of file password.c.

70 {
71 if (!prompt || !password || max_len < 2) {
72 return -1;
73 }
74
75 // Check for non-interactive mode first
77 return -1;
78 }
79
80 // Build prompt with colon suffix, using byte-length for memcpy
81 char full_prompt[BUFFER_SIZE_SMALL];
82 size_t prompt_byte_len = strlen(prompt);
83 if (prompt_byte_len >= sizeof(full_prompt) - 2) {
84 prompt_byte_len = sizeof(full_prompt) - 3;
85 }
86 memcpy(full_prompt, prompt, prompt_byte_len);
87 full_prompt[prompt_byte_len] = ':';
88 full_prompt[prompt_byte_len + 1] = '\0';
89
90 // Prompt for password with asterisk masking, same line
92 int result = platform_prompt_question(full_prompt, password, max_len, opts);
93
94 // Validate password is valid UTF-8 (should always succeed since platform_prompt_question handles it)
95 if (result == 0 && !utf8_is_valid(password)) {
96 log_warn("Password contains invalid UTF-8 sequence, input may be corrupted");
97 }
98
99 return result;
100}
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
int platform_prompt_question(const char *prompt, char *buffer, size_t max_len, prompt_opts_t opts)
Prompt the user for text input.
#define PROMPT_OPTS_PASSWORD
Prompt options for password input (no echo, asterisk masking, same line)
Definition question.h:45
bool platform_is_interactive(void)
Check if interactive prompting is available.
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:156
Options for text prompts.
Definition question.h:31

References BUFFER_SIZE_SMALL, log_warn, platform_is_interactive(), platform_prompt_question(), PROMPT_OPTS_PASSWORD, and utf8_is_valid().

Referenced by parse_ssh_private_key(), and read_password_from_stdin().

◆ safe_parse_audio_message()

asciichat_error_t safe_parse_audio_message ( const char *  message,
unsigned int *  num_samples 
)

#include <parsing.h>

Parse AUDIO message format.

Parameters
messageInput message string (must not be NULL, format: "AUDIO:num_samples")
num_samplesOutput parameter for parsed sample count (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Parses an AUDIO protocol message in the format "AUDIO:num_samples" where num_samples is an unsigned integer. Validates the message format, parses the numeric value, and checks for overflow conditions.

MESSAGE FORMAT:

  • Must start with "AUDIO:"
  • Sample count must be a valid unsigned integer
  • No whitespace allowed in value
Note
num_samples is only modified on successful parsing.
Returns ASCIICHAT_ERROR_INVALID_ARGUMENT if format is invalid.
Returns ASCIICHAT_ERROR_OVERFLOW if numeric value exceeds limits.
Example
unsigned int num_samples;
if (safe_parse_audio_message("AUDIO:44100", &num_samples) == ASCIICHAT_OK) {
// num_samples = 44100
}
asciichat_error_t safe_parse_audio_message(const char *message, unsigned int *num_samples)
Parse AUDIO message format.

Definition at line 64 of file util/parsing.c.

64 {
65 if (!message || !num_samples) {
66 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for audio message parsing");
67 }
68
69 // Check if message starts with "AUDIO:"
70 if (strncmp(message, "AUDIO:", 6) != 0) {
71 return SET_ERRNO(ERROR_INVALID_PARAM, "Message does not start with 'AUDIO:'");
72 }
73
74 const char *ptr = message + 6; // Skip "AUDIO:"
75 char *endptr;
76
77 // Parse number
78 errno = 0;
79 unsigned long samples = strtoul(ptr, &endptr, 10);
80 if (errno != 0 || endptr == ptr || samples > UINT_MAX || samples == 0) {
81 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid sample count value in audio message");
82 }
83
84 // Should end with newline or null terminator
85 if (*endptr != '\n' && *endptr != '\0') {
86 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid format: audio message should end with newline or null terminator");
87 }
88
89 *num_samples = (unsigned int)samples;
90 return ASCIICHAT_OK;
91}
int errno

References ASCIICHAT_OK, errno, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ safe_parse_size_message()

asciichat_error_t safe_parse_size_message ( const char *  message,
unsigned int *  width,
unsigned int *  height 
)

#include <parsing.h>

Parse SIZE message format.

Parameters
messageInput message string (must not be NULL, format: "SIZE:width,height")
widthOutput parameter for parsed width (must not be NULL)
heightOutput parameter for parsed height (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Parses a SIZE protocol message in the format "SIZE:width,height" where width and height are unsigned integers. Validates the message format, parses numeric values, and checks for overflow conditions.

MESSAGE FORMAT:

  • Must start with "SIZE:"
  • Width and height separated by comma
  • Both values must be valid unsigned integers
  • No whitespace allowed in values
Note
Width and height are only modified on successful parsing.
Returns ASCIICHAT_ERROR_INVALID_ARGUMENT if format is invalid.
Returns ASCIICHAT_ERROR_OVERFLOW if numeric values exceed limits.
Example
unsigned int width, height;
if (safe_parse_size_message("SIZE:1920,1080", &width, &height) == ASCIICHAT_OK) {
// width = 1920, height = 1080
}
asciichat_error_t safe_parse_size_message(const char *message, unsigned int *width, unsigned int *height)
Parse SIZE message format.

Definition at line 16 of file util/parsing.c.

16 {
17 if (!message || !width || !height) {
18 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for size message parsing");
19 }
20
21 // Check if message starts with "SIZE:"
22 if (strncmp(message, "SIZE:", 5) != 0) {
23 return SET_ERRNO(ERROR_INVALID_PARAM, "Message does not start with 'SIZE:'");
24 }
25
26 const char *ptr = message + 5; // Skip "SIZE:"
27 char *endptr;
28
29 // Parse first number (width)
30 errno = 0;
31 unsigned long w = strtoul(ptr, &endptr, 10);
32 if (errno != 0 || endptr == ptr || w > UINT_MAX || w == 0) {
33 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid width value in size message");
34 }
35
36 // Check for comma separator
37 if (*endptr != ',') {
38 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing comma separator in size message");
39 }
40 ptr = endptr + 1;
41
42 // Parse second number (height)
43 errno = 0;
44 unsigned long h = strtoul(ptr, &endptr, 10);
45 if (errno != 0 || endptr == ptr || h > UINT_MAX || h == 0) {
46 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid height value in size message");
47 }
48
49 // Should end with newline or null terminator
50 if (*endptr != '\n' && *endptr != '\0') {
51 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid format: size message should end with newline or null terminator");
52 }
53
54 // Additional bounds checking
55 if (w > 65535 || h > 65535) {
56 return SET_ERRNO(ERROR_INVALID_PARAM, "Size values too large (max 65535)");
57 }
58
59 *width = (unsigned int)w;
60 *height = (unsigned int)h;
61 return ASCIICHAT_OK;
62}

References ASCIICHAT_OK, errno, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ string_needs_shell_quoting()

bool string_needs_shell_quoting ( const char *  str)

#include <string.h>

Check if a string needs shell quoting (contains spaces or special chars)

Parameters
strString to check (must not be NULL)
Returns
true if string contains characters that require shell quoting, false otherwise

Detects if a string contains whitespace or special characters that require shell quoting when used in command-line arguments. Useful for determining whether to apply shell escaping functions.

CHARACTERS REQUIRING QUOTING:

  • Whitespace: space, tab
  • Special shell characters: " ' $ ` \ < > & ; | ( ) [ ] { } * ? ! ~ # @ - Control characters: newline, carriage return, etc. @note Empty strings return false (no quoting needed). @note Alphanumeric and some safe punctuation (- . _) return false. @par Example @code if (string_needs_shell_quoting("/path/to file.txt")) { // Path contains a space, needs quoting }

Definition at line 219 of file util/string.c.

219 {
220 if (!str || str[0] == '\0') {
221 return false; // Empty strings don't need quoting
222 }
223
224 // Check for characters that need shell quoting
225 for (size_t i = 0; str[i] != '\0'; i++) {
226 char c = str[i];
227
228 // Whitespace characters always need quoting
229 if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
230 return true;
231 }
232
233 // Shell special characters
234 if (c == '"' || c == '\'' || c == '$' || c == '`' || c == '\\' || c == '<' || c == '>' || c == '&' || c == ';' ||
235 c == '|' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '*' || c == '?' ||
236 c == '!' || c == '~' || c == '#' || c == '@') {
237 return true;
238 }
239 }
240
241 return false;
242}

Referenced by escape_path_for_shell().

◆ strip_ansi_codes()

void strip_ansi_codes ( const char *  input,
char *  output,
size_t  output_size 
)

#include <string.h>

Strip ANSI escape codes from a string.

Removes all ANSI escape sequences from the input string, suitable for displaying colored/styled text in environments that don't support ANSI codes (e.g., browser console, plain text output).

Handles patterns like:

  • \033[...m (color and style codes)
  • \033[0m (reset code)
  • \033[38;5;Nm (256-color codes)
  • \033[38;2;R;G;Bm (truecolor codes)
  • \033[2J, \033[H, etc. (cursor and screen control)
Parameters
inputInput string with ANSI codes (must not be NULL)
outputOutput buffer for cleaned string (must not be NULL)
output_sizeSize of output buffer (must be > 0)

EXAMPLE:

char input[] = "\\033[31mError\\033[0m"; // Red "Error" text
char output[256];
strip_ansi_codes(input, output, sizeof(output));
// output now contains "Error" without ANSI codes
void strip_ansi_codes(const char *input, char *output, size_t output_size)
Strip ANSI escape codes from a string.
Note
Output buffer must be large enough for input (worst case: same size).
Function truncates output if buffer is too small.
Safe for use with formatted log messages.

Definition at line 407 of file util/string.c.

407 {
408 if (!input || !output || output_size == 0) {
409 return;
410 }
411
412 size_t out_idx = 0;
413 size_t in_idx = 0;
414
415 while (input[in_idx] != '\0' && out_idx < output_size - 1) {
416 // Check for ANSI escape sequence: ESC[
417 // Standard ANSI codes use escape character (0x1B or \033) followed by [
418 if (input[in_idx] == '\033' && input[in_idx + 1] == '[') {
419 in_idx += 2; // Skip \033[
420
421 // Skip until we find the terminating letter (m is most common, also H, J, K, etc.)
422 // This handles patterns like: 38;5;74m, 0m, 38;2;255;0;0m, 2J, H, etc.
423 while (input[in_idx] != '\0') {
424 if ((input[in_idx] >= 'A' && input[in_idx] <= 'Z') || (input[in_idx] >= 'a' && input[in_idx] <= 'z')) {
425 in_idx++; // Skip the terminating letter
426 break;
427 }
428 in_idx++;
429 }
430 } else {
431 // Regular character (including [ in normal text), copy to output
432 output[out_idx++] = input[in_idx++];
433 }
434 }
435
436 output[out_idx] = '\0';
437}

Referenced by platform_log_hook().

◆ truncate_with_ellipsis()

void truncate_with_ellipsis ( const char *  input,
char *  output,
size_t  output_size,
int  max_width 
)

#include <string.h>

Truncate a string to a maximum display width, with ellipsis.

Handles ANSI escape sequences (never cuts inside one), multi-byte UTF-8, and wide characters (CJK = 2 columns). If truncation is needed, appends a color reset (\033[0m) followed by "…" (Unicode ellipsis, 1 column).

Safe for strings with or without ANSI codes. For plain text the reset is harmless (terminals ignore redundant resets).

Parameters
inputInput string (may contain ANSI escape sequences and UTF-8)
outputOutput buffer for truncated string
output_sizeSize of output buffer (must be >= 10 for reset + ellipsis + NUL)
max_widthMaximum display width in columns

Definition at line 322 of file util/string.c.

322 {
323 if (!input || !output || output_size < 4) {
324 if (output && output_size > 0) {
325 output[0] = '\0';
326 }
327 return;
328 }
329
330 int content_width = display_width(input);
331
332 // Content fits — copy as-is
333 if (content_width <= max_width) {
334 size_t len = strlen(input);
335 if (len >= output_size) {
336 len = output_size - 1;
337 }
338 memcpy(output, input, len);
339 output[len] = '\0';
340 return;
341 }
342
343 // Need to truncate. Walk forward copying bytes while counting visible
344 // columns. ANSI escape sequences are copied whole (zero display width).
345 // UTF-8 multi-byte characters are decoded to get their display width
346 // (e.g., CJK = 2 columns, emoji = 2 columns) and copied as whole
347 // codepoints so we never split a multi-byte sequence.
348 const char *src = input;
349 const char *src_end = input + strlen(input);
350 char *dst = output;
351 char *dst_end = output + output_size - 10; // Reserve for reset + ellipsis + NUL
352 int cols = 0;
353 int target = max_width - 1; // Reserve 1 column for "…"
354
355 if (target < 0) {
356 target = 0;
357 }
358
359 while (src < src_end && dst < dst_end) {
360 if (*src == '\x1b') {
361 // Copy the entire escape sequence (zero display width)
362 const char *esc_end = ansi_skip_escape(src, src_end);
363 size_t esc_len = (size_t)(esc_end - src);
364 if (dst + esc_len < dst_end) {
365 memcpy(dst, src, esc_len);
366 dst += esc_len;
367 }
368 src = esc_end;
369 } else {
370 // Decode one UTF-8 codepoint to get its byte length and display width
371 utf8proc_int32_t codepoint;
372 utf8proc_ssize_t cp_len = utf8proc_iterate((const utf8proc_uint8_t *)src, src_end - src, &codepoint);
373 if (cp_len <= 0) {
374 // Invalid UTF-8 — copy single byte, count as 1 column
375 cp_len = 1;
376 codepoint = (unsigned char)*src;
377 }
378
379 int char_width = utf8proc_charwidth(codepoint);
380 if (char_width < 0) {
381 char_width = 0; // Control characters
382 }
383
384 if (cols + char_width > target) {
385 break;
386 }
387
388 // Copy the full codepoint bytes
389 if (dst + cp_len < dst_end) {
390 memcpy(dst, src, (size_t)cp_len);
391 dst += cp_len;
392 }
393 src += cp_len;
394 cols += char_width;
395 }
396 }
397
398 // Reset colors and append ellipsis (U+2026, 3 bytes)
399 size_t remaining = output_size - (size_t)(dst - output);
400 int n = snprintf(dst, remaining, "\033[0m…");
401 if (n > 0) {
402 dst += n;
403 }
404 *dst = '\0';
405}
const char * ansi_skip_escape(const char *ptr, const char *end)
Find the end of an ANSI CSI escape sequence.
Definition ansi.c:74

References ansi_skip_escape(), and display_width().

◆ url_is_tcp()

bool url_is_tcp ( const char *  url)

#include <url.h>

Check if a URL string is a valid TCP URL.

Parameters
urlURL string to validate
Returns
True if URL is valid and has TCP scheme, false otherwise

Parses a URL and checks if it has a TCP scheme. This is the primary function for TCP URL validation - use this instead of manually calling url_parse() and url_is_tcp_scheme().

Example
if (url_is_tcp("tcp://example.com:5000")) {
// Safe to use as TCP URL
connection_attempt_tcp(&ctx, "tcp://example.com:5000");
} else if (url_is_valid(address)) {
// Use regular HTTP(S) transport
}
asciichat_error_t connection_attempt_tcp(connection_attempt_context_t *ctx, const char *server_address, uint16_t server_port)
Attempt direct TCP connection.
bool url_is_valid(const char *url)
Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)
Definition url.c:81
bool url_is_tcp(const char *url)
Check if a URL string is a valid TCP URL.
Definition url.c:342

Definition at line 342 of file url.c.

342 {
343 if (!url || !*url) {
344 SET_ERRNO(ERROR_INVALID_PARAM, "url is NULL or empty");
345 return false;
346 }
347
348 /* Parse URL to validate and check scheme */
349 url_parts_t parts = {0};
350 asciichat_error_t result = url_parse(url, &parts);
351 bool is_tcp = (result == ASCIICHAT_OK && url_is_tcp_scheme(parts.scheme));
352 url_parts_destroy(&parts);
353
354 return is_tcp;
355}
void url_parts_destroy(url_parts_t *parts)
Free parsed URL components.
Definition url.c:281
bool url_is_tcp_scheme(const char *scheme)
Check if URL scheme is TCP.
Definition url.c:332
asciichat_error_t url_parse(const char *url, url_parts_t *parts_out)
Parse HTTP(S), WebSocket, and TCP URL into components.
Definition url.c:166
Structure containing parsed URL components.
Definition url.h:57
char * scheme
Extracted scheme: "http" or "https" (allocated)
Definition url.h:58

References ASCIICHAT_OK, ERROR_INVALID_PARAM, url_parts_t::scheme, SET_ERRNO, url_is_tcp_scheme(), url_parse(), and url_parts_destroy().

Referenced by connection_endpoint_resolve().

◆ url_is_tcp_scheme()

bool url_is_tcp_scheme ( const char *  scheme)

#include <url.h>

Check if URL scheme is TCP.

Parameters
schemeScheme string to check (case-insensitive)
Returns
True if scheme is "tcp", false otherwise

Utility function to determine if a parsed URL is a TCP URL.

Example
url_parts_t parts = {0};
if (url_parse("tcp://example.com:5000", &parts) == ASCIICHAT_OK) {
if (url_is_tcp_scheme(parts.scheme)) {
// Handle TCP connection
}
}

Definition at line 332 of file url.c.

332 {
333 if (!scheme || !*scheme) {
334 SET_ERRNO(ERROR_INVALID_PARAM, "scheme is NULL or empty");
335 return false;
336 }
337
338 /* Case-insensitive comparison for "tcp" */
339 return (strcasecmp(scheme, "tcp") == 0);
340}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by url_is_tcp().

◆ url_is_valid()

bool url_is_valid ( const char *  url)

#include <url.h>

Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)

Parameters
urlInput URL string to validate
Returns
True if URL is valid, false otherwise

Quickly validates a URL without extracting components. Uses PCRE2 with JIT compilation for 10-100x performance over manual parsing.

URL ACCEPTANCE CRITERIA:

  • Scheme: http, https, ws, wss, or tcp (case-insensitive)
  • Host: Public IPv4, IPv6, localhost, FQDN, or bare hostname
  • Rejects: Private IPs (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x)
  • Port: Optional, must be 1-65535 if present
  • Path: Optional, may include query and fragment
Example
if (url_is_valid("https://github.com/octocat")) {
// Safe to use URL
}

Definition at line 81 of file url.c.

81 {
82 if (!url || !*url) {
83 SET_ERRNO(ERROR_INVALID_PARAM, "URL is NULL or empty");
84 return false;
85 }
86
87 pcre2_code *regex = url_regex_get();
88 if (!regex) {
89 return false;
90 }
91
92 /* Check if URL needs http:// prefix (bare hostname or IP) */
93 char url_with_scheme[2048];
94 const char *url_to_match = url;
95
96 if (!strstr(url, "://")) {
97 /* No scheme - check if it looks like a bare hostname/IP */
98
99 /* Reject bare scheme words like "http", "https", "ftp" */
100 if (strcmp(url, "http") == 0 || strcmp(url, "https") == 0 || strcmp(url, "ftp") == 0 || strcmp(url, "ftps") == 0) {
101 return false;
102 }
103
104 /* Reject URLs that look like malformed schemes (http/ instead of http://) */
105 if (strncmp(url, "http/", 5) == 0 || strncmp(url, "https/", 6) == 0) {
106 return false;
107 }
108
109 /* Reject if it contains @ (email-like) */
110 if (strchr(url, '@')) {
111 return false;
112 }
113
114 /* Reject pure hex strings (raw keys, not hostnames) */
115 if (strlen(url) == 64) {
116 bool all_hex = true;
117 for (const char *p = url; *p && all_hex; p++) {
118 if (!((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) {
119 all_hex = false;
120 }
121 }
122 if (all_hex) {
123 return false; /* Looks like raw hex key, not hostname */
124 }
125 }
126
127 /* Check colon handling */
128 const char *colon_pos = strchr(url, ':');
129 if (colon_pos) {
130 /* Has colon - reject unless colon is followed by port number */
131 const char *after_colon = colon_pos + 1;
132 bool looks_like_port = true;
133 for (const char *p = after_colon; *p && *p != '/'; p++) {
134 if (!(*p >= '0' && *p <= '9')) {
135 looks_like_port = false;
136 break;
137 }
138 }
139 if (!looks_like_port) {
140 return false; /* Colon but not a port number */
141 }
142 }
143
144 /* Looks like a bare hostname/IP - prepend http:// */
145 int result = snprintf(url_with_scheme, sizeof(url_with_scheme), "http://%s", url);
146 if (result < 0 || result >= (int)sizeof(url_with_scheme)) {
147 return false; /* URL too long */
148 }
149 url_to_match = url_with_scheme;
150 }
151
152 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
153 if (!match_data) {
154 return false;
155 }
156
157 /* Perform JIT match (falls back to interpreted if JIT unavailable) */
158 int rc = asciichat_pcre2_match(regex, (PCRE2_SPTR)url_to_match, strlen(url_to_match), 0, /* startoffset */
159 0, /* options */
160 match_data, NULL); /* mcontext */
161
162 pcre2_match_data_free(match_data);
163 return rc >= 0; /* rc >= 0 means successful match */
164}
int asciichat_pcre2_match(const pcre2_code *code, PCRE2_SPTR subject, PCRE2_SIZE length, PCRE2_SIZE startoffset, uint32_t options, pcre2_match_data *match_data, pcre2_match_context *context)
Definition pcre2.c:399

References asciichat_pcre2_match(), ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by ffmpeg_decoder_create(), options_init(), parse_public_key(), and yt_dlp_extract_stream_url().

◆ url_is_websocket()

bool url_is_websocket ( const char *  url)

#include <url.h>

Check if a URL string is a valid WebSocket URL.

Parameters
urlURL string to validate
Returns
True if URL is valid and has WebSocket scheme (ws or wss), false otherwise

Parses a URL and checks if it has a WebSocket scheme. This is the primary function for WebSocket URL validation - use this instead of manually calling url_parse() and url_is_websocket_scheme().

Example
if (url_is_websocket("wss://example.com:443")) {
// Safe to use as WebSocket URL
connection_attempt_websocket(&ctx, "wss://example.com:443");
} else if (url_is_valid(address)) {
// Use regular HTTP(S) transport
}
asciichat_error_t connection_attempt_websocket(connection_attempt_context_t *ctx, const char *ws_url)
Attempt WebSocket connection (ws:// or wss://)
bool url_is_websocket(const char *url)
Check if a URL string is a valid WebSocket URL.
Definition url.c:307

Definition at line 307 of file url.c.

307 {
308 if (!url || !*url) {
309 SET_ERRNO(ERROR_INVALID_PARAM, "url is NULL or empty");
310 return false;
311 }
312
313 /* Parse URL to validate and check scheme */
314 url_parts_t parts = {0};
315 asciichat_error_t result = url_parse(url, &parts);
316 bool is_ws = (result == ASCIICHAT_OK && url_is_websocket_scheme(parts.scheme));
317 url_parts_destroy(&parts);
318
319 return is_ws;
320}
bool url_is_websocket_scheme(const char *scheme)
Check if URL scheme is WebSocket (ws or wss)
Definition url.c:297

References ASCIICHAT_OK, ERROR_INVALID_PARAM, url_parts_t::scheme, SET_ERRNO, url_is_websocket_scheme(), url_parse(), and url_parts_destroy().

Referenced by client_main(), and connection_endpoint_resolve().

◆ url_is_websocket_scheme()

bool url_is_websocket_scheme ( const char *  scheme)

#include <url.h>

Check if URL scheme is WebSocket (ws or wss)

Parameters
schemeScheme string to check (case-insensitive)
Returns
True if scheme is "ws" or "wss", false otherwise

Utility function to determine if a parsed URL is a WebSocket URL.

Example
url_parts_t parts = {0};
if (url_parse("wss://example.com:443", &parts) == ASCIICHAT_OK) {
// Handle WebSocket connection
}
}

Definition at line 297 of file url.c.

297 {
298 if (!scheme || !*scheme) {
299 SET_ERRNO(ERROR_INVALID_PARAM, "scheme is NULL or empty");
300 return false;
301 }
302
303 /* Case-insensitive comparison for "ws" or "wss" */
304 return (strcasecmp(scheme, "ws") == 0 || strcasecmp(scheme, "wss") == 0);
305}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by url_is_websocket().

◆ url_looks_like_tcp()

bool url_looks_like_tcp ( const char *  url)

#include <url.h>

Quick check if a string is a TCP URL.

Parameters
urlString to check
Returns
True if string starts with tcp://, false otherwise

Fast check without full URL parsing. Useful for determining transport type.

Example
if (url_looks_like_tcp("tcp://example.com:5000")) {
// Use TCP transport
} else if (url_is_valid(address)) {
// Use regular HTTP(S) transport
}
bool url_looks_like_tcp(const char *url)
Quick check if a string is a TCP URL.
Definition url.c:357

Definition at line 357 of file url.c.

357 {
358 if (!url || !*url) {
359 SET_ERRNO(ERROR_INVALID_PARAM, "url is NULL or empty");
360 return false;
361 }
362
363 /* Quick check for tcp:// prefix (case-insensitive) */
364 return (strncasecmp(url, "tcp://", 6) == 0);
365}

References ERROR_INVALID_PARAM, and SET_ERRNO.

◆ url_looks_like_websocket()

bool url_looks_like_websocket ( const char *  url)

#include <url.h>

Quick check if a string is a WebSocket URL.

Parameters
urlString to check
Returns
True if string starts with ws:// or wss://, false otherwise

Fast check without full URL parsing. Useful for determining transport type.

Example
if (url_looks_like_websocket("wss://example.com:443")) {
// Use WebSocket transport
} else if (url_is_valid(address)) {
// Use regular HTTP(S) transport
}
bool url_looks_like_websocket(const char *url)
Quick check if a string is a WebSocket URL.
Definition url.c:322

Definition at line 322 of file url.c.

322 {
323 if (!url || !*url) {
324 SET_ERRNO(ERROR_INVALID_PARAM, "url is NULL or empty");
325 return false;
326 }
327
328 /* Quick check for ws:// or wss:// prefix (case-insensitive) */
329 return (strncasecmp(url, "ws://", 5) == 0 || strncasecmp(url, "wss://", 6) == 0);
330}

References ERROR_INVALID_PARAM, and SET_ERRNO.

◆ url_parse()

asciichat_error_t url_parse ( const char *  url,
url_parts_t *  parts_out 
)

#include <url.h>

Parse HTTP(S), WebSocket, and TCP URL into components.

Parameters
urlInput URL string to parse
parts_outOutput structure for parsed components (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Validates and parses an HTTP(S), WebSocket, or TCP URL into its component parts using production-grade PCRE2 regex with JIT compilation.

URL ACCEPTANCE CRITERIA:

  • Scheme: http, https, ws, wss, or tcp (case-insensitive)
  • Host: Public IPv4, IPv6, localhost, FQDN, or bare hostname
  • Rejects: Private IPs (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x)
  • Port: Optional, 1-65535
  • Path/Query/Fragment: Optional

EXTRACTED COMPONENTS:

  • scheme: "http", "https", "ws", "wss", or "tcp" (always present)
  • host: Hostname, IPv4, or IPv6 (always present)
  • port: Port number (0 if not specified)
  • userinfo: user:pass@ prefix (NULL if not present)
  • ipv6: IPv6 address (NULL unless host is bracketed IPv6)
  • path: Path, query, and fragment (NULL if not present)

MEMORY MANAGEMENT:

Note
Returns ERROR_INVALID_PARAM if URL format is invalid
Returns ERROR_MEMORY if allocation fails
Example
url_parts_t parts = {0};
if (url_parse("https://user:pass@example.com:8443/api/v1?key=val#section", &parts) == ASCIICHAT_OK) {
printf("Host: %s, Port: %d, Path: %s\\n", parts.host, parts.port, parts.path);
}
char * path
Extracted path, query, and fragment (allocated if present, may be NULL)
Definition url.h:63
char * host
Extracted hostname or IP address (allocated)
Definition url.h:60
int port
Extracted port number (0 if not specified)
Definition url.h:62

Definition at line 166 of file url.c.

166 {
167 if (!url || !*url) {
168 return SET_ERRNO(ERROR_INVALID_PARAM, "URL is NULL or empty");
169 }
170
171 if (!parts_out) {
172 return SET_ERRNO(ERROR_INVALID_PARAM, "parts_out is NULL");
173 }
174
175 /* Clear output structure */
176 memset(parts_out, 0, sizeof(*parts_out));
177
178 pcre2_code *regex = url_regex_get();
179 if (!regex) {
180 return SET_ERRNO(ERROR_CONFIG, "URL validator not initialized");
181 }
182
183 /* Check if URL needs http:// prefix (bare hostname or IP) */
184 // allocate twice the "safe limit" of 2048 for website URLs, even though modern browsers can handle up
185 // to 80k character URLs in some cases.
186 char url_with_scheme[4096];
187 const char *url_to_match = url;
188 const char *original_url = url;
189
190 if (!strstr(url, "://")) {
191 /* No scheme - check if it looks like a bare hostname/IP */
192
193 /* Reject bare scheme words like "http", "https", "ftp" */
194 if (strcmp(url, "http") == 0 || strcmp(url, "https") == 0 || strcmp(url, "ftp") == 0 || strcmp(url, "ftps") == 0) {
195 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid URL format: %s", url);
196 }
197
198 /* Reject URLs that look like malformed schemes (http/ instead of http://) */
199 if (strncmp(url, "http/", 5) == 0 || strncmp(url, "https/", 6) == 0) {
200 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid URL format (looks like malformed scheme): %s", url);
201 }
202
203 /* Reject pure hex strings (raw keys, not hostnames) */
204 if (strlen(url) == 64) {
205 bool all_hex = true;
206 for (const char *p = url; *p && all_hex; p++) {
207 if (!((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) {
208 all_hex = false;
209 }
210 }
211 if (all_hex) {
212 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid URL: appears to be raw hex data, not a URL");
213 }
214 }
215
216 const char *colon_pos = strchr(url, ':');
217 if (colon_pos) {
218 /* Has colon - check if what follows is numeric (port) */
219 const char *after_colon = colon_pos + 1;
220 bool looks_like_port = true;
221 for (const char *p = after_colon; *p && *p != '/'; p++) {
222 if (!(*p >= '0' && *p <= '9')) {
223 looks_like_port = false;
224 break;
225 }
226 }
227 if (!looks_like_port) {
228 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid URL format (invalid scheme): %s", url);
229 }
230 }
231
232 /* Looks like a bare hostname/IP - prepend http:// */
233 int result = snprintf(url_with_scheme, sizeof(url_with_scheme), "http://%s", url);
234 if (result < 0 || result >= (int)sizeof(url_with_scheme)) {
235 return SET_ERRNO(ERROR_INVALID_PARAM, "URL too long");
236 }
237 url_to_match = url_with_scheme;
238 }
239
240 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
241 if (!match_data) {
242 return SET_ERRNO(ERROR_MEMORY, "Failed to create match data");
243 }
244
245 /* Perform JIT match (falls back to interpreted if JIT unavailable) */
246 int rc = asciichat_pcre2_match(regex, (PCRE2_SPTR)url_to_match, strlen(url_to_match), 0, /* startoffset */
247 0, /* options */
248 match_data, NULL); /* mcontext */
249
250 if (rc < 0) {
251 pcre2_match_data_free(match_data);
252 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid URL format: %s", original_url);
253 }
254
255 /* Extract named groups */
256 parts_out->scheme = asciichat_pcre2_extract_named_group(regex, match_data, "scheme", url_to_match);
257 parts_out->userinfo = asciichat_pcre2_extract_named_group(regex, match_data, "userinfo", url_to_match);
258 parts_out->host = asciichat_pcre2_extract_named_group(regex, match_data, "host", url_to_match);
259 parts_out->ipv6 = asciichat_pcre2_extract_named_group(regex, match_data, "ipv6", url_to_match);
260 parts_out->path = asciichat_pcre2_extract_named_group(regex, match_data, "path_query_fragment", url_to_match);
261
262 /* Extract port number */
263 parts_out->port = 0;
264 char *port_str = asciichat_pcre2_extract_named_group(regex, match_data, "port", url_to_match);
265 if (port_str) {
266 parts_out->port = (int)strtol(port_str, NULL, 10);
267 SAFE_FREE(port_str);
268 }
269
270 pcre2_match_data_free(match_data);
271
272 /* Verify we got required fields */
273 if (!parts_out->scheme || !parts_out->host) {
274 url_parts_destroy(parts_out);
275 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing required URL components");
276 }
277
278 return ASCIICHAT_OK;
279}
@ ERROR_CONFIG
Definition error_codes.h:57
char * asciichat_pcre2_extract_named_group(pcre2_code *regex, pcre2_match_data *match_data, const char *group_name, const char *subject)
Extract named substring from PCRE2 match data.
Definition pcre2.c:269
char * userinfo
Optional userinfo (user:pass) - may be NULL (allocated if present)
Definition url.h:59
char * ipv6
IPv6 address if host is bracketed IPv6 - may be NULL (allocated if present)
Definition url.h:61

References ASCIICHAT_OK, asciichat_pcre2_extract_named_group(), asciichat_pcre2_match(), ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_MEMORY, url_parts_t::host, url_parts_t::ipv6, url_parts_t::path, url_parts_t::port, SAFE_FREE, url_parts_t::scheme, SET_ERRNO, url_parts_destroy(), and url_parts_t::userinfo.

Referenced by connection_endpoint_resolve(), parse_public_key(), url_is_tcp(), and url_is_websocket().

◆ url_parts_destroy()

void url_parts_destroy ( url_parts_t *  parts)

#include <url.h>

Free parsed URL components.

Parameters
partsPointer to url_parts_t structure to free

Safely frees all allocated string fields in a url_parts_t structure. Safe to call multiple times on same structure.

Example
url_parts_t parts = {0};
url_parse("https://example.com", &parts);
// Use parts...
// All internal strings are freed, parts is zeroed

Definition at line 281 of file url.c.

281 {
282 if (!parts) {
283 SET_ERRNO(ERROR_INVALID_PARAM, "parts is NULL");
284 return;
285 }
286
287 SAFE_FREE(parts->scheme);
288 SAFE_FREE(parts->userinfo);
289 SAFE_FREE(parts->host);
290 SAFE_FREE(parts->ipv6);
291 SAFE_FREE(parts->path);
292 parts->port = 0;
293
294 memset(parts, 0, sizeof(*parts));
295}

References ERROR_INVALID_PARAM, url_parts_t::host, url_parts_t::ipv6, url_parts_t::path, url_parts_t::port, SAFE_FREE, url_parts_t::scheme, SET_ERRNO, and url_parts_t::userinfo.

Referenced by connection_endpoint_resolve(), parse_public_key(), url_is_tcp(), url_is_websocket(), and url_parse().

◆ utf8_char_count()

size_t utf8_char_count ( const char *  str)

#include <utf8.h>

Count UTF-8 characters (not bytes)

Parameters
strString to count (must not be NULL)
Returns
Number of UTF-8 characters, or SIZE_MAX if invalid UTF-8 detected

Counts the number of Unicode codepoints in the string, not byte count. Properly handles multi-byte UTF-8 sequences (1-4 bytes per character).

Returns SIZE_MAX if the string contains invalid UTF-8 sequences.

Note
Empty string has 0 characters
Returns SIZE_MAX for NULL input or invalid UTF-8
Slower than strlen() but necessary for correct UTF-8 processing
Used by Levenshtein distance and other algorithms needing character counts
Example
// Check if password is long enough (in characters, not bytes)
size_t pwd_len = utf8_char_count(password);
if (pwd_len == SIZE_MAX) {
log_error("Password contains invalid UTF-8");
return false;
}
if (pwd_len < 8) {
return false; // Too short
}
size_t utf8_char_count(const char *str)
Count UTF-8 characters (not bytes)
Definition utf8.c:136

Definition at line 136 of file utf8.c.

136 {
137 if (!str) {
138 SET_ERRNO(ERROR_INVALID_PARAM, "str is NULL");
139 return -1; // SIZE_MAX
140 }
141
142 size_t count = 0;
143 const uint8_t *p = (const uint8_t *)str;
144 while (*p) {
145 uint32_t codepoint;
146 int decode_len = utf8_decode(p, &codepoint);
147 if (decode_len < 0) {
148 return -1; // SIZE_MAX - Invalid UTF-8
149 }
150 count++;
151 p += decode_len;
152 }
153 return count;
154}

References ERROR_INVALID_PARAM, SET_ERRNO, and utf8_decode().

Referenced by levenshtein(), and utf8_is_valid().

◆ utf8_continuation_bytes_needed()

int utf8_continuation_bytes_needed ( unsigned char  first_byte)

#include <utf8.h>

Determine how many additional bytes are needed to complete a UTF-8 character.

Parameters
first_byteThe first byte of a potential UTF-8 sequence
Returns
Number of continuation bytes needed (0-3), or -1 if invalid start byte

Given the first byte of a UTF-8 sequence, determines how many additional bytes are needed to complete the character. Returns 0 for ASCII (1-byte chars), 1 for 2-byte sequences, 2 for 3-byte sequences, 3 for 4-byte sequences.

Useful in interactive input loops where bytes arrive one at a time and you need to know when a complete UTF-8 character has been received.

Example
// Read multi-byte UTF-8 character interactively
unsigned char first_byte = getchar();
int continuation_bytes = utf8_continuation_bytes_needed(first_byte);
if (continuation_bytes < 0) {
// Invalid start byte
continue;
}
// Read continuation_bytes more bytes
for (int i = 0; i < continuation_bytes; i++) {
unsigned char next_byte = getchar();
// Validate it's a continuation byte (10xxxxxx)
if ((next_byte & 0xC0) != 0x80) {
// Invalid continuation byte
break;
}
}
int utf8_continuation_bytes_needed(unsigned char first_byte)
Determine how many additional bytes are needed to complete a UTF-8 character.
Definition utf8.c:222

Definition at line 222 of file utf8.c.

222 {
223 if ((first_byte & 0x80) == 0) {
224 return 0; // ASCII (0xxxxxxx) - single byte, no continuation needed
225 }
226 if ((first_byte & 0xE0) == 0xC0) {
227 return 1; // 110xxxxx - 2 byte sequence, 1 continuation byte needed
228 }
229 if ((first_byte & 0xF0) == 0xE0) {
230 return 2; // 1110xxxx - 3 byte sequence, 2 continuation bytes needed
231 }
232 if ((first_byte & 0xF8) == 0xF0) {
233 return 3; // 11110xxx - 4 byte sequence, 3 continuation bytes needed
234 }
235 return -1; // Invalid UTF-8 start byte
236}

◆ utf8_decode()

int utf8_decode ( const uint8_t *  s,
uint32_t *  codepoint 
)

#include <utf8.h>

Decode a UTF-8 sequence to a Unicode codepoint.

Parameters
sPointer to UTF-8 byte sequence (must not be NULL)
codepointOutput parameter for decoded codepoint (must not be NULL)
Returns
Number of bytes consumed (1-4), or -1 if invalid sequence

Decodes a UTF-8 byte sequence starting at s to a Unicode codepoint. Validates the UTF-8 encoding during decoding and returns the number of bytes consumed. Returns -1 if the sequence is invalid (overlong encoding, invalid bytes, etc.).

Note
Input pointer must point to the start of a valid UTF-8 sequence.
Output codepoint is set even if sequence is invalid (check return value).
Function does not advance the pointer - caller must advance by return value.
Return value of -1 indicates invalid UTF-8 sequence.
Example
const uint8_t *str = (uint8_t*)"Hello 世界";
int len;
// Decode 'H' (1 byte)
len = utf8_decode(str, &cp); // Returns 1, cp = 'H'
str += len;
// Decode '世' (3 bytes)
len = utf8_decode(str, &cp); // Returns 3, cp = U+4E16
str += len;

Definition at line 18 of file utf8.c.

18 {
19 if (s[0] < 0x80) {
20 *codepoint = s[0];
21 return 1;
22 }
23 if ((s[0] & 0xE0) == 0xC0) {
24 // Validate continuation byte
25 if ((s[1] & 0xC0) != 0x80)
26 return -1;
27 *codepoint = (((uint32_t)(s[0] & 0x1F) << 6) | (uint32_t)(s[1] & 0x3F));
28 return 2;
29 } else if ((s[0] & 0xF0) == 0xE0) {
30 // Validate continuation bytes
31 if ((s[1] & 0xC0) != 0x80 || (s[2] & 0xC0) != 0x80)
32 return -1;
33 *codepoint = (((uint32_t)(s[0] & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) | (uint32_t)(s[2] & 0x3F));
34 return 3;
35 } else if ((s[0] & 0xF8) == 0xF0) {
36 // Validate continuation bytes
37 if ((s[1] & 0xC0) != 0x80 || (s[2] & 0xC0) != 0x80 || (s[3] & 0xC0) != 0x80)
38 return -1;
39 *codepoint = (((uint32_t)(s[0] & 0x07) << 18) | ((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) |
40 (uint32_t)(s[3] & 0x3F));
41 return 4;
42 }
43 return -1; // Invalid
44}

Referenced by digital_rain_apply(), log_search_get_match_info(), utf8_char_count(), utf8_to_codepoints(), and validate_shell_safe().

◆ utf8_display_width()

int utf8_display_width ( const char *  str)

#include <utf8.h>

Calculate terminal display width of a UTF-8 string.

Parameters
strUTF-8 string (must not be NULL)
Returns
Display width in terminal columns (0 on empty or invalid input)

Converts UTF-8 byte sequences to Unicode codepoints and calculates the terminal display width using utf8proc_charwidth(). Properly handles:

  • Multi-byte UTF-8 sequences (1-4 bytes per character)
  • Wide characters (CJK, emoji - typically 2 columns)
  • Control characters and combining marks (0 width)
  • Invalid UTF-8 sequences (stops processing)

Terminal display width differs from byte count:

  • ASCII 'A' = 1 byte = 1 column
  • 'é' (é) = 2 bytes = 1 column
  • '中' (CJK) = 3 bytes = 2 columns
  • '😀' (emoji) = 4 bytes = 2 columns
Note
Uses utf8proc library for accurate Unicode character-width computation
Returns 0 for NULL or empty string
Invalid UTF-8 sequences stop processing (partial width returned)
Example
int width = utf8_display_width("Hello"); // Returns 5 (5 ASCII chars)
int width = utf8_display_width("Café"); // Returns 4 (é is 1 column)
int width = utf8_display_width("中国"); // Returns 4 (2 CJK chars × 2 columns)
int width = utf8_display_width("😀"); // Returns 2 (emoji is 2 columns)

Definition at line 46 of file utf8.c.

46 {
47 if (!str || !*str) {
48 return 0;
49 }
50
51 int width = 0;
52 const utf8proc_uint8_t *p = (const utf8proc_uint8_t *)str;
53
54 while (*p) {
55 // Skip ANSI escape sequences (ESC [ ... m)
56 // Pattern: \x1b [ [0-9;]* m
57 if (p[0] == 0x1b && p[1] == '[') {
58 // Found ANSI escape sequence start
59 p += 2; // Skip ESC[
60 // Skip digits and semicolons until we find 'm'
61 while (*p && *p != 'm') {
62 p++;
63 }
64 if (*p == 'm') {
65 p++; // Skip the 'm'
66 }
67 continue;
68 }
69
70 utf8proc_int32_t codepoint;
71 utf8proc_ssize_t len = utf8proc_iterate(p, -1, &codepoint);
72
73 if (len <= 0) {
74 // End of string or invalid UTF-8 sequence, stop processing
75 break;
76 }
77
78 // Get display width of this codepoint
79 int char_width = utf8proc_charwidth(codepoint);
80 if (char_width < 0) {
81 // Control character or unprintable - treat as 0 width
82 char_width = 0;
83 }
84 width += char_width;
85 p += len;
86 }
87
88 return width;
89}

Referenced by display_height(), display_width(), layout_print_two_column_row(), options_config_calculate_max_col_width(), prompt_password(), and validate_palette_chars().

◆ utf8_display_width_n()

int utf8_display_width_n ( const char *  str,
size_t  max_bytes 
)

#include <utf8.h>

Calculate terminal display width of a UTF-8 string up to byte limit.

Parameters
strUTF-8 string (must not be NULL)
max_bytesMaximum bytes to process
Returns
Display width in terminal columns

Like utf8_display_width() but stops after processing max_bytes bytes. Useful for calculating display width of substrings or with length-limited buffers.

Note
Stops processing at max_bytes boundary or null terminator, whichever comes first
Invalid UTF-8 sequences stop processing (partial width returned)
Uses utf8proc library for accurate Unicode character-width computation
Example
const char *str = "Hello World";
int width = utf8_display_width_n(str, 5); // Returns 5 (width of "Hello")
int utf8_display_width_n(const char *str, size_t max_bytes)
Calculate terminal display width of a UTF-8 string up to byte limit.
Definition utf8.c:91

Definition at line 91 of file utf8.c.

91 {
92 if (!str || max_bytes == 0) {
93 return 0;
94 }
95
96 int width = 0;
97 const utf8proc_uint8_t *p = (const utf8proc_uint8_t *)str;
98 const utf8proc_uint8_t *end = p + max_bytes;
99
100 while (p < end && *p) {
101 // Skip ANSI escape sequences (ESC [ ... m)
102 if (p + 1 < end && p[0] == 0x1b && p[1] == '[') {
103 // Found ANSI escape sequence start
104 p += 2; // Skip ESC[
105 // Skip digits and semicolons until we find 'm'
106 while (p < end && *p && *p != 'm') {
107 p++;
108 }
109 if (p < end && *p == 'm') {
110 p++; // Skip the 'm'
111 }
112 continue;
113 }
114
115 utf8proc_int32_t codepoint;
116 utf8proc_ssize_t len = utf8proc_iterate(p, end - p, &codepoint);
117
118 if (len <= 0) {
119 // End of string or invalid UTF-8 sequence
120 break;
121 }
122
123 // Get display width of this codepoint
124 int char_width = utf8proc_charwidth(codepoint);
125 if (char_width < 0) {
126 // Control character or unprintable - treat as 0 width
127 char_width = 0;
128 }
129 width += char_width;
130 p += len;
131 }
132
133 return width;
134}

Referenced by utf8_palette_create().

◆ utf8_is_ascii_only()

bool utf8_is_ascii_only ( const char *  str)

#include <utf8.h>

Check if a string contains only ASCII characters.

Parameters
strString to check (must not be NULL)
Returns
true if all characters are ASCII (0x00-0x7F), false if any non-ASCII found

Validates UTF-8 encoding and checks that all decoded codepoints are ASCII. Useful for security-sensitive strings that should only contain ASCII (e.g., session identifiers to prevent homograph attacks).

Returns false if string contains invalid UTF-8 sequences.

Note
Empty string is considered ASCII-only (returns true)
Returns false for NULL input (not validated)
Prevents homograph attack vector (e.g., Cyrillic 'а' vs ASCII 'a')
Example
// Verify session string contains only ASCII to prevent homograph spoofing
if (!utf8_is_ascii_only(session_string)) {
log_warn("Session string contains non-ASCII characters");
return false;
}
bool utf8_is_ascii_only(const char *str)
Check if a string contains only ASCII characters.
Definition utf8.c:165

Definition at line 165 of file utf8.c.

165 {
166 if (!str) {
167 SET_ERRNO(ERROR_INVALID_PARAM, "str is NULL");
168 return false;
169 }
170
171 // Fast path: Check if all bytes are in ASCII range (0x00-0x7F)
172 // ASCII characters are 0x00-0x7F (single byte, high bit clear)
173 // Multi-byte UTF-8 has continuation bytes with high bit set (10xxxxxx pattern)
174 // So if all bytes have high bit clear, it's guaranteed ASCII-only
175 const unsigned char *p = (const unsigned char *)str;
176 while (*p) {
177 if ((*p & ~0x7F) != 0) {
178 return false; // Non-ASCII byte found (high bit set)
179 }
180 p++;
181 }
182 return true;
183}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by is_session_string(), and options_init().

◆ utf8_is_valid()

bool utf8_is_valid ( const char *  str)

#include <utf8.h>

Check if a string is valid UTF-8.

Parameters
strString to validate (must not be NULL)
Returns
true if valid UTF-8, false if invalid sequences detected

Scans the entire string and validates that all byte sequences conform to UTF-8 encoding rules. Returns false immediately on first invalid sequence.

Note
Returns true for empty string (valid)
Returns false for NULL input (not validated)
More efficient than decoding all codepoints if only validation is needed
Example
if (!utf8_is_valid(user_input)) {
log_error("Invalid UTF-8 in user input");
return false;
}

Definition at line 156 of file utf8.c.

156 {
157 if (!str) {
158 SET_ERRNO(ERROR_INVALID_PARAM, "str is NULL");
159 return false;
160 }
161 // Reuse utf8_char_count to validate without duplicating loop
162 return utf8_char_count(str) != (size_t)-1;
163}

References ERROR_INVALID_PARAM, SET_ERRNO, and utf8_char_count().

Referenced by options_init(), prompt_password(), prompt_password_simple(), validate_private_key(), validate_public_key(), and validate_shell_safe().

◆ utf8_next_char_bytes()

int utf8_next_char_bytes ( const char *  str,
size_t  max_bytes 
)

#include <utf8.h>

Get byte length of next UTF-8 character.

Parameters
strPointer to UTF-8 byte sequence (must not be NULL)
max_bytesMaximum bytes to read from str
Returns
Number of bytes in next character (1-4), or -1 if invalid UTF-8

Determines how many bytes are needed for the next complete UTF-8 character. Uses utf8proc for robust UTF-8 handling.

Useful for interactive text input where you need to handle multi-byte UTF-8 characters character-by-character (e.g., password prompts).

Note
Returns -1 for invalid UTF-8 sequences or if end of string reached
Requires utf8proc library which is already a project dependency
Example
// Handle multi-byte UTF-8 in password input
const char *input = "café";
int bytes_in_char = utf8_next_char_bytes(input, strlen(input));
// Returns 1 for 'c', 1 for 'a', 1 for 'f', 2 for 'é'
int utf8_next_char_bytes(const char *str, size_t max_bytes)
Get byte length of next UTF-8 character.
Definition utf8.c:205

Definition at line 205 of file utf8.c.

205 {
206 if (!str || max_bytes == 0) {
207 SET_ERRNO(ERROR_INVALID_PARAM, "str is NULL or max_bytes is 0");
208 return -1;
209 }
210
211 // Use utf8proc_iterate to get byte length of next character
212 utf8proc_int32_t codepoint;
213 utf8proc_ssize_t len = utf8proc_iterate((const utf8proc_uint8_t *)str, (utf8proc_ssize_t)max_bytes, &codepoint);
214
215 if (len <= 0) {
216 return -1; // Invalid UTF-8 or end of string
217 }
218
219 return (int)len;
220}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by layout_print_wrapped_description().

◆ utf8_read_and_insert_continuation_bytes()

int utf8_read_and_insert_continuation_bytes ( char *  buffer,
size_t *  cursor,
size_t *  len,
size_t  max_len,
int  continuation_bytes,
int(*)(void)  read_byte_fn 
)

#include <utf8.h>

Read continuation bytes and insert them into buffer at cursor position.

Parameters
bufferText buffer to insert bytes into
cursorCurrent cursor position (modified to reflect new position)
lenCurrent buffer length (modified to reflect new length)
max_lenMaximum buffer size
continuation_bytesNumber of continuation bytes to read
read_byte_fnCallback function to read next byte (should return int like getchar)
Returns
0 on success, -1 if EOF or buffer overflow

Reads continuation_bytes bytes from input using the provided callback function, shifts existing buffer content right, and inserts each byte at the cursor position. Updates cursor and len to reflect the new state.

Useful for handling multi-byte UTF-8 input in interactive prompts where bytes arrive one at a time (password input, terminal prompts, etc).

Example
// In interactive password input
int continuation_bytes = utf8_continuation_bytes_needed((unsigned char)first_byte);
if (continuation_bytes > 0) {
buffer, &cursor, &len, max_len, continuation_bytes, getchar);
if (result < 0) {
// Handle EOF or overflow
}
}
int utf8_read_and_insert_continuation_bytes(char *buffer, size_t *cursor, size_t *len, size_t max_len, int continuation_bytes, int(*read_byte_fn)(void))
Read continuation bytes and insert them into buffer at cursor position.
Definition utf8.c:238

Definition at line 238 of file utf8.c.

239 {
240 if (!buffer || !cursor || !len || continuation_bytes <= 0 || !read_byte_fn) {
241 SET_ERRNO(ERROR_INVALID_PARAM, "invalid params");
242 return -1;
243 }
244
245 for (int i = 0; i < continuation_bytes && *len < max_len - 1; i++) {
246 int next_byte = read_byte_fn();
247 if (next_byte == EOF) {
248 return -1; // EOF reached
249 }
250
251 // Shift characters right to make room
252 memmove(&buffer[*cursor + 1], &buffer[*cursor], *len - *cursor);
253 buffer[*cursor] = (char)next_byte;
254 (*len)++;
255 (*cursor)++;
256 }
257
258 return 0;
259}

References ERROR_INVALID_PARAM, and SET_ERRNO.

◆ utf8_strcasestr()

const char * utf8_strcasestr ( const char *  haystack,
const char *  needle 
)

#include <utf8.h>

Case-insensitive substring search with full Unicode support.

Parameters
haystackString to search in (must not be NULL)
needleSubstring to search for (must not be NULL)
Returns
Pointer to first occurrence of needle in haystack, or NULL if not found

Performs case-insensitive substring search using Unicode case folding. Properly handles all Unicode characters including accented letters, Greek, Cyrillic, CJK, emoji, and other scripts.

Uses utf8proc library for accurate Unicode case folding according to Unicode standard. This ensures correct matching for:

  • ASCII: "TEST" matches "test"
  • Accented: "CAFÉ" matches "café"
  • Greek: "ΕΛΛΗΝΙΚΆ" matches "ελληνικά"
  • Cyrillic: "РУССКИЙ" matches "русский"
Note
Returns NULL if needle is empty string
Returns haystack if needle is empty
Case folding follows Unicode standard (more complex than simple toupper/tolower)
Uses utf8proc for accurate Unicode handling
Example
const char *found = utf8_strcasestr("Hello WORLD", "world");
// Returns pointer to "WORLD" in the haystack
const char *found2 = utf8_strcasestr("Café français", "FRANÇAIS");
// Returns pointer to "français" (case-insensitive + accents)
const char * utf8_strcasestr(const char *haystack, const char *needle)
Case-insensitive substring search with full Unicode support.
Definition utf8.c:272

Uses utf8proc for Unicode case folding according to Unicode standard. This properly handles all Unicode scripts including Greek, Cyrillic, accented characters, and more.

Definition at line 272 of file utf8.c.

272 {
273 if (!haystack || !needle) {
274 SET_ERRNO(ERROR_INVALID_PARAM, "invalid params");
275 return NULL;
276 }
277
278 // Empty needle matches at start of haystack
279 if (needle[0] == '\0') {
280 return haystack;
281 }
282
283 // Get lengths
284 size_t haystack_len = strlen(haystack);
285 size_t needle_len = strlen(needle);
286
287 if (needle_len > haystack_len) {
288 return NULL;
289 }
290
291 // Case-fold both strings using utf8proc
292 // UTF8PROC_CASEFOLD performs Unicode case folding
293 // UTF8PROC_STABLE ensures stable output
294 // UTF8PROC_COMPOSE normalizes composed characters
295 utf8proc_option_t options = UTF8PROC_CASEFOLD | UTF8PROC_STABLE | UTF8PROC_COMPOSE;
296
297 // Case-fold the needle (pattern to search for)
298 utf8proc_uint8_t *needle_folded = NULL;
299 utf8proc_ssize_t needle_folded_len =
300 utf8proc_map((const utf8proc_uint8_t *)needle, (utf8proc_ssize_t)needle_len, &needle_folded, options);
301
302 if (needle_folded_len < 0 || !needle_folded) {
303 // Invalid UTF-8 in needle
304 if (needle_folded) {
305 free(needle_folded);
306 }
307 return NULL;
308 }
309
310 // Try each position in haystack
311 const char *haystack_pos = haystack;
312 while (*haystack_pos != '\0') {
313 // Calculate remaining haystack length
314 size_t remaining = haystack_len - (size_t)(haystack_pos - haystack);
315
316 if (remaining < needle_len) {
317 // Not enough characters left to match
318 break;
319 }
320
321 // Case-fold the current haystack window
322 utf8proc_uint8_t *haystack_folded = NULL;
323 utf8proc_ssize_t haystack_folded_len =
324 utf8proc_map((const utf8proc_uint8_t *)haystack_pos, (utf8proc_ssize_t)needle_len, &haystack_folded, options);
325
326 if (haystack_folded_len >= 0 && haystack_folded) {
327 // Compare case-folded strings
328 if ((size_t)haystack_folded_len == (size_t)needle_folded_len &&
329 memcmp(haystack_folded, needle_folded, (size_t)needle_folded_len) == 0) {
330 // Match found!
331 free(haystack_folded);
332 free(needle_folded);
333 return haystack_pos;
334 }
335 free(haystack_folded);
336 }
337
338 // Move to next UTF-8 character in haystack
339 utf8proc_int32_t codepoint;
340 utf8proc_ssize_t bytes = utf8proc_iterate((const utf8proc_uint8_t *)haystack_pos, -1, &codepoint);
341 if (bytes <= 0) {
342 // Invalid UTF-8, move by one byte
343 haystack_pos++;
344 } else {
345 haystack_pos += bytes;
346 }
347 }
348
349 free(needle_folded);
350 return NULL;
351}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by grep_highlight_colored(), grep_should_output(), log_search_gather_and_filter_logs(), and log_search_get_match_info().

◆ utf8_to_codepoints()

size_t utf8_to_codepoints ( const char *  str,
uint32_t *  out_codepoints,
size_t  max_codepoints 
)

#include <utf8.h>

Convert UTF-8 string to array of Unicode codepoints.

Parameters
strUTF-8 string to convert (must not be NULL)
out_codepointsOutput array for decoded codepoints (must not be NULL)
max_codepointsMaximum number of codepoints to decode
Returns
Number of codepoints decoded, or SIZE_MAX if invalid UTF-8 detected

Decodes a UTF-8 string into an array of Unicode codepoints (32-bit values). Stops when reaching end of string, hitting max_codepoints limit, or detecting invalid UTF-8.

Returns SIZE_MAX if invalid UTF-8 sequences are encountered during decoding.

Note
Returns 0 if str is NULL, out_codepoints is NULL, or max_codepoints is 0
Used by fuzzy matching (Levenshtein), Unicode algorithms, and validation
More efficient than calling utf8_decode multiple times in a loop
Example
// Compare two strings at codepoint level
uint32_t str1_codepoints[256];
uint32_t str2_codepoints[256];
size_t count1 = utf8_to_codepoints("café", str1_codepoints, 256);
size_t count2 = utf8_to_codepoints("cafe", str2_codepoints, 256);
if (count1 == SIZE_MAX || count2 == SIZE_MAX) {
return false; // Invalid UTF-8
}
// Now compare at codepoint level instead of bytes
for (size_t i = 0; i < count1 && i < count2; i++) {
if (str1_codepoints[i] != str2_codepoints[i]) {
// Different codepoint
}
}
size_t utf8_to_codepoints(const char *str, uint32_t *out_codepoints, size_t max_codepoints)
Convert UTF-8 string to array of Unicode codepoints.
Definition utf8.c:185

Definition at line 185 of file utf8.c.

185 {
186 if (!str || !out_codepoints || max_codepoints == 0) {
187 SET_ERRNO(ERROR_INVALID_PARAM, "str is NULL or out_codepoints is NULL or max_codepoints is 0");
188 return 0;
189 }
190
191 size_t count = 0;
192 const uint8_t *p = (const uint8_t *)str;
193 while (*p && count < max_codepoints) {
194 uint32_t codepoint;
195 int decode_len = utf8_decode(p, &codepoint);
196 if (decode_len < 0) {
197 return -1; // SIZE_MAX - Invalid UTF-8
198 }
199 out_codepoints[count++] = codepoint;
200 p += decode_len;
201 }
202 return count;
203}

References ERROR_INVALID_PARAM, SET_ERRNO, and utf8_decode().

Referenced by levenshtein(), and validate_palette_chars().

◆ utf8_write_lowercase()

void utf8_write_lowercase ( FILE *  output,
const char *  text 
)

#include <utf8.h>

Write UTF-8 string to file with ASCII lowercase conversion.

Parameters
outputFile pointer to write to (must not be NULL)
textUTF-8 string to convert and write (may be NULL)

Writes text to output file, converting ASCII uppercase letters (A-Z) to lowercase (a-z) while preserving all UTF-8 multi-byte sequences unchanged. This is UTF-8 aware and does not modify non-ASCII characters.

Useful for terminal output where uppercase letters should be displayed as lowercase while preserving Unicode characters like emoji and accented letters.

Note
If text is NULL, function returns immediately without writing anything
Multi-byte UTF-8 sequences (e.g., emoji, CJK characters) are copied unchanged
Only ASCII uppercase (A-Z) is converted to lowercase (a-z)
Continuation bytes in multi-byte sequences are never modified
Example
FILE *fp = fopen("output.txt", "w");
// ASCII text: converts uppercase
utf8_write_lowercase(fp, "HELLO World"); // Writes "hello world"
// UTF-8 emoji and non-ASCII: preserved
utf8_write_lowercase(fp, "CAFÉ 😀"); // Writes "café 😀" (emoji unchanged)
// Mixed case with non-ASCII
utf8_write_lowercase(fp, "HELLO 世界"); // Writes "hello 世界" (CJK unchanged)
fclose(fp);
void utf8_write_lowercase(FILE *output, const char *text)
Write UTF-8 string to file with ASCII lowercase conversion.
Definition utf8.c:360

Converts ASCII uppercase to lowercase while preserving all UTF-8 multi-byte sequences unchanged. This ensures proper handling of emoji and non-ASCII characters while still performing ASCII case conversion.

Definition at line 360 of file utf8.c.

360 {
361 if (!text) {
362 return;
363 }
364
365 for (const unsigned char *p = (const unsigned char *)text; *p; p++) {
366 // Check if this is the start of a multi-byte UTF-8 sequence
367 if (*p >= 0xC0) {
368 // Multi-byte UTF-8 character - copy as-is without modification
369 fputc(*p, output);
370 // Copy continuation bytes (0x80-0xBF)
371 while (p[1] >= 0x80 && p[1] < 0xC0) {
372 fputc(*++p, output);
373 }
374 } else if (*p >= 'A' && *p <= 'Z') {
375 // ASCII uppercase - convert to lowercase
376 fputc(*p + 32, output);
377 } else {
378 // ASCII lowercase or control character - copy as-is
379 fputc(*p, output);
380 }
381 }
382}

Referenced by __attribute__().

◆ validate_shell_safe()

bool validate_shell_safe ( const char *  str,
const char *  allowed_chars 
)

#include <string.h>

Validate that a string contains only safe characters for shell commands.

Parameters
strString to validate (must not be NULL)
allowed_charsAdditional allowed characters (e.g., "-:_", can be NULL)
Returns
true if string is safe, false otherwise

Validates that a string contains only safe characters for use in shell commands. Rejects shell metacharacters that could be used for command injection or shell expansion.

REJECTED METACHARACTERS:

  • Control operators: ; & | $ ‘
  • Quote characters: " ’
  • Redirection: < >
  • Brackets: ( ) [ ] { }
  • Wildcards: * ?
  • Special: \ ! ~ # @
Note
The allowed_chars parameter adds exceptions to the safety check.
Empty strings are considered safe (returns true).
This function is essential for preventing command injection attacks.
Example
if (!validate_shell_safe(user_input, NULL)) {
// Reject unsafe input
}
bool validate_shell_safe(const char *str, const char *allowed_chars)
Validate that a string contains only safe characters for shell commands.
Definition util/string.c:62

Definition at line 62 of file util/string.c.

62 {
63 if (!str) {
64 return false;
65 }
66
67 // Shell metacharacters that are dangerous
68 // ; & | $ ` \ " ' < > ( ) [ ] { } * ? ! ~ # @ space tab newline
69 const char *dangerous = ";|$`\\\"'<>()[]{}*?!~#@ \t\n\r";
70
71 // First pass: validate UTF-8 encoding
72 if (!utf8_is_valid(str)) {
73 return false;
74 }
75
76 // Second pass: validate each character for shell safety
77 // Convert to codepoints and check only ASCII range for dangerous characters
78 // (non-ASCII characters are allowed by default)
79 const uint8_t *p = (const uint8_t *)str;
80 while (*p) {
81 uint32_t codepoint;
82 int decode_len = utf8_decode(p, &codepoint);
83 if (decode_len < 0) {
84 return false; // Should not happen after utf8_is_valid check
85 }
86
87 // Only validate ASCII range where shell metacharacters exist
88 // Non-ASCII characters (codepoint > 127) are allowed
89 if (codepoint <= 127) {
90 unsigned char c = (unsigned char)codepoint;
91
92 // Allow ASCII alphanumeric characters
93 if (isalnum(c)) {
94 p += decode_len;
95 continue;
96 }
97
98 // Check if character is in allowed_chars list
99 if (allowed_chars) {
100 bool is_allowed = false;
101 for (size_t j = 0; allowed_chars[j] != '\0'; j++) {
102 if (c == (unsigned char)allowed_chars[j]) {
103 is_allowed = true;
104 break;
105 }
106 }
107 if (is_allowed) {
108 p += decode_len;
109 continue;
110 }
111 }
112
113 // Check if character is dangerous
114 bool is_dangerous = false;
115 for (size_t j = 0; dangerous[j] != '\0'; j++) {
116 if (c == (unsigned char)dangerous[j]) {
117 is_dangerous = true;
118 break;
119 }
120 }
121 if (is_dangerous) {
122 return false;
123 }
124 }
125
126 p += decode_len;
127 }
128
129 return true;
130}

References utf8_decode(), and utf8_is_valid().

Referenced by gpg_get_public_key().