diff --git a/CMakeLists.txt b/CMakeLists.txt index 7121d932..b88cc287 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -388,9 +388,6 @@ message(STATUS "Excluding main.c from CORE_SOURCES (library build)") file(GLOB DATABASE_SOURCES "src/database/*.c") file(GLOB STORAGE_SOURCES "src/storage/*.c") file(GLOB UTILS_SOURCES "src/utils/*.c") -# Exclude rebuild_recordings.c from UTILS_SOURCES to avoid multiple main functions -list(FILTER UTILS_SOURCES EXCLUDE REGEX ".*rebuild_recordings\\.c$") -message(STATUS "Excluding rebuild_recordings.c from main executable") file(GLOB_RECURSE WEB_SOURCES "src/web/*.c") # Filter web sources - exclude libuv sources (they're added separately via LIBUV_SERVER_SOURCES) @@ -410,10 +407,10 @@ list(APPEND ROOT_SOURCES # errors when linking test executables against lightnvr_lib. list(FILTER ROOT_SOURCES EXCLUDE REGEX ".*sod/sod\\.c$") list(FILTER ROOT_SOURCES EXCLUDE REGEX ".*core/main\\.c$") -list(FILTER ROOT_SOURCES EXCLUDE REGEX ".*utils/rebuild_recordings\\.c$") +list(FILTER ROOT_SOURCES EXCLUDE REGEX ".*tools/.*\\.c$") # Also exclude libuv sources from ROOT_SOURCES (they're added separately) list(FILTER ROOT_SOURCES EXCLUDE REGEX ".*libuv_.*\\.c$") -message(STATUS "Excluding main.c and rebuild_recordings.c from ROOT_SOURCES") +message(STATUS "Excluding main.c and tools from ROOT_SOURCES") # Collect video sources from src/video, then exclude motion_detection_optimized.c, # detection_thread_pool.c, and the original hls_writer_thread.c (since we're using our split version). @@ -504,9 +501,10 @@ add_executable(lightnvr src/core/main.c) # Define source files for rebuild_recordings utility (excluding inih) set(REBUILD_RECORDINGS_SOURCES - src/utils/rebuild_recordings.c + src/tools/rebuild_recordings.c src/core/config.c src/core/logger.c + src/utils/strings.c src/database/db_core.c src/database/db_streams.c src/database/db_recordings.c diff --git a/examples/onvif_motion_recording_example.c b/examples/onvif_motion_recording_example.c index 01d4cb3f..47c2130e 100644 --- a/examples/onvif_motion_recording_example.c +++ b/examples/onvif_motion_recording_example.c @@ -30,8 +30,8 @@ void example_enable_motion_recording(void) { }; // Set codec and quality - strncpy(config.codec, "h264", sizeof(config.codec) - 1); - strncpy(config.quality, "high", sizeof(config.quality) - 1); + safe_strcpy(config.codec, "h264", sizeof(config.codec), 0); + safe_strcpy(config.quality, "high", sizeof(config.quality), 0); // Enable for a stream const char *stream_name = "front_door"; @@ -115,7 +115,7 @@ void example_check_status(void) { } // Get current recording path - char path[512]; + char path[MAX_PATH_LENGTH]; if (get_current_motion_recording_path(stream_name, path, sizeof(path)) == 0) { printf("Current recording: %s\n", path); } else { @@ -140,8 +140,8 @@ void example_update_configuration(void) { .retention_days = 60 // Increased to 60 days }; - strncpy(new_config.codec, "h265", sizeof(new_config.codec) - 1); - strncpy(new_config.quality, "medium", sizeof(new_config.quality) - 1); + safe_strcpy(new_config.codec, "h265", sizeof(new_config.codec), 0); + safe_strcpy(new_config.quality, "medium", sizeof(new_config.quality), 0); int result = update_motion_recording_config(stream_name, &new_config); @@ -199,8 +199,8 @@ void example_multiple_cameras(void) { .retention_days = 30 }; - strncpy(config.codec, "h264", sizeof(config.codec) - 1); - strncpy(config.quality, "high", sizeof(config.quality) - 1); + safe_strcpy(config.codec, "h264", sizeof(config.codec), 0); + safe_strcpy(config.quality, "high", sizeof(config.quality), 0); int result = enable_motion_recording(cameras[i], &config); diff --git a/include/core/config.h b/include/core/config.h index 7ab76025..5f00663a 100644 --- a/include/core/config.h +++ b/include/core/config.h @@ -127,7 +127,7 @@ typedef struct { // New recording format options bool record_mp4_directly; // Record directly to MP4 alongside HLS - char mp4_storage_path[256]; // Path for MP4 recordings storage + char mp4_storage_path[MAX_PATH_LENGTH]; // Path for MP4 recordings storage int mp4_segment_duration; // Duration of each MP4 segment in seconds int mp4_retention_days; // Number of days to keep MP4 recordings diff --git a/include/database/db_recordings.h b/include/database/db_recordings.h index ec854820..a37fe2d8 100644 --- a/include/database/db_recordings.h +++ b/include/database/db_recordings.h @@ -5,11 +5,13 @@ #include #include +#include "core/config.h" + // Recording metadata structure typedef struct { uint64_t id; char stream_name[64]; - char file_path[256]; + char file_path[MAX_PATH_LENGTH]; time_t start_time; time_t end_time; uint64_t size_bytes; diff --git a/include/storage/storage_manager.h b/include/storage/storage_manager.h index 719c60f9..795fa3a1 100644 --- a/include/storage/storage_manager.h +++ b/include/storage/storage_manager.h @@ -5,9 +5,11 @@ #include #include +#include "core/config.h" + // Recording file information structure typedef struct { - char path[256]; + char path[MAX_PATH_LENGTH]; char stream_name[64]; time_t start_time; time_t end_time; diff --git a/include/utils/memory.h b/include/utils/memory.h index 7a6e03f8..05c27937 100644 --- a/include/utils/memory.h +++ b/include/utils/memory.h @@ -40,34 +40,6 @@ void *safe_calloc(size_t nmemb, size_t size); */ void safe_free(void *ptr); -/** - * Safe string duplication - * - * @param str String to duplicate - * @return Pointer to duplicated string or NULL on failure - */ -char *safe_strdup(const char *str); - -/** - * Safe string copy with size checking - * - * @param dest Destination buffer - * @param src Source string - * @param size Size of destination buffer - * @return 0 on success, -1 on failure - */ -int safe_strcpy(char *dest, const char *src, size_t size); - -/** - * Safe string concatenation with size checking - * - * @param dest Destination buffer - * @param src Source string - * @param size Size of destination buffer - * @return 0 on success, -1 on failure - */ -int safe_strcat(char *dest, const char *src, size_t size); - /** * Secure memory clearing function that won't be optimized away * diff --git a/include/utils/strings.h b/include/utils/strings.h index 8f75a6a8..b5edcb61 100644 --- a/include/utils/strings.h +++ b/include/utils/strings.h @@ -3,7 +3,40 @@ #ifndef STRINGS_H #define STRINGS_H +#include +#include #include +#include + +/** + * Safe string duplication + * + * @param str String to duplicate + * @return Pointer to duplicated string or NULL on failure + */ +char *safe_strdup(const char *str); + +/** + * Safe string copy with size checking. It is safe to pass an unterminated + * string in `src`: the string will not be checked beyond `src_size` bytes. + * + * @param dest Destination buffer + * @param src Source string + * @param dst_size Size of destination buffer + * @param src_size Size of source buffer if not null-terminated + * @return 0 on success (including if the string is truncated), -1 on failure + */ +int safe_strcpy(char *dest, const char *src, size_t dst_size, size_t src_size); + +/** + * Safe string concatenation with size checking + * + * @param dest Destination buffer + * @param src Source string + * @param size Size of destination buffer + * @return 0 on success, -1 on failure + */ +int safe_strcat(char *dest, const char *src, size_t size); /** * Check if a string ends with a given suffix @@ -14,4 +47,70 @@ */ bool ends_with(const char *str, const char *suffix); +/** + * Returns a pointer to the first printable non-space character in the input string + * and terminates the string after the last printable non-space character. + * + * @param value The input string + * @return A pointer into the original string + */ +char *trim_ascii_whitespace(char *value); + +/** + * Copies up to `output_size` bytes of the input string excluding any leading + * and trailing whitespace or non-printing characters into the output buffer. + * Guaranteed to null-terminate the output buffer. + * + * @param output The output buffer + * @param output_size The size of the output buffer + * @param input The input string + * @param input_size The maximum size of the input string to check, or zero to + * not limit the input string size + * @return The number of bytes copied, not counting the terminator + */ +size_t copy_trimmed_value(char *output, size_t output_size, const char *input, size_t input_size); + +/** + * Returns a pointer to the first printable character in the input string. + */ +static inline const char *ltrim_pos(const char *input) { + if (!input) { + return NULL; + } + + unsigned char *start = (unsigned char *)input; + while (*start && !isgraph(*start)) { + start++; + } + return (const char *)start; +} + +/** + * Returns a pointer to the byte _after_ the last printable character + * in the input string. Set the returned pointer to '\0' to terminate + * the string after the last printable character. + */ +static inline const char *rtrim_pos(const char *input, size_t input_size) { + if (!input) { + return NULL; + } + + const char *end; + if (input_size > 0) { + end = (input + strnlen(input, input_size) - 1); + } else { + // If input_size is zero, use the unbounded strlen + end = (input + strlen(input) - 1); + } + // `end` will now point to the last non-null character. If the input is + // empty, `end` will be (input-1), the character *before* the terminating + // null. + while (end > input && !isgraph((unsigned char)*end)) { + end--; + } + // Point to the character after the last printable character. If input was + // empty, this will now point to the terminating null. + return end + 1; +} + #endif //STRINGS_H diff --git a/include/video/onvif_soap.h b/include/video/onvif_soap.h index 29344bab..26e9da30 100644 --- a/include/video/onvif_soap.h +++ b/include/video/onvif_soap.h @@ -28,14 +28,13 @@ char *onvif_create_security_header(const char *username, const char *password); * can be parsed at the error level. If the XML cannot be parsed at all, * logs the raw response (truncated) as a fallback. * - * @param response The raw XML response body (will be modified by the - * XML parser — pass a copy if the original is needed). + * @param response The raw XML response body. * @param response_len Length of the response in bytes. * @param context A short description of the request that failed * (e.g. "PullMessages", "CreatePullPointSubscription"), * used to prefix the log message. May be NULL. */ -void onvif_log_soap_fault(char *response, size_t response_len, const char *context); +void onvif_log_soap_fault(const char *response, size_t response_len, const char *context); #endif /* ONVIF_SOAP_H */ diff --git a/include/web/api_handlers_timeline.h b/include/web/api_handlers_timeline.h index 19f39bb8..55bdca1f 100644 --- a/include/web/api_handlers_timeline.h +++ b/include/web/api_handlers_timeline.h @@ -10,7 +10,7 @@ typedef struct { uint64_t id; char stream_name[64]; - char file_path[256]; + char file_path[MAX_PATH_LENGTH]; time_t start_time; time_t end_time; uint64_t size_bytes; diff --git a/include/web/http_server.h b/include/web/http_server.h index 8b8016e5..f156de63 100644 --- a/include/web/http_server.h +++ b/include/web/http_server.h @@ -28,12 +28,12 @@ typedef struct { char allowed_methods[256]; // CORS allowed methods char allowed_headers[256]; // CORS allowed headers bool ssl_enabled; // SSL/TLS enabled - char cert_path[256]; // SSL/TLS certificate path - char key_path[256]; // SSL/TLS key path + char cert_path[MAX_PATH_LENGTH]; // SSL/TLS certificate path + char key_path[MAX_PATH_LENGTH]; // SSL/TLS key path int max_connections; // Maximum number of connections int connection_timeout; // Connection timeout in seconds bool daemon_mode; // Daemon mode - char pid_file[256]; // PID file path + char pid_file[MAX_PATH_LENGTH]; // PID file path } http_server_config_t; /** diff --git a/include/web/libuv_server.h b/include/web/libuv_server.h index 4297d85e..6806288c 100644 --- a/include/web/libuv_server.h +++ b/include/web/libuv_server.h @@ -91,7 +91,7 @@ typedef struct libuv_connection { // Thread pool offloading (uv_queue_work) bool handler_on_worker; // Handler is running on a thread pool worker bool deferred_file_serve; // File serving deferred until back on loop thread - char deferred_file_path[1024]; // Deferred file path to serve + char deferred_file_path[MAX_PATH_LENGTH]; // Deferred file path to serve char deferred_content_type[128]; // Deferred content type (empty = auto-detect) char deferred_extra_headers[512]; // Deferred extra headers (empty = none) write_complete_action_t deferred_action; // Action to take after async response completes diff --git a/rtsp_test.c b/rtsp_test.c index f10cf272..567a8567 100644 --- a/rtsp_test.c +++ b/rtsp_test.c @@ -35,7 +35,7 @@ void handle_signal(int sig) { // Function to log FFmpeg errors void log_error(int err, const char *message) { char error_buf[AV_ERROR_MAX_STRING_SIZE] = {0}; - av_strerror(ret, error_buf, AV_ERROR_MAX_STRING_SIZE); + av_strerror(err, error_buf, AV_ERROR_MAX_STRING_SIZE); fprintf(stderr, "%s: %s\n", message, error_buf); } diff --git a/src/core/config.c b/src/core/config.c index 4a0eab3d..f55e57f9 100644 --- a/src/core/config.c +++ b/src/core/config.c @@ -17,6 +17,7 @@ #include "core/config.h" #include "core/logger.h" #include "database/database_manager.h" +#include "utils/strings.h" // Global configuration variable config_t g_config; @@ -246,8 +247,7 @@ static void apply_env_overrides(config_t *config) { } case CONFIG_TYPE_STRING: { char *str_ptr = (char *)field_ptr; - strncpy(str_ptr, env_value, mapping->size - 1); - str_ptr[mapping->size - 1] = '\0'; + safe_strcpy(str_ptr, env_value, mapping->size, 0); // Log with masked value for sensitive fields if (strstr(mapping->env_name, "PASSWORD") != NULL || strstr(mapping->env_name, "SECRET") != NULL) { @@ -441,8 +441,7 @@ void load_default_config(config_t *config) { config->streams[i].pre_detection_buffer = 5; // 5 seconds before detection config->streams[i].post_detection_buffer = 10; // 10 seconds after detection config->streams[i].detection_api_url[0] = '\0'; // Empty = use global config - strncpy(config->streams[i].detection_object_filter, "none", sizeof(config->streams[i].detection_object_filter) - 1); - config->streams[i].detection_object_filter_list[0] = '\0'; + safe_strcpy(config->streams[i].detection_object_filter, "none", sizeof(config->streams[i].detection_object_filter), 0); config->streams[i].streaming_enabled = true; // Enable streaming by default config->streams[i].record_audio = false; // Disable audio recording by default @@ -538,9 +537,11 @@ static int ensure_directories(const config_t *config) { } // Database directory - char db_dir[MAX_PATH_LENGTH]; - strncpy(db_dir, config->db_path, MAX_PATH_LENGTH); - char *dir = dirname(db_dir); + // Some dirname implementations actually modify the path argument and + // will segfault when passed a read-only const string. + char tmp_path[MAX_PATH_LENGTH]; + safe_strcpy(tmp_path, config->db_path, MAX_PATH_LENGTH, 0); + char *dir = dirname(tmp_path); if (create_directory(dir) != 0) { log_error("Failed to create database directory: %s", dir); return -1; @@ -553,9 +554,8 @@ static int ensure_directories(const config_t *config) { } // Log directory - char log_dir[MAX_PATH_LENGTH]; - strncpy(log_dir, config->log_file, MAX_PATH_LENGTH); - dir = dirname(log_dir); + safe_strcpy(tmp_path, config->log_file, MAX_PATH_LENGTH, 0); + dir = dirname(tmp_path); if (create_directory(dir) != 0) { log_error("Failed to create log directory: %s", dir); return -1; @@ -647,15 +647,15 @@ static int config_ini_handler(void* user, const char* section, const char* name, // General settings if (strcmp(section, "general") == 0) { if (strcmp(name, "pid_file") == 0) { - strncpy(config->pid_file, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->pid_file, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "log_file") == 0) { - strncpy(config->log_file, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->log_file, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "log_level") == 0) { config->log_level = safe_atoi(value, 0); } else if (strcmp(name, "syslog_enabled") == 0) { config->syslog_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "syslog_ident") == 0) { - strncpy(config->syslog_ident, value, sizeof(config->syslog_ident) - 1); + safe_strcpy(config->syslog_ident, value, sizeof(config->syslog_ident), 0); } else if (strcmp(name, "syslog_facility") == 0) { // Parse syslog facility - support both numeric and string values if (isdigit(value[0])) { @@ -678,9 +678,9 @@ static int config_ini_handler(void* user, const char* section, const char* name, // Storage settings else if (strcmp(section, "storage") == 0) { if (strcmp(name, "path") == 0) { - strncpy(config->storage_path, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->storage_path, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "path_hls") == 0) { - strncpy(config->storage_path_hls, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->storage_path_hls, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "max_size") == 0) { config->max_storage_size = strtoull(value, NULL, 10); } else if (strcmp(name, "retention_days") == 0) { @@ -690,8 +690,7 @@ static int config_ini_handler(void* user, const char* section, const char* name, } else if (strcmp(name, "record_mp4_directly") == 0) { config->record_mp4_directly = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "mp4_path") == 0) { - strncpy(config->mp4_storage_path, value, sizeof(config->mp4_storage_path) - 1); - config->mp4_storage_path[sizeof(config->mp4_storage_path) - 1] = '\0'; + safe_strcpy(config->mp4_storage_path, value, sizeof(config->mp4_storage_path), 0); } else if (strcmp(name, "mp4_segment_duration") == 0) { config->mp4_segment_duration = safe_atoi(value, 0); } else if (strcmp(name, "mp4_retention_days") == 0) { @@ -703,16 +702,15 @@ static int config_ini_handler(void* user, const char* section, const char* name, // Models settings else if (strcmp(section, "models") == 0) { if (strcmp(name, "path") == 0) { - strncpy(config->models_path, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->models_path, value, MAX_PATH_LENGTH, 0); } } // API detection settings else if (strcmp(section, "api_detection") == 0) { if (strcmp(name, "url") == 0) { - strncpy(config->api_detection_url, value, MAX_URL_LENGTH - 1); + safe_strcpy(config->api_detection_url, value, MAX_URL_LENGTH, 0); } else if (strcmp(name, "backend") == 0) { - strncpy(config->api_detection_backend, value, 31); - config->api_detection_backend[31] = '\0'; + safe_strcpy(config->api_detection_backend, value, sizeof(config->api_detection_backend), 0); } else if (strcmp(name, "detection_threshold") == 0) { config->default_detection_threshold = safe_atoi(value, 0); // Clamp to valid range @@ -729,21 +727,19 @@ static int config_ini_handler(void* user, const char* section, const char* name, if (config->default_post_detection_buffer < 0) config->default_post_detection_buffer = 0; if (config->default_post_detection_buffer > 300) config->default_post_detection_buffer = 300; } else if (strcmp(name, "buffer_strategy") == 0) { - strncpy(config->default_buffer_strategy, value, 31); - config->default_buffer_strategy[31] = '\0'; + safe_strcpy(config->default_buffer_strategy, value, sizeof(config->default_buffer_strategy), 0); } } // Database settings else if (strcmp(section, "database") == 0) { if (strcmp(name, "path") == 0) { - strncpy(config->db_path, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->db_path, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "backup_interval_minutes") == 0) { config->db_backup_interval_minutes = safe_atoi(value, 0); } else if (strcmp(name, "backup_retention_count") == 0) { config->db_backup_retention_count = safe_atoi(value, 0); } else if (strcmp(name, "post_backup_script") == 0) { - strncpy(config->db_post_backup_script, value, MAX_PATH_LENGTH - 1); - config->db_post_backup_script[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(config->db_post_backup_script, value, MAX_PATH_LENGTH, 0); } } // Web server settings @@ -751,16 +747,15 @@ static int config_ini_handler(void* user, const char* section, const char* name, if (strcmp(name, "port") == 0) { config->web_port = safe_atoi(value, 0); } else if (strcmp(name, "bind_ip") == 0) { - strncpy(config->web_bind_ip, value, sizeof(config->web_bind_ip) - 1); - config->web_bind_ip[sizeof(config->web_bind_ip) - 1] = '\0'; + safe_strcpy(config->web_bind_ip, value, sizeof(config->web_bind_ip), 0); } else if (strcmp(name, "root") == 0) { - strncpy(config->web_root, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->web_root, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "auth_enabled") == 0) { config->web_auth_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "username") == 0) { - strncpy(config->web_username, value, 31); + safe_strcpy(config->web_username, value, sizeof(config->web_username), 0); } else if (strcmp(name, "password") == 0) { - strncpy(config->web_password, value, 31); + safe_strcpy(config->web_password, value, sizeof(config->web_password), 0); } else if (strcmp(name, "webrtc_disabled") == 0) { config->webrtc_disabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "auth_timeout_hours") == 0) { @@ -785,8 +780,7 @@ static int config_ini_handler(void* user, const char* section, const char* name, config->trusted_device_days = (INT_MAX / 86400); } } else if (strcmp(name, "trusted_proxy_cidrs") == 0) { - strncpy(config->trusted_proxy_cidrs, value, sizeof(config->trusted_proxy_cidrs) - 1); - config->trusted_proxy_cidrs[sizeof(config->trusted_proxy_cidrs) - 1] = '\0'; + safe_strcpy(config->trusted_proxy_cidrs, value, sizeof(config->trusted_proxy_cidrs), 0); } else if (strcmp(name, "demo_mode") == 0) { config->demo_mode = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "force_mfa_on_login") == 0) { @@ -842,7 +836,7 @@ static int config_ini_handler(void* user, const char* section, const char* name, if (strcmp(last_warned_section, section) != 0) { log_warn("Ignoring stale INI section [%s]: stream config lives in the database. " "Remove this section from the config file.", section); - strncpy(last_warned_section, section, sizeof(last_warned_section) - 1); + safe_strcpy(last_warned_section, section, sizeof(last_warned_section), 0); } } // Memory optimization @@ -852,7 +846,7 @@ static int config_ini_handler(void* user, const char* section, const char* name, } else if (strcmp(name, "use_swap") == 0) { config->use_swap = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "swap_file") == 0) { - strncpy(config->swap_file, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->swap_file, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "swap_size") == 0) { config->swap_size = strtoull(value, NULL, 10); } @@ -862,7 +856,7 @@ static int config_ini_handler(void* user, const char* section, const char* name, if (strcmp(name, "hw_accel_enabled") == 0) { config->hw_accel_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "hw_accel_device") == 0) { - strncpy(config->hw_accel_device, value, 31); + safe_strcpy(config->hw_accel_device, value, sizeof(config->hw_accel_device), 0); } } // go2rtc settings @@ -870,9 +864,9 @@ static int config_ini_handler(void* user, const char* section, const char* name, if (strcmp(name, "enabled") == 0) { config->go2rtc_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "binary_path") == 0) { - strncpy(config->go2rtc_binary_path, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->go2rtc_binary_path, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "config_dir") == 0) { - strncpy(config->go2rtc_config_dir, value, MAX_PATH_LENGTH - 1); + safe_strcpy(config->go2rtc_config_dir, value, MAX_PATH_LENGTH, 0); } else if (strcmp(name, "api_port") == 0) { config->go2rtc_api_port = safe_atoi(value, 0); } else if (strcmp(name, "rtsp_port") == 0) { @@ -884,14 +878,11 @@ static int config_ini_handler(void* user, const char* section, const char* name, } else if (strcmp(name, "stun_enabled") == 0) { config->go2rtc_stun_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "stun_server") == 0) { - strncpy(config->go2rtc_stun_server, value, sizeof(config->go2rtc_stun_server) - 1); - config->go2rtc_stun_server[sizeof(config->go2rtc_stun_server) - 1] = '\0'; + safe_strcpy(config->go2rtc_stun_server, value, sizeof(config->go2rtc_stun_server), 0); } else if (strcmp(name, "external_ip") == 0) { - strncpy(config->go2rtc_external_ip, value, sizeof(config->go2rtc_external_ip) - 1); - config->go2rtc_external_ip[sizeof(config->go2rtc_external_ip) - 1] = '\0'; + safe_strcpy(config->go2rtc_external_ip, value, sizeof(config->go2rtc_external_ip), 0); } else if (strcmp(name, "ice_servers") == 0) { - strncpy(config->go2rtc_ice_servers, value, sizeof(config->go2rtc_ice_servers) - 1); - config->go2rtc_ice_servers[sizeof(config->go2rtc_ice_servers) - 1] = '\0'; + safe_strcpy(config->go2rtc_ice_servers, value, sizeof(config->go2rtc_ice_servers), 0); } else if (strcmp(name, "force_native_hls") == 0) { config->go2rtc_force_native_hls = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "proxy_max_inflight") == 0) { @@ -905,14 +896,11 @@ static int config_ini_handler(void* user, const char* section, const char* name, } else if (strcmp(name, "turn_enabled") == 0) { config->turn_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "turn_server_url") == 0) { - strncpy(config->turn_server_url, value, sizeof(config->turn_server_url) - 1); - config->turn_server_url[sizeof(config->turn_server_url) - 1] = '\0'; + safe_strcpy(config->turn_server_url, value, sizeof(config->turn_server_url), 0); } else if (strcmp(name, "turn_username") == 0) { - strncpy(config->turn_username, value, sizeof(config->turn_username) - 1); - config->turn_username[sizeof(config->turn_username) - 1] = '\0'; + safe_strcpy(config->turn_username, value, sizeof(config->turn_username), 0); } else if (strcmp(name, "turn_password") == 0) { - strncpy(config->turn_password, value, sizeof(config->turn_password) - 1); - config->turn_password[sizeof(config->turn_password) - 1] = '\0'; + safe_strcpy(config->turn_password, value, sizeof(config->turn_password), 0); } } // ONVIF settings @@ -929,8 +917,7 @@ static int config_ini_handler(void* user, const char* section, const char* name, config->onvif_discovery_interval = 3600; } } else if (strcmp(name, "discovery_network") == 0) { - strncpy(config->onvif_discovery_network, value, sizeof(config->onvif_discovery_network) - 1); - config->onvif_discovery_network[sizeof(config->onvif_discovery_network) - 1] = '\0'; + safe_strcpy(config->onvif_discovery_network, value, sizeof(config->onvif_discovery_network), 0); } } // MQTT settings for detection event streaming @@ -938,25 +925,20 @@ static int config_ini_handler(void* user, const char* section, const char* name, if (strcmp(name, "enabled") == 0) { config->mqtt_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "broker_host") == 0) { - strncpy(config->mqtt_broker_host, value, sizeof(config->mqtt_broker_host) - 1); - config->mqtt_broker_host[sizeof(config->mqtt_broker_host) - 1] = '\0'; + safe_strcpy(config->mqtt_broker_host, value, sizeof(config->mqtt_broker_host), 0); } else if (strcmp(name, "broker_port") == 0) { config->mqtt_broker_port = safe_atoi(value, 0); if (config->mqtt_broker_port <= 0 || config->mqtt_broker_port > 65535) { config->mqtt_broker_port = 1883; // Default port } } else if (strcmp(name, "username") == 0) { - strncpy(config->mqtt_username, value, sizeof(config->mqtt_username) - 1); - config->mqtt_username[sizeof(config->mqtt_username) - 1] = '\0'; + safe_strcpy(config->mqtt_username, value, sizeof(config->mqtt_username), 0); } else if (strcmp(name, "password") == 0) { - strncpy(config->mqtt_password, value, sizeof(config->mqtt_password) - 1); - config->mqtt_password[sizeof(config->mqtt_password) - 1] = '\0'; + safe_strcpy(config->mqtt_password, value, sizeof(config->mqtt_password), 0); } else if (strcmp(name, "client_id") == 0) { - strncpy(config->mqtt_client_id, value, sizeof(config->mqtt_client_id) - 1); - config->mqtt_client_id[sizeof(config->mqtt_client_id) - 1] = '\0'; + safe_strcpy(config->mqtt_client_id, value, sizeof(config->mqtt_client_id), 0); } else if (strcmp(name, "topic_prefix") == 0) { - strncpy(config->mqtt_topic_prefix, value, sizeof(config->mqtt_topic_prefix) - 1); - config->mqtt_topic_prefix[sizeof(config->mqtt_topic_prefix) - 1] = '\0'; + safe_strcpy(config->mqtt_topic_prefix, value, sizeof(config->mqtt_topic_prefix), 0); } else if (strcmp(name, "tls_enabled") == 0) { config->mqtt_tls_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "keepalive") == 0) { @@ -977,8 +959,7 @@ static int config_ini_handler(void* user, const char* section, const char* name, } else if (strcmp(name, "ha_discovery") == 0 || strcmp(name, "ha_discovery_enabled") == 0) { config->mqtt_ha_discovery = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else if (strcmp(name, "ha_discovery_prefix") == 0) { - strncpy(config->mqtt_ha_discovery_prefix, value, sizeof(config->mqtt_ha_discovery_prefix) - 1); - config->mqtt_ha_discovery_prefix[sizeof(config->mqtt_ha_discovery_prefix) - 1] = '\0'; + safe_strcpy(config->mqtt_ha_discovery_prefix, value, sizeof(config->mqtt_ha_discovery_prefix), 0); } else if (strcmp(name, "ha_snapshot_interval") == 0) { config->mqtt_ha_snapshot_interval = safe_atoi(value, 0); if (config->mqtt_ha_snapshot_interval < 0) { @@ -1220,8 +1201,7 @@ void set_custom_config_path(const char *path) { return; } - strncpy(g_custom_config_path, path, MAX_PATH_LENGTH - 1); - g_custom_config_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(g_custom_config_path, path, MAX_PATH_LENGTH, 0); log_info("Custom config path set to: %s", g_custom_config_path); } @@ -1238,8 +1218,7 @@ const char* get_loaded_config_path(void) { // Function to set the loaded config path static void set_loaded_config_path(const char *path) { if (path && path[0] != '\0') { - strncpy(g_loaded_config_path, path, MAX_PATH_LENGTH - 1); - g_loaded_config_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(g_loaded_config_path, path, MAX_PATH_LENGTH, 0); log_info("Loaded config path set to: %s", g_loaded_config_path); } } @@ -1353,17 +1332,13 @@ int reload_config(config_t *config) { int old_log_level = config->log_level; int old_web_port = config->web_port; char old_web_bind_ip[32]; - strncpy(old_web_bind_ip, config->web_bind_ip, 31); - old_web_bind_ip[31] = '\0'; + safe_strcpy(old_web_bind_ip, config->web_bind_ip, sizeof(old_web_bind_ip), 0); char old_storage_path[MAX_PATH_LENGTH]; - strncpy(old_storage_path, config->storage_path, sizeof(old_storage_path) - 1); - old_storage_path[sizeof(old_storage_path) - 1] = '\0'; + safe_strcpy(old_storage_path, config->storage_path, sizeof(old_storage_path), 0); char old_storage_path_hls[MAX_PATH_LENGTH]; - strncpy(old_storage_path_hls, config->storage_path_hls, sizeof(old_storage_path_hls) - 1); - old_storage_path_hls[sizeof(old_storage_path_hls) - 1] = '\0'; + safe_strcpy(old_storage_path_hls, config->storage_path_hls, sizeof(old_storage_path_hls), 0); char old_models_path[MAX_PATH_LENGTH]; - strncpy(old_models_path, config->models_path, sizeof(old_models_path) - 1); - old_models_path[sizeof(old_models_path) - 1] = '\0'; + safe_strcpy(old_models_path, config->models_path, sizeof(old_models_path), 0); uint64_t old_max_storage_size = config->max_storage_size; int old_retention_days = config->retention_days; @@ -1466,8 +1441,7 @@ int save_config(const config_t *config, const char *path) { // Check if directory exists and is writable char dir_path[MAX_PATH_LENGTH]; - strncpy(dir_path, save_path, MAX_PATH_LENGTH - 1); - dir_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(dir_path, save_path, MAX_PATH_LENGTH, 0); // Get directory part char *last_slash = strrchr(dir_path, '/'); @@ -1502,8 +1476,7 @@ int save_config(const config_t *config, const char *path) { char validated_filename[MAX_PATH_LENGTH]; { char tmp[MAX_PATH_LENGTH]; - strncpy(tmp, save_path, MAX_PATH_LENGTH - 1); - tmp[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(tmp, save_path, MAX_PATH_LENGTH, 0); const char *fname; diff --git a/src/core/daemon.c b/src/core/daemon.c index 7fb7f56e..d697238f 100644 --- a/src/core/daemon.c +++ b/src/core/daemon.c @@ -17,11 +17,12 @@ #include "web/web_server.h" #include "core/logger.h" #include "core/daemon.h" +#include "utils/strings.h" extern volatile bool running; // Reference to the global variable defined in main.c // Global variable to store PID file path -static char pid_file_path[256] = "/run/lightnvr.pid"; +static char pid_file_path[MAX_PATH_LENGTH] = "/run/lightnvr.pid"; // Forward declarations static void daemon_signal_handler(int sig); @@ -32,8 +33,7 @@ static int check_running_daemon(const char *pid_file); int init_daemon(const char *pid_file) { // Store PID file path if (pid_file) { - strncpy(pid_file_path, pid_file, sizeof(pid_file_path) - 1); - pid_file_path[sizeof(pid_file_path) - 1] = '\0'; + safe_strcpy(pid_file_path, pid_file, sizeof(pid_file_path), 0); } // Check if daemon is already running @@ -211,9 +211,9 @@ static int write_pid_file(const char *pid_file) { // Make sure the directory exists const char *last_slash = strrchr(pid_file, '/'); if (last_slash) { - char dir_path[256] = {0}; + char dir_path[MAX_PATH_LENGTH] = {0}; size_t dir_len = (size_t)(last_slash - pid_file); - strncpy(dir_path, pid_file, dir_len); + memcpy(dir_path, pid_file, dir_len); dir_path[dir_len] = '\0'; // Create directory if it doesn't exist @@ -359,14 +359,12 @@ static int check_running_daemon(const char *pid_file) { // Stop running daemon int stop_daemon(const char *pid_file) { - char file_path[256]; + char file_path[MAX_PATH_LENGTH]; if (pid_file) { - strncpy(file_path, pid_file, sizeof(file_path) - 1); - file_path[sizeof(file_path) - 1] = '\0'; + safe_strcpy(file_path, pid_file, sizeof(file_path), 0); } else { - strncpy(file_path, pid_file_path, sizeof(file_path) - 1); - file_path[sizeof(file_path) - 1] = '\0'; + safe_strcpy(file_path, pid_file_path, sizeof(file_path), 0); } // Open and read PID file @@ -510,14 +508,12 @@ int stop_daemon(const char *pid_file) { // Get status of daemon int daemon_status(const char *pid_file) { - char file_path[256]; + char file_path[MAX_PATH_LENGTH]; if (pid_file) { - strncpy(file_path, pid_file, sizeof(file_path) - 1); - file_path[sizeof(file_path) - 1] = '\0'; + safe_strcpy(file_path, pid_file, sizeof(file_path), 0); } else { - strncpy(file_path, pid_file_path, sizeof(file_path) - 1); - file_path[sizeof(file_path) - 1] = '\0'; + safe_strcpy(file_path, pid_file_path, sizeof(file_path), 0); } // First try to open the file with exclusive locking diff --git a/src/core/logger.c b/src/core/logger.c index bb6e4dbf..b1c4b139 100644 --- a/src/core/logger.c +++ b/src/core/logger.c @@ -18,6 +18,7 @@ #include "core/config.h" #include "core/logger.h" #include "core/logger_json.h" +#include "utils/strings.h" // Logger state static struct { @@ -55,10 +56,8 @@ static __thread char tls_log_component[64] = {0}; static __thread char tls_log_stream[128] = {0}; void log_set_thread_context(const char *component, const char *stream_name) { - strncpy(tls_log_component, component ? component : "", sizeof(tls_log_component) - 1); - tls_log_component[sizeof(tls_log_component) - 1] = '\0'; - strncpy(tls_log_stream, stream_name ? stream_name : "", sizeof(tls_log_stream) - 1); - tls_log_stream[sizeof(tls_log_stream) - 1] = '\0'; + safe_strcpy(tls_log_component, component ? component : "", sizeof(tls_log_component), 0); + safe_strcpy(tls_log_stream, stream_name ? stream_name : "", sizeof(tls_log_stream), 0); } void log_clear_thread_context(void) { @@ -263,8 +262,7 @@ int set_log_file(const char *filename) { } // Store filename for potential log rotation - strncpy(logger.log_filename, filename, sizeof(logger.log_filename) - 1); - logger.log_filename[sizeof(logger.log_filename) - 1] = '\0'; + safe_strcpy(logger.log_filename, filename, sizeof(logger.log_filename), 0); pthread_mutex_unlock(&logger.mutex); @@ -605,8 +603,7 @@ int enable_syslog(const char *ident, int facility) { } // Store the identifier - strncpy(logger.syslog_ident, ident, sizeof(logger.syslog_ident) - 1); - logger.syslog_ident[sizeof(logger.syslog_ident) - 1] = '\0'; + safe_strcpy(logger.syslog_ident, ident, sizeof(logger.syslog_ident), 0); // Open syslog connection // LOG_PID: include PID with each message diff --git a/src/core/logger_json.c b/src/core/logger_json.c index 89d76a25..fc46bede 100644 --- a/src/core/logger_json.c +++ b/src/core/logger_json.c @@ -13,6 +13,7 @@ #include "core/config.h" #include "core/logger.h" #include "core/logger_json.h" +#include "utils/strings.h" #include // JSON logger state @@ -120,8 +121,7 @@ int init_json_logger(const char *filename) { } // Store filename for potential log rotation - strncpy(json_logger.log_filename, filename, sizeof(json_logger.log_filename) - 1); - json_logger.log_filename[sizeof(json_logger.log_filename) - 1] = '\0'; + safe_strcpy(json_logger.log_filename, filename, sizeof(json_logger.log_filename), 0); json_logger.initialized = 1; diff --git a/src/core/main.c b/src/core/main.c index 9cebb42e..dd5cfb4e 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -23,7 +23,9 @@ #include "core/logger.h" #include "core/daemon.h" #include "core/shutdown_coordinator.h" +#include "core/curl_init.h" #include "core/mqtt_client.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/stream_state.h" #include "video/stream_state_adapter.h" @@ -42,7 +44,6 @@ #include "video/onvif_discovery.h" #include "video/ffmpeg_leak_detector.h" #include "video/onvif_motion_recording.h" -#include "core/curl_init.h" #include "telemetry/stream_metrics.h" #include "telemetry/player_telemetry.h" @@ -358,10 +359,9 @@ static int create_pid_file(const char *pid_file) { // Make sure the directory exists const char *last_slash = strrchr(pid_file, '/'); if (last_slash) { - char dir_path[MAX_PATH_LENGTH] = {0}; + char dir_path[MAX_PATH_LENGTH]; size_t dir_len = (size_t)(last_slash - pid_file); - strncpy(dir_path, pid_file, dir_len); - dir_path[dir_len] = '\0'; + safe_strcpy(dir_path, pid_file, MAX_PATH_LENGTH, dir_len); // Create directory if it doesn't exist struct stat st; @@ -549,8 +549,7 @@ int main(int argc, char *argv[]) { } else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) { if (i + 1 < argc) { // Set config file path - strncpy(custom_config_path, argv[i+1], MAX_PATH_LENGTH - 1); - custom_config_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(custom_config_path, argv[i+1], MAX_PATH_LENGTH, 0); i++; } else { log_error("Missing config file path"); @@ -757,7 +756,7 @@ int main(int argc, char *argv[]) { // Create parent directory for symlink if needed char parent_dir[MAX_PATH_LENGTH]; - strncpy(parent_dir, config.web_root, sizeof(parent_dir) - 1); + safe_strcpy(parent_dir, config.web_root, sizeof(parent_dir), 0); char *last_slash = strrchr(parent_dir, '/'); if (last_slash) { *last_slash = '\0'; @@ -772,7 +771,7 @@ int main(int argc, char *argv[]) { config.web_root, storage_web_path, strerror(errno)); // Fall back to using the storage path directly - strncpy(config.web_root, storage_web_path, MAX_PATH_LENGTH - 1); + safe_strcpy(config.web_root, storage_web_path, MAX_PATH_LENGTH, 0); log_warn("Using storage path directly for web root: %s", config.web_root); } else { log_info("Created symlink from %s to %s", config.web_root, storage_web_path); @@ -923,13 +922,13 @@ int main(int argc, char *argv[]) { }; // Set CORS allowed origins, methods, and headers - strncpy(server_config.allowed_origins, "*", sizeof(server_config.allowed_origins) - 1); - strncpy(server_config.allowed_methods, "GET, POST, PUT, DELETE, OPTIONS", sizeof(server_config.allowed_methods) - 1); - strncpy(server_config.allowed_headers, "Content-Type, Authorization", sizeof(server_config.allowed_headers) - 1); + safe_strcpy(server_config.allowed_origins, "*", sizeof(server_config.allowed_origins), 0); + safe_strcpy(server_config.allowed_methods, "GET, POST, PUT, DELETE, OPTIONS", sizeof(server_config.allowed_methods), 0); + safe_strcpy(server_config.allowed_headers, "Content-Type, Authorization", sizeof(server_config.allowed_headers), 0); if (config.web_auth_enabled) { - strncpy(server_config.username, config.web_username, sizeof(server_config.username) - 1); - strncpy(server_config.password, config.web_password, sizeof(server_config.password) - 1); + safe_strcpy(server_config.username, config.web_username, sizeof(server_config.username), 0); + safe_strcpy(server_config.password, config.web_password, sizeof(server_config.password), 0); } // Initialize HTTP server (libuv + llhttp) @@ -1017,8 +1016,7 @@ int main(int argc, char *argv[]) { if (is_api_based) { // For API-based or built-in detection (motion, onvif), use the model string as-is - strncpy(model_path, config.streams[i].detection_model, MAX_PATH_LENGTH - 1); - model_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(model_path, config.streams[i].detection_model, MAX_PATH_LENGTH, 0); log_info("Using built-in/API detection for stream %s: %s", config.streams[i].name, model_path); } else if (config.streams[i].detection_model[0] != '/') { @@ -1048,8 +1046,7 @@ int main(int argc, char *argv[]) { } } else { // Absolute path - strncpy(model_path, config.streams[i].detection_model, MAX_PATH_LENGTH - 1); - model_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(model_path, config.streams[i].detection_model, MAX_PATH_LENGTH, 0); // Check if file exists FILE *model_file = fopen(model_path, "r"); diff --git a/src/core/mqtt_client.c b/src/core/mqtt_client.c index 1b6db734..fc5801d8 100644 --- a/src/core/mqtt_client.c +++ b/src/core/mqtt_client.c @@ -16,6 +16,7 @@ #include "core/path_utils.h" #include "core/version.h" #include "database/db_streams.h" +#include "utils/strings.h" #include "video/go2rtc/go2rtc_snapshot.h" #define MAX_TOPIC_LENGTH 512 @@ -758,7 +759,7 @@ void mqtt_set_motion_state(const char *stream_name, const detection_result_t *re if (!state && num_motion_states < MAX_MOTION_STREAMS) { state = &motion_states[num_motion_states++]; memset(state, 0, sizeof(*state)); - strncpy(state->stream_name, stream_name, sizeof(state->stream_name) - 1); + safe_strcpy(state->stream_name, stream_name, sizeof(state->stream_name), 0); } if (!state) { @@ -792,8 +793,7 @@ void mqtt_set_motion_state(const char *stream_name, const detection_result_t *re } if (label_idx < 0 && state->num_labels < 32) { label_idx = state->num_labels++; - strncpy(state->object_labels[label_idx], label, - sizeof(state->object_labels[0]) - 1); + safe_strcpy(state->object_labels[label_idx], label, sizeof(state->object_labels[0]), 0); } if (label_idx >= 0) { state->object_counts[label_idx]++; @@ -920,8 +920,7 @@ static void *ha_motion_thread_func(void *arg) { motion_states[i].motion_active = false; char stream_name[256]; - strncpy(stream_name, motion_states[i].stream_name, sizeof(stream_name) - 1); - stream_name[sizeof(stream_name) - 1] = '\0'; + safe_strcpy(stream_name, motion_states[i].stream_name, sizeof(stream_name), 0); char safe_name[256]; sanitize_stream_name(stream_name, safe_name, sizeof(safe_name)); diff --git a/src/core/shutdown_coordinator.c b/src/core/shutdown_coordinator.c index abee227d..a98e8d89 100644 --- a/src/core/shutdown_coordinator.c +++ b/src/core/shutdown_coordinator.c @@ -11,6 +11,7 @@ #include "core/logger.h" #include "core/shutdown_coordinator.h" +#include "utils/strings.h" // Global shutdown coordinator instance static shutdown_coordinator_t g_coordinator; @@ -108,8 +109,7 @@ int register_component(const char *name, component_type_t type, void *context, i // Initialize the component slot component_info_t *component = &g_coordinator.components[slot]; - strncpy(component->name, name, sizeof(component->name) - 1); - component->name[sizeof(component->name) - 1] = '\0'; + safe_strcpy(component->name, name, sizeof(component->name), 0); component->type = type; atomic_store(&component->state, COMPONENT_RUNNING); component->context = context; diff --git a/src/core/url_utils.c b/src/core/url_utils.c index dc67e748..e864ca83 100644 --- a/src/core/url_utils.c +++ b/src/core/url_utils.c @@ -34,14 +34,11 @@ static int split_url_base(const char *url, char **base_url, const char **suffix) const char *fragment = strchr(url, '#'); size_t base_len = fragment ? (size_t)(fragment - url) : strlen(url); - char *base = malloc(base_len + 1); + char *base = strndup(url, base_len); if (!base) { return -1; } - memcpy(base, url, base_len); - base[base_len] = '\0'; - *base_url = base; if (suffix) { *suffix = fragment; diff --git a/src/database/db_auth.c b/src/database/db_auth.c index 1bafa4d2..9e7ce5c2 100644 --- a/src/database/db_auth.c +++ b/src/database/db_auth.c @@ -20,6 +20,7 @@ #include "database/db_schema_cache.h" // For cached_column_exists #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" // For password hashing #include @@ -104,33 +105,16 @@ static bool tracking_value_differs(const char *stored_value, const char *current return strcmp(normalized_stored, current_value) != 0; } -static char *trim_ascii_whitespace(char *value) { - if (!value) { - return NULL; - } - - while (*value && isspace((unsigned char)*value)) { - value++; - } - - size_t len = strlen(value); - while (len > 0 && isspace((unsigned char)value[len - 1])) { - value[--len] = '\0'; - } - - return value; -} - static int parse_cidr_entry(const char *cidr, int *family, unsigned char *network, int *prefix_len) { if (!cidr || !family || !network || !prefix_len) { return -1; } - char entry[INET6_ADDRSTRLEN + 5] = {0}; + char entry[INET6_ADDRSTRLEN + 5]; if (strlen(cidr) >= sizeof(entry)) { return -1; } - strncpy(entry, cidr, sizeof(entry) - 1); + safe_strcpy(entry, cidr, sizeof(entry), 0); char *trimmed = trim_ascii_whitespace(entry); if (!trimmed || trimmed[0] == '\0') { @@ -209,8 +193,8 @@ static int normalize_allowed_login_cidrs(const char *allowed_login_cidrs, return -1; } - char input[USER_ALLOWED_LOGIN_CIDRS_MAX] = {0}; - strncpy(input, allowed_login_cidrs, sizeof(input) - 1); + char input[USER_ALLOWED_LOGIN_CIDRS_MAX]; + safe_strcpy(input, allowed_login_cidrs, sizeof(input), 0); char *saveptr = NULL; for (char *token = strtok_r(input, ",\n", &saveptr); @@ -305,7 +289,7 @@ static int prepare_user_lookup_stmt(sqlite3 *db, const char *where_clause, sqlit bool has_allowed_tags = cached_column_exists("users", "allowed_tags"); bool has_allowed_login_cidrs = cached_column_exists("users", "allowed_login_cidrs"); - char sql[768] = {0}; + char sql[768]; int written = snprintf(sql, sizeof(sql), "SELECT id, username, email, role, api_key, created_at, " "updated_at, last_login, is_active, password_change_locked, %s, %s, %s " @@ -325,21 +309,18 @@ static void populate_user_from_stmt(sqlite3_stmt *stmt, user_t *user) { memset(user, 0, sizeof(*user)); user->id = sqlite3_column_int64(stmt, 0); - strncpy(user->username, (const char *)sqlite3_column_text(stmt, 1), sizeof(user->username) - 1); - user->username[sizeof(user->username) - 1] = '\0'; + safe_strcpy(user->username, (const char *)sqlite3_column_text(stmt, 1), sizeof(user->username), 0); const char *email = (const char *)sqlite3_column_text(stmt, 2); if (email) { - strncpy(user->email, email, sizeof(user->email) - 1); - user->email[sizeof(user->email) - 1] = '\0'; + safe_strcpy(user->email, email, sizeof(user->email), 0); } user->role = (user_role_t)sqlite3_column_int(stmt, 3); const char *api_key = (const char *)sqlite3_column_text(stmt, 4); if (api_key) { - strncpy(user->api_key, api_key, sizeof(user->api_key) - 1); - user->api_key[sizeof(user->api_key) - 1] = '\0'; + safe_strcpy(user->api_key, api_key, sizeof(user->api_key), 0); } user->created_at = sqlite3_column_int64(stmt, 5); @@ -351,15 +332,13 @@ static void populate_user_from_stmt(sqlite3_stmt *stmt, user_t *user) { const char *allowed_tags = (const char *)sqlite3_column_text(stmt, 11); if (allowed_tags && allowed_tags[0] != '\0') { - strncpy(user->allowed_tags, allowed_tags, sizeof(user->allowed_tags) - 1); - user->allowed_tags[sizeof(user->allowed_tags) - 1] = '\0'; + safe_strcpy(user->allowed_tags, allowed_tags, sizeof(user->allowed_tags), 0); user->has_tag_restriction = true; } const char *allowed_login_cidrs = (const char *)sqlite3_column_text(stmt, 12); if (allowed_login_cidrs && allowed_login_cidrs[0] != '\0') { - strncpy(user->allowed_login_cidrs, allowed_login_cidrs, sizeof(user->allowed_login_cidrs) - 1); - user->allowed_login_cidrs[sizeof(user->allowed_login_cidrs) - 1] = '\0'; + safe_strcpy(user->allowed_login_cidrs, allowed_login_cidrs, sizeof(user->allowed_login_cidrs), 0); user->has_login_cidr_restriction = true; } } @@ -742,22 +721,22 @@ int db_auth_update_user(int64_t user_id, const char *username, const char *email char query[512] = "UPDATE users SET updated_at = ?"; if (username) { - strncat(query, ", username = ?", sizeof(query) - strlen(query) - 1); + safe_strcat(query, ", username = ?", sizeof(query)); } if (email) { - strncat(query, ", email = ?", sizeof(query) - strlen(query) - 1); + safe_strcat(query, ", email = ?", sizeof(query)); } if (role >= 0) { - strncat(query, ", role = ?", sizeof(query) - strlen(query) - 1); + safe_strcat(query, ", role = ?", sizeof(query)); } if (is_active >= 0) { - strncat(query, ", is_active = ?", sizeof(query) - strlen(query) - 1); + safe_strcat(query, ", is_active = ?", sizeof(query)); } - strncat(query, " WHERE id = ?;", sizeof(query) - strlen(query) - 1); + safe_strcat(query, " WHERE id = ?;", sizeof(query)); // Prepare the statement rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL); @@ -1885,9 +1864,9 @@ int db_auth_list_user_sessions(int64_t user_id, session_t *sessions, int max_cou const char *token = (const char *)sqlite3_column_text(stmt, 2); const char *ip = (const char *)sqlite3_column_text(stmt, 7); const char *ua = (const char *)sqlite3_column_text(stmt, 8); - if (token) strncpy(session->token, token, sizeof(session->token) - 1); - if (ip) strncpy(session->ip_address, ip, sizeof(session->ip_address) - 1); - if (ua) strncpy(session->user_agent, ua, sizeof(session->user_agent) - 1); + if (token) safe_strcpy(session->token, token, sizeof(session->token), 0); + if (ip) safe_strcpy(session->ip_address, ip, sizeof(session->ip_address), 0); + if (ua) safe_strcpy(session->user_agent, ua, sizeof(session->user_agent), 0); session->created_at = sqlite3_column_int64(stmt, 3); session->last_activity_at = sqlite3_column_int64(stmt, 4); session->idle_expires_at = sqlite3_column_int64(stmt, 5); @@ -2100,8 +2079,8 @@ int db_auth_list_trusted_devices(int64_t user_id, trusted_device_t *devices, int device->user_id = sqlite3_column_int64(stmt, 1); const char *ip = (const char *)sqlite3_column_text(stmt, 5); const char *ua = (const char *)sqlite3_column_text(stmt, 6); - if (ip) strncpy(device->ip_address, ip, sizeof(device->ip_address) - 1); - if (ua) strncpy(device->user_agent, ua, sizeof(device->user_agent) - 1); + if (ip) safe_strcpy(device->ip_address, ip, sizeof(device->ip_address), 0); + if (ua) safe_strcpy(device->user_agent, ua, sizeof(device->user_agent), 0); device->created_at = sqlite3_column_int64(stmt, 2); device->last_used_at = sqlite3_column_int64(stmt, 3); device->expires_at = sqlite3_column_int64(stmt, 4); @@ -2209,8 +2188,7 @@ int db_auth_get_totp_info(int64_t user_id, char *secret, size_t secret_size, boo const char *db_secret = (const char *)sqlite3_column_text(stmt, 0); if (db_secret && db_secret[0] != '\0') { - strncpy(secret, db_secret, secret_size - 1); - secret[secret_size - 1] = '\0'; + safe_strcpy(secret, db_secret, secret_size, 0); } *enabled = sqlite3_column_int(stmt, 1) != 0; @@ -2419,10 +2397,8 @@ bool db_auth_ip_allowed_for_user(const user_t *user, const char *client_ip) { return false; } - char cidr_list[USER_ALLOWED_LOGIN_CIDRS_MAX] = {0}; - size_t cidr_len = strnlen(user->allowed_login_cidrs, sizeof(cidr_list) - 1); - memcpy(cidr_list, user->allowed_login_cidrs, cidr_len); - cidr_list[cidr_len] = '\0'; + char cidr_list[USER_ALLOWED_LOGIN_CIDRS_MAX]; + safe_strcpy(cidr_list, user->allowed_login_cidrs, USER_ALLOWED_LOGIN_CIDRS_MAX, 0); char *saveptr = NULL; for (char *token = strtok_r(cidr_list, ",\n", &saveptr); @@ -2458,8 +2434,7 @@ bool db_auth_stream_allowed_for_user(const user_t *user, const char *stream_tags // Tokenize stream_tags and check each against user's allowed_tags char stream_copy[256]; - strncpy(stream_copy, stream_tags, sizeof(stream_copy) - 1); - stream_copy[sizeof(stream_copy) - 1] = '\0'; + safe_strcpy(stream_copy, stream_tags, sizeof(stream_copy), 0); char *saveptr = NULL; char *token = strtok_r(stream_copy, ",", &saveptr); diff --git a/src/database/db_core.c b/src/database/db_core.c index cdfab9ef..6545fd3f 100644 --- a/src/database/db_core.c +++ b/src/database/db_core.c @@ -26,6 +26,7 @@ #include "database/db_backup.h" #include "core/config.h" #include "core/logger.h" +#include "utils/strings.h" // Database handle static sqlite3 *db = NULL; @@ -34,10 +35,10 @@ static sqlite3 *db = NULL; static pthread_mutex_t db_mutex = PTHREAD_MUTEX_INITIALIZER; // Database path for backup/recovery operations -static char db_file_path[1024] = {0}; +static char db_file_path[PATH_MAX] = {0}; // Backup file path -static char db_backup_path[1024] = {0}; +static char db_backup_path[PATH_MAX] = {0}; // Last backup time static time_t last_backup_time = 0; @@ -572,8 +573,7 @@ int init_database(const char *db_path) { sqlite3_soft_heap_limit64((sqlite3_int64)8 * 1024 * 1024); // 8MB soft limit // Store the database path for backup/recovery operations - strncpy(db_file_path, db_path, sizeof(db_file_path) - 1); - db_file_path[sizeof(db_file_path) - 1] = '\0'; + safe_strcpy(db_file_path, db_path, sizeof(db_file_path), 0); // Create backup path by appending .bak to the database path snprintf(db_backup_path, sizeof(db_backup_path), "%s.bak", db_path); @@ -688,33 +688,25 @@ int init_database(const char *db_path) { // Make a copy of the directory name before freeing dir_path char *dir = dirname(dir_path); - char *dir_copy = strdup(dir); - if (!dir_copy) { - log_error("Failed to allocate memory for directory name copy"); - free(dir_path); - return -1; - } - log_info("Creating database directory if needed: %s", dir_copy); - if (create_directory(dir_copy) != 0) { - log_error("Failed to create database directory: %s", dir_copy); + log_info("Creating database directory if needed: %s", dir); + if (create_directory(dir) != 0) { + log_error("Failed to create database directory: %s", dir); free(dir_path); - free(dir_copy); return -1; } - free(dir_path); // Check directory permissions struct stat st; - if (stat(dir_copy, &st) == 0) { + if (stat(dir, &st) == 0) { log_info("Database directory permissions: %o", st.st_mode & 0777); if ((st.st_mode & 0200) == 0) { log_warn("Database directory is not writable"); } } - // Free the directory name copy - free(dir_copy); + // Free the directory path, invalidating the dir pointer + free(dir_path); // Open database with extended options for better error handling log_info("Opening database at: %s", db_path); diff --git a/src/database/db_detections.c b/src/database/db_detections.c index 8e699b11..1ba2cf47 100644 --- a/src/database/db_detections.c +++ b/src/database/db_detections.c @@ -13,6 +13,7 @@ #include "database/db_detections.h" #include "database/db_core.h" #include "core/logger.h" +#include "utils/strings.h" #include "video/detection_result.h" /** @@ -329,10 +330,9 @@ int get_detections_from_db_time_range(const char *stream_name, detection_result_ // Store in result if (label) { - strncpy(result->detections[count].label, label, MAX_LABEL_LENGTH - 1); - result->detections[count].label[MAX_LABEL_LENGTH - 1] = '\0'; + safe_strcpy(result->detections[count].label, label, MAX_LABEL_LENGTH, 0); } else { - strncpy(result->detections[count].label, "unknown", MAX_LABEL_LENGTH - 1); + safe_strcpy(result->detections[count].label, "unknown", MAX_LABEL_LENGTH, 0); } result->detections[count].confidence = confidence; @@ -704,8 +704,7 @@ int get_detection_labels_summary(const char *stream_name, time_t start_time, tim int label_count = sqlite3_column_int(stmt, 1); if (label) { - strncpy(labels[count].label, label, MAX_LABEL_LENGTH - 1); - labels[count].label[MAX_LABEL_LENGTH - 1] = '\0'; + safe_strcpy(labels[count].label, label, MAX_LABEL_LENGTH, 0); labels[count].count = label_count; count++; } @@ -766,8 +765,7 @@ int get_all_unique_detection_labels(char labels[][MAX_LABEL_LENGTH], int max_lab const char *label = (const char *)sqlite3_column_text(stmt, 0); if (label) { - strncpy(labels[count], label, MAX_LABEL_LENGTH - 1); - labels[count][MAX_LABEL_LENGTH - 1] = '\0'; + safe_strcpy(labels[count], label, MAX_LABEL_LENGTH, 0); count++; } } diff --git a/src/database/db_events.c b/src/database/db_events.c index 88451ccc..224ea34b 100644 --- a/src/database/db_events.c +++ b/src/database/db_events.c @@ -12,6 +12,7 @@ #include "database/db_events.h" #include "database/db_core.h" #include "core/logger.h" +#include "utils/strings.h" // Add an event to the database uint64_t add_event(event_type_t type, const char *stream_name, @@ -103,26 +104,25 @@ int get_events(time_t start_time, time_t end_time, int type, // Build query based on filters char sql[1024]; - strncpy(sql, "SELECT id, type, timestamp, stream_name, description, details FROM events WHERE 1=1", sizeof(sql) - 1); - sql[sizeof(sql) - 1] = '\0'; + safe_strcpy(sql, "SELECT id, type, timestamp, stream_name, description, details FROM events WHERE 1=1", sizeof(sql), 0); if (start_time > 0) { - strncat(sql, " AND timestamp >= ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND timestamp >= ?", sizeof(sql)); } if (end_time > 0) { - strncat(sql, " AND timestamp <= ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND timestamp <= ?", sizeof(sql)); } if (type >= 0) { - strncat(sql, " AND type = ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND type = ?", sizeof(sql)); } if (stream_name) { - strncat(sql, " AND stream_name = ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND stream_name = ?", sizeof(sql)); } - strncat(sql, " ORDER BY timestamp DESC LIMIT ?;", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " ORDER BY timestamp DESC LIMIT ?;", sizeof(sql)); rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { @@ -160,24 +160,21 @@ int get_events(time_t start_time, time_t end_time, int type, const char *stream = (const char *)sqlite3_column_text(stmt, 3); if (stream) { - strncpy(events[count].stream_name, stream, sizeof(events[count].stream_name) - 1); - events[count].stream_name[sizeof(events[count].stream_name) - 1] = '\0'; + safe_strcpy(events[count].stream_name, stream, sizeof(events[count].stream_name), 0); } else { events[count].stream_name[0] = '\0'; } const char *desc = (const char *)sqlite3_column_text(stmt, 4); if (desc) { - strncpy(events[count].description, desc, sizeof(events[count].description) - 1); - events[count].description[sizeof(events[count].description) - 1] = '\0'; + safe_strcpy(events[count].description, desc, sizeof(events[count].description), 0); } else { events[count].description[0] = '\0'; } const char *details = (const char *)sqlite3_column_text(stmt, 5); if (details) { - strncpy(events[count].details, details, sizeof(events[count].details) - 1); - events[count].details[sizeof(events[count].details) - 1] = '\0'; + safe_strcpy(events[count].details, details, sizeof(events[count].details), 0); } else { events[count].details[0] = '\0'; } diff --git a/src/database/db_migrations.c b/src/database/db_migrations.c index 69bf71ad..9a65795c 100644 --- a/src/database/db_migrations.c +++ b/src/database/db_migrations.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "database/db_migrations.h" #include "database/sqlite_migrate.h" @@ -71,7 +72,7 @@ static const char *find_migrations_dir(void) { } // Try relative to executable - char exe_path[256]; + char exe_path[PATH_MAX]; ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); if (len > 0) { exe_path[len] = '\0'; @@ -79,7 +80,7 @@ static const char *find_migrations_dir(void) { char *last_slash = strrchr(exe_path, '/'); if (last_slash) { *last_slash = '\0'; - static char migrations_path[512] = {0}; + static char migrations_path[PATH_MAX]; snprintf(migrations_path, sizeof(migrations_path), "%s/../share/lightnvr/migrations", exe_path); if (access(migrations_path, R_OK) == 0) { diff --git a/src/database/db_motion_config.c b/src/database/db_motion_config.c index dc5521a3..47f0fe3f 100644 --- a/src/database/db_motion_config.c +++ b/src/database/db_motion_config.c @@ -1,6 +1,7 @@ #include "database/db_motion_config.h" #include "database/db_core.h" #include "core/logger.h" +#include "utils/strings.h" #include #include #include @@ -104,14 +105,12 @@ int load_motion_config(const char *stream_name, motion_recording_config_t *confi const char *codec = (const char *)sqlite3_column_text(stmt, 4); if (codec) { - strncpy(config->codec, codec, sizeof(config->codec) - 1); - config->codec[sizeof(config->codec) - 1] = '\0'; + safe_strcpy(config->codec, codec, sizeof(config->codec), 0); } const char *quality = (const char *)sqlite3_column_text(stmt, 5); if (quality) { - strncpy(config->quality, quality, sizeof(config->quality) - 1); - config->quality[sizeof(config->quality) - 1] = '\0'; + safe_strcpy(config->quality, quality, sizeof(config->quality), 0); } config->retention_days = sqlite3_column_int(stmt, 6); @@ -207,8 +206,7 @@ int load_all_motion_configs(motion_recording_config_t *configs, char stream_name while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && count < max_count) { const char *stream_name = (const char *)sqlite3_column_text(stmt, 0); if (stream_name) { - strncpy(stream_names[count], stream_name, 255); - stream_names[count][255] = '\0'; + safe_strcpy(stream_names[count], stream_name, 256, 0); configs[count].enabled = sqlite3_column_int(stmt, 1) != 0; configs[count].pre_buffer_seconds = sqlite3_column_int(stmt, 2); @@ -217,14 +215,12 @@ int load_all_motion_configs(motion_recording_config_t *configs, char stream_name const char *codec = (const char *)sqlite3_column_text(stmt, 5); if (codec) { - strncpy(configs[count].codec, codec, sizeof(configs[count].codec) - 1); - configs[count].codec[sizeof(configs[count].codec) - 1] = '\0'; + safe_strcpy(configs[count].codec, codec, sizeof(configs[count].codec), 0); } const char *quality = (const char *)sqlite3_column_text(stmt, 6); if (quality) { - strncpy(configs[count].quality, quality, sizeof(configs[count].quality) - 1); - configs[count].quality[sizeof(configs[count].quality) - 1] = '\0'; + safe_strcpy(configs[count].quality, quality, sizeof(configs[count].quality), 0); } configs[count].retention_days = sqlite3_column_int(stmt, 7); @@ -435,7 +431,7 @@ int get_motion_recording_db_stats(const char *stream_name, int get_motion_recordings_list(const char *stream_name, time_t start_time, time_t end_time, - char paths[][512], + char paths[][MAX_PATH_LENGTH], time_t *timestamps, uint64_t *sizes, int max_count) { @@ -487,8 +483,7 @@ int get_motion_recordings_list(const char *stream_name, while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && count < max_count) { const char *file_path = (const char *)sqlite3_column_text(stmt, 0); if (file_path) { - strncpy(paths[count], file_path, 511); - paths[count][511] = '\0'; + safe_strcpy(paths[count], file_path, MAX_PATH_LENGTH, 0); timestamps[count] = sqlite3_column_int64(stmt, 1); sizes[count] = sqlite3_column_int64(stmt, 2); count++; diff --git a/src/database/db_query_builder.c b/src/database/db_query_builder.c index 9efde325..8dc73b58 100644 --- a/src/database/db_query_builder.c +++ b/src/database/db_query_builder.c @@ -13,6 +13,7 @@ #include "database/db_query_builder.h" #include "database/db_schema_cache.h" #include "core/logger.h" +#include "utils/strings.h" int qb_init(query_builder_t *qb, const char *table_name) { if (!qb || !table_name) { @@ -38,8 +39,7 @@ int qb_add_column(query_builder_t *qb, const char *column_name, bool is_required } column_info_t *col = &qb->columns[qb->column_count]; - strncpy(col->name, column_name, sizeof(col->name) - 1); - col->name[sizeof(col->name) - 1] = '\0'; + safe_strcpy(col->name, column_name, sizeof(col->name), 0); // Check if column exists using the cached schema lookup col->present = cached_column_exists(qb->table_name, column_name); @@ -180,8 +180,7 @@ const char *qb_get_text(sqlite3_stmt *stmt, const query_builder_t *qb, int idx = qb_get_column_index(qb, column_name); if (idx < 0) { if (default_value && buffer && buffer_len > 0) { - strncpy(buffer, default_value, buffer_len - 1); - buffer[buffer_len - 1] = '\0'; + safe_strcpy(buffer, default_value, buffer_len, 0); } return default_value; } @@ -189,15 +188,13 @@ const char *qb_get_text(sqlite3_stmt *stmt, const query_builder_t *qb, const char *text = (const char *)sqlite3_column_text(stmt, idx); if (!text) { if (default_value && buffer && buffer_len > 0) { - strncpy(buffer, default_value, buffer_len - 1); - buffer[buffer_len - 1] = '\0'; + safe_strcpy(buffer, default_value, buffer_len, 0); } return default_value; } if (buffer && buffer_len > 0) { - strncpy(buffer, text, buffer_len - 1); - buffer[buffer_len - 1] = '\0'; + safe_strcpy(buffer, text, buffer_len, 0); } return text; diff --git a/src/database/db_recording_tags.c b/src/database/db_recording_tags.c index 04d6c7ee..af648a63 100644 --- a/src/database/db_recording_tags.c +++ b/src/database/db_recording_tags.c @@ -11,26 +11,16 @@ #include "database/db_recording_tags.h" #include "database/db_core.h" #include "core/logger.h" - -/* ---- helpers ---- */ - -/** Trim leading/trailing whitespace in-place into a stack buffer. */ -static void trim_tag(const char *src, char *dst, size_t dst_size) { - while (*src && isspace((unsigned char)*src)) src++; - size_t len = strlen(src); - while (len > 0 && isspace((unsigned char)src[len - 1])) len--; - if (len >= dst_size) len = dst_size - 1; - memcpy(dst, src, len); - dst[len] = '\0'; -} +#include "utils/strings.h" /* ---- single-recording ops ---- */ int db_recording_tag_add(uint64_t recording_id, const char *tag) { if (!tag) return -1; char trimmed[MAX_TAG_LENGTH]; - trim_tag(tag, trimmed, sizeof(trimmed)); - if (trimmed[0] == '\0') return -1; + if (copy_trimmed_value(trimmed, sizeof(trimmed), tag, 0) == 0) { + return -1; + } sqlite3 *db = get_db_handle(); pthread_mutex_t *mtx = get_db_mutex(); @@ -58,8 +48,9 @@ int db_recording_tag_add(uint64_t recording_id, const char *tag) { int db_recording_tag_remove(uint64_t recording_id, const char *tag) { if (!tag) return -1; char trimmed[MAX_TAG_LENGTH]; - trim_tag(tag, trimmed, sizeof(trimmed)); - if (trimmed[0] == '\0') return -1; + if (copy_trimmed_value(trimmed, sizeof(trimmed), tag, 0) == 0) { + return -1; + } sqlite3 *db = get_db_handle(); pthread_mutex_t *mtx = get_db_mutex(); @@ -105,8 +96,7 @@ int db_recording_tag_get(uint64_t recording_id, char tags[][MAX_TAG_LENGTH], int while (sqlite3_step(stmt) == SQLITE_ROW && count < max_tags) { const char *t = (const char *)sqlite3_column_text(stmt, 0); if (t) { - strncpy(tags[count], t, MAX_TAG_LENGTH - 1); - tags[count][MAX_TAG_LENGTH - 1] = '\0'; + safe_strcpy(tags[count], t, MAX_TAG_LENGTH, 0); count++; } } @@ -149,8 +139,9 @@ int db_recording_tag_set(uint64_t recording_id, const char **tags, int tag_count for (int i = 0; i < tag_count; i++) { if (!tags[i]) continue; char trimmed[MAX_TAG_LENGTH]; - trim_tag(tags[i], trimmed, sizeof(trimmed)); - if (trimmed[0] == '\0') continue; + if (copy_trimmed_value(trimmed, sizeof(trimmed), tags[i], 0) == 0) { + continue; + } sqlite3_reset(ins_stmt); sqlite3_bind_int64(ins_stmt, 1, (sqlite3_int64)recording_id); sqlite3_bind_text(ins_stmt, 2, trimmed, -1, SQLITE_STATIC); @@ -183,8 +174,7 @@ int db_recording_tag_get_all_unique(char tags[][MAX_TAG_LENGTH], int max_tags) { while (sqlite3_step(stmt) == SQLITE_ROW && count < max_tags) { const char *t = (const char *)sqlite3_column_text(stmt, 0); if (t) { - strncpy(tags[count], t, MAX_TAG_LENGTH - 1); - tags[count][MAX_TAG_LENGTH - 1] = '\0'; + safe_strcpy(tags[count], t, MAX_TAG_LENGTH, 0); count++; } } @@ -196,8 +186,9 @@ int db_recording_tag_get_all_unique(char tags[][MAX_TAG_LENGTH], int max_tags) { int db_recording_tag_batch_add(const uint64_t *recording_ids, int count, const char *tag) { if (!recording_ids || !tag || count <= 0) return -1; char trimmed[MAX_TAG_LENGTH]; - trim_tag(tag, trimmed, sizeof(trimmed)); - if (trimmed[0] == '\0') return -1; + if (copy_trimmed_value(trimmed, sizeof(trimmed), tag, 0) == 0) { + return -1; + } sqlite3 *db = get_db_handle(); pthread_mutex_t *mtx = get_db_mutex(); @@ -229,8 +220,9 @@ int db_recording_tag_batch_add(const uint64_t *recording_ids, int count, const c int db_recording_tag_batch_remove(const uint64_t *recording_ids, int count, const char *tag) { if (!recording_ids || !tag || count <= 0) return -1; char trimmed[MAX_TAG_LENGTH]; - trim_tag(tag, trimmed, sizeof(trimmed)); - if (trimmed[0] == '\0') return -1; + if (copy_trimmed_value(trimmed, sizeof(trimmed), tag, 0) == 0) { + return -1; + } sqlite3 *db = get_db_handle(); pthread_mutex_t *mtx = get_db_mutex(); @@ -262,8 +254,9 @@ int db_recording_tag_batch_remove(const uint64_t *recording_ids, int count, cons int db_recording_tag_get_recordings_by_tag(const char *tag, uint64_t *recording_ids, int max_ids) { if (!tag || !recording_ids) return -1; char trimmed[MAX_TAG_LENGTH]; - trim_tag(tag, trimmed, sizeof(trimmed)); - if (trimmed[0] == '\0') return -1; + if (copy_trimmed_value(trimmed, sizeof(trimmed), tag, 0) == 0) { + return -1; + } sqlite3 *db = get_db_handle(); pthread_mutex_t *mtx = get_db_mutex(); diff --git a/src/database/db_recordings.c b/src/database/db_recordings.c index 16fe4e61..125746e4 100644 --- a/src/database/db_recordings.c +++ b/src/database/db_recordings.c @@ -14,30 +14,11 @@ #include "database/db_recordings.h" #include "database/db_core.h" #include "core/logger.h" +#include "utils/strings.h" #define MAX_MULTI_FILTER_VALUES 32 #define MAX_MULTI_FILTER_VALUE_LEN 128 -static void trim_whitespace(char *value) { - if (!value) { - return; - } - - char *start = value; - while (*start && isspace((unsigned char)*start)) { - start++; - } - - if (start != value) { - memmove(value, start, strlen(start) + 1); - } - - size_t len = strlen(value); - while (len > 0 && isspace((unsigned char)value[len - 1])) { - value[--len] = '\0'; - } -} - static int parse_csv_filter_values(const char *csv, char values[][MAX_MULTI_FILTER_VALUE_LEN], int max_values) { @@ -46,15 +27,14 @@ static int parse_csv_filter_values(const char *csv, } char buffer[MAX_MULTI_FILTER_VALUES * MAX_MULTI_FILTER_VALUE_LEN]; - strncpy(buffer, csv, sizeof(buffer) - 1); - buffer[sizeof(buffer) - 1] = '\0'; + safe_strcpy(buffer, csv, sizeof(buffer), 0); int count = 0; char *saveptr = NULL; char *token = strtok_r(buffer, ",", &saveptr); while (token && count < max_values) { - trim_whitespace(token); + token = trim_ascii_whitespace(token); if (*token) { bool duplicate = false; for (int i = 0; i < count; i++) { @@ -65,8 +45,7 @@ static int parse_csv_filter_values(const char *csv, } if (!duplicate) { - strncpy(values[count], token, MAX_MULTI_FILTER_VALUE_LEN - 1); - values[count][MAX_MULTI_FILTER_VALUE_LEN - 1] = '\0'; + safe_strcpy(values[count], token, MAX_MULTI_FILTER_VALUE_LEN, 0); count++; } } @@ -299,16 +278,14 @@ int get_recording_metadata_by_id(uint64_t id, recording_metadata_t *metadata) { const char *stream = (const char *)sqlite3_column_text(stmt, 1); if (stream) { - strncpy(metadata->stream_name, stream, sizeof(metadata->stream_name) - 1); - metadata->stream_name[sizeof(metadata->stream_name) - 1] = '\0'; + safe_strcpy(metadata->stream_name, stream, sizeof(metadata->stream_name), 0); } else { metadata->stream_name[0] = '\0'; } const char *path = (const char *)sqlite3_column_text(stmt, 2); if (path) { - strncpy(metadata->file_path, path, sizeof(metadata->file_path) - 1); - metadata->file_path[sizeof(metadata->file_path) - 1] = '\0'; + safe_strcpy(metadata->file_path, path, sizeof(metadata->file_path), 0); } else { metadata->file_path[0] = '\0'; } @@ -328,8 +305,7 @@ int get_recording_metadata_by_id(uint64_t id, recording_metadata_t *metadata) { const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(metadata->codec, codec, sizeof(metadata->codec) - 1); - metadata->codec[sizeof(metadata->codec) - 1] = '\0'; + safe_strcpy(metadata->codec, codec, sizeof(metadata->codec), 0); } else { metadata->codec[0] = '\0'; } @@ -338,10 +314,9 @@ int get_recording_metadata_by_id(uint64_t id, recording_metadata_t *metadata) { const char *trigger_type = (const char *)sqlite3_column_text(stmt, 11); if (trigger_type) { - strncpy(metadata->trigger_type, trigger_type, sizeof(metadata->trigger_type) - 1); - metadata->trigger_type[sizeof(metadata->trigger_type) - 1] = '\0'; + safe_strcpy(metadata->trigger_type, trigger_type, sizeof(metadata->trigger_type), 0); } else { - strncpy(metadata->trigger_type, "scheduled", sizeof(metadata->trigger_type) - 1); + safe_strcpy(metadata->trigger_type, "scheduled", sizeof(metadata->trigger_type), 0); } metadata->protected = sqlite3_column_int(stmt, 12) != 0; @@ -407,16 +382,14 @@ int get_recording_metadata_by_path(const char *file_path, recording_metadata_t * const char *stream = (const char *)sqlite3_column_text(stmt, 1); if (stream) { - strncpy(metadata->stream_name, stream, sizeof(metadata->stream_name) - 1); - metadata->stream_name[sizeof(metadata->stream_name) - 1] = '\0'; + safe_strcpy(metadata->stream_name, stream, sizeof(metadata->stream_name), 0); } else { metadata->stream_name[0] = '\0'; } const char *path = (const char *)sqlite3_column_text(stmt, 2); if (path) { - strncpy(metadata->file_path, path, sizeof(metadata->file_path) - 1); - metadata->file_path[sizeof(metadata->file_path) - 1] = '\0'; + safe_strcpy(metadata->file_path, path, sizeof(metadata->file_path), 0); } else { metadata->file_path[0] = '\0'; } @@ -436,8 +409,7 @@ int get_recording_metadata_by_path(const char *file_path, recording_metadata_t * const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(metadata->codec, codec, sizeof(metadata->codec) - 1); - metadata->codec[sizeof(metadata->codec) - 1] = '\0'; + safe_strcpy(metadata->codec, codec, sizeof(metadata->codec), 0); } else { metadata->codec[0] = '\0'; } @@ -446,10 +418,9 @@ int get_recording_metadata_by_path(const char *file_path, recording_metadata_t * const char *trigger_type = (const char *)sqlite3_column_text(stmt, 11); if (trigger_type) { - strncpy(metadata->trigger_type, trigger_type, sizeof(metadata->trigger_type) - 1); - metadata->trigger_type[sizeof(metadata->trigger_type) - 1] = '\0'; + safe_strcpy(metadata->trigger_type, trigger_type, sizeof(metadata->trigger_type), 0); } else { - strncpy(metadata->trigger_type, "scheduled", sizeof(metadata->trigger_type) - 1); + safe_strcpy(metadata->trigger_type, "scheduled", sizeof(metadata->trigger_type), 0); } metadata->protected = sqlite3_column_int(stmt, 12) != 0; @@ -505,18 +476,18 @@ int get_recording_metadata(time_t start_time, time_t end_time, "FROM recordings WHERE is_complete = 1 AND end_time IS NOT NULL"); // Only complete recordings with end_time set if (start_time > 0) { - strncat(sql, " AND start_time >= ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND start_time >= ?", sizeof(sql)); } if (end_time > 0) { - strncat(sql, " AND start_time <= ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND start_time <= ?", sizeof(sql)); } if (stream_name) { - strncat(sql, " AND stream_name = ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND stream_name = ?", sizeof(sql)); } - strncat(sql, " ORDER BY start_time DESC LIMIT ?;", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " ORDER BY start_time DESC LIMIT ?;", sizeof(sql)); rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { @@ -553,16 +524,14 @@ int get_recording_metadata(time_t start_time, time_t end_time, const char *stream = (const char *)sqlite3_column_text(stmt, 1); if (stream) { - strncpy(metadata[count].stream_name, stream, sizeof(metadata[count].stream_name) - 1); - metadata[count].stream_name[sizeof(metadata[count].stream_name) - 1] = '\0'; + safe_strcpy(metadata[count].stream_name, stream, sizeof(metadata[count].stream_name), 0); } else { metadata[count].stream_name[0] = '\0'; } const char *path = (const char *)sqlite3_column_text(stmt, 2); if (path) { - strncpy(metadata[count].file_path, path, sizeof(metadata[count].file_path) - 1); - metadata[count].file_path[sizeof(metadata[count].file_path) - 1] = '\0'; + safe_strcpy(metadata[count].file_path, path, sizeof(metadata[count].file_path), 0); } else { metadata[count].file_path[0] = '\0'; } @@ -582,8 +551,7 @@ int get_recording_metadata(time_t start_time, time_t end_time, const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(metadata[count].codec, codec, sizeof(metadata[count].codec) - 1); - metadata[count].codec[sizeof(metadata[count].codec) - 1] = '\0'; + safe_strcpy(metadata[count].codec, codec, sizeof(metadata[count].codec), 0); } else { metadata[count].codec[0] = '\0'; } @@ -592,10 +560,9 @@ int get_recording_metadata(time_t start_time, time_t end_time, const char *trigger_type = (const char *)sqlite3_column_text(stmt, 11); if (trigger_type) { - strncpy(metadata[count].trigger_type, trigger_type, sizeof(metadata[count].trigger_type) - 1); - metadata[count].trigger_type[sizeof(metadata[count].trigger_type) - 1] = '\0'; + safe_strcpy(metadata[count].trigger_type, trigger_type, sizeof(metadata[count].trigger_type), 0); } else { - strncpy(metadata[count].trigger_type, "scheduled", sizeof(metadata[count].trigger_type) - 1); + safe_strcpy(metadata[count].trigger_type, "scheduled", sizeof(metadata[count].trigger_type), 0); } metadata[count].protected = sqlite3_column_int(stmt, 12) != 0; @@ -664,96 +631,96 @@ int get_recording_count(time_t start_time, time_t end_time, if (has_detection == 1) { // Filter by trigger_type = 'detection' OR existence of linked detections via recording_id (fast index lookup) // Falls back to timestamp range scan for legacy detections without recording_id - strncat(sql, " AND (r.trigger_type = 'detection'" + safe_strcat(sql, " AND (r.trigger_type = 'detection'" " OR EXISTS (SELECT 1 FROM detections d WHERE d.recording_id = r.id)" " OR EXISTS (SELECT 1 FROM detections d WHERE d.stream_name = r.stream_name" " AND d.timestamp >= r.start_time AND d.timestamp <= r.end_time))", - sizeof(sql) - strlen(sql) - 1); + sizeof(sql)); log_debug("Adding detection filter (trigger_type OR recording_id OR timestamp range)"); } else if (has_detection == -1) { // Filter to recordings with NO detections - strncat(sql, " AND (r.trigger_type != 'detection' OR r.trigger_type IS NULL)" + safe_strcat(sql, " AND (r.trigger_type != 'detection' OR r.trigger_type IS NULL)" " AND NOT EXISTS (SELECT 1 FROM detections d WHERE d.recording_id = r.id)" " AND NOT EXISTS (SELECT 1 FROM detections d WHERE d.stream_name = r.stream_name" " AND d.timestamp >= r.start_time AND d.timestamp <= r.end_time)", - sizeof(sql) - strlen(sql) - 1); + sizeof(sql)); log_debug("Adding no-detection filter (no trigger_type AND no linked detections)"); } if (detection_label_count > 0) { // Filter by specific detection label - prefer recording_id FK lookup, fall back to timestamp range - strncat(sql, " AND (EXISTS (SELECT 1 FROM detections d WHERE d.recording_id = r.id AND (", - sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND (EXISTS (SELECT 1 FROM detections d WHERE d.recording_id = r.id AND (", + sizeof(sql)); for (int i = 0; i < detection_label_count; i++) { - if (i > 0) strncat(sql, " OR ", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "d.label LIKE ?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, " OR ", sizeof(sql)); + safe_strcat(sql, "d.label LIKE ?", sizeof(sql)); } - strncat(sql, ")) OR EXISTS (SELECT 1 FROM detections d WHERE d.recording_id IS NULL" + safe_strcat(sql, ")) OR EXISTS (SELECT 1 FROM detections d WHERE d.recording_id IS NULL" " AND d.stream_name = r.stream_name AND d.timestamp >= r.start_time" " AND d.timestamp <= r.end_time AND (", - sizeof(sql) - strlen(sql) - 1); + sizeof(sql)); for (int i = 0; i < detection_label_count; i++) { - if (i > 0) strncat(sql, " OR ", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "d.label LIKE ?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, " OR ", sizeof(sql)); + safe_strcat(sql, "d.label LIKE ?", sizeof(sql)); } - strncat(sql, ")))", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, ")))", sizeof(sql)); log_debug("Adding %d detection_label filters", detection_label_count); } if (start_time > 0) { - strncat(sql, " AND r.start_time >= ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.start_time >= ?", sizeof(sql)); log_debug("Adding start_time filter: %ld", (long)start_time); } if (end_time > 0) { - strncat(sql, " AND r.start_time <= ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.start_time <= ?", sizeof(sql)); log_debug("Adding end_time filter: %ld", (long)end_time); } if (stream_filter_count > 0) { - strncat(sql, " AND r.stream_name IN (", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.stream_name IN (", sizeof(sql)); for (int i = 0; i < stream_filter_count; i++) { - if (i > 0) strncat(sql, ",", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, ",", sizeof(sql)); + safe_strcat(sql, "?", sizeof(sql)); } - strncat(sql, ")", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, ")", sizeof(sql)); } else if (allowed_streams && allowed_streams_count > 0) { // Tag-based RBAC: restrict to the user's whitelisted streams via IN clause - strncat(sql, " AND r.stream_name IN (", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.stream_name IN (", sizeof(sql)); for (int i = 0; i < allowed_streams_count; i++) { - if (i > 0) strncat(sql, ",", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, ",", sizeof(sql)); + safe_strcat(sql, "?", sizeof(sql)); } - strncat(sql, ")", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, ")", sizeof(sql)); log_debug("Adding allowed_streams IN filter (%d streams)", allowed_streams_count); } if (protected_filter == 0) { - strncat(sql, " AND r.protected = 0", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.protected = 0", sizeof(sql)); log_debug("Adding protected_filter=0 (unprotected only)"); } else if (protected_filter == 1) { - strncat(sql, " AND r.protected = 1", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.protected = 1", sizeof(sql)); log_debug("Adding protected_filter=1 (protected only)"); } if (tag_filter_count > 0) { - strncat(sql, " AND EXISTS (SELECT 1 FROM recording_tags rt WHERE rt.recording_id = r.id AND rt.tag IN (", - sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND EXISTS (SELECT 1 FROM recording_tags rt WHERE rt.recording_id = r.id AND rt.tag IN (", + sizeof(sql)); for (int i = 0; i < tag_filter_count; i++) { - if (i > 0) strncat(sql, ",", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, ",", sizeof(sql)); + safe_strcat(sql, "?", sizeof(sql)); } - strncat(sql, "))", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, "))", sizeof(sql)); log_debug("Adding %d tag filters", tag_filter_count); } if (capture_method_count > 0) { - strncat(sql, " AND COALESCE(r.trigger_type, 'scheduled') IN (", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND COALESCE(r.trigger_type, 'scheduled') IN (", sizeof(sql)); for (int i = 0; i < capture_method_count; i++) { - if (i > 0) strncat(sql, ",", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, ",", sizeof(sql)); + safe_strcat(sql, "?", sizeof(sql)); } - strncat(sql, ")", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, ")", sizeof(sql)); log_debug("Adding %d capture_method filters", capture_method_count); } @@ -871,8 +838,7 @@ int get_recording_metadata_paginated(time_t start_time, time_t end_time, strcmp(sort_field, "start_time") == 0 || strcmp(sort_field, "end_time") == 0 || strcmp(sort_field, "size_bytes") == 0) { - strncpy(safe_sort_field, sort_field, sizeof(safe_sort_field) - 1); - safe_sort_field[sizeof(safe_sort_field) - 1] = '\0'; + safe_strcpy(safe_sort_field, sort_field, sizeof(safe_sort_field), 0); } else { log_warn("Invalid sort field: %s, using default", sort_field); } @@ -903,108 +869,108 @@ int get_recording_metadata_paginated(time_t start_time, time_t end_time, if (has_detection == 1) { // Filter by trigger_type = 'detection' OR existence of linked detections via recording_id (fast index lookup) // Falls back to timestamp range scan for legacy detections without recording_id - strncat(sql, " AND (r.trigger_type = 'detection'" + safe_strcat(sql, " AND (r.trigger_type = 'detection'" " OR EXISTS (SELECT 1 FROM detections d WHERE d.recording_id = r.id)" " OR EXISTS (SELECT 1 FROM detections d WHERE d.stream_name = r.stream_name" " AND d.timestamp >= r.start_time AND d.timestamp <= r.end_time))", - sizeof(sql) - strlen(sql) - 1); + sizeof(sql)); log_info("Adding detection filter (trigger_type OR recording_id OR timestamp range)"); } else if (has_detection == -1) { // Filter to recordings with NO detections - strncat(sql, " AND (r.trigger_type != 'detection' OR r.trigger_type IS NULL)" + safe_strcat(sql, " AND (r.trigger_type != 'detection' OR r.trigger_type IS NULL)" " AND NOT EXISTS (SELECT 1 FROM detections d WHERE d.recording_id = r.id)" " AND NOT EXISTS (SELECT 1 FROM detections d WHERE d.stream_name = r.stream_name" " AND d.timestamp >= r.start_time AND d.timestamp <= r.end_time)", - sizeof(sql) - strlen(sql) - 1); + sizeof(sql)); log_info("Adding no-detection filter (no trigger_type AND no linked detections)"); } if (detection_label_count > 0) { // Filter by specific detection label - prefer recording_id FK lookup, fall back to timestamp range - strncat(sql, " AND (EXISTS (SELECT 1 FROM detections d WHERE d.recording_id = r.id AND (", - sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND (EXISTS (SELECT 1 FROM detections d WHERE d.recording_id = r.id AND (", + sizeof(sql)); for (int i = 0; i < detection_label_count; i++) { - if (i > 0) strncat(sql, " OR ", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "d.label LIKE ?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, " OR ", sizeof(sql)); + safe_strcat(sql, "d.label LIKE ?", sizeof(sql)); } - strncat(sql, ")) OR EXISTS (SELECT 1 FROM detections d WHERE d.recording_id IS NULL" + safe_strcat(sql, ")) OR EXISTS (SELECT 1 FROM detections d WHERE d.recording_id IS NULL" " AND d.stream_name = r.stream_name AND d.timestamp >= r.start_time" " AND d.timestamp <= r.end_time AND (", - sizeof(sql) - strlen(sql) - 1); + sizeof(sql)); for (int i = 0; i < detection_label_count; i++) { - if (i > 0) strncat(sql, " OR ", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "d.label LIKE ?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, " OR ", sizeof(sql)); + safe_strcat(sql, "d.label LIKE ?", sizeof(sql)); } - strncat(sql, ")))", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, ")))", sizeof(sql)); log_info("Adding %d detection_label filters", detection_label_count); } if (start_time > 0) { - strncat(sql, " AND r.start_time >= ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.start_time >= ?", sizeof(sql)); log_info("Adding start_time filter to paginated query: %ld", (long)start_time); } if (end_time > 0) { - strncat(sql, " AND r.start_time <= ?", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.start_time <= ?", sizeof(sql)); log_info("Adding end_time filter to paginated query: %ld", (long)end_time); } if (stream_filter_count > 0) { - strncat(sql, " AND r.stream_name IN (", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.stream_name IN (", sizeof(sql)); for (int i = 0; i < stream_filter_count; i++) { - if (i > 0) strncat(sql, ",", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, ",", sizeof(sql)); + safe_strcat(sql, "?", sizeof(sql)); } - strncat(sql, ")", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, ")", sizeof(sql)); } else if (allowed_streams && allowed_streams_count > 0) { // Tag-based RBAC: restrict to the user's whitelisted streams via IN clause - strncat(sql, " AND r.stream_name IN (", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.stream_name IN (", sizeof(sql)); for (int i = 0; i < allowed_streams_count; i++) { - if (i > 0) strncat(sql, ",", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, ",", sizeof(sql)); + safe_strcat(sql, "?", sizeof(sql)); } - strncat(sql, ")", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, ")", sizeof(sql)); log_debug("Adding allowed_streams IN filter (%d streams) to paginated query", allowed_streams_count); } if (protected_filter == 0) { - strncat(sql, " AND r.protected = 0", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.protected = 0", sizeof(sql)); log_debug("Adding protected_filter=0 (unprotected only) to paginated query"); } else if (protected_filter == 1) { - strncat(sql, " AND r.protected = 1", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND r.protected = 1", sizeof(sql)); log_debug("Adding protected_filter=1 (protected only) to paginated query"); } if (tag_filter_count > 0) { - strncat(sql, " AND EXISTS (SELECT 1 FROM recording_tags rt WHERE rt.recording_id = r.id AND rt.tag IN (", - sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND EXISTS (SELECT 1 FROM recording_tags rt WHERE rt.recording_id = r.id AND rt.tag IN (", + sizeof(sql)); for (int i = 0; i < tag_filter_count; i++) { - if (i > 0) strncat(sql, ",", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, ",", sizeof(sql)); + safe_strcat(sql, "?", sizeof(sql)); } - strncat(sql, "))", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, "))", sizeof(sql)); log_debug("Adding %d tag filters to paginated query", tag_filter_count); } if (capture_method_count > 0) { - strncat(sql, " AND COALESCE(r.trigger_type, 'scheduled') IN (", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, " AND COALESCE(r.trigger_type, 'scheduled') IN (", sizeof(sql)); for (int i = 0; i < capture_method_count; i++) { - if (i > 0) strncat(sql, ",", sizeof(sql) - strlen(sql) - 1); - strncat(sql, "?", sizeof(sql) - strlen(sql) - 1); + if (i > 0) safe_strcat(sql, ",", sizeof(sql)); + safe_strcat(sql, "?", sizeof(sql)); } - strncat(sql, ")", sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, ")", sizeof(sql)); log_debug("Adding %d capture_method filters to paginated query", capture_method_count); } // Add ORDER BY clause with sanitized field and order char order_clause[64]; snprintf(order_clause, sizeof(order_clause), " ORDER BY r.%s %s", safe_sort_field, safe_sort_order); - strncat(sql, order_clause, sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, order_clause, sizeof(sql)); // Add LIMIT and OFFSET for pagination char limit_clause[64]; snprintf(limit_clause, sizeof(limit_clause), " LIMIT ? OFFSET ?"); - strncat(sql, limit_clause, sizeof(sql) - strlen(sql) - 1); + safe_strcat(sql, limit_clause, sizeof(sql)); log_debug("SQL query for get_recording_metadata_paginated: %s", sql); @@ -1072,16 +1038,14 @@ int get_recording_metadata_paginated(time_t start_time, time_t end_time, const char *stream = (const char *)sqlite3_column_text(stmt, 1); if (stream) { - strncpy(metadata[count].stream_name, stream, sizeof(metadata[count].stream_name) - 1); - metadata[count].stream_name[sizeof(metadata[count].stream_name) - 1] = '\0'; + safe_strcpy(metadata[count].stream_name, stream, sizeof(metadata[count].stream_name), 0); } else { metadata[count].stream_name[0] = '\0'; } const char *path = (const char *)sqlite3_column_text(stmt, 2); if (path) { - strncpy(metadata[count].file_path, path, sizeof(metadata[count].file_path) - 1); - metadata[count].file_path[sizeof(metadata[count].file_path) - 1] = '\0'; + safe_strcpy(metadata[count].file_path, path, sizeof(metadata[count].file_path), 0); } else { metadata[count].file_path[0] = '\0'; } @@ -1101,8 +1065,7 @@ int get_recording_metadata_paginated(time_t start_time, time_t end_time, const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(metadata[count].codec, codec, sizeof(metadata[count].codec) - 1); - metadata[count].codec[sizeof(metadata[count].codec) - 1] = '\0'; + safe_strcpy(metadata[count].codec, codec, sizeof(metadata[count].codec), 0); } else { metadata[count].codec[0] = '\0'; } @@ -1111,10 +1074,9 @@ int get_recording_metadata_paginated(time_t start_time, time_t end_time, const char *trigger_type = (const char *)sqlite3_column_text(stmt, 11); if (trigger_type) { - strncpy(metadata[count].trigger_type, trigger_type, sizeof(metadata[count].trigger_type) - 1); - metadata[count].trigger_type[sizeof(metadata[count].trigger_type) - 1] = '\0'; + safe_strcpy(metadata[count].trigger_type, trigger_type, sizeof(metadata[count].trigger_type), 0); } else { - strncpy(metadata[count].trigger_type, "scheduled", sizeof(metadata[count].trigger_type) - 1); + safe_strcpy(metadata[count].trigger_type, "scheduled", sizeof(metadata[count].trigger_type), 0); } metadata[count].protected = sqlite3_column_int(stmt, 12) != 0; @@ -1512,16 +1474,14 @@ int get_recordings_for_retention(const char *stream_name, const char *stream = (const char *)sqlite3_column_text(stmt, 1); if (stream) { - strncpy(recordings[count].stream_name, stream, sizeof(recordings[count].stream_name) - 1); - recordings[count].stream_name[sizeof(recordings[count].stream_name) - 1] = '\0'; + safe_strcpy(recordings[count].stream_name, stream, sizeof(recordings[count].stream_name), 0); } else { recordings[count].stream_name[0] = '\0'; } const char *path = (const char *)sqlite3_column_text(stmt, 2); if (path) { - strncpy(recordings[count].file_path, path, sizeof(recordings[count].file_path) - 1); - recordings[count].file_path[sizeof(recordings[count].file_path) - 1] = '\0'; + safe_strcpy(recordings[count].file_path, path, sizeof(recordings[count].file_path), 0); } else { recordings[count].file_path[0] = '\0'; } @@ -1541,8 +1501,7 @@ int get_recordings_for_retention(const char *stream_name, const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(recordings[count].codec, codec, sizeof(recordings[count].codec) - 1); - recordings[count].codec[sizeof(recordings[count].codec) - 1] = '\0'; + safe_strcpy(recordings[count].codec, codec, sizeof(recordings[count].codec), 0); } else { recordings[count].codec[0] = '\0'; } @@ -1551,10 +1510,9 @@ int get_recordings_for_retention(const char *stream_name, const char *trigger_type = (const char *)sqlite3_column_text(stmt, 11); if (trigger_type) { - strncpy(recordings[count].trigger_type, trigger_type, sizeof(recordings[count].trigger_type) - 1); - recordings[count].trigger_type[sizeof(recordings[count].trigger_type) - 1] = '\0'; + safe_strcpy(recordings[count].trigger_type, trigger_type, sizeof(recordings[count].trigger_type), 0); } else { - strncpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type) - 1); + safe_strcpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type), 0); } recordings[count].protected = sqlite3_column_int(stmt, 12) != 0; @@ -1637,16 +1595,14 @@ int get_recordings_for_quota_enforcement(const char *stream_name, const char *stream = (const char *)sqlite3_column_text(stmt, 1); if (stream) { - strncpy(recordings[count].stream_name, stream, sizeof(recordings[count].stream_name) - 1); - recordings[count].stream_name[sizeof(recordings[count].stream_name) - 1] = '\0'; + safe_strcpy(recordings[count].stream_name, stream, sizeof(recordings[count].stream_name), 0); } else { recordings[count].stream_name[0] = '\0'; } const char *path = (const char *)sqlite3_column_text(stmt, 2); if (path) { - strncpy(recordings[count].file_path, path, sizeof(recordings[count].file_path) - 1); - recordings[count].file_path[sizeof(recordings[count].file_path) - 1] = '\0'; + safe_strcpy(recordings[count].file_path, path, sizeof(recordings[count].file_path), 0); } else { recordings[count].file_path[0] = '\0'; } @@ -1666,8 +1622,7 @@ int get_recordings_for_quota_enforcement(const char *stream_name, const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(recordings[count].codec, codec, sizeof(recordings[count].codec) - 1); - recordings[count].codec[sizeof(recordings[count].codec) - 1] = '\0'; + safe_strcpy(recordings[count].codec, codec, sizeof(recordings[count].codec), 0); } else { recordings[count].codec[0] = '\0'; } @@ -1676,10 +1631,9 @@ int get_recordings_for_quota_enforcement(const char *stream_name, const char *trigger_type = (const char *)sqlite3_column_text(stmt, 11); if (trigger_type) { - strncpy(recordings[count].trigger_type, trigger_type, sizeof(recordings[count].trigger_type) - 1); - recordings[count].trigger_type[sizeof(recordings[count].trigger_type) - 1] = '\0'; + safe_strcpy(recordings[count].trigger_type, trigger_type, sizeof(recordings[count].trigger_type), 0); } else { - strncpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type) - 1); + safe_strcpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type), 0); } recordings[count].protected = sqlite3_column_int(stmt, 12) != 0; @@ -1763,14 +1717,12 @@ int get_orphaned_db_entries(recording_metadata_t *recordings, int max_count, const char *stream = (const char *)sqlite3_column_text(stmt, 1); if (stream) { - strncpy(recordings[count].stream_name, stream, sizeof(recordings[count].stream_name) - 1); - recordings[count].stream_name[sizeof(recordings[count].stream_name) - 1] = '\0'; + safe_strcpy(recordings[count].stream_name, stream, sizeof(recordings[count].stream_name), 0); } else { recordings[count].stream_name[0] = '\0'; } - strncpy(recordings[count].file_path, path, sizeof(recordings[count].file_path) - 1); - recordings[count].file_path[sizeof(recordings[count].file_path) - 1] = '\0'; + safe_strcpy(recordings[count].file_path, path, sizeof(recordings[count].file_path), 0); recordings[count].start_time = (time_t)sqlite3_column_int64(stmt, 3); @@ -1787,8 +1739,7 @@ int get_orphaned_db_entries(recording_metadata_t *recordings, int max_count, const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(recordings[count].codec, codec, sizeof(recordings[count].codec) - 1); - recordings[count].codec[sizeof(recordings[count].codec) - 1] = '\0'; + safe_strcpy(recordings[count].codec, codec, sizeof(recordings[count].codec), 0); } else { recordings[count].codec[0] = '\0'; } @@ -1797,10 +1748,9 @@ int get_orphaned_db_entries(recording_metadata_t *recordings, int max_count, const char *trigger_type = (const char *)sqlite3_column_text(stmt, 11); if (trigger_type) { - strncpy(recordings[count].trigger_type, trigger_type, sizeof(recordings[count].trigger_type) - 1); - recordings[count].trigger_type[sizeof(recordings[count].trigger_type) - 1] = '\0'; + safe_strcpy(recordings[count].trigger_type, trigger_type, sizeof(recordings[count].trigger_type), 0); } else { - strncpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type) - 1); + safe_strcpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type), 0); } count++; @@ -1916,14 +1866,12 @@ int get_recordings_for_tiered_retention(const char *stream_name, const char *sname = (const char *)sqlite3_column_text(stmt, 1); if (sname) { - strncpy(recordings[count].stream_name, sname, sizeof(recordings[count].stream_name) - 1); - recordings[count].stream_name[sizeof(recordings[count].stream_name) - 1] = '\0'; + safe_strcpy(recordings[count].stream_name, sname, sizeof(recordings[count].stream_name), 0); } const char *fpath = (const char *)sqlite3_column_text(stmt, 2); if (fpath) { - strncpy(recordings[count].file_path, fpath, sizeof(recordings[count].file_path) - 1); - recordings[count].file_path[sizeof(recordings[count].file_path) - 1] = '\0'; + safe_strcpy(recordings[count].file_path, fpath, sizeof(recordings[count].file_path), 0); } recordings[count].start_time = (time_t)sqlite3_column_int64(stmt, 3); @@ -1936,18 +1884,16 @@ int get_recordings_for_tiered_retention(const char *stream_name, const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(recordings[count].codec, codec, sizeof(recordings[count].codec) - 1); - recordings[count].codec[sizeof(recordings[count].codec) - 1] = '\0'; + safe_strcpy(recordings[count].codec, codec, sizeof(recordings[count].codec), 0); } recordings[count].is_complete = sqlite3_column_int(stmt, 10) != 0; const char *ttype = (const char *)sqlite3_column_text(stmt, 11); if (ttype) { - strncpy(recordings[count].trigger_type, ttype, sizeof(recordings[count].trigger_type) - 1); - recordings[count].trigger_type[sizeof(recordings[count].trigger_type) - 1] = '\0'; + safe_strcpy(recordings[count].trigger_type, ttype, sizeof(recordings[count].trigger_type), 0); } else { - strncpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type) - 1); + safe_strcpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type), 0); } recordings[count].protected = sqlite3_column_int(stmt, 12) != 0; @@ -2024,14 +1970,12 @@ int get_recordings_for_pressure_cleanup(recording_metadata_t *recordings, const char *sname = (const char *)sqlite3_column_text(stmt, 1); if (sname) { - strncpy(recordings[count].stream_name, sname, sizeof(recordings[count].stream_name) - 1); - recordings[count].stream_name[sizeof(recordings[count].stream_name) - 1] = '\0'; + safe_strcpy(recordings[count].stream_name, sname, sizeof(recordings[count].stream_name), 0); } const char *fpath = (const char *)sqlite3_column_text(stmt, 2); if (fpath) { - strncpy(recordings[count].file_path, fpath, sizeof(recordings[count].file_path) - 1); - recordings[count].file_path[sizeof(recordings[count].file_path) - 1] = '\0'; + safe_strcpy(recordings[count].file_path, fpath, sizeof(recordings[count].file_path), 0); } recordings[count].start_time = (time_t)sqlite3_column_int64(stmt, 3); @@ -2044,18 +1988,16 @@ int get_recordings_for_pressure_cleanup(recording_metadata_t *recordings, const char *codec = (const char *)sqlite3_column_text(stmt, 9); if (codec) { - strncpy(recordings[count].codec, codec, sizeof(recordings[count].codec) - 1); - recordings[count].codec[sizeof(recordings[count].codec) - 1] = '\0'; + safe_strcpy(recordings[count].codec, codec, sizeof(recordings[count].codec), 0); } recordings[count].is_complete = sqlite3_column_int(stmt, 10) != 0; const char *ttype = (const char *)sqlite3_column_text(stmt, 11); if (ttype) { - strncpy(recordings[count].trigger_type, ttype, sizeof(recordings[count].trigger_type) - 1); - recordings[count].trigger_type[sizeof(recordings[count].trigger_type) - 1] = '\0'; + safe_strcpy(recordings[count].trigger_type, ttype, sizeof(recordings[count].trigger_type), 0); } else { - strncpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type) - 1); + safe_strcpy(recordings[count].trigger_type, "scheduled", sizeof(recordings[count].trigger_type), 0); } recordings[count].protected = sqlite3_column_int(stmt, 12) != 0; diff --git a/src/database/db_recordings_sync.c b/src/database/db_recordings_sync.c index 06cfa197..08fc9f11 100644 --- a/src/database/db_recordings_sync.c +++ b/src/database/db_recordings_sync.c @@ -22,6 +22,7 @@ #include "core/shutdown_coordinator.h" #include "database/database_manager.h" #include "database/db_recordings.h" +#include "utils/strings.h" // Thread state static struct { @@ -132,7 +133,7 @@ static int sync_recordings_needing_size_update(void) { // Collect recordings to sync (we'll update them outside the query loop) typedef struct { uint64_t id; - char file_path[512]; + char file_path[MAX_PATH_LENGTH]; } sync_item_t; sync_item_t *items = NULL; @@ -160,8 +161,7 @@ static int sync_recordings_needing_size_update(void) { items[item_count].id = (uint64_t)sqlite3_column_int64(stmt, 0); const char *path = (const char *)sqlite3_column_text(stmt, 1); if (path) { - strncpy(items[item_count].file_path, path, sizeof(items[item_count].file_path) - 1); - items[item_count].file_path[sizeof(items[item_count].file_path) - 1] = '\0'; + safe_strcpy(items[item_count].file_path, path, sizeof(items[item_count].file_path), 0); } else { items[item_count].file_path[0] = '\0'; } diff --git a/src/database/db_schema_cache.c b/src/database/db_schema_cache.c index af53b684..ba128244 100644 --- a/src/database/db_schema_cache.c +++ b/src/database/db_schema_cache.c @@ -9,6 +9,7 @@ #include "database/db_schema_utils.h" #include "database/db_core.h" #include "core/logger.h" +#include "utils/strings.h" // Cache for column existence to avoid repeated database queries typedef struct { @@ -57,30 +58,30 @@ void init_schema_cache(void) { // Add them to the cache manually if (column_cache_size < column_cache_capacity) { - strncpy(column_cache[column_cache_size].table_name, "streams", sizeof(column_cache[column_cache_size].table_name) - 1); - strncpy(column_cache[column_cache_size].column_name, "detection_based_recording", sizeof(column_cache[column_cache_size].column_name) - 1); + safe_strcpy(column_cache[column_cache_size].table_name, "streams", sizeof(column_cache[column_cache_size].table_name), 0); + safe_strcpy(column_cache[column_cache_size].column_name, "detection_based_recording", sizeof(column_cache[column_cache_size].column_name), 0); column_cache[column_cache_size].exists = detection_exists; column_cache_size++; } if (column_cache_size < column_cache_capacity) { - strncpy(column_cache[column_cache_size].table_name, "streams", sizeof(column_cache[column_cache_size].table_name) - 1); - strncpy(column_cache[column_cache_size].column_name, "protocol", sizeof(column_cache[column_cache_size].column_name) - 1); + safe_strcpy(column_cache[column_cache_size].table_name, "streams", sizeof(column_cache[column_cache_size].table_name), 0); + safe_strcpy(column_cache[column_cache_size].column_name, "protocol", sizeof(column_cache[column_cache_size].column_name), 0); column_cache[column_cache_size].exists = protocol_exists; column_cache_size++; } if (column_cache_size < column_cache_capacity) { - strncpy(column_cache[column_cache_size].table_name, "streams", sizeof(column_cache[column_cache_size].table_name) - 1); - strncpy(column_cache[column_cache_size].column_name, "is_onvif", sizeof(column_cache[column_cache_size].column_name) - 1); + safe_strcpy(column_cache[column_cache_size].table_name, "streams", sizeof(column_cache[column_cache_size].table_name), 0); + safe_strcpy(column_cache[column_cache_size].column_name, "is_onvif", sizeof(column_cache[column_cache_size].column_name), 0); column_cache[column_cache_size].exists = onvif_exists; column_cache_size++; } // Add record_audio column to cache if (column_cache_size < column_cache_capacity) { - strncpy(column_cache[column_cache_size].table_name, "streams", sizeof(column_cache[column_cache_size].table_name) - 1); - strncpy(column_cache[column_cache_size].column_name, "record_audio", sizeof(column_cache[column_cache_size].column_name) - 1); + safe_strcpy(column_cache[column_cache_size].table_name, "streams", sizeof(column_cache[column_cache_size].table_name), 0); + safe_strcpy(column_cache[column_cache_size].column_name, "record_audio", sizeof(column_cache[column_cache_size].column_name), 0); column_cache[column_cache_size].exists = record_audio_exists; column_cache_size++; } @@ -147,11 +148,9 @@ bool cached_column_exists(const char *table_name, const char *column_name) { } // Add the new entry - strncpy(column_cache[column_cache_size].table_name, table_name, sizeof(column_cache[column_cache_size].table_name) - 1); - column_cache[column_cache_size].table_name[sizeof(column_cache[column_cache_size].table_name) - 1] = '\0'; + safe_strcpy(column_cache[column_cache_size].table_name, table_name, sizeof(column_cache[column_cache_size].table_name), 0); - strncpy(column_cache[column_cache_size].column_name, column_name, sizeof(column_cache[column_cache_size].column_name) - 1); - column_cache[column_cache_size].column_name[sizeof(column_cache[column_cache_size].column_name) - 1] = '\0'; + safe_strcpy(column_cache[column_cache_size].column_name, column_name, sizeof(column_cache[column_cache_size].column_name), 0); column_cache[column_cache_size].exists = exists; column_cache_size++; diff --git a/src/database/db_streams.c b/src/database/db_streams.c index b8ab6f99..6b5c06fe 100644 --- a/src/database/db_streams.c +++ b/src/database/db_streams.c @@ -15,6 +15,7 @@ #include "database/db_schema_cache.h" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" /** * Serialize recording_schedule uint8_t[168] to comma-separated text string. @@ -47,8 +48,7 @@ static void deserialize_recording_schedule(const char *text, uint8_t *schedule) return; } char buf[RECORDING_SCHEDULE_TEXT_SIZE]; - strncpy(buf, text, sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; + safe_strcpy(buf, text, sizeof(buf), 0); int idx = 0; char *saveptr = NULL; @@ -578,7 +578,7 @@ int update_stream_video_params(const char *stream_name, int width, int height, i old_fps = sqlite3_column_int(stmt, 2); const char *c = (const char *)sqlite3_column_text(stmt, 3); if (c) { - strncpy(old_codec, c, sizeof(old_codec) - 1); + safe_strcpy(old_codec, c, sizeof(old_codec), 0); } } sqlite3_finalize(stmt); @@ -808,14 +808,12 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) { // Basic stream settings const char *stream_name = (const char *)sqlite3_column_text(stmt, COL_NAME); if (stream_name) { - strncpy(stream->name, stream_name, MAX_STREAM_NAME - 1); - stream->name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream->name, stream_name, MAX_STREAM_NAME, 0); } const char *url = (const char *)sqlite3_column_text(stmt, COL_URL); if (url) { - strncpy(stream->url, url, MAX_URL_LENGTH - 1); - stream->url[MAX_URL_LENGTH - 1] = '\0'; + safe_strcpy(stream->url, url, MAX_URL_LENGTH, 0); } stream->enabled = sqlite3_column_int(stmt, COL_ENABLED) != 0; @@ -826,8 +824,7 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) { const char *codec = (const char *)sqlite3_column_text(stmt, COL_CODEC); if (codec) { - strncpy(stream->codec, codec, sizeof(stream->codec) - 1); - stream->codec[sizeof(stream->codec) - 1] = '\0'; + safe_strcpy(stream->codec, codec, sizeof(stream->codec), 0); } stream->priority = sqlite3_column_int(stmt, COL_PRIORITY); @@ -839,8 +836,7 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) { const char *detection_model = (const char *)sqlite3_column_text(stmt, COL_DETECTION_MODEL); if (detection_model) { - strncpy(stream->detection_model, detection_model, MAX_PATH_LENGTH - 1); - stream->detection_model[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(stream->detection_model, detection_model, MAX_PATH_LENGTH, 0); } stream->detection_threshold = (sqlite3_column_type(stmt, COL_DETECTION_THRESHOLD) != SQLITE_NULL) @@ -854,23 +850,20 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) { const char *detection_api_url = (const char *)sqlite3_column_text(stmt, COL_DETECTION_API_URL); if (detection_api_url) { - strncpy(stream->detection_api_url, detection_api_url, MAX_URL_LENGTH - 1); - stream->detection_api_url[MAX_URL_LENGTH - 1] = '\0'; + safe_strcpy(stream->detection_api_url, detection_api_url, MAX_URL_LENGTH, 0); } // Detection object filter settings const char *detection_object_filter = (const char *)sqlite3_column_text(stmt, COL_DETECTION_OBJECT_FILTER); if (detection_object_filter) { - strncpy(stream->detection_object_filter, detection_object_filter, sizeof(stream->detection_object_filter) - 1); - stream->detection_object_filter[sizeof(stream->detection_object_filter) - 1] = '\0'; + safe_strcpy(stream->detection_object_filter, detection_object_filter, sizeof(stream->detection_object_filter), 0); } else { - strncpy(stream->detection_object_filter, "none", sizeof(stream->detection_object_filter) - 1); + safe_strcpy(stream->detection_object_filter, "none", sizeof(stream->detection_object_filter), 0); } const char *detection_object_filter_list = (const char *)sqlite3_column_text(stmt, COL_DETECTION_OBJECT_FILTER_LIST); if (detection_object_filter_list) { - strncpy(stream->detection_object_filter_list, detection_object_filter_list, sizeof(stream->detection_object_filter_list) - 1); - stream->detection_object_filter_list[sizeof(stream->detection_object_filter_list) - 1] = '\0'; + safe_strcpy(stream->detection_object_filter_list, detection_object_filter_list, sizeof(stream->detection_object_filter_list), 0); } // Protocol and ONVIF @@ -911,20 +904,17 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) { // ONVIF credentials const char *onvif_username = (const char *)sqlite3_column_text(stmt, COL_ONVIF_USERNAME); if (onvif_username) { - strncpy(stream->onvif_username, onvif_username, sizeof(stream->onvif_username) - 1); - stream->onvif_username[sizeof(stream->onvif_username) - 1] = '\0'; + safe_strcpy(stream->onvif_username, onvif_username, sizeof(stream->onvif_username), 0); } const char *onvif_password = (const char *)sqlite3_column_text(stmt, COL_ONVIF_PASSWORD); if (onvif_password) { - strncpy(stream->onvif_password, onvif_password, sizeof(stream->onvif_password) - 1); - stream->onvif_password[sizeof(stream->onvif_password) - 1] = '\0'; + safe_strcpy(stream->onvif_password, onvif_password, sizeof(stream->onvif_password), 0); } const char *onvif_profile = (const char *)sqlite3_column_text(stmt, COL_ONVIF_PROFILE); if (onvif_profile) { - strncpy(stream->onvif_profile, onvif_profile, sizeof(stream->onvif_profile) - 1); - stream->onvif_profile[sizeof(stream->onvif_profile) - 1] = '\0'; + safe_strcpy(stream->onvif_profile, onvif_profile, sizeof(stream->onvif_profile), 0); } stream->onvif_port = (sqlite3_column_type(stmt, COL_ONVIF_PORT) != SQLITE_NULL) @@ -938,16 +928,14 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) { // Tags const char *tags_val = (const char *)sqlite3_column_text(stmt, COL_TAGS); if (tags_val) { - strncpy(stream->tags, tags_val, sizeof(stream->tags) - 1); - stream->tags[sizeof(stream->tags) - 1] = '\0'; + safe_strcpy(stream->tags, tags_val, sizeof(stream->tags), 0); } else { stream->tags[0] = '\0'; } const char *admin_url = (const char *)sqlite3_column_text(stmt, COL_ADMIN_URL); if (admin_url) { - strncpy(stream->admin_url, admin_url, sizeof(stream->admin_url) - 1); - stream->admin_url[sizeof(stream->admin_url) - 1] = '\0'; + safe_strcpy(stream->admin_url, admin_url, sizeof(stream->admin_url), 0); } else { stream->admin_url[0] = '\0'; } @@ -958,8 +946,7 @@ int get_stream_config_by_name(const char *name, stream_config_t *stream) { // Cross-stream motion trigger source const char *motion_trigger_source = (const char *)sqlite3_column_text(stmt, COL_MOTION_TRIGGER_SOURCE); if (motion_trigger_source) { - strncpy(stream->motion_trigger_source, motion_trigger_source, sizeof(stream->motion_trigger_source) - 1); - stream->motion_trigger_source[sizeof(stream->motion_trigger_source) - 1] = '\0'; + safe_strcpy(stream->motion_trigger_source, motion_trigger_source, sizeof(stream->motion_trigger_source), 0); } else { stream->motion_trigger_source[0] = '\0'; } @@ -1048,14 +1035,12 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) { // Basic settings const char *name = (const char *)sqlite3_column_text(stmt, COL_NAME); if (name) { - strncpy(s->name, name, MAX_STREAM_NAME - 1); - s->name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(s->name, name, MAX_STREAM_NAME, 0); } const char *url = (const char *)sqlite3_column_text(stmt, COL_URL); if (url) { - strncpy(s->url, url, MAX_URL_LENGTH - 1); - s->url[MAX_URL_LENGTH - 1] = '\0'; + safe_strcpy(s->url, url, MAX_URL_LENGTH, 0); } s->enabled = sqlite3_column_int(stmt, COL_ENABLED) != 0; @@ -1066,8 +1051,7 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) { const char *codec = (const char *)sqlite3_column_text(stmt, COL_CODEC); if (codec) { - strncpy(s->codec, codec, sizeof(s->codec) - 1); - s->codec[sizeof(s->codec) - 1] = '\0'; + safe_strcpy(s->codec, codec, sizeof(s->codec), 0); } s->priority = sqlite3_column_int(stmt, COL_PRIORITY); @@ -1079,8 +1063,7 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) { const char *detection_model = (const char *)sqlite3_column_text(stmt, COL_DETECTION_MODEL); if (detection_model) { - strncpy(s->detection_model, detection_model, MAX_PATH_LENGTH - 1); - s->detection_model[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(s->detection_model, detection_model, MAX_PATH_LENGTH, 0); } s->detection_threshold = (sqlite3_column_type(stmt, COL_DETECTION_THRESHOLD) != SQLITE_NULL) @@ -1094,23 +1077,20 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) { const char *detection_api_url = (const char *)sqlite3_column_text(stmt, COL_DETECTION_API_URL); if (detection_api_url) { - strncpy(s->detection_api_url, detection_api_url, MAX_URL_LENGTH - 1); - s->detection_api_url[MAX_URL_LENGTH - 1] = '\0'; + safe_strcpy(s->detection_api_url, detection_api_url, MAX_URL_LENGTH, 0); } // Detection object filter settings const char *detection_object_filter = (const char *)sqlite3_column_text(stmt, COL_DETECTION_OBJECT_FILTER); if (detection_object_filter) { - strncpy(s->detection_object_filter, detection_object_filter, sizeof(s->detection_object_filter) - 1); - s->detection_object_filter[sizeof(s->detection_object_filter) - 1] = '\0'; + safe_strcpy(s->detection_object_filter, detection_object_filter, sizeof(s->detection_object_filter), 0); } else { - strncpy(s->detection_object_filter, "none", sizeof(s->detection_object_filter) - 1); + safe_strcpy(s->detection_object_filter, "none", sizeof(s->detection_object_filter), 0); } const char *detection_object_filter_list = (const char *)sqlite3_column_text(stmt, COL_DETECTION_OBJECT_FILTER_LIST); if (detection_object_filter_list) { - strncpy(s->detection_object_filter_list, detection_object_filter_list, sizeof(s->detection_object_filter_list) - 1); - s->detection_object_filter_list[sizeof(s->detection_object_filter_list) - 1] = '\0'; + safe_strcpy(s->detection_object_filter_list, detection_object_filter_list, sizeof(s->detection_object_filter_list), 0); } // Protocol and ONVIF @@ -1151,20 +1131,17 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) { // ONVIF credentials and settings const char *onvif_username = (const char *)sqlite3_column_text(stmt, COL_ONVIF_USERNAME); if (onvif_username) { - strncpy(s->onvif_username, onvif_username, sizeof(s->onvif_username) - 1); - s->onvif_username[sizeof(s->onvif_username) - 1] = '\0'; + safe_strcpy(s->onvif_username, onvif_username, sizeof(s->onvif_username), 0); } const char *onvif_password = (const char *)sqlite3_column_text(stmt, COL_ONVIF_PASSWORD); if (onvif_password) { - strncpy(s->onvif_password, onvif_password, sizeof(s->onvif_password) - 1); - s->onvif_password[sizeof(s->onvif_password) - 1] = '\0'; + safe_strcpy(s->onvif_password, onvif_password, sizeof(s->onvif_password), 0); } const char *onvif_profile = (const char *)sqlite3_column_text(stmt, COL_ONVIF_PROFILE); if (onvif_profile) { - strncpy(s->onvif_profile, onvif_profile, sizeof(s->onvif_profile) - 1); - s->onvif_profile[sizeof(s->onvif_profile) - 1] = '\0'; + safe_strcpy(s->onvif_profile, onvif_profile, sizeof(s->onvif_profile), 0); } s->onvif_port = (sqlite3_column_type(stmt, COL_ONVIF_PORT) != SQLITE_NULL) @@ -1178,16 +1155,14 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) { // Tags const char *tags_val = (const char *)sqlite3_column_text(stmt, COL_TAGS); if (tags_val) { - strncpy(s->tags, tags_val, sizeof(s->tags) - 1); - s->tags[sizeof(s->tags) - 1] = '\0'; + safe_strcpy(s->tags, tags_val, sizeof(s->tags), 0); } else { s->tags[0] = '\0'; } const char *admin_url = (const char *)sqlite3_column_text(stmt, COL_ADMIN_URL); if (admin_url) { - strncpy(s->admin_url, admin_url, sizeof(s->admin_url) - 1); - s->admin_url[sizeof(s->admin_url) - 1] = '\0'; + safe_strcpy(s->admin_url, admin_url, sizeof(s->admin_url), 0); } else { s->admin_url[0] = '\0'; } @@ -1198,8 +1173,7 @@ int get_all_stream_configs(stream_config_t *streams, int max_count) { // Cross-stream motion trigger source const char *motion_trigger_src = (const char *)sqlite3_column_text(stmt, COL_MOTION_TRIGGER_SOURCE); if (motion_trigger_src) { - strncpy(s->motion_trigger_source, motion_trigger_src, sizeof(s->motion_trigger_source) - 1); - s->motion_trigger_source[sizeof(s->motion_trigger_source) - 1] = '\0'; + safe_strcpy(s->motion_trigger_source, motion_trigger_src, sizeof(s->motion_trigger_source), 0); } else { s->motion_trigger_source[0] = '\0'; } @@ -1524,8 +1498,7 @@ int get_all_stream_names(char names[][MAX_STREAM_NAME], int max_count) { while (sqlite3_step(stmt) == SQLITE_ROW && count < max_count) { const char *name = (const char *)sqlite3_column_text(stmt, 0); if (name) { - strncpy(names[count], name, MAX_STREAM_NAME-1); - names[count][MAX_STREAM_NAME-1] = '\0'; + safe_strcpy(names[count], name, MAX_STREAM_NAME, 0); count++; } } diff --git a/src/database/db_system_settings.c b/src/database/db_system_settings.c index a3a2d227..fc52ec6e 100644 --- a/src/database/db_system_settings.c +++ b/src/database/db_system_settings.c @@ -8,6 +8,7 @@ #include "database/db_system_settings.h" #include "database/db_core.h" #include "core/logger.h" +#include "utils/strings.h" int db_get_system_setting(const char *key, char *out, int out_len) { if (!key || !out || out_len <= 0) return -1; @@ -27,8 +28,7 @@ int db_get_system_setting(const char *key, char *out, int out_len) { if (sqlite3_step(stmt) == SQLITE_ROW) { const char *val = (const char *)sqlite3_column_text(stmt, 0); if (val) { - strncpy(out, val, out_len - 1); - out[out_len - 1] = '\0'; + safe_strcpy(out, val, out_len, 0); rc = 0; } } diff --git a/src/database/db_zones.c b/src/database/db_zones.c index 3098fba0..8cb415bb 100644 --- a/src/database/db_zones.c +++ b/src/database/db_zones.c @@ -12,8 +12,7 @@ #include "database/db_zones.h" #include "database/db_core.h" #include "core/logger.h" - - +#include "utils/strings.h" /** * Convert polygon points to JSON string @@ -224,12 +223,11 @@ int get_detection_zones(const char *stream_name, detection_zone_t *zones, int ma while (sqlite3_step(stmt) == SQLITE_ROW && count < max_zones) { detection_zone_t *zone = &zones[count]; - strncpy(zone->id, (const char *)sqlite3_column_text(stmt, 0), MAX_ZONE_ID - 1); - strncpy(zone->stream_name, (const char *)sqlite3_column_text(stmt, 1), MAX_STREAM_NAME - 1); - strncpy(zone->name, (const char *)sqlite3_column_text(stmt, 2), MAX_ZONE_NAME - 1); + safe_strcpy(zone->id, (const char *)sqlite3_column_text(stmt, 0), MAX_ZONE_ID, 0); + safe_strcpy(zone->stream_name, (const char *)sqlite3_column_text(stmt, 1), MAX_STREAM_NAME, 0); + safe_strcpy(zone->name, (const char *)sqlite3_column_text(stmt, 2), MAX_ZONE_NAME, 0); zone->enabled = sqlite3_column_int(stmt, 3) != 0; - strncpy(zone->color, (const char *)sqlite3_column_text(stmt, 4), 7); - zone->color[7] = '\0'; + safe_strcpy(zone->color, (const char *)sqlite3_column_text(stmt, 4), 8, 0); const char *polygon_json = (const char *)sqlite3_column_text(stmt, 5); if (json_to_polygon(polygon_json, zone->polygon, &zone->polygon_count, MAX_ZONE_POINTS) != 0) { @@ -239,8 +237,7 @@ int get_detection_zones(const char *stream_name, detection_zone_t *zones, int ma const char *filter_classes = (const char *)sqlite3_column_text(stmt, 6); if (filter_classes) { - strncpy(zone->filter_classes, filter_classes, 255); - zone->filter_classes[255] = '\0'; + safe_strcpy(zone->filter_classes, filter_classes, sizeof(zone->filter_classes), 0); } else { zone->filter_classes[0] = '\0'; } diff --git a/src/database/sqlite_migrate.c b/src/database/sqlite_migrate.c index 41efc9a6..e67f9aee 100644 --- a/src/database/sqlite_migrate.c +++ b/src/database/sqlite_migrate.c @@ -16,6 +16,7 @@ #include "database/sqlite_migrate.h" #include "core/logger.h" +#include "utils/strings.h" /** * Internal migration context @@ -158,28 +159,25 @@ static int parse_migration_filename(const char *filename, char *version, char *d // Version is everything before the underscore size_t version_len = underscore - filename; if (version_len >= MIGRATE_VERSION_LEN) { + log_error("Migration file version too long in %s", filename); return -1; } // Verify version is all digits for (size_t i = 0; i < version_len; i++) { if (!isdigit((unsigned char)filename[i])) { + log_warn("Unexpected version character in %s", filename); return -1; } } - strncpy(version, filename, version_len); - version[version_len] = '\0'; + safe_strcpy(version, filename, MIGRATE_VERSION_LEN, version_len); // Description is everything after underscore, minus .sql const char *desc_start = underscore + 1; size_t desc_len = len - 4 - (desc_start - filename); - if (desc_len >= MIGRATE_DESC_LEN) { - desc_len = MIGRATE_DESC_LEN - 1; - } - strncpy(description, desc_start, desc_len); - description[desc_len] = '\0'; + safe_strcpy(description, desc_start, MIGRATE_DESC_LEN, desc_len); // Replace underscores with spaces for readability for (char *p = description; *p; p++) { @@ -372,21 +370,15 @@ static char *extract_sql_section(const char *content, const char *marker) { if (!end) { end = content + strlen(content); } + // Trim trailing whitespace + end = rtrim_pos(start, end - start); size_t len = end - start; - char *sql = malloc(len + 1); + char *sql = strndup(start, len); if (!sql) { return NULL; } - memcpy(sql, start, len); - sql[len] = '\0'; - - // Trim trailing whitespace - while (len > 0 && isspace((unsigned char)sql[len - 1])) { - sql[--len] = '\0'; - } - return sql; } @@ -610,7 +602,7 @@ static int execute_single_statement(sqlite3 *db, const char *sql) { // Check for ALTER TABLE ... ADD COLUMN and handle idempotently if (strncasecmp(sql, "ALTER TABLE", 11) == 0) { // Parse: ALTER TABLE tablename ADD COLUMN colname ... - char table[128] = {0}; + char table[128]; const char *p = sql + 11; while (*p && isspace((unsigned char)*p)) p++; @@ -632,7 +624,7 @@ static int execute_single_statement(sqlite3 *db, const char *sql) { while (*p && isspace((unsigned char)*p)) p++; // Extract column name - char column[128] = {0}; + char column[128]; i = 0; while (*p && !isspace((unsigned char)*p) && i < 127) { column[i++] = *p++; @@ -1150,8 +1142,7 @@ int migrate_get_version(const sqlite_migrate_t *ctx, char *version, size_t versi // Find the most recent applied migration for (int i = ctx->migration_count - 1; i >= 0; i--) { if (ctx->migrations[i].status == MIGRATE_STATUS_APPLIED) { - strncpy(version, ctx->migrations[i].version, version_len - 1); - version[version_len - 1] = '\0'; + safe_strcpy(version, ctx->migrations[i].version, version_len, 0); return 0; } } diff --git a/src/storage/storage_manager.c b/src/storage/storage_manager.c index 8f9453c5..20eca4fb 100644 --- a/src/storage/storage_manager.c +++ b/src/storage/storage_manager.c @@ -21,6 +21,7 @@ #include "core/logger.h" #include "core/mqtt_client.h" #include "core/path_utils.h" +#include "utils/strings.h" // Maximum number of streams to process at once #define MAX_STREAMS_BATCH 64 @@ -38,7 +39,6 @@ #define ORPHAN_SAFETY_THRESHOLD 0.5 #define MIN_RECORDINGS_FOR_THRESHOLD 10 #define MP4_SUBDIR "mp4" -#define MAX_STORAGE_PATH_LENGTH 256 #define MAX_RECORDING_PATH_LENGTH 768 // Forward declarations @@ -46,7 +46,7 @@ static int apply_legacy_retention_policy(void); // Storage manager state static struct { - char storage_path[MAX_STORAGE_PATH_LENGTH]; + char storage_path[MAX_PATH_LENGTH]; uint64_t max_size; int retention_days; bool auto_delete_oldest; @@ -118,8 +118,7 @@ int init_storage_manager(const char *storage_path, uint64_t max_size) { } // Copy storage path - strncpy(storage_manager.storage_path, storage_path, sizeof(storage_manager.storage_path) - 1); - storage_manager.storage_path[sizeof(storage_manager.storage_path) - 1] = '\0'; + safe_strcpy(storage_manager.storage_path, storage_path, sizeof(storage_manager.storage_path), 0); // Set maximum size storage_manager.max_size = max_size; diff --git a/src/storage/storage_manager_streams.c b/src/storage/storage_manager_streams.c index defe5409..0329ce9f 100644 --- a/src/storage/storage_manager_streams.c +++ b/src/storage/storage_manager_streams.c @@ -14,6 +14,7 @@ #include "database/db_streams.h" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" #include /** @@ -61,9 +62,8 @@ int get_stream_storage_usage(const char *storage_path, stream_storage_info_t *st // Only include streams that have recordings or storage // (include all to match previous behavior of including dirs with HLS segments) - strncpy(stream_info[stream_count].name, stream_names[i], - sizeof(stream_info[stream_count].name) - 1); - stream_info[stream_count].name[sizeof(stream_info[stream_count].name) - 1] = '\0'; + safe_strcpy(stream_info[stream_count].name, stream_names[i], + sizeof(stream_info[stream_count].name), 0); stream_info[stream_count].size_bytes = (unsigned long)total_bytes; stream_info[stream_count].recording_count = rec_count; stream_count++; diff --git a/src/telemetry/stream_metrics.c b/src/telemetry/stream_metrics.c index ed98bfe8..4bcd11d7 100644 --- a/src/telemetry/stream_metrics.c +++ b/src/telemetry/stream_metrics.c @@ -18,6 +18,7 @@ #include "core/shutdown_coordinator.h" #define LOG_COMPONENT "Metrics" #include "core/logger.h" +#include "utils/strings.h" /* ------------------------------------------------------------------ */ /* Global state */ @@ -225,7 +226,7 @@ stream_metrics_t *metrics_get_slot(const char *stream_name) { /* Double-check after acquiring lock */ if (!m->active) { memset(m->stream_name, 0, sizeof(m->stream_name)); - strncpy(m->stream_name, stream_name, MAX_STREAM_NAME - 1); + safe_strcpy(m->stream_name, stream_name, MAX_STREAM_NAME, 0); m->active = true; m->stream_start_time = time(NULL); m->prev_sample_time = 0; diff --git a/src/utils/rebuild_recordings.c b/src/tools/rebuild_recordings.c similarity index 94% rename from src/utils/rebuild_recordings.c rename to src/tools/rebuild_recordings.c index 195ff04d..3d9e4068 100644 --- a/src/utils/rebuild_recordings.c +++ b/src/tools/rebuild_recordings.c @@ -28,6 +28,7 @@ #include "database/db_recordings.h" #include "database/db_schema.h" #include "database/db_schema_cache.h" +#include "utils/strings.h" // Dummy URL for soft-deleted streams #define DUMMY_URL "rtsp://dummy.url/stream" @@ -152,14 +153,14 @@ static bool create_disabled_stream(const char *stream_name) { // Initialize stream configuration with default values memset(&stream, 0, sizeof(stream_config_t)); - strncpy(stream.name, stream_name, MAX_STREAM_NAME - 1); - strncpy(stream.url, DUMMY_URL, MAX_URL_LENGTH - 1); + safe_strcpy(stream.name, stream_name, MAX_STREAM_NAME, 0); + safe_strcpy(stream.url, DUMMY_URL, MAX_URL_LENGTH, 0); stream.enabled = false; stream.streaming_enabled = false; stream.width = 1280; stream.height = 720; stream.fps = 30; - strncpy(stream.codec, "h264", sizeof(stream.codec) - 1); + safe_strcpy(stream.codec, "h264", sizeof(stream.codec), 0); stream.priority = 5; stream.record = false; // Set record to false for disabled streams stream.segment_duration = 60; @@ -195,7 +196,7 @@ static bool extract_recording_info(const char *file_path, recording_file_info_t // Initialize info structure memset(info, 0, sizeof(recording_file_info_t)); - strncpy(info->path, file_path, MAX_PATH_LENGTH - 1); + safe_strcpy(info->path, file_path, MAX_PATH_LENGTH, 0); // Extract stream name from path // Assuming path format: /storage_path/mp4/stream_name/recording.mp4 @@ -212,12 +213,7 @@ static bool extract_recording_info(const char *file_path, recording_file_info_t return false; } - size_t stream_name_len = stream_name_end - stream_name_start; - if (stream_name_len >= MAX_STREAM_NAME) { - stream_name_len = MAX_STREAM_NAME - 1; - } - strncpy(info->stream_name, stream_name_start, stream_name_len); - info->stream_name[stream_name_len] = '\0'; + safe_strcpy(info->stream_name, stream_name_start, MAX_STREAM_NAME, stream_name_end - stream_name_start); // Get file size if (stat(file_path, &st) == 0) { @@ -264,9 +260,9 @@ static bool extract_recording_info(const char *file_path, recording_file_info_t // Get codec name const AVCodecDescriptor *codec_desc = avcodec_descriptor_get(codec_params->codec_id); if (codec_desc) { - strncpy(info->codec, codec_desc->name, sizeof(info->codec) - 1); + safe_strcpy(info->codec, codec_desc->name, sizeof(info->codec), 0); } else { - strncpy(info->codec, "unknown", sizeof(info->codec) - 1); + safe_strcpy(info->codec, "unknown", sizeof(info->codec), 0); } // Calculate FPS @@ -315,15 +311,15 @@ static bool add_recording_to_db(const recording_file_info_t *info) { // Fill in metadata memset(&metadata, 0, sizeof(recording_metadata_t)); - strncpy(metadata.stream_name, info->stream_name, sizeof(metadata.stream_name) - 1); - strncpy(metadata.file_path, info->path, sizeof(metadata.file_path) - 1); + safe_strcpy(metadata.stream_name, info->stream_name, sizeof(metadata.stream_name), 0); + safe_strcpy(metadata.file_path, info->path, sizeof(metadata.file_path), 0); metadata.start_time = info->start_time; metadata.end_time = info->end_time; metadata.size_bytes = info->size_bytes; metadata.width = info->width; metadata.height = info->height; metadata.fps = info->fps; - strncpy(metadata.codec, info->codec, sizeof(metadata.codec) - 1); + safe_strcpy(metadata.codec, info->codec, sizeof(metadata.codec), 0); metadata.is_complete = true; // Add to database @@ -525,10 +521,10 @@ int main(int argc, const char *argv[]) { // Parse command line arguments if (argc > 1) { - strncpy(storage_path, argv[1], sizeof(storage_path) - 1); + safe_strcpy(storage_path, argv[1], sizeof(storage_path), 0); } else { // Use storage path from config - strncpy(storage_path, config.storage_path, sizeof(storage_path) - 1); + safe_strcpy(storage_path, config.storage_path, sizeof(storage_path), 0); } printf("Using storage path: %s\n", storage_path); diff --git a/src/utils/memory.c b/src/utils/memory.c index eb71024a..582d1c7c 100644 --- a/src/utils/memory.c +++ b/src/utils/memory.c @@ -61,53 +61,6 @@ void safe_free(void *ptr) { } } -// Safe string duplication -char *safe_strdup(const char *str) { - if (!str) { - return NULL; - } - - char *ptr = strdup(str); - if (!ptr) { - log_error("String duplication failed for length %zu", strlen(str)); - } - - return ptr; -} - -// Safe string copy -int safe_strcpy(char *dest, const char *src, size_t size) { - if (!dest || !src || size == 0) { - return -1; - } - - size_t src_len = strlen(src); - if (src_len >= size) { - // Not enough space, truncate but still null-terminate - memcpy(dest, src, size - 1); - dest[size - 1] = '\0'; - return -1; - } - - memcpy(dest, src, src_len + 1); // Include null terminator - return 0; -} - -// Safe string concatenation -int safe_strcat(char *dest, const char *src, size_t size) { - if (!dest || !src || size == 0) { - return -1; - } - - size_t dest_len = strlen(dest); - if (dest_len >= size) { - // Destination already fills the buffer - return -1; - } - - return safe_strcpy(dest + dest_len, src, size - dest_len); -} - // Secure memory clearing that won't be optimized away void secure_zero_memory(void *ptr, size_t size) { if (!ptr || size == 0) { diff --git a/src/utils/strings.c b/src/utils/strings.c index 63600fb0..caae83f2 100644 --- a/src/utils/strings.c +++ b/src/utils/strings.c @@ -1,6 +1,60 @@ -#include -#include +#include "utils/strings.h" +#include "core/logger.h" +// Safe string duplication +char *safe_strdup(const char *str) { + if (!str) { + return NULL; + } + + char *ptr = strdup(str); + if (!ptr) { + log_error("String duplication failed for length %zu", strlen(str)); + } + + return ptr; +} + +// Safe string copy +int safe_strcpy(char *dest, const char *src, size_t dst_size, size_t src_size) { + if (!dest || !src || dst_size == 0) { + return -1; + } + + size_t src_len; + if (src_size > 0) { + src_len = strnlen(src, src_size); + // Note that if src_len == src_size, src may not be null-terminated + // and we will need to account for that. + } else { + src_len = strlen(src); + } + + if (src_len >= dst_size) { + // Not enough space: truncate before null-terminating + src_len = dst_size - 1; + } + memcpy(dest, src, src_len); + dest[src_len] = '\0'; + + return 0; +} + +// Safe string concatenation +int safe_strcat(char *dest, const char *src, size_t size) { + if (!dest || !src || size == 0) { + return -1; + } + + // Do not read beyond the destination buffer + size_t dest_len = strnlen(dest, size); + if (dest_len >= size) { + // Destination already fills the buffer + return -1; + } + + return safe_strcpy(dest + dest_len, src, size - dest_len, size - dest_len); +} bool ends_with(const char *str, const char *suffix) { if (!str || !suffix) @@ -14,3 +68,36 @@ bool ends_with(const char *str, const char *suffix) { return strcmp(str + str_len - suffix_len, suffix) == 0; } + +char *trim_ascii_whitespace(char *value) { + if (!value) { + return NULL; + } + + // Input is not const so output is not const + char *end = (char *)rtrim_pos(value, 0); + *end = '\0'; + + return (char *)ltrim_pos(value); +} + +size_t copy_trimmed_value(char *output, size_t output_size, const char *input, size_t input_size) { + if (!input || !output || output_size == 0) { + return false; + } + + const char *start = ltrim_pos(input); + + // Allow `input_size` to limit the length of the input string. This allows us to pass + // unterminated input strings and only copy up to the specified limit. + const char *end = rtrim_pos(input, input_size); + + // Does not include the null terminator + size_t len = end - start; + if (len >= output_size) { + len = output_size - 1; + } + memcpy(output, start, len); + output[len] = '\0'; + return len; +} diff --git a/src/video/api_detection.c b/src/video/api_detection.c index 2c287bff..19646ccb 100644 --- a/src/video/api_detection.c +++ b/src/video/api_detection.c @@ -15,6 +15,7 @@ #include "core/curl_init.h" #include "core/shutdown_coordinator.h" #include "core/mqtt_client.h" +#include "utils/strings.h" #include "video/api_detection.h" #include "video/detection_result.h" #include "video/stream_manager.h" @@ -591,7 +592,7 @@ int detect_objects_api(const char *api_url, const unsigned char *frame_data, goto cleanup; } - char preview[API_DETECTION_RESPONSE_PREVIEW_LEN] = {0}; + char preview[API_DETECTION_RESPONSE_PREVIEW_LEN]; int preview_len = (int)(chunk.size < (API_DETECTION_RESPONSE_PREVIEW_LEN - 1) ? chunk.size : (API_DETECTION_RESPONSE_PREVIEW_LEN - 1)); @@ -677,8 +678,7 @@ int detect_objects_api(const char *api_url, const unsigned char *frame_data, } // Add the detection to the result - strncpy(result->detections[result->count].label, label->valuestring, MAX_LABEL_LENGTH - 1); - result->detections[result->count].label[MAX_LABEL_LENGTH - 1] = '\0'; + safe_strcpy(result->detections[result->count].label, label->valuestring, MAX_LABEL_LENGTH, 0); result->detections[result->count].confidence = (float)confidence->valuedouble; result->detections[result->count].x = (float)x_min->valuedouble; result->detections[result->count].y = (float)y_min->valuedouble; @@ -696,8 +696,7 @@ int detect_objects_api(const char *api_url, const unsigned char *frame_data, // Parse optional zone_id field cJSON *zone_id = cJSON_GetObjectItem(detection, "zone_id"); if (zone_id && cJSON_IsString(zone_id)) { - strncpy(result->detections[result->count].zone_id, zone_id->valuestring, MAX_ZONE_ID_LENGTH - 1); - result->detections[result->count].zone_id[MAX_ZONE_ID_LENGTH - 1] = '\0'; + safe_strcpy(result->detections[result->count].zone_id, zone_id->valuestring, MAX_ZONE_ID_LENGTH, 0); } else { result->detections[result->count].zone_id[0] = '\0'; // Empty zone } @@ -1039,8 +1038,7 @@ int detect_objects_api_snapshot(const char *api_url, const char *stream_name, } // Add detection to result - strncpy(result->detections[result->count].label, label->valuestring, MAX_LABEL_LENGTH - 1); - result->detections[result->count].label[MAX_LABEL_LENGTH - 1] = '\0'; + safe_strcpy(result->detections[result->count].label, label->valuestring, MAX_LABEL_LENGTH, 0); result->detections[result->count].confidence = (float)confidence->valuedouble; result->detections[result->count].x = (float)x_min->valuedouble; result->detections[result->count].y = (float)y_min->valuedouble; @@ -1053,8 +1051,7 @@ int detect_objects_api_snapshot(const char *api_url, const char *stream_name, cJSON *zone_id = cJSON_GetObjectItem(detection, "zone_id"); if (zone_id && cJSON_IsString(zone_id)) { - strncpy(result->detections[result->count].zone_id, zone_id->valuestring, MAX_ZONE_ID_LENGTH - 1); - result->detections[result->count].zone_id[MAX_ZONE_ID_LENGTH - 1] = '\0'; + safe_strcpy(result->detections[result->count].zone_id, zone_id->valuestring, MAX_ZONE_ID_LENGTH, 0); } else { result->detections[result->count].zone_id[0] = '\0'; } diff --git a/src/video/buffer_strategy_go2rtc.c b/src/video/buffer_strategy_go2rtc.c index 8d6bdcee..a451b898 100644 --- a/src/video/buffer_strategy_go2rtc.c +++ b/src/video/buffer_strategy_go2rtc.c @@ -28,6 +28,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/url_utils.h" +#include "utils/strings.h" // go2rtc session state typedef struct { @@ -118,16 +119,15 @@ static int go2rtc_init_session(go2rtc_strategy_data_t *data) { const char *id_end = strchr(id_start, '\n'); if (!id_end) id_end = strchr(id_start, '\r'); if (!id_end) id_end = id_start + strlen(id_start); + // Remove any trailing whitespace + id_end = rtrim_pos(id_start, id_end - id_start); size_t id_len = id_end - id_start; if (id_len > 0 && id_len < sizeof(data->session_id)) { - strncpy(data->session_id, id_start, id_len); - data->session_id[id_len] = '\0'; - // Remove any trailing whitespace or query params + safe_strcpy(data->session_id, id_start, sizeof(data->session_id), id_len); + // Remove query params char *amp = strchr(data->session_id, '&'); if (amp) *amp = '\0'; - char *ws = strchr(data->session_id, ' '); - if (ws) *ws = '\0'; } } @@ -190,7 +190,7 @@ static int go2rtc_strategy_init(pre_buffer_strategy_t *self, const buffer_config go2rtc_strategy_data_t *data = (go2rtc_strategy_data_t *)self->private_data; if (config->go2rtc_url && config->go2rtc_url[0]) { - strncpy(data->go2rtc_url, config->go2rtc_url, sizeof(data->go2rtc_url) - 1); + safe_strcpy(data->go2rtc_url, config->go2rtc_url, sizeof(data->go2rtc_url), 0); } else { // Default to localhost snprintf(data->go2rtc_url, sizeof(data->go2rtc_url), @@ -392,11 +392,11 @@ pre_buffer_strategy_t* create_go2rtc_strategy(const char *stream_name, return NULL; } - strncpy(data->stream_name, stream_name, sizeof(data->stream_name) - 1); + safe_strcpy(data->stream_name, stream_name, sizeof(data->stream_name), 0); strategy->name = "go2rtc_native"; strategy->type = BUFFER_STRATEGY_GO2RTC_NATIVE; - strncpy(strategy->stream_name, stream_name, sizeof(strategy->stream_name) - 1); + safe_strcpy(strategy->stream_name, stream_name, sizeof(strategy->stream_name), 0); strategy->private_data = data; // Set interface methods diff --git a/src/video/buffer_strategy_hls_segment.c b/src/video/buffer_strategy_hls_segment.c index 96c52707..d47a755e 100644 --- a/src/video/buffer_strategy_hls_segment.c +++ b/src/video/buffer_strategy_hls_segment.c @@ -24,6 +24,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/path_utils.h" +#include "utils/strings.h" // Maximum segments to track #define MAX_TRACKED_SEGMENTS 32 @@ -118,7 +119,7 @@ static int scan_existing_segments(hls_segment_strategy_data_t *data) { // Add to circular buffer tracked_segment_t *seg = &data->segments[data->head]; - strncpy(seg->path, path, sizeof(seg->path) - 1); + safe_strcpy(seg->path, path, sizeof(seg->path), 0); seg->mtime = st.st_mtime; seg->duration_seconds = duration; seg->size_bytes = st.st_size; @@ -236,7 +237,7 @@ static int hls_segment_strategy_add_segment(pre_buffer_strategy_t *self, // Add new segment tracked_segment_t *seg = &data->segments[data->head]; - strncpy(seg->path, segment_path, sizeof(seg->path) - 1); + safe_strcpy(seg->path, segment_path, sizeof(seg->path), 0); seg->mtime = st.st_mtime; seg->duration_seconds = duration > 0 ? duration : data->default_segment_duration; seg->size_bytes = st.st_size; @@ -308,7 +309,7 @@ static int hls_segment_strategy_get_segments(pre_buffer_strategy_t *self, const tracked_segment_t *seg = &data->segments[idx]; if (seg->valid) { - strncpy(segments[count].path, seg->path, sizeof(segments[count].path) - 1); + safe_strcpy(segments[count].path, seg->path, sizeof(segments[count].path), 0); segments[count].timestamp = seg->mtime; segments[count].duration = seg->duration_seconds; segments[count].size_bytes = seg->size_bytes; @@ -441,11 +442,11 @@ pre_buffer_strategy_t* create_hls_segment_strategy(const char *stream_name, return NULL; } - strncpy(data->stream_name, stream_name, sizeof(data->stream_name) - 1); + safe_strcpy(data->stream_name, stream_name, sizeof(data->stream_name), 0); strategy->name = "hls_segment"; strategy->type = BUFFER_STRATEGY_HLS_SEGMENT; - strncpy(strategy->stream_name, stream_name, sizeof(strategy->stream_name) - 1); + safe_strcpy(strategy->stream_name, stream_name, sizeof(strategy->stream_name), 0); strategy->private_data = data; // Set interface methods diff --git a/src/video/buffer_strategy_memory_packet.c b/src/video/buffer_strategy_memory_packet.c index 48e3e2d2..b3be378b 100644 --- a/src/video/buffer_strategy_memory_packet.c +++ b/src/video/buffer_strategy_memory_packet.c @@ -29,6 +29,7 @@ #include "video/packet_buffer.h" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" // Strategy private data typedef struct { @@ -318,11 +319,11 @@ pre_buffer_strategy_t* create_memory_packet_strategy(const char *stream_name, return NULL; } - strncpy(data->stream_name, stream_name, sizeof(data->stream_name) - 1); + safe_strcpy(data->stream_name, stream_name, sizeof(data->stream_name), 0); strategy->name = "memory_packet"; strategy->type = BUFFER_STRATEGY_MEMORY_PACKET; - strncpy(strategy->stream_name, stream_name, sizeof(strategy->stream_name) - 1); + safe_strcpy(strategy->stream_name, stream_name, sizeof(strategy->stream_name), 0); strategy->private_data = data; // Set interface methods diff --git a/src/video/buffer_strategy_mmap.c b/src/video/buffer_strategy_mmap.c index 2903e4f5..3664a069 100644 --- a/src/video/buffer_strategy_mmap.c +++ b/src/video/buffer_strategy_mmap.c @@ -35,6 +35,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/path_utils.h" +#include "utils/strings.h" // Packet entry in mmap buffer (fixed size for simplicity) typedef struct { @@ -130,7 +131,7 @@ static int create_mmap_file(mmap_strategy_data_t *data, size_t size) { data->header->tail = 0; data->header->total_size = size; data->header->data_offset = sizeof(mmap_buffer_header_t); - strncpy(data->header->stream_name, data->stream_name, sizeof(data->header->stream_name) - 1); + safe_strcpy(data->header->stream_name, data->stream_name, sizeof(data->header->stream_name), 0); // Advise kernel about access pattern madvise(data->mapped_data, size, MADV_SEQUENTIAL); @@ -398,12 +399,12 @@ pre_buffer_strategy_t* create_mmap_hybrid_strategy(const char *stream_name, return NULL; } - strncpy(data->stream_name, stream_name, sizeof(data->stream_name) - 1); + safe_strcpy(data->stream_name, stream_name, sizeof(data->stream_name), 0); data->fd = -1; strategy->name = "mmap_hybrid"; strategy->type = BUFFER_STRATEGY_MMAP_HYBRID; - strncpy(strategy->stream_name, stream_name, sizeof(strategy->stream_name) - 1); + safe_strcpy(strategy->stream_name, stream_name, sizeof(strategy->stream_name), 0); strategy->private_data = data; // Set interface methods diff --git a/src/video/detection_model.c b/src/video/detection_model.c index 14b04798..38cbd2e5 100644 --- a/src/video/detection_model.c +++ b/src/video/detection_model.c @@ -226,15 +226,14 @@ static detection_model_t load_tflite_model(const char *model_path, float thresho } // Initialize model structure - strncpy(model->type, MODEL_TYPE_TFLITE, sizeof(model->type) - 1); + safe_strcpy(model->type, MODEL_TYPE_TFLITE, sizeof(model->type), 0); model->tflite.handle = handle; model->tflite.model = tflite_model; model->tflite.threshold = threshold; model->tflite.load_model = tflite_load_model; model->tflite.free_model = tflite_free_model; model->tflite.detect = tflite_detect; - strncpy(model->path, model_path, MAX_PATH_LENGTH - 1); - model->path[MAX_PATH_LENGTH - 1] = '\0'; // Ensure null termination + safe_strcpy(model->path, model_path, MAX_PATH_LENGTH, 0); log_info("TFLite model loaded: %s", model_path); return model; @@ -350,11 +349,10 @@ detection_model_t load_detection_model(const char *model_path, float threshold) // For API models, we just need to store the URL model_t *m = (model_t *)malloc(sizeof(model_t)); if (m) { - strncpy(m->type, MODEL_TYPE_API, sizeof(m->type) - 1); + safe_strcpy(m->type, MODEL_TYPE_API, sizeof(m->type), 0); m->sod = NULL; // We don't need a model handle for API m->threshold = threshold; - strncpy(m->path, model_path, MAX_PATH_LENGTH - 1); - m->path[MAX_PATH_LENGTH - 1] = '\0'; // Ensure null termination + safe_strcpy(m->path, model_path, MAX_PATH_LENGTH, 0); model = m; // Initialize the API detection system @@ -365,11 +363,10 @@ detection_model_t load_detection_model(const char *model_path, float threshold) // For ONVIF models, we just need to store the URL model_t *m = (model_t *)malloc(sizeof(model_t)); if (m) { - strncpy(m->type, MODEL_TYPE_ONVIF, sizeof(m->type) - 1); + safe_strcpy(m->type, MODEL_TYPE_ONVIF, sizeof(m->type), 0); m->sod = NULL; // We don't need a model handle for ONVIF m->threshold = threshold; - strncpy(m->path, model_path, MAX_PATH_LENGTH - 1); - m->path[MAX_PATH_LENGTH - 1] = '\0'; // Ensure null termination + safe_strcpy(m->path, model_path, MAX_PATH_LENGTH, 0); model = m; // Initialize the ONVIF detection system @@ -382,11 +379,10 @@ detection_model_t load_detection_model(const char *model_path, float threshold) // The actual detection is done by detect_motion() in motion_detection.c model_t *m = (model_t *)malloc(sizeof(model_t)); if (m) { - strncpy(m->type, MODEL_TYPE_MOTION, sizeof(m->type) - 1); + safe_strcpy(m->type, MODEL_TYPE_MOTION, sizeof(m->type), 0); m->sod = NULL; // No external model handle needed m->threshold = threshold; - strncpy(m->path, model_path, MAX_PATH_LENGTH - 1); - m->path[MAX_PATH_LENGTH - 1] = '\0'; // Ensure null termination + safe_strcpy(m->path, model_path, MAX_PATH_LENGTH, 0); model = m; log_info("Built-in motion detection model handle created"); } @@ -397,11 +393,10 @@ detection_model_t load_detection_model(const char *model_path, float threshold) // Create model structure model_t *m = (model_t *)malloc(sizeof(model_t)); if (m) { - strncpy(m->type, MODEL_TYPE_SOD_REALNET, sizeof(m->type) - 1); + safe_strcpy(m->type, MODEL_TYPE_SOD_REALNET, sizeof(m->type), 0); m->sod_realnet = realnet_model; m->threshold = threshold; - strncpy(m->path, model_path, MAX_PATH_LENGTH - 1); - m->path[MAX_PATH_LENGTH - 1] = '\0'; // Ensure null termination + safe_strcpy(m->path, model_path, MAX_PATH_LENGTH, 0); model = m; } else { free_sod_realnet_model(realnet_model); @@ -433,11 +428,11 @@ void unload_detection_model(detection_model_t model) { } model_t *m = (model_t *)model; - char model_path[MAX_PATH_LENGTH] = {0}; + char model_path[MAX_PATH_LENGTH]; // Save the path for logging if (m->path[0] != '\0') { - strncpy(model_path, m->path, MAX_PATH_LENGTH - 1); + safe_strcpy(model_path, m->path, MAX_PATH_LENGTH, 0); } else { strcpy(model_path, "unknown"); } diff --git a/src/video/detection_stream.c b/src/video/detection_stream.c index 968199f3..6fcbf221 100644 --- a/src/video/detection_stream.c +++ b/src/video/detection_stream.c @@ -18,6 +18,7 @@ #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/streams.h" #include "video/stream_reader.h" @@ -147,7 +148,7 @@ int start_detection_stream_reader(const char *stream_name, int detection_interva // Just store the detection configuration // Initialize detection stream - strncpy(detection_streams[slot].stream_name, stream_name, MAX_STREAM_NAME - 1); + safe_strcpy(detection_streams[slot].stream_name, stream_name, MAX_STREAM_NAME, 0); detection_streams[slot].detection_interval = detection_interval; detection_streams[slot].frame_counter = 0; diff --git a/src/video/go2rtc/go2rtc_api.c b/src/video/go2rtc/go2rtc_api.c index 30b9eb7c..f16fb15b 100644 --- a/src/video/go2rtc/go2rtc_api.c +++ b/src/video/go2rtc/go2rtc_api.c @@ -3,11 +3,6 @@ * @brief Implementation of the go2rtc API client */ -#include "video/go2rtc/go2rtc_api.h" -#include "core/config.h" -#include "core/logger.h" -#include "core/url_utils.h" - #include #include #include @@ -15,6 +10,11 @@ #include #include +#include "video/go2rtc/go2rtc_api.h" +#include "core/config.h" +#include "core/logger.h" +#include "core/url_utils.h" +#include "utils/strings.h" // API client configuration static char *g_api_host = NULL; @@ -130,7 +130,7 @@ bool go2rtc_api_add_stream(const char *stream_id, const char *stream_url) { // Format the URL for the API endpoint with query parameters (simple method) // This is the method that works according to user feedback // URL encode the stream_url to handle special characters - char encoded_url[URL_BUFFER_SIZE * 3] = {0}; // Extra space for URL encoding + char encoded_url[URL_BUFFER_SIZE * 3]; // Extra space for URL encoding simple_url_escape(stream_url, encoded_url, URL_BUFFER_SIZE * 3); // Sanitize the stream name so that names with spaces work correctly. @@ -226,7 +226,7 @@ bool go2rtc_api_add_stream_multi(const char *stream_id, const char **sources, in // Add each source as a src parameter for (int i = 0; i < num_sources; i++) { // URL encode the source - char encoded_url[URL_BUFFER_SIZE * 3] = {0}; + char encoded_url[URL_BUFFER_SIZE * 3]; simple_url_escape(sources[i], encoded_url, URL_BUFFER_SIZE * 3); if (i > 0) { @@ -452,8 +452,7 @@ bool go2rtc_api_stream_exists(const char *stream_id) { } else { // Log first 200 chars of body to help diagnose parsing issues char preview[201]; - strncpy(preview, chunk.memory, 200); - preview[200] = '\0'; + safe_strcpy(preview, chunk.memory, 201, 0); log_warn("Failed to parse go2rtc /api/streams response as JSON. " "Body preview (first 200 chars): %.200s", preview); } @@ -583,8 +582,7 @@ bool go2rtc_api_get_streams(char *buffer, size_t buffer_size) { long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); if (http_code == 200) { - strncpy(buffer, resp.buffer, buffer_size - 1); - buffer[buffer_size - 1] = '\0'; + safe_strcpy(buffer, resp.buffer, buffer_size, 0); success = true; } } diff --git a/src/video/go2rtc/go2rtc_consumer.c b/src/video/go2rtc/go2rtc_consumer.c index b379b66a..eb2e15a7 100644 --- a/src/video/go2rtc/go2rtc_consumer.c +++ b/src/video/go2rtc/go2rtc_consumer.c @@ -3,18 +3,19 @@ * @brief Implementation of the go2rtc consumer module for recording and HLS streaming */ +#include +#include +#include +#include +#include + #include "video/go2rtc/go2rtc_consumer.h" #include "video/go2rtc/go2rtc_api.h" #include "video/go2rtc/go2rtc_stream.h" #include "core/logger.h" #include "core/config.h" #include "core/url_utils.h" - -#include -#include -#include -#include -#include +#include "utils/strings.h" // Buffer sizes #define URL_BUFFER_SIZE 2048 @@ -76,11 +77,9 @@ static consumer_state_t *add_consumer(consumer_state_t *consumers, const char *s // Find an empty slot for (int i = 0; i < MAX_CONSUMERS; i++) { if (!consumers[i].is_active) { - strncpy(consumers[i].stream_id, stream_id, MAX_STREAM_NAME - 1); - consumers[i].stream_id[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(consumers[i].stream_id, stream_id, MAX_STREAM_NAME, 0); - strncpy(consumers[i].output_path, output_path, MAX_PATH_LENGTH - 1); - consumers[i].output_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(consumers[i].output_path, output_path, MAX_PATH_LENGTH, 0); consumers[i].segment_duration = segment_duration; consumers[i].is_active = true; diff --git a/src/video/go2rtc/go2rtc_integration.c b/src/video/go2rtc/go2rtc_integration.c index 9d4bda2f..bf7c445f 100644 --- a/src/video/go2rtc/go2rtc_integration.c +++ b/src/video/go2rtc/go2rtc_integration.c @@ -7,6 +7,15 @@ * - Process-level health (restarting go2rtc when it becomes unresponsive) */ +#include +#include +#include +#include +#include +#include +#include +#include + #include "video/go2rtc/go2rtc_integration.h" #include "video/go2rtc/go2rtc_consumer.h" #include "video/go2rtc/go2rtc_stream.h" @@ -16,6 +25,7 @@ #include "core/config.h" #include "core/url_utils.h" #include "core/shutdown_coordinator.h" // For is_shutdown_initiated +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/mp4_recording.h" #include "video/hls/hls_api.h" @@ -24,15 +34,6 @@ #include "video/stream_state.h" #include "video/unified_detection_thread.h" -#include -#include -#include -#include -#include -#include -#include -#include - // Tracking for streams using go2rtc #define MAX_TRACKED_STREAMS MAX_STREAMS @@ -152,7 +153,7 @@ static stuck_stream_tracker_t *get_or_create_stuck_tracker(const char *stream_na for (int i = 0; i < MAX_TRACKED_STREAMS; i++) { if (!g_stuck_trackers[i].tracking_active) { memset(&g_stuck_trackers[i], 0, sizeof(stuck_stream_tracker_t)); - strncpy(g_stuck_trackers[i].stream_name, stream_name, MAX_STREAM_NAME - 1); + safe_strcpy(g_stuck_trackers[i].stream_name, stream_name, MAX_STREAM_NAME, 0); g_stuck_trackers[i].tracking_active = true; g_stuck_trackers[i].last_bytes_recv = -1; // -1 = not yet initialized g_stuck_trackers[i].last_bytes_send = -1; @@ -340,17 +341,10 @@ static void save_original_config(const char *stream_name, const char *url, const char *username, const char *password) { for (int i = 0; i < MAX_TRACKED_STREAMS; i++) { if (g_original_configs[i].stream_name[0] == '\0') { - strncpy(g_original_configs[i].stream_name, stream_name, MAX_STREAM_NAME - 1); - g_original_configs[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; - - strncpy(g_original_configs[i].original_url, url, MAX_PATH_LENGTH - 1); - g_original_configs[i].original_url[MAX_PATH_LENGTH - 1] = '\0'; - - strncpy(g_original_configs[i].original_username, username, MAX_STREAM_NAME - 1); - g_original_configs[i].original_username[MAX_STREAM_NAME - 1] = '\0'; - - strncpy(g_original_configs[i].original_password, password, MAX_STREAM_NAME - 1); - g_original_configs[i].original_password[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(g_original_configs[i].stream_name, stream_name, MAX_STREAM_NAME, 0); + safe_strcpy(g_original_configs[i].original_url, url, MAX_PATH_LENGTH, 0); + safe_strcpy(g_original_configs[i].original_username, username, MAX_STREAM_NAME, 0); + safe_strcpy(g_original_configs[i].original_password, password, MAX_STREAM_NAME, 0); return; } @@ -376,14 +370,9 @@ static bool get_original_config(const char *stream_name, char *url, size_t url_s if (g_original_configs[i].stream_name[0] != '\0' && strcmp(g_original_configs[i].stream_name, stream_name) == 0) { - strncpy(url, g_original_configs[i].original_url, url_size - 1); - url[url_size - 1] = '\0'; - - strncpy(username, g_original_configs[i].original_username, username_size - 1); - username[username_size - 1] = '\0'; - - strncpy(password, g_original_configs[i].original_password, password_size - 1); - password[password_size - 1] = '\0'; + safe_strcpy(url, g_original_configs[i].original_url, url_size, 0); + safe_strcpy(username, g_original_configs[i].original_username, username_size, 0); + safe_strcpy(password, g_original_configs[i].original_password, password_size, 0); // Clear the entry g_original_configs[i].stream_name[0] = '\0'; @@ -427,8 +416,7 @@ static go2rtc_stream_tracking_t *add_tracked_stream(const char *stream_name) { // Find an empty slot for (int i = 0; i < MAX_TRACKED_STREAMS; i++) { if (g_tracked_streams[i].stream_name[0] == '\0') { - strncpy(g_tracked_streams[i].stream_name, stream_name, MAX_STREAM_NAME - 1); - g_tracked_streams[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(g_tracked_streams[i].stream_name, stream_name, MAX_STREAM_NAME, 0); g_tracked_streams[i].using_go2rtc_for_recording = false; g_tracked_streams[i].using_go2rtc_for_hls = false; return &g_tracked_streams[i]; @@ -1797,10 +1785,10 @@ bool go2rtc_integration_register_stream(const char *stream_name) { char password[64] = {0}; if (config.onvif_username[0] != '\0') { - strncpy(username, config.onvif_username, sizeof(username) - 1); + safe_strcpy(username, config.onvif_username, sizeof(username), 0); } if (config.onvif_password[0] != '\0') { - strncpy(password, config.onvif_password, sizeof(password) - 1); + safe_strcpy(password, config.onvif_password, sizeof(password), 0); } // If credentials not in onvif fields, try to extract from URL @@ -1815,15 +1803,13 @@ bool go2rtc_integration_register_stream(const char *stream_name) { // Extract username size_t username_len = colon - (url + 7); if (username_len < sizeof(username)) { - strncpy(username, url + 7, username_len); - username[username_len] = '\0'; + safe_strcpy(username, url + 7, sizeof(username), username_len); // Extract password if not already set if (password[0] == '\0') { size_t password_len = at_sign - (colon + 1); if (password_len < sizeof(password)) { - strncpy(password, colon + 1, password_len); - password[password_len] = '\0'; + safe_strcpy(password, colon + 1, sizeof(password), password_len); } } } diff --git a/src/video/go2rtc/go2rtc_process.c b/src/video/go2rtc/go2rtc_process.c index 352b05c4..1692d845 100644 --- a/src/video/go2rtc/go2rtc_process.c +++ b/src/video/go2rtc/go2rtc_process.c @@ -2,12 +2,6 @@ * @file go2rtc_process.c * @brief Implementation of the go2rtc process management module */ - -#include "video/go2rtc/go2rtc_process.h" -#include "video/go2rtc/go2rtc_api.h" -#include "core/logger.h" -#include "core/config.h" - #include #include #include @@ -23,6 +17,13 @@ #include #include +#include "video/go2rtc/go2rtc_process.h" +#include "video/go2rtc/go2rtc_api.h" +#include "core/logger.h" +#include "core/config.h" +#include "utils/strings.h" + + // Define PATH_MAX if not defined #ifndef PATH_MAX #define PATH_MAX 4096 @@ -212,8 +213,7 @@ static bool check_tcp_port_open(int port) { char *fields[5] = {NULL}; char *saveptr = NULL; char linecopy[512]; - strncpy(linecopy, line, sizeof(linecopy) - 1); - linecopy[sizeof(linecopy) - 1] = '\0'; + safe_strcpy(linecopy, line, sizeof(linecopy), 0); int f = 0; char *tok = strtok_r(linecopy, " \t", &saveptr); @@ -250,8 +250,7 @@ static void find_binary_in_path(const char *name, char *out, size_t out_size) { if (!path_env) path_env = "/usr/local/bin:/usr/bin:/bin"; char path_copy[4096]; - strncpy(path_copy, path_env, sizeof(path_copy) - 1); - path_copy[sizeof(path_copy) - 1] = '\0'; + safe_strcpy(path_copy, path_env, sizeof(path_copy), 0); char *saveptr = NULL; const char *dir = strtok_r(path_copy, ":", &saveptr); @@ -259,15 +258,13 @@ static void find_binary_in_path(const char *name, char *out, size_t out_size) { char candidate[PATH_MAX]; int n = snprintf(candidate, sizeof(candidate), "%s/%s", dir, name); if (n > 0 && n < (int)sizeof(candidate) && access(candidate, X_OK) == 0) { - strncpy(out, candidate, out_size - 1); - out[out_size - 1] = '\0'; + safe_strcpy(out, candidate, out_size, 0); return; } dir = strtok_r(NULL, ":", &saveptr); } // Not found – use bare name and let execvp search PATH at exec time - strncpy(out, name, out_size - 1); - out[out_size - 1] = '\0'; + safe_strcpy(out, name, out_size, 0); } /** @@ -342,8 +339,7 @@ static void recursive_remove_at(int parent_dfd, const char *name) { */ static void recursive_remove(const char *path) { char parent[PATH_MAX]; - strncpy(parent, path, sizeof(parent) - 1); - parent[sizeof(parent) - 1] = '\0'; + safe_strcpy(parent, path, sizeof(parent), 0); const char *name; int parent_fd; @@ -468,14 +464,12 @@ static bool check_go2rtc_in_path(char *binary_path, size_t buffer_size) { if (path[0] != '\0' && access(path, X_OK) == 0) { log_info("Found go2rtc binary in PATH: %s", path); - strncpy(binary_path, path, buffer_size - 1); - binary_path[buffer_size - 1] = '\0'; + safe_strcpy(binary_path, path, buffer_size, 0); return true; } // If not found in PATH, just use "go2rtc" and let execl resolve it - strncpy(binary_path, "go2rtc", buffer_size - 1); - binary_path[buffer_size - 1] = '\0'; + safe_strcpy(binary_path, "go2rtc", buffer_size, 0); log_info("Using 'go2rtc' from PATH"); return true; } @@ -529,7 +523,7 @@ bool go2rtc_process_init(const char *binary_path, const char *config_dir, int ap if (binary_path && access(binary_path, X_OK) == 0) { // Use the provided binary path - strncpy(final_binary_path, binary_path, sizeof(final_binary_path) - 1); + safe_strcpy(final_binary_path, binary_path, sizeof(final_binary_path), 0); log_info("Using provided go2rtc binary: %s", final_binary_path); } else { if (binary_path) { @@ -632,8 +626,7 @@ bool go2rtc_process_generate_config(const char *config_path, int api_port) { if (global_config->go2rtc_ice_servers[0] != '\0') { // Parse comma-separated ICE servers char ice_servers_copy[512]; - strncpy(ice_servers_copy, global_config->go2rtc_ice_servers, sizeof(ice_servers_copy) - 1); - ice_servers_copy[sizeof(ice_servers_copy) - 1] = '\0'; + safe_strcpy(ice_servers_copy, global_config->go2rtc_ice_servers, sizeof(ice_servers_copy), 0); char *token = strtok(ice_servers_copy, ","); while (token != NULL) { @@ -1253,9 +1246,9 @@ bool go2rtc_process_start(int api_port) { char log_path[PATH_MAX]; // Use PATH_MAX to accommodate full filesystem paths // Extract directory from g_config->log_file - char log_dir[PATH_MAX] = {0}; if (g_config.log_file[0] != '\0') { - strncpy(log_dir, g_config.log_file, sizeof(log_dir) - 1); + char log_dir[PATH_MAX]; + safe_strcpy(log_dir, g_config.log_file, sizeof(log_dir), 0); // Find the last slash to get the directory char *last_slash = strrchr(log_dir, '/'); diff --git a/src/video/go2rtc/go2rtc_stream.c b/src/video/go2rtc/go2rtc_stream.c index 43aab418..1d7bd986 100644 --- a/src/video/go2rtc/go2rtc_stream.c +++ b/src/video/go2rtc/go2rtc_stream.c @@ -3,15 +3,6 @@ * @brief Implementation of the go2rtc stream integration module */ -#include "video/go2rtc/go2rtc_stream.h" -#include "video/go2rtc/go2rtc_process.h" -#include "video/go2rtc/go2rtc_api.h" -#include "video/go2rtc/go2rtc_integration.h" -#include "video/go2rtc/dns_cleanup.h" -#include "core/config.h" -#include "core/logger.h" -#include "core/url_utils.h" - #include #include #include @@ -25,6 +16,16 @@ #include #include +#include "video/go2rtc/go2rtc_stream.h" +#include "video/go2rtc/go2rtc_process.h" +#include "video/go2rtc/go2rtc_api.h" +#include "video/go2rtc/go2rtc_integration.h" +#include "video/go2rtc/dns_cleanup.h" +#include "core/config.h" +#include "core/logger.h" +#include "core/url_utils.h" +#include "utils/strings.h" + // Default API host #define DEFAULT_API_HOST "localhost" @@ -134,8 +135,7 @@ bool go2rtc_stream_register(const char *stream_id, const char *stream_url, // Use a static buffer for the modified URL to avoid memory allocation issues char modified_url[URL_BUFFER_SIZE]; - strncpy(modified_url, stream_url, URL_BUFFER_SIZE - 1); - modified_url[URL_BUFFER_SIZE - 1] = '\0'; + safe_strcpy(modified_url, stream_url, URL_BUFFER_SIZE, 0); // Inject credentials into URL if provided and not already embedded { @@ -143,8 +143,7 @@ bool go2rtc_stream_register(const char *stream_id, const char *stream_url, if (url_apply_credentials(modified_url, username, password, credentialed_url, sizeof(credentialed_url)) == 0) { if (strcmp(credentialed_url, modified_url) != 0) { - strncpy(modified_url, credentialed_url, URL_BUFFER_SIZE - 1); - modified_url[URL_BUFFER_SIZE - 1] = '\0'; + safe_strcpy(modified_url, credentialed_url, URL_BUFFER_SIZE, 0); log_info("Applied credentials to go2rtc source URL for registration"); } } @@ -181,8 +180,7 @@ bool go2rtc_stream_register(const char *stream_id, const char *stream_url, // Append fragment parameters to URL char new_url[URL_BUFFER_SIZE]; snprintf(new_url, URL_BUFFER_SIZE, "%s%s", modified_url, fragment_params); - strncpy(modified_url, new_url, URL_BUFFER_SIZE - 1); - modified_url[URL_BUFFER_SIZE - 1] = '\0'; + safe_strcpy(modified_url, new_url, URL_BUFFER_SIZE, 0); log_info("Prepared go2rtc source URL for stream registration of %s: %s", stream_id, modified_url); @@ -513,8 +511,6 @@ bool go2rtc_stream_is_ready(void) { // Use libcurl to check if the API is responsive CURL *curl = NULL; CURLcode res; - char url[URL_BUFFER_SIZE] = {0}; // Initialize to zeros - long http_code = 0; // Initialize curl with safety checks curl = curl_easy_init(); @@ -523,6 +519,9 @@ bool go2rtc_stream_is_ready(void) { return false; } + char url[URL_BUFFER_SIZE]; + long http_code = 0; + // Format the URL for the API endpoint with safety checks int url_result = snprintf(url, sizeof(url), "http://localhost:%d" GO2RTC_BASE_PATH "/api/streams", g_api_port); if (url_result < 0 || url_result >= (int)sizeof(url)) { @@ -658,8 +657,8 @@ bool go2rtc_stream_is_ready(void) { } // Log a truncated response to avoid buffer overflows in logging - char truncated_response[64] = {0}; - strncpy(truncated_response, response, sizeof(truncated_response) - 1); + char truncated_response[64]; + safe_strcpy(truncated_response, response, sizeof(truncated_response), 0); log_warn("go2rtc_stream_is_ready: socket HTTP request failed: %s...", truncated_response); close(sockfd); @@ -846,12 +845,12 @@ bool go2rtc_stream_start_service(void) { } // Try to get the process log - char log_path[1024]; + char log_path[MAX_PATH_LENGTH]; // Extract directory from g_config.log_file - char log_dir[1024] = {0}; if (g_config.log_file[0] != '\0') { - strncpy(log_dir, g_config.log_file, sizeof(log_dir) - 1); + char log_dir[MAX_PATH_LENGTH]; + safe_strcpy(log_dir, g_config.log_file, sizeof(log_dir), 0); // Find the last slash to get the directory char *last_slash = strrchr(log_dir, '/'); diff --git a/src/video/hls/hls_context.c b/src/video/hls/hls_context.c index 2d4d9270..78f3918b 100644 --- a/src/video/hls/hls_context.c +++ b/src/video/hls/hls_context.c @@ -3,7 +3,9 @@ #include #include #include + #include "core/logger.h" +#include "utils/strings.h" #include "video/stream_state.h" #include "video/hls/hls_context.h" @@ -108,8 +110,7 @@ void mark_stream_stopping(const char *stream_name) { // Add to the list if there's space if (stopping_stream_count < MAX_STREAMS) { - strncpy(stopping_streams[stopping_stream_count], stream_name, MAX_STREAM_NAME - 1); - stopping_streams[stopping_stream_count][MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stopping_streams[stopping_stream_count], stream_name, MAX_STREAM_NAME, 0); stopping_stream_count++; log_info("Marked stream %s as stopping (legacy method)", stream_name); } @@ -159,7 +160,7 @@ void unmark_stream_stopping(const char *stream_name) { if (strcmp(stopping_streams[i], stream_name) == 0) { // Remove by shifting remaining entries for (int j = i; j < stopping_stream_count - 1; j++) { - strncpy(stopping_streams[j], stopping_streams[j + 1], MAX_STREAM_NAME); + safe_strcpy(stopping_streams[j], stopping_streams[j + 1], MAX_STREAM_NAME, 0); } stopping_stream_count--; log_info("Unmarked stream %s as stopping (legacy method)", stream_name); diff --git a/src/video/hls/hls_directory.c b/src/video/hls/hls_directory.c index 7deb166e..10bfaf97 100644 --- a/src/video/hls/hls_directory.c +++ b/src/video/hls/hls_directory.c @@ -11,6 +11,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/path_utils.h" +#include "utils/strings.h" #include "video/streams.h" #include "video/hls/hls_directory.h" #include "video/hls/hls_context.h" @@ -60,8 +61,7 @@ int ensure_hls_directory(const char *output_dir, const char *stream_name) { // Recreate it using direct C functions to handle paths with spaces char temp_path[MAX_PATH_LENGTH]; - strncpy(temp_path, output_dir, MAX_PATH_LENGTH - 1); - temp_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(temp_path, output_dir, MAX_PATH_LENGTH, 0); // Create parent directories one by one for (char *p = temp_path + 1; *p; p++) { @@ -129,13 +129,12 @@ int ensure_hls_directory(const char *output_dir, const char *stream_name) { if (last_slash) { char parent_dir[MAX_PATH_LENGTH]; size_t parent_len = last_slash - output_dir; - strncpy(parent_dir, output_dir, parent_len); + safe_strcpy(parent_dir, output_dir, MAX_PATH_LENGTH, parent_len); parent_dir[parent_len] = '\0'; // Create parent directory using direct C functions char temp_path[MAX_PATH_LENGTH]; - strncpy(temp_path, parent_dir, MAX_PATH_LENGTH - 1); - temp_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(temp_path, parent_dir, MAX_PATH_LENGTH, 0); // Create parent directories one by one for (char *p = temp_path + 1; *p; p++) { @@ -167,8 +166,7 @@ int ensure_hls_directory(const char *output_dir, const char *stream_name) { // Try to create parent directory with full permissions using direct C functions char retry_path[MAX_PATH_LENGTH]; - strncpy(retry_path, parent_dir, MAX_PATH_LENGTH - 1); - retry_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(retry_path, parent_dir, MAX_PATH_LENGTH, 0); // Create parent directories one by one for (char *p = retry_path + 1; *p; p++) { diff --git a/src/video/hls/hls_unified_thread.c b/src/video/hls/hls_unified_thread.c index 7c797e9d..3fd49e77 100644 --- a/src/video/hls/hls_unified_thread.c +++ b/src/video/hls/hls_unified_thread.c @@ -45,6 +45,7 @@ #include "core/url_utils.h" #include "core/path_utils.h" #include "core/shutdown_coordinator.h" +#include "utils/strings.h" #include "telemetry/stream_metrics.h" // MEMORY LEAK FIX: Forward declaration for FFmpeg buffer cleanup function @@ -660,12 +661,11 @@ static void safe_cleanup_resources(AVFormatContext **input_ctx, AVPacket **pkt, if (writer_valid) { // Get a copy of the stream name for logging - char writer_stream_name[MAX_STREAM_NAME] = {0}; + char writer_stream_name[MAX_STREAM_NAME]; if (writer_to_free->stream_name) { - strncpy(writer_stream_name, writer_to_free->stream_name, MAX_STREAM_NAME - 1); - writer_stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(writer_stream_name, writer_to_free->stream_name, MAX_STREAM_NAME, 0); } else { - strcpy(writer_stream_name, "unknown"); + safe_strcpy(writer_stream_name, "unknown", MAX_STREAM_NAME, 0); } log_debug("Preparing to close HLS writer for stream %s", writer_stream_name); @@ -941,8 +941,7 @@ void *hls_unified_thread_func(void *arg) { // Create a local copy of the stream name for thread safety char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, ctx->stream_name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, ctx->stream_name, MAX_STREAM_NAME, 0); log_set_thread_context("HLSWriter", stream_name); log_info("Starting unified HLS thread for stream %s", stream_name); @@ -1132,8 +1131,7 @@ void *hls_unified_thread_func(void *arg) { // Copy the URL and protocol with safety checks if (ctx && ctx->rtsp_url[0] != '\0') { - strncpy(local_rtsp_url, ctx->rtsp_url, MAX_PATH_LENGTH - 1); - local_rtsp_url[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(local_rtsp_url, ctx->rtsp_url, MAX_PATH_LENGTH, 0); local_protocol = ctx->protocol; } else { log_error("Invalid RTSP URL for stream %s", stream_name); @@ -1560,8 +1558,7 @@ void *hls_unified_thread_func(void *arg) { // Copy the URL and protocol with safety checks if (ctx && ctx->rtsp_url[0] != '\0') { - strncpy(reconnect_rtsp_url, ctx->rtsp_url, MAX_PATH_LENGTH - 1); - reconnect_rtsp_url[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(reconnect_rtsp_url, ctx->rtsp_url, MAX_PATH_LENGTH, 0); reconnect_protocol = ctx->protocol; } else { log_error("Invalid RTSP URL for stream %s during reconnection", stream_name); @@ -1797,9 +1794,8 @@ void *hls_unified_thread_func(void *arg) { bool context_valid_for_exit = ctx_for_exit && !is_context_already_freed(ctx_for_exit) && !is_context_pending_deletion(ctx_for_exit); // Store stream name in local buffer for logging even if context becomes invalid - char stream_name_buf[MAX_STREAM_NAME] = {0}; - strncpy(stream_name_buf, stream_name, MAX_STREAM_NAME - 1); - stream_name_buf[MAX_STREAM_NAME - 1] = '\0'; + char stream_name_buf[MAX_STREAM_NAME]; + safe_strcpy(stream_name_buf, stream_name, MAX_STREAM_NAME, 0); // CRITICAL FIX: Ensure all resources are cleaned up before exiting // This is a safety measure in case we exited the loop without proper cleanup @@ -1941,9 +1937,8 @@ void *hls_unified_thread_func(void *arg) { // CRITICAL FIX: Store a local copy of the stream name for logging if (writer_to_cleanup && writer_to_cleanup->stream_name) { - char writer_stream_name[MAX_STREAM_NAME] = {0}; - strncpy(writer_stream_name, writer_to_cleanup->stream_name, MAX_STREAM_NAME - 1); - writer_stream_name[MAX_STREAM_NAME - 1] = '\0'; + char writer_stream_name[MAX_STREAM_NAME]; + safe_strcpy(writer_stream_name, writer_to_cleanup->stream_name, MAX_STREAM_NAME, 0); log_info("Preparing to clean up HLS writer for stream %s", writer_stream_name); } } @@ -2338,13 +2333,11 @@ int start_hls_unified_stream(const char *stream_name) { // Initialize the memory to zero memset(ctx, 0, sizeof(hls_unified_thread_ctx_t)); - strncpy(ctx->stream_name, stream_name, MAX_STREAM_NAME - 1); - ctx->stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(ctx->stream_name, stream_name, MAX_STREAM_NAME, 0); // Get RTSP URL char actual_url[MAX_PATH_LENGTH]; - strncpy(actual_url, config.url, sizeof(actual_url) - 1); - actual_url[sizeof(actual_url) - 1] = '\0'; + safe_strcpy(actual_url, config.url, sizeof(actual_url), 0); // If the stream is using go2rtc for HLS, get the go2rtc RTSP URL if (go2rtc_integration_is_using_go2rtc_for_hls(stream_name)) { @@ -2385,7 +2378,7 @@ int start_hls_unified_stream(const char *stream_name) { const char *suffix = "?video"; size_t suffix_len = strlen(suffix); if (url_len + suffix_len < sizeof(actual_url)) { - strncat(actual_url, suffix, sizeof(actual_url) - url_len - 1); + safe_strcat(actual_url, suffix, sizeof(actual_url)); log_info("Audio recording disabled for %s, using video-only go2rtc RTSP URL for HLS", stream_name); } else { @@ -2403,14 +2396,12 @@ int start_hls_unified_stream(const char *stream_name) { config.onvif_username[0] ? config.onvif_username : NULL, config.onvif_password[0] ? config.onvif_password : NULL, credentialed_url, sizeof(credentialed_url)) == 0) { - strncpy(actual_url, credentialed_url, sizeof(actual_url) - 1); - actual_url[sizeof(actual_url) - 1] = '\0'; + safe_strcpy(actual_url, credentialed_url, sizeof(actual_url), 0); } } // Copy the URL to the context - strncpy(ctx->rtsp_url, actual_url, MAX_PATH_LENGTH - 1); - ctx->rtsp_url[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(ctx->rtsp_url, actual_url, MAX_PATH_LENGTH, 0); // Set protocol information in the context ctx->protocol = config.protocol; @@ -2452,8 +2443,7 @@ int start_hls_unified_stream(const char *stream_name) { // Create parent directories one by one char temp_path[MAX_PATH_LENGTH]; - strncpy(temp_path, ctx->output_path, MAX_PATH_LENGTH - 1); - temp_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(temp_path, ctx->output_path, MAX_PATH_LENGTH, 0); for (char *p = temp_path + 1; *p; p++) { if (*p == '/') { @@ -2761,7 +2751,7 @@ int stop_hls_unified_stream(const char *stream_name) { alarm(1); // 1 second timeout // Store a local copy of the stream name for logging - char writer_stream_name[MAX_STREAM_NAME] = {0}; + char writer_stream_name[MAX_STREAM_NAME]; snprintf(writer_stream_name, sizeof(writer_stream_name), "%s", stream_name); // Use the stream_name we already have // Safely get and clear the writer pointer @@ -3323,8 +3313,7 @@ static void *hls_watchdog_thread_func(void *arg) { // Store the stream name before unlocking the mutex char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, ctx->stream_name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, ctx->stream_name, MAX_STREAM_NAME, 0); // Unlock the mutex before restarting the stream pthread_mutex_unlock(&unified_contexts_mutex); diff --git a/src/video/hls_streaming.c b/src/video/hls_streaming.c index dd82ff70..2d6c36f7 100644 --- a/src/video/hls_streaming.c +++ b/src/video/hls_streaming.c @@ -14,6 +14,7 @@ #include #include "core/logger.h" +#include "utils/strings.h" #include "video/hls_streaming.h" #include "video/stream_state.h" #include "video/hls/hls_unified_thread.h" @@ -157,8 +158,7 @@ void cleanup_hls_streaming_backend(void) { if (unified_contexts[i] != NULL) { char stream_name_copy[MAX_STREAM_NAME] = "unknown"; if (unified_contexts[i]->stream_name[0] != '\0') { - strncpy(stream_name_copy, unified_contexts[i]->stream_name, MAX_STREAM_NAME - 1); - stream_name_copy[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name_copy, unified_contexts[i]->stream_name, MAX_STREAM_NAME, 0); } // Check if thread has actually stopped diff --git a/src/video/hls_writer.c b/src/video/hls_writer.c index d6d615c1..dc17daa1 100644 --- a/src/video/hls_writer.c +++ b/src/video/hls_writer.c @@ -19,6 +19,7 @@ #include #include "core/logger.h" +#include "utils/strings.h" #include "video/hls_writer.h" #include "video/detection_integration.h" #include "video/detection_frame_processing.h" @@ -99,8 +100,7 @@ static void cleanup_old_segments(const char *output_dir, int max_segments) { // Get file stats snprintf(filepath, sizeof(filepath), "%s/%s", output_dir, entry->d_name); if (stat(filepath, &st) == 0) { - strncpy(segments[i].filename, entry->d_name, 255); - segments[i].filename[255] = '\0'; + safe_strcpy(segments[i].filename, entry->d_name, 256, 0); segments[i].mtime = st.st_mtime; i++; } @@ -164,8 +164,8 @@ hls_writer_t *hls_writer_create(const char *output_dir, const char *stream_name, } // Copy output directory and stream name - strncpy(writer->output_dir, output_dir, MAX_PATH_LENGTH - 1); - strncpy(writer->stream_name, stream_name, MAX_STREAM_NAME - 1); + safe_strcpy(writer->output_dir, output_dir, MAX_PATH_LENGTH, 0); + safe_strcpy(writer->stream_name, stream_name, MAX_STREAM_NAME, 0); // Ensure segment duration is reasonable but allow lower values for lower latency if (segment_duration < 1) { @@ -408,8 +408,7 @@ static int ensure_output_directory(hls_writer_t *writer) { dir_path, safe_dir_path); // Update the writer's output_dir field with the safe path - strncpy(writer->output_dir, safe_dir_path, MAX_PATH_LENGTH - 1); - writer->output_dir[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(writer->output_dir, safe_dir_path, MAX_PATH_LENGTH, 0); } // Always use the safe path @@ -421,8 +420,7 @@ static int ensure_output_directory(hls_writer_t *writer) { // Create directory using direct C functions to handle paths with spaces char temp_path[MAX_PATH_LENGTH]; - strncpy(temp_path, dir_path, MAX_PATH_LENGTH - 1); - temp_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(temp_path, dir_path, MAX_PATH_LENGTH, 0); // Create parent directories one by one for (char *p = temp_path + 1; *p; p++) { @@ -936,12 +934,11 @@ void hls_writer_close(hls_writer_t *writer) { __sync_synchronize(); // Store stream name for logging - use a local copy to avoid potential race conditions - char stream_name[MAX_STREAM_NAME] = {0}; + char stream_name[MAX_STREAM_NAME]; // Copy stream name for logging; fall back to "unknown" if not yet set if (writer->stream_name[0] != '\0') { - strncpy(stream_name, writer->stream_name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, writer->stream_name, MAX_STREAM_NAME, 0); } else { strcpy(stream_name, "unknown"); } diff --git a/src/video/motion_detection.c b/src/video/motion_detection.c index 29350015..ec254394 100644 --- a/src/video/motion_detection.c +++ b/src/video/motion_detection.c @@ -20,6 +20,7 @@ #include "video/detection_result.h" #include "video/zone_filter.h" #include "utils/memory.h" +#include "utils/strings.h" #define MAX_MOTION_STREAMS MAX_STREAMS #define DEFAULT_SENSITIVITY 0.15f // Lower sensitivity threshold (was 0.25) @@ -241,8 +242,7 @@ static motion_stream_t *get_motion_stream(const char *stream_name) { return NULL; } - strncpy(motion_streams[i]->stream_name, stream_name, MAX_STREAM_NAME - 1); - motion_streams[i]->stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(motion_streams[i]->stream_name, stream_name, MAX_STREAM_NAME, 0); // Initialize default values motion_streams[i]->sensitivity = DEFAULT_SENSITIVITY; @@ -1401,8 +1401,7 @@ int detect_motion(const char *stream_name, const unsigned char *frame_data, float bh = (float)(max_y - min_y + 1) / (float)gs; int ri = result->count; - strncpy(result->detections[ri].label, MOTION_LABEL, MAX_LABEL_LENGTH - 1); - result->detections[ri].label[MAX_LABEL_LENGTH - 1] = '\0'; + safe_strcpy(result->detections[ri].label, MOTION_LABEL, MAX_LABEL_LENGTH, 0); result->detections[ri].confidence = conf; result->detections[ri].x = bx; result->detections[ri].y = by; @@ -1416,8 +1415,7 @@ int detect_motion(const char *stream_name, const unsigned char *frame_data, } else { // Non-grid path: single full-frame detection result->count = 1; - strncpy(result->detections[0].label, MOTION_LABEL, MAX_LABEL_LENGTH - 1); - result->detections[0].label[MAX_LABEL_LENGTH - 1] = '\0'; + safe_strcpy(result->detections[0].label, MOTION_LABEL, MAX_LABEL_LENGTH, 0); result->detections[0].confidence = motion_score; result->detections[0].x = 0.0f; result->detections[0].y = 0.0f; diff --git a/src/video/mp4_recording_core.c b/src/video/mp4_recording_core.c index 49200756..cdbf1aff 100644 --- a/src/video/mp4_recording_core.c +++ b/src/video/mp4_recording_core.c @@ -30,6 +30,7 @@ #include "core/url_utils.h" #include "core/path_utils.h" #include "core/shutdown_coordinator.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/streams.h" #include "video/mp4_writer.h" @@ -106,8 +107,7 @@ static void *mp4_recording_thread(void *arg) { // Make a local copy of the stream name for thread safety char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, ctx->config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, ctx->config.name, MAX_STREAM_NAME, 0); log_set_thread_context("MP4Recorder", stream_name); log_info("Starting MP4 recording thread for stream %s", stream_name); @@ -120,8 +120,7 @@ static void *mp4_recording_thread(void *arg) { // Verify output directory exists and is writable char mp4_dir[MAX_PATH_LENGTH]; - strncpy(mp4_dir, ctx->output_path, MAX_PATH_LENGTH - 1); - mp4_dir[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(mp4_dir, ctx->output_path, MAX_PATH_LENGTH, 0); // Remove filename from path to get directory char *last_slash = strrchr(mp4_dir, '/'); @@ -186,8 +185,7 @@ static void *mp4_recording_thread(void *arg) { // Set trigger type on the writer if (ctx->trigger_type[0] != '\0') { - strncpy(ctx->mp4_writer->trigger_type, ctx->trigger_type, sizeof(ctx->mp4_writer->trigger_type) - 1); - ctx->mp4_writer->trigger_type[sizeof(ctx->mp4_writer->trigger_type) - 1] = '\0'; + safe_strcpy(ctx->mp4_writer->trigger_type, ctx->trigger_type, sizeof(ctx->mp4_writer->trigger_type), 0); } log_info("Created MP4 writer for %s at %s (trigger_type: %s)", stream_name, ctx->output_path, ctx->mp4_writer->trigger_type); @@ -235,8 +233,7 @@ static void *mp4_recording_thread(void *arg) { if (!success) { log_error("Failed to get go2rtc RTSP URL for stream %s after multiple retries, falling back to original URL", stream_name); - strncpy(actual_url, ctx->config.url, sizeof(actual_url) - 1); - actual_url[sizeof(actual_url) - 1] = '\0'; + safe_strcpy(actual_url, ctx->config.url, sizeof(actual_url), 0); } // When audio recording is disabled, append ?video to the go2rtc RTSP URL @@ -248,7 +245,7 @@ static void *mp4_recording_thread(void *arg) { const char *suffix = "?video"; size_t suffix_len = strlen(suffix); if (url_len + suffix_len < sizeof(actual_url)) { - strncat(actual_url, suffix, sizeof(actual_url) - url_len - 1); + safe_strcat(actual_url, suffix, sizeof(actual_url)); log_info("Audio recording disabled for %s, using video-only go2rtc RTSP URL", stream_name); } else { @@ -263,8 +260,7 @@ static void *mp4_recording_thread(void *arg) { actual_url, sizeof(actual_url)) != 0) { log_warn("Failed to inject credentials into URL for stream %s, using original URL", stream_name); - strncpy(actual_url, ctx->config.url, sizeof(actual_url) - 1); - actual_url[sizeof(actual_url) - 1] = '\0'; + safe_strcpy(actual_url, ctx->config.url, sizeof(actual_url), 0); } } @@ -289,8 +285,7 @@ static void *mp4_recording_thread(void *arg) { // Keep a copy of the recording URL for self-healing restarts char restart_url[MAX_PATH_LENGTH]; - strncpy(restart_url, actual_url, sizeof(restart_url) - 1); - restart_url[sizeof(restart_url) - 1] = '\0'; + safe_strcpy(restart_url, actual_url, sizeof(restart_url), 0); // Dead-detection state for the inner RTSP writer thread int dead_check_seconds = 0; // consecutive seconds mp4_writer_is_recording() == 0 @@ -356,8 +351,7 @@ static void *mp4_recording_thread(void *arg) { if (using_go2rtc) { char fresh_url[MAX_PATH_LENGTH]; if (go2rtc_get_rtsp_url(stream_name, fresh_url, sizeof(fresh_url))) { - strncpy(restart_url, fresh_url, sizeof(restart_url) - 1); - restart_url[sizeof(restart_url) - 1] = '\0'; + safe_strcpy(restart_url, fresh_url, sizeof(restart_url), 0); log_info("Refreshed go2rtc URL for stream %s after cooldown", stream_name); } @@ -386,8 +380,7 @@ static void *mp4_recording_thread(void *arg) { if (using_go2rtc) { char fresh_url[MAX_PATH_LENGTH]; if (go2rtc_get_rtsp_url(stream_name, fresh_url, sizeof(fresh_url))) { - strncpy(restart_url, fresh_url, sizeof(restart_url) - 1); - restart_url[sizeof(restart_url) - 1] = '\0'; + safe_strcpy(restart_url, fresh_url, sizeof(restart_url), 0); log_info("Refreshed go2rtc URL for stream %s", stream_name); } } @@ -504,10 +497,8 @@ void cleanup_mp4_recording_backend(void) { items_to_cleanup[cleanup_count].ctx = recording_contexts[i]; items_to_cleanup[cleanup_count].thread = recording_contexts[i]->thread; - strncpy(items_to_cleanup[cleanup_count].stream_name, - recording_contexts[i]->config.name, - MAX_STREAM_NAME - 1); - items_to_cleanup[cleanup_count].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(items_to_cleanup[cleanup_count].stream_name, + recording_contexts[i]->config.name, MAX_STREAM_NAME, 0); items_to_cleanup[cleanup_count].index = i; // Null the slot now so new recordings (if any race) see it as free @@ -640,7 +631,7 @@ int start_mp4_recording(const char *stream_name) { memset(ctx, 0, sizeof(mp4_recording_ctx_t)); memcpy(&ctx->config, &config, sizeof(stream_config_t)); ctx->running = 1; - strncpy(ctx->trigger_type, "scheduled", sizeof(ctx->trigger_type) - 1); + safe_strcpy(ctx->trigger_type, "scheduled", sizeof(ctx->trigger_type), 0); // Create output paths const config_t *global_config = get_streaming_config(); @@ -676,7 +667,7 @@ int start_mp4_recording(const char *stream_name) { // Try to create the parent directory first char parent_dir[MAX_PATH_LENGTH]; if (global_config->record_mp4_directly && global_config->mp4_storage_path[0] != '\0') { - strncpy(parent_dir, global_config->mp4_storage_path, MAX_PATH_LENGTH - 1); + safe_strcpy(parent_dir, global_config->mp4_storage_path, MAX_PATH_LENGTH, 0); } else { snprintf(parent_dir, MAX_PATH_LENGTH, "%s/mp4", global_config->storage_path); } @@ -807,11 +798,10 @@ int start_mp4_recording_with_url(const char *stream_name, const char *url) { memcpy(&ctx->config, &config, sizeof(stream_config_t)); // Override the URL in the config with the provided URL - strncpy(ctx->config.url, url, MAX_PATH_LENGTH - 1); - ctx->config.url[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(ctx->config.url, url, MAX_PATH_LENGTH, 0); ctx->running = 1; - strncpy(ctx->trigger_type, "scheduled", sizeof(ctx->trigger_type) - 1); + safe_strcpy(ctx->trigger_type, "scheduled", sizeof(ctx->trigger_type), 0); // Create output paths const config_t *global_config = get_streaming_config(); @@ -847,7 +837,7 @@ int start_mp4_recording_with_url(const char *stream_name, const char *url) { // Try to create the parent directory first char parent_dir[MAX_PATH_LENGTH]; if (global_config->record_mp4_directly && global_config->mp4_storage_path[0] != '\0') { - strncpy(parent_dir, global_config->mp4_storage_path, MAX_PATH_LENGTH - 1); + safe_strcpy(parent_dir, global_config->mp4_storage_path, MAX_PATH_LENGTH, 0); } else { snprintf(parent_dir, MAX_PATH_LENGTH, "%s/mp4", global_config->storage_path); } @@ -1047,10 +1037,9 @@ int start_mp4_recording_with_trigger(const char *stream_name, const char *trigge // Set trigger type if (trigger_type) { - strncpy(ctx->trigger_type, trigger_type, sizeof(ctx->trigger_type) - 1); - ctx->trigger_type[sizeof(ctx->trigger_type) - 1] = '\0'; + safe_strcpy(ctx->trigger_type, trigger_type, sizeof(ctx->trigger_type), 0); } else { - strncpy(ctx->trigger_type, "scheduled", sizeof(ctx->trigger_type) - 1); + safe_strcpy(ctx->trigger_type, "scheduled", sizeof(ctx->trigger_type), 0); } // Create output paths @@ -1133,8 +1122,7 @@ int start_mp4_recording_with_url_and_trigger(const char *stream_name, const char } // Override the URL with the provided one - strncpy(config.url, url, sizeof(config.url) - 1); - config.url[sizeof(config.url) - 1] = '\0'; + safe_strcpy(config.url, url, sizeof(config.url), 0); // Check if already running — also verify the recording is actually healthy. // FIX: treat writer==NULL + ctx->running==1 as "initializing" to prevent @@ -1204,10 +1192,9 @@ int start_mp4_recording_with_url_and_trigger(const char *stream_name, const char // Set trigger type if (trigger_type) { - strncpy(ctx->trigger_type, trigger_type, sizeof(ctx->trigger_type) - 1); - ctx->trigger_type[sizeof(ctx->trigger_type) - 1] = '\0'; + safe_strcpy(ctx->trigger_type, trigger_type, sizeof(ctx->trigger_type), 0); } else { - strncpy(ctx->trigger_type, "scheduled", sizeof(ctx->trigger_type) - 1); + safe_strcpy(ctx->trigger_type, "scheduled", sizeof(ctx->trigger_type), 0); } // Create output paths diff --git a/src/video/mp4_recording_writer.c b/src/video/mp4_recording_writer.c index 3c7f81de..19b3a7f5 100644 --- a/src/video/mp4_recording_writer.c +++ b/src/video/mp4_recording_writer.c @@ -20,6 +20,7 @@ #include #include "core/logger.h" +#include "utils/strings.h" #include "video/mp4_recording.h" #include "video/mp4_recording_internal.h" #include "video/mp4_writer.h" @@ -50,8 +51,7 @@ int register_mp4_writer_for_stream(const char *stream_name, mp4_writer_t *writer // Make a local copy of the stream name for thread safety char local_stream_name[MAX_STREAM_NAME]; - strncpy(local_stream_name, stream_name, MAX_STREAM_NAME - 1); - local_stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(local_stream_name, stream_name, MAX_STREAM_NAME, 0); // Find empty slot or existing entry for this stream int slot = -1; @@ -86,8 +86,7 @@ int register_mp4_writer_for_stream(const char *stream_name, mp4_writer_t *writer // Register the new writer mp4_writers[slot] = writer; - strncpy(mp4_writer_stream_names[slot], local_stream_name, sizeof(mp4_writer_stream_names[0]) - 1); - mp4_writer_stream_names[slot][sizeof(mp4_writer_stream_names[0]) - 1] = '\0'; + safe_strcpy(mp4_writer_stream_names[slot], local_stream_name, sizeof(mp4_writer_stream_names[0]), 0); log_info("Registered MP4 writer for stream %s in slot %d", local_stream_name, slot); @@ -110,8 +109,7 @@ mp4_writer_t *get_mp4_writer_for_stream(const char *stream_name) { // Make a local copy of the stream name for thread safety char local_stream_name[MAX_STREAM_NAME]; - strncpy(local_stream_name, stream_name, MAX_STREAM_NAME - 1); - local_stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(local_stream_name, stream_name, MAX_STREAM_NAME, 0); // Use a local variable to store the writer pointer mp4_writer_t *writer_copy = NULL; @@ -162,8 +160,7 @@ void unregister_mp4_writer_for_stream(const char *stream_name) { // Make a local copy of the stream name for thread safety char local_stream_name[MAX_STREAM_NAME]; - strncpy(local_stream_name, stream_name, MAX_STREAM_NAME - 1); - local_stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(local_stream_name, stream_name, MAX_STREAM_NAME, 0); log_info("Unregistering MP4 writer for stream %s", local_stream_name); @@ -213,10 +210,8 @@ void close_all_mp4_writers(void) { writers_to_close[num_writers_to_close] = writer; // Make a safe copy of the stream name FIRST from the static array (known safe memory) - strncpy(stream_names_to_close[num_writers_to_close], - mp4_writer_stream_names[i], - sizeof(stream_names_to_close[0]) - 1); - stream_names_to_close[num_writers_to_close][sizeof(stream_names_to_close[0]) - 1] = '\0'; + safe_strcpy(stream_names_to_close[num_writers_to_close], + mp4_writer_stream_names[i], sizeof(stream_names_to_close[0]), 0); // Clear the entry in the global array IMMEDIATELY to prevent any race conditions // This must be done before we access the writer's fields @@ -233,10 +228,8 @@ void close_all_mp4_writers(void) { } if (writer_valid && writer->output_path[0] != '\0') { - strncpy(file_paths_to_close[num_writers_to_close], - writer->output_path, - MAX_PATH_LENGTH - 1); - file_paths_to_close[num_writers_to_close][MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(file_paths_to_close[num_writers_to_close], + writer->output_path, MAX_PATH_LENGTH, 0); // Log the path we're about to check log_info("Checking MP4 file: %s", file_paths_to_close[num_writers_to_close]); diff --git a/src/video/mp4_segment_recorder.c b/src/video/mp4_segment_recorder.c index 25735d30..dd9741b7 100644 --- a/src/video/mp4_segment_recorder.c +++ b/src/video/mp4_segment_recorder.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -761,17 +762,14 @@ int record_segment(const char *rtsp_url, const char *output_file, int duration, // Additional diagnostics char *dir_path = strdup(output_file); if (dir_path) { - char *last_slash = strrchr(dir_path, '/'); - if (last_slash) { - *last_slash = '\0'; - struct stat dir_st; - if (stat(dir_path, &dir_st) != 0) { - log_error("Directory does not exist: %s", dir_path); - } else if (!S_ISDIR(dir_st.st_mode)) { - log_error("Path exists but is not a directory: %s", dir_path); - } else if (access(dir_path, W_OK) != 0) { - log_error("Directory is not writable: %s", dir_path); - } + char *dir = dirname(dir_path); + struct stat dir_st; + if (stat(dir, &dir_st) != 0) { + log_error("Directory does not exist: %s", dir_path); + } else if (!S_ISDIR(dir_st.st_mode)) { + log_error("Path exists but is not a directory: %s", dir_path); + } else if (access(dir_path, W_OK) != 0) { + log_error("Directory is not writable: %s", dir_path); } free(dir_path); } diff --git a/src/video/mp4_writer_core.c b/src/video/mp4_writer_core.c index 0d919c92..3498bc65 100644 --- a/src/video/mp4_writer_core.c +++ b/src/video/mp4_writer_core.c @@ -27,6 +27,7 @@ #include "database/database_manager.h" #include "core/config.h" #include "core/logger.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/streams.h" #include "video/mp4_writer.h" @@ -46,8 +47,8 @@ mp4_writer_t *mp4_writer_create(const char *output_path, const char *stream_name } // Initialize writer - strncpy(writer->output_path, output_path, sizeof(writer->output_path) - 1); - strncpy(writer->stream_name, stream_name, sizeof(writer->stream_name) - 1); + safe_strcpy(writer->output_path, output_path, sizeof(writer->output_path), 0); + safe_strcpy(writer->stream_name, stream_name, sizeof(writer->stream_name), 0); writer->first_dts = AV_NOPTS_VALUE; writer->first_pts = AV_NOPTS_VALUE; writer->last_dts = AV_NOPTS_VALUE; @@ -56,7 +57,7 @@ mp4_writer_t *mp4_writer_create(const char *output_path, const char *stream_name writer->last_packet_time = 0; // Initialize to 0 to indicate no packets written yet writer->has_audio = 1; // Initialize to 1 to enable audio by default writer->current_recording_id = 0; // Initialize to 0 to indicate no recording ID yet - strncpy(writer->trigger_type, "scheduled", sizeof(writer->trigger_type) - 1); // Default to scheduled + safe_strcpy(writer->trigger_type, "scheduled", sizeof(writer->trigger_type), 0); // Default to scheduled // Initialize audio state writer->audio.stream_idx = -1; // Initialize to -1 to indicate no audio stream @@ -76,7 +77,7 @@ mp4_writer_t *mp4_writer_create(const char *output_path, const char *stream_name writer->shutdown_component_id = -1; // Initialize to -1 to indicate not registered // Extract output directory from output path - strncpy(writer->output_dir, output_path, sizeof(writer->output_dir) - 1); + safe_strcpy(writer->output_dir, output_path, sizeof(writer->output_dir), 0); char *last_slash = strrchr(writer->output_dir, '/'); if (last_slash) { *last_slash = '\0'; // Truncate at the last slash to get directory diff --git a/src/video/mp4_writer_thread.c b/src/video/mp4_writer_thread.c index 12a4294f..f282138f 100644 --- a/src/video/mp4_writer_thread.c +++ b/src/video/mp4_writer_thread.c @@ -29,6 +29,7 @@ #include "core/logger.h" #include "core/shutdown_coordinator.h" +#include "utils/strings.h" #include "video/mp4_writer.h" #include "video/mp4_writer_internal.h" #include "video/mp4_writer_thread.h" @@ -55,13 +56,13 @@ static void on_segment_started_cb(void *user_ctx) { if (thread_ctx->writer->output_path[0] != '\0') { recording_metadata_t metadata; memset(&metadata, 0, sizeof(recording_metadata_t)); - strncpy(metadata.stream_name, stream_name, sizeof(metadata.stream_name) - 1); - strncpy(metadata.file_path, thread_ctx->writer->output_path, sizeof(metadata.file_path) - 1); + safe_strcpy(metadata.stream_name, stream_name, sizeof(metadata.stream_name), 0); + safe_strcpy(metadata.file_path, thread_ctx->writer->output_path, sizeof(metadata.file_path), 0); metadata.start_time = time(NULL); // Align to keyframe time metadata.end_time = 0; metadata.size_bytes = 0; metadata.is_complete = false; - strncpy(metadata.trigger_type, thread_ctx->writer->trigger_type, sizeof(metadata.trigger_type) - 1); + safe_strcpy(metadata.trigger_type, thread_ctx->writer->trigger_type, sizeof(metadata.trigger_type), 0); uint64_t recording_id = add_recording_metadata(&metadata); if (recording_id == 0) { @@ -91,8 +92,7 @@ static void *mp4_writer_rtsp_thread(void *arg) { // Create a local copy of needed values to prevent use-after-free char rtsp_url[MAX_PATH_LENGTH]; - strncpy(rtsp_url, thread_ctx->rtsp_url, sizeof(rtsp_url) - 1); - rtsp_url[sizeof(rtsp_url) - 1] = '\0'; + safe_strcpy(rtsp_url, thread_ctx->rtsp_url, sizeof(rtsp_url), 0); int segment_duration; @@ -108,8 +108,8 @@ static void *mp4_writer_rtsp_thread(void *arg) { thread_ctx->segment_info.pending_video_keyframe = NULL; memset(thread_ctx->segment_info.stream_name, 0, sizeof(thread_ctx->segment_info.stream_name)); if (thread_ctx->writer && thread_ctx->writer->stream_name[0] != '\0') { - strncpy(thread_ctx->segment_info.stream_name, thread_ctx->writer->stream_name, - MAX_STREAM_NAME - 1); + safe_strcpy(thread_ctx->segment_info.stream_name, thread_ctx->writer->stream_name, + MAX_STREAM_NAME, 0); } thread_ctx->video_params_detected = false; pthread_mutex_init(&thread_ctx->context_mutex, NULL); @@ -121,10 +121,9 @@ static void *mp4_writer_rtsp_thread(void *arg) { // Make a local copy of the stream name for thread safety char stream_name[MAX_STREAM_NAME]; if (thread_ctx->writer && thread_ctx->writer->stream_name[0] != '\0') { - strncpy(stream_name, thread_ctx->writer->stream_name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, thread_ctx->writer->stream_name, MAX_STREAM_NAME, 0); } else { - strncpy(stream_name, "unknown", MAX_STREAM_NAME - 1); + safe_strcpy(stream_name, "unknown", MAX_STREAM_NAME, 0); } log_set_thread_context("MP4Writer", stream_name); @@ -252,8 +251,8 @@ static void *mp4_writer_rtsp_thread(void *arg) { // Audio disabled on a go2rtc URL: append ?video to request // video-only and avoid phantom audio track issues. if (url_len + suffix_len < sizeof(thread_ctx->rtsp_url)) { - strncat(thread_ctx->rtsp_url, video_suffix, - sizeof(thread_ctx->rtsp_url) - url_len - 1); + safe_strcat(thread_ctx->rtsp_url, video_suffix, + sizeof(thread_ctx->rtsp_url)); log_info("Appended ?video suffix to RTSP URL for stream %s (audio now disabled): %s", stream_name, thread_ctx->rtsp_url); } @@ -304,8 +303,7 @@ static void *mp4_writer_rtsp_thread(void *arg) { // Get the current output path before closing char current_path[MAX_PATH_LENGTH]; - strncpy(current_path, thread_ctx->writer->output_path, MAX_PATH_LENGTH - 1); - current_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(current_path, thread_ctx->writer->output_path, MAX_PATH_LENGTH, 0); // Defer creation of DB metadata for the new file until first keyframe via callback // so that start_time aligns to a playable keyframe. @@ -345,8 +343,7 @@ static void *mp4_writer_rtsp_thread(void *arg) { } // Update the output path - strncpy(thread_ctx->writer->output_path, new_path, MAX_PATH_LENGTH - 1); - thread_ctx->writer->output_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(thread_ctx->writer->output_path, new_path, MAX_PATH_LENGTH, 0); // Reset current recording ID; new ID will be assigned on first keyframe of next segment thread_ctx->writer->current_recording_id = 0; @@ -734,8 +731,7 @@ int mp4_writer_start_recording_thread(mp4_writer_t *writer, const char *rtsp_url writer->thread_ctx->running = 0; atomic_store(&writer->thread_ctx->shutdown_requested, 0); atomic_store(&writer->thread_ctx->force_reconnect, 0); - strncpy(writer->thread_ctx->rtsp_url, rtsp_url, sizeof(writer->thread_ctx->rtsp_url) - 1); - writer->thread_ctx->rtsp_url[sizeof(writer->thread_ctx->rtsp_url) - 1] = '\0'; + safe_strcpy(writer->thread_ctx->rtsp_url, rtsp_url, sizeof(writer->thread_ctx->rtsp_url), 0); // Create thread with proper error handling int ret = pthread_create(&writer->thread_ctx->thread, NULL, mp4_writer_rtsp_thread, writer->thread_ctx); @@ -809,11 +805,9 @@ void mp4_writer_stop_recording_thread(mp4_writer_t *writer) { // copy ensures we always have a safe string for logging. char sname[MAX_STREAM_NAME]; if (writer->stream_name[0] > 0x1F && writer->stream_name[0] < 0x7F) { - strncpy(sname, writer->stream_name, MAX_STREAM_NAME - 1); - sname[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(sname, writer->stream_name, MAX_STREAM_NAME, 0); } else { - strncpy(sname, "unknown", MAX_STREAM_NAME - 1); - sname[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(sname, "unknown", MAX_STREAM_NAME, 0); } // Capture thread handle locally before any operations that might diff --git a/src/video/mp4_writer_utils.c b/src/video/mp4_writer_utils.c index 34fa22c5..12db2099 100644 --- a/src/video/mp4_writer_utils.c +++ b/src/video/mp4_writer_utils.c @@ -33,6 +33,7 @@ #include "core/config.h" #include "core/logger.h" +#include "utils/strings.h" #include "video/mp4_writer.h" #include "video/mp4_writer_internal.h" #include "video/ffmpeg_utils.h" @@ -295,8 +296,7 @@ static int init_audio_transcoder(const char *stream_name, audio_transcoders[slot].initialized = 1; // Store stream name - strncpy(audio_transcoder_stream_names[slot], stream_name, MAX_STREAM_NAME - 1); - audio_transcoder_stream_names[slot][MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(audio_transcoder_stream_names[slot], stream_name, MAX_STREAM_NAME, 0); log_info("Successfully initialized audio transcoder from PCM to AAC for %s", stream_name); @@ -995,8 +995,7 @@ int mp4_writer_initialize(mp4_writer_t *writer, const AVPacket *pkt, const AVStr const char *last_slash = strrchr(writer->output_path, '/'); if (last_slash) { size_t dir_len = last_slash - writer->output_path; - strncpy(dir_path, writer->output_path, dir_len); - dir_path[dir_len] = '\0'; + safe_strcpy(dir_path, writer->output_path, PATH_MAX, dir_len); // Log the directory we're working with log_info("Ensuring MP4 output directory exists: %s", dir_path); diff --git a/src/video/onvif_detection.c b/src/video/onvif_detection.c index 902ba8ca..3781e1eb 100644 --- a/src/video/onvif_detection.c +++ b/src/video/onvif_detection.c @@ -15,6 +15,7 @@ #include "core/curl_init.h" #include "core/shutdown_coordinator.h" #include "core/mqtt_client.h" +#include "utils/strings.h" #include "video/onvif_detection.h" #include "video/onvif_soap.h" #include "video/detection_result.h" @@ -263,12 +264,7 @@ static char *extract_subscription_address(const char *response) { start += strlen(patterns[(ptrdiff_t)i * 2]); int length = (int)(end - start); - char *address = (char *)malloc(length + 1); - if (address) { - strncpy(address, start, length); - address[length] = '\0'; - return address; - } + return strndup(start, length); } } @@ -312,11 +308,7 @@ static char *discover_event_service_url(const char *url, const char *username, c const char *val_end = strstr(val_start, xaddr_close[i]); if (val_end) { size_t url_len = (size_t)(val_end - val_start); - event_url = malloc(url_len + 1); - if (event_url) { - strncpy(event_url, val_start, url_len); - event_url[url_len] = '\0'; - } + event_url = strndup(val_start, url_len); break; } } @@ -416,19 +408,15 @@ static onvif_subscription_t *get_subscription(const char *url, const char *usern // If we found a slot, use it if (slot >= 0) { // Store camera URL, username, and password - strncpy(subscriptions[slot].camera_url, url, sizeof(subscriptions[slot].camera_url) - 1); - subscriptions[slot].camera_url[sizeof(subscriptions[slot].camera_url) - 1] = '\0'; + safe_strcpy(subscriptions[slot].camera_url, url, sizeof(subscriptions[slot].camera_url), 0); - strncpy(subscriptions[slot].username, username, sizeof(subscriptions[slot].username) - 1); - subscriptions[slot].username[sizeof(subscriptions[slot].username) - 1] = '\0'; + safe_strcpy(subscriptions[slot].username, username, sizeof(subscriptions[slot].username), 0); - strncpy(subscriptions[slot].password, password, sizeof(subscriptions[slot].password) - 1); - subscriptions[slot].password[sizeof(subscriptions[slot].password) - 1] = '\0'; + safe_strcpy(subscriptions[slot].password, password, sizeof(subscriptions[slot].password), 0); // Store subscription address - strncpy(subscriptions[slot].subscription_address, subscription_address, - sizeof(subscriptions[slot].subscription_address) - 1); - subscriptions[slot].subscription_address[sizeof(subscriptions[slot].subscription_address) - 1] = '\0'; + safe_strcpy(subscriptions[slot].subscription_address, subscription_address, + sizeof(subscriptions[slot].subscription_address), 0); // Set timestamps time(&subscriptions[slot].creation_time); @@ -653,8 +641,7 @@ int detect_motion_onvif(const char *onvif_url, const char *username, const char // Create a single detection that covers the whole frame result->count = 1; - strncpy(result->detections[0].label, "motion", MAX_LABEL_LENGTH - 1); - result->detections[0].label[MAX_LABEL_LENGTH - 1] = '\0'; + safe_strcpy(result->detections[0].label, "motion", MAX_LABEL_LENGTH, 0); result->detections[0].confidence = 1.0f; result->detections[0].x = 0.0f; result->detections[0].y = 0.0f; diff --git a/src/video/onvif_device_management.c b/src/video/onvif_device_management.c index 023d8da9..6c6fc300 100644 --- a/src/video/onvif_device_management.c +++ b/src/video/onvif_device_management.c @@ -1,17 +1,19 @@ -#include "video/onvif_device_management.h" -#include "video/onvif_soap.h" -#include "video/stream_manager.h" -#include "core/logger.h" -#include "database/db_streams.h" -#include "video/stream_protocol.h" #include #include #include #include #include -#include "ezxml.h" #include + +#include "ezxml.h" +#include "video/onvif_device_management.h" +#include "video/onvif_soap.h" +#include "video/stream_manager.h" +#include "core/logger.h" #include "core/url_utils.h" +#include "utils/strings.h" +#include "database/db_streams.h" +#include "video/stream_protocol.h" // Structure to store memory for CURL responses typedef struct { @@ -168,8 +170,8 @@ static char* send_soap_request(const char *device_url, const char *soap_action, // Log response if available if (response) { // Log first 200 characters of response for debugging - char debug_response[201] = {0}; - strncpy(debug_response, response, 200); + char debug_response[201]; + safe_strcpy(debug_response, response, 201, 0); log_info("Response (first 200 chars): %s", debug_response); } @@ -285,8 +287,7 @@ static char* get_media_service_url(const char *device_url, const char *username, media_url = strdup(ezxml_txt(xaddr)); char safe_media_url[MAX_URL_LENGTH]; if (url_redact_for_logging(media_url, safe_media_url, sizeof(safe_media_url)) != 0) { - strncpy(safe_media_url, "[invalid-url]", sizeof(safe_media_url) - 1); - safe_media_url[sizeof(safe_media_url) - 1] = '\0'; + safe_strcpy(safe_media_url, "[invalid-url]", sizeof(safe_media_url), 0); } log_info("Found media service URL: %s", safe_media_url); break; @@ -317,13 +318,12 @@ static char* get_media_service_url(const char *device_url, const char *username, size_t prefix_len = media_path - device_url; media_url = malloc(prefix_len + strlen("/onvif/media_service") + 1); if (media_url) { - strncpy(media_url, device_url, prefix_len); - media_url[prefix_len] = '\0'; - strncat(media_url, "/onvif/media_service", strlen("/onvif/media_service")); + memcpy(media_url, device_url, prefix_len); + // Copies null terminator + memcpy(media_url + prefix_len, "/onvif/media_service", sizeof("/onvif/media_service")); char safe_media_url[MAX_URL_LENGTH]; if (url_redact_for_logging(media_url, safe_media_url, sizeof(safe_media_url)) != 0) { - strncpy(safe_media_url, "[invalid-url]", sizeof(safe_media_url) - 1); - safe_media_url[sizeof(safe_media_url) - 1] = '\0'; + safe_strcpy(safe_media_url, "[invalid-url]", sizeof(safe_media_url), 0); } log_info("Created fallback media URL: %s", safe_media_url); } @@ -332,8 +332,7 @@ static char* get_media_service_url(const char *device_url, const char *username, media_url = strdup(device_url); char safe_media_url[MAX_URL_LENGTH]; if (url_redact_for_logging(media_url, safe_media_url, sizeof(safe_media_url)) != 0) { - strncpy(safe_media_url, "[invalid-url]", sizeof(safe_media_url) - 1); - safe_media_url[sizeof(safe_media_url) - 1] = '\0'; + safe_strcpy(safe_media_url, "[invalid-url]", sizeof(safe_media_url), 0); } log_info("Using device URL as media URL: %s", safe_media_url); } @@ -360,12 +359,10 @@ int get_onvif_device_profiles(const char *device_url, const char *username, char safe_device_url[MAX_URL_LENGTH]; char safe_media_url[MAX_URL_LENGTH]; if (url_redact_for_logging(device_url, safe_device_url, sizeof(safe_device_url)) != 0) { - strncpy(safe_device_url, "[invalid-url]", sizeof(safe_device_url) - 1); - safe_device_url[sizeof(safe_device_url) - 1] = '\0'; + safe_strcpy(safe_device_url, "[invalid-url]", sizeof(safe_device_url), 0); } if (url_redact_for_logging(media_url, safe_media_url, sizeof(safe_media_url)) != 0) { - strncpy(safe_media_url, "[invalid-url]", sizeof(safe_media_url) - 1); - safe_media_url[sizeof(safe_media_url) - 1] = '\0'; + safe_strcpy(safe_media_url, "[invalid-url]", sizeof(safe_media_url), 0); } log_info("Getting profiles for ONVIF device: %s (Media URL: %s)", safe_device_url, safe_media_url); @@ -420,15 +417,13 @@ int get_onvif_device_profiles(const char *device_url, const char *username, // Get profile token const char *token = ezxml_attr(profile, "token"); if (token) { - strncpy(profiles[i].token, token, sizeof(profiles[i].token) - 1); - profiles[i].token[sizeof(profiles[i].token) - 1] = '\0'; + safe_strcpy(profiles[i].token, token, sizeof(profiles[i].token), 0); } // Get profile name ezxml_t name = find_child(profile, "tt:Name"); if (name) { - strncpy(profiles[i].name, ezxml_txt(name), sizeof(profiles[i].name) - 1); - profiles[i].name[sizeof(profiles[i].name) - 1] = '\0'; + safe_strcpy(profiles[i].name, ezxml_txt(name), sizeof(profiles[i].name), 0); } // Get video encoder configuration @@ -436,8 +431,7 @@ int get_onvif_device_profiles(const char *device_url, const char *username, if (video_encoder) { ezxml_t encoding = find_child(video_encoder, "tt:Encoding"); if (encoding) { - strncpy(profiles[i].encoding, ezxml_txt(encoding), sizeof(profiles[i].encoding) - 1); - profiles[i].encoding[sizeof(profiles[i].encoding) - 1] = '\0'; + safe_strcpy(profiles[i].encoding, ezxml_txt(encoding), sizeof(profiles[i].encoding), 0); } ezxml_t resolution = find_child(video_encoder, "tt:Resolution"); @@ -492,8 +486,7 @@ int get_onvif_stream_url(const char *device_url, const char *username, char safe_device_url[MAX_URL_LENGTH]; if (url_redact_for_logging(device_url, safe_device_url, sizeof(safe_device_url)) != 0) { - strncpy(safe_device_url, "[invalid-url]", sizeof(safe_device_url) - 1); - safe_device_url[sizeof(safe_device_url) - 1] = '\0'; + safe_strcpy(safe_device_url, "[invalid-url]", sizeof(safe_device_url), 0); } log_info("Getting stream URL for ONVIF device: %s, profile: %s", safe_device_url, profile_token); @@ -554,15 +547,13 @@ int get_onvif_stream_url(const char *device_url, const char *username, { char safe_uri[MAX_URL_LENGTH]; if (url_redact_for_logging(uri, safe_uri, sizeof(safe_uri)) != 0) { - strncpy(safe_uri, "[invalid-url]", sizeof(safe_uri) - 1); - safe_uri[sizeof(safe_uri) - 1] = '\0'; + safe_strcpy(safe_uri, "[invalid-url]", sizeof(safe_uri), 0); } log_info("Got stream URI: %s", safe_uri); } // Copy the URI to the output parameter - strncpy(stream_url, uri, url_size - 1); - stream_url[url_size - 1] = '\0'; + safe_strcpy(stream_url, uri, url_size, 0); (void)username; (void)password; @@ -591,13 +582,11 @@ int add_onvif_device_as_stream(const onvif_device_info_t *device_info, memset(&config, 0, sizeof(config)); // Set stream name - strncpy(config.name, stream_name, MAX_STREAM_NAME - 1); - config.name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(config.name, stream_name, MAX_STREAM_NAME, 0); // Store the raw stream URL and keep credentials in dedicated ONVIF fields. if (url_strip_credentials(profile->stream_uri, config.url, sizeof(config.url)) != 0) { - strncpy(config.url, profile->stream_uri, MAX_URL_LENGTH - 1); - config.url[MAX_URL_LENGTH - 1] = '\0'; + safe_strcpy(config.url, profile->stream_uri, MAX_URL_LENGTH, 0); } // Set stream parameters @@ -608,15 +597,14 @@ int add_onvif_device_as_stream(const onvif_device_info_t *device_info, // Set codec - convert ONVIF encoding format to our format if (strcasecmp(profile->encoding, "H264") == 0) { - strncpy(config.codec, "h264", sizeof(config.codec) - 1); + safe_strcpy(config.codec, "h264", sizeof(config.codec), 0); } else if (strcasecmp(profile->encoding, "H265") == 0) { - strncpy(config.codec, "h265", sizeof(config.codec) - 1); + safe_strcpy(config.codec, "h265", sizeof(config.codec), 0); } else { // Default to h264 if unknown - strncpy(config.codec, "h264", sizeof(config.codec) - 1); + safe_strcpy(config.codec, "h264", sizeof(config.codec), 0); log_warn("Unknown encoding format '%s', defaulting to h264", profile->encoding); } - config.codec[sizeof(config.codec) - 1] = '\0'; // Set default values config.priority = 5; @@ -630,8 +618,7 @@ int add_onvif_device_as_stream(const onvif_device_info_t *device_info, config.streaming_enabled = true; // Enable live streaming by default // Set default detection model to "motion" which doesn't require a separate model file - strncpy(config.detection_model, "motion", sizeof(config.detection_model) - 1); - config.detection_model[sizeof(config.detection_model) - 1] = '\0'; + safe_strcpy(config.detection_model, "motion", sizeof(config.detection_model), 0); // Set protocol to TCP or UDP based on URL (most ONVIF cameras use TCP/RTSP) config.protocol = STREAM_PROTOCOL_TCP; @@ -646,16 +633,14 @@ int add_onvif_device_as_stream(const onvif_device_info_t *device_info, // Set ONVIF-specific fields if (username) { - strncpy(config.onvif_username, username, sizeof(config.onvif_username) - 1); - config.onvif_username[sizeof(config.onvif_username) - 1] = '\0'; + safe_strcpy(config.onvif_username, username, sizeof(config.onvif_username), 0); // For onvif_simple_server compatibility, log the username log_info("Setting ONVIF username for stream %s: %s", stream_name, username); } if (password) { - strncpy(config.onvif_password, password, sizeof(config.onvif_password) - 1); - config.onvif_password[sizeof(config.onvif_password) - 1] = '\0'; + safe_strcpy(config.onvif_password, password, sizeof(config.onvif_password), 0); // For onvif_simple_server compatibility, log that we have a password log_info("Setting ONVIF password for stream %s", stream_name); @@ -664,14 +649,12 @@ int add_onvif_device_as_stream(const onvif_device_info_t *device_info, { char safe_url[MAX_URL_LENGTH]; if (url_redact_for_logging(config.url, safe_url, sizeof(safe_url)) != 0) { - strncpy(safe_url, "[invalid-url]", sizeof(safe_url) - 1); - safe_url[sizeof(safe_url) - 1] = '\0'; + safe_strcpy(safe_url, "[invalid-url]", sizeof(safe_url), 0); } log_info("Using ONVIF stream URI without embedded credentials: %s", safe_url); } - strncpy(config.onvif_profile, profile->token, sizeof(config.onvif_profile) - 1); - config.onvif_profile[sizeof(config.onvif_profile) - 1] = '\0'; + safe_strcpy(config.onvif_profile, profile->token, sizeof(config.onvif_profile), 0); config.onvif_discovery_enabled = true; @@ -704,8 +687,7 @@ int test_onvif_connection(const char *url, const char *username, const char *pas // Attempt to get device profiles as a way to test the connection char safe_device_url[MAX_URL_LENGTH]; if (url_redact_for_logging(url, safe_device_url, sizeof(safe_device_url)) != 0) { - strncpy(safe_device_url, "[invalid-url]", sizeof(safe_device_url) - 1); - safe_device_url[sizeof(safe_device_url) - 1] = '\0'; + safe_strcpy(safe_device_url, "[invalid-url]", sizeof(safe_device_url), 0); } log_info("Testing connection to ONVIF device: %s", safe_device_url); @@ -728,13 +710,11 @@ int test_onvif_connection(const char *url, const char *username, const char *pas (username && strlen(username) > 0) ? username : NULL, (password && strlen(password) > 0) ? password : NULL, stream_url, sizeof(stream_url)) != 0) { - strncpy(stream_url, profiles[0].stream_uri, sizeof(stream_url) - 1); - stream_url[sizeof(stream_url) - 1] = '\0'; + safe_strcpy(stream_url, profiles[0].stream_uri, sizeof(stream_url), 0); } if (url_redact_for_logging(stream_url, safe_stream_url, sizeof(safe_stream_url)) != 0) { - strncpy(safe_stream_url, "[invalid-url]", sizeof(safe_stream_url) - 1); - safe_stream_url[sizeof(safe_stream_url) - 1] = '\0'; + safe_strcpy(safe_stream_url, "[invalid-url]", sizeof(safe_stream_url), 0); } log_info("Testing stream connection for profile: %s, URI: %s", diff --git a/src/video/onvif_discovery.c b/src/video/onvif_discovery.c index 5284c949..354c7385 100644 --- a/src/video/onvif_discovery.c +++ b/src/video/onvif_discovery.c @@ -1,16 +1,6 @@ #define _GNU_SOURCE #define _POSIX_C_SOURCE 200112L -#include "video/onvif_discovery.h" -#include "video/onvif_discovery_messages.h" -#include "video/onvif_discovery_network.h" -#include "video/onvif_discovery_probe.h" -#include "video/onvif_discovery_response.h" -#include "video/onvif_discovery_thread.h" -#include "video/onvif_device_management.h" -#include "core/logger.h" -#include "core/config.h" -#include "core/curl_init.h" #include #include #include @@ -27,6 +17,18 @@ #include #include +#include "video/onvif_discovery.h" +#include "video/onvif_discovery_messages.h" +#include "video/onvif_discovery_network.h" +#include "video/onvif_discovery_probe.h" +#include "video/onvif_discovery_response.h" +#include "video/onvif_discovery_thread.h" +#include "video/onvif_device_management.h" +#include "core/logger.h" +#include "core/config.h" +#include "core/curl_init.h" +#include "utils/strings.h" + // Maximum number of networks to detect #define MAX_DETECTED_NETWORKS 10 @@ -179,7 +181,7 @@ int discover_onvif_devices(const char *network, onvif_device_info_t *devices, char ip_addr[16]; struct in_addr addr; int count = 0; - char selected_network[64] = {0}; + char selected_network[64]; int discovery_sock = -1; int broadcast_enabled = 1; @@ -194,16 +196,14 @@ int discover_onvif_devices(const char *network, onvif_device_info_t *devices, const char *env_network = getenv("LIGHTNVR_ONVIF_NETWORK"); if (env_network && strlen(env_network) > 0 && strcmp(env_network, "auto") != 0) { log_info("Using ONVIF discovery network from environment variable: %s", env_network); - strncpy(selected_network, env_network, sizeof(selected_network) - 1); - selected_network[sizeof(selected_network) - 1] = '\0'; + safe_strcpy(selected_network, env_network, sizeof(selected_network), 0); network = selected_network; } // Priority 2: Check config file setting else if (g_config.onvif_discovery_network[0] != '\0' && strcmp(g_config.onvif_discovery_network, "auto") != 0) { log_info("Using ONVIF discovery network from config file: %s", g_config.onvif_discovery_network); - strncpy(selected_network, g_config.onvif_discovery_network, sizeof(selected_network) - 1); - selected_network[sizeof(selected_network) - 1] = '\0'; + safe_strcpy(selected_network, g_config.onvif_discovery_network, sizeof(selected_network), 0); network = selected_network; } // Priority 3: Auto-detect networks @@ -220,8 +220,7 @@ int discover_onvif_devices(const char *network, onvif_device_info_t *devices, } // Use the first detected network - strncpy(selected_network, detected_networks[0], sizeof(selected_network) - 1); - selected_network[sizeof(selected_network) - 1] = '\0'; + safe_strcpy(selected_network, detected_networks[0], sizeof(selected_network), 0); log_info("Auto-detected network for ONVIF discovery: %s", selected_network); @@ -265,7 +264,7 @@ int discover_onvif_devices(const char *network, onvif_device_info_t *devices, log_debug("Found potential ONVIF device at %s", ip_addr); // Add to candidate list - strncpy(candidate_ips[candidate_count], ip_addr, 16); + safe_strcpy(candidate_ips[candidate_count], ip_addr, 16, 0); candidate_count++; } } @@ -525,9 +524,9 @@ int try_direct_http_discovery(char candidate_ips[][16], int candidate_count, memset(&devices[count], 0, sizeof(onvif_device_info_t)); // Set device info - strncpy(devices[count].ip_address, ip, sizeof(devices[count].ip_address) - 1); - strncpy(devices[count].device_service, url, sizeof(devices[count].device_service) - 1); - strncpy(devices[count].endpoint, url, sizeof(devices[count].endpoint) - 1); + safe_strcpy(devices[count].ip_address, ip, sizeof(devices[count].ip_address), 0); + safe_strcpy(devices[count].device_service, url, sizeof(devices[count].device_service), 0); + safe_strcpy(devices[count].endpoint, url, sizeof(devices[count].endpoint), 0); snprintf(devices[count].model, sizeof(devices[count].model), "Unknown (%s discovery)", is_https ? "HTTPS" : "HTTP"); diff --git a/src/video/onvif_discovery_network.c b/src/video/onvif_discovery_network.c index f1a5264e..481d95fb 100644 --- a/src/video/onvif_discovery_network.c +++ b/src/video/onvif_discovery_network.c @@ -1,5 +1,3 @@ -#include "video/onvif_discovery_network.h" -#include "core/logger.h" #include #include #include @@ -14,6 +12,10 @@ #include #include +#include "video/onvif_discovery_network.h" +#include "core/logger.h" +#include "utils/strings.h" + // Define constants if not already defined #ifndef NI_MAXHOST #define NI_MAXHOST 1025 @@ -47,8 +49,7 @@ int parse_network(const char *network, uint32_t *base_addr, uint32_t *subnet_mas } // Make a copy of the network string - strncpy(network_copy, network, sizeof(network_copy) - 1); - network_copy[sizeof(network_copy) - 1] = '\0'; + safe_strcpy(network_copy, network, sizeof(network_copy), 0); // Find the slash slash = strchr(network_copy, '/'); diff --git a/src/video/onvif_discovery_response.c b/src/video/onvif_discovery_response.c index 612298b1..1df3b8d6 100644 --- a/src/video/onvif_discovery_response.c +++ b/src/video/onvif_discovery_response.c @@ -1,5 +1,3 @@ -#include "video/onvif_discovery_response.h" -#include "core/logger.h" #include #include #include @@ -15,6 +13,10 @@ #include #include +#include "video/onvif_discovery_response.h" +#include "core/logger.h" +#include "utils/strings.h" + // Helper function to extract content between XML tags static char* extract_xml_content(const char *xml, const char *tag_start, const char *tag_end, char *buffer, size_t buffer_size) { const char *start = strstr(xml, tag_start); @@ -29,24 +31,9 @@ static char* extract_xml_content(const char *xml, const char *tag_start, const c } size_t len = end - start; - if (len >= buffer_size) { - len = buffer_size - 1; - } - - strncpy(buffer, start, len); - buffer[len] = '\0'; - // Trim leading/trailing whitespace - char *trim_start = buffer; - char *trim_end = buffer + len - 1; - - while (*trim_start && isspace(*trim_start)) trim_start++; - while (trim_end > trim_start && isspace(*trim_end)) *trim_end-- = '\0'; - - if (trim_start != buffer) { - memmove(buffer, trim_start, strlen(trim_start) + 1); - } - + copy_trimmed_value(buffer, buffer_size, start, len); + return buffer; } @@ -60,8 +47,7 @@ int parse_device_info(const char *response, onvif_device_info_t *device_info) { // Log the first 500 characters of the response for debugging char debug_buffer[501]; - strncpy(debug_buffer, response, 500); - debug_buffer[500] = '\0'; + safe_strcpy(debug_buffer, response, 501, 0); log_info("Parsing response: %s...", debug_buffer); // Check if this is a valid ONVIF response and not a probe message @@ -93,21 +79,8 @@ int parse_device_info(const char *response, onvif_device_info_t *device_info) { const char *end = strstr(start, " trim_start && isspace(*trim_end)) *trim_end-- = '\0'; - - if (trim_start != xaddrs) { - memmove(xaddrs, trim_start, strlen(trim_start) + 1); - } - } + // Trim whitespace + copy_trimmed_value(xaddrs, sizeof(xaddrs), start, len); } } } @@ -124,12 +97,10 @@ int parse_device_info(const char *response, onvif_device_info_t *device_info) { // Split multiple URLs if present (some devices return multiple space-separated URLs) const char *url = strtok(xaddrs, " \t\n\r"); if (url) { - strncpy(device_info->device_service, url, MAX_URL_LENGTH - 1); - device_info->device_service[MAX_URL_LENGTH - 1] = '\0'; + safe_strcpy(device_info->device_service, url, MAX_URL_LENGTH, 0); // Also store as endpoint - strncpy(device_info->endpoint, url, MAX_URL_LENGTH - 1); - device_info->endpoint[MAX_URL_LENGTH - 1] = '\0'; + safe_strcpy(device_info->endpoint, url, MAX_URL_LENGTH, 0); log_debug("Found device service URL: %s", device_info->device_service); } else { @@ -152,8 +123,7 @@ int parse_device_info(const char *response, onvif_device_info_t *device_info) { len = sizeof(device_info->ip_address) - 1; } - strncpy(device_info->ip_address, ip_start, len); - device_info->ip_address[len] = '\0'; + safe_strcpy(device_info->ip_address, ip_start, 64, len); log_debug("Extracted IP address: %s", device_info->ip_address); } } @@ -170,8 +140,7 @@ int parse_device_info(const char *response, onvif_device_info_t *device_info) { if (strlen(types) > 0) { // Extract model information if available if (strstr(types, "NetworkVideoTransmitter")) { - strncpy(device_info->model, "NetworkVideoTransmitter", sizeof(device_info->model) - 1); - device_info->model[sizeof(device_info->model) - 1] = '\0'; + safe_strcpy(device_info->model, "NetworkVideoTransmitter", sizeof(device_info->model), 0); } } @@ -417,8 +386,7 @@ int receive_discovery_responses(onvif_device_info_t *devices, int max_devices) { // Dump the first 500 characters of response for debugging char debug_buffer[501]; - strncpy(debug_buffer, buffer, 500); - debug_buffer[500] = '\0'; + safe_strcpy(debug_buffer, buffer, 501, 0); log_info("Response (first 500 chars): %s", debug_buffer); // Parse device information diff --git a/src/video/onvif_motion_recording.c b/src/video/onvif_motion_recording.c index d43f5e97..6b8183be 100644 --- a/src/video/onvif_motion_recording.c +++ b/src/video/onvif_motion_recording.c @@ -21,6 +21,7 @@ #include "core/config.h" #include "core/path_utils.h" #include "core/shutdown_coordinator.h" +#include "utils/strings.h" #include "database/database_manager.h" #include "database/db_recordings.h" #include "database/db_streams.h" @@ -210,8 +211,7 @@ static motion_recording_context_t* create_recording_context(const char *stream_n for (int i = 0; i < g_config.max_streams; i++) { if (!recording_contexts[i].active) { memset(&recording_contexts[i], 0, sizeof(motion_recording_context_t)); - strncpy(recording_contexts[i].stream_name, stream_name, MAX_STREAM_NAME - 1); - recording_contexts[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(recording_contexts[i].stream_name, stream_name, MAX_STREAM_NAME, 0); recording_contexts[i].state = RECORDING_STATE_IDLE; recording_contexts[i].active = true; @@ -828,12 +828,11 @@ int process_motion_event(const char *stream_name, bool motion_detected, time_t t // Create motion event motion_event_t event; memset(&event, 0, sizeof(motion_event_t)); - strncpy(event.stream_name, stream_name, MAX_STREAM_NAME - 1); - event.stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(event.stream_name, stream_name, MAX_STREAM_NAME, 0); event.timestamp = timestamp; event.active = motion_detected; event.confidence = 1.0f; - strncpy(event.event_type, "motion", sizeof(event.event_type) - 1); + safe_strcpy(event.event_type, "motion", sizeof(event.event_type), 0); // Push to event queue if (push_event(&event) != 0) { @@ -865,12 +864,11 @@ int process_motion_event(const char *stream_name, bool motion_detected, time_t t // Path A: ONVIF event queue (for ONVIF-managed slave streams) motion_event_t linked_event; memset(&linked_event, 0, sizeof(motion_event_t)); - strncpy(linked_event.stream_name, all_streams[i].name, MAX_STREAM_NAME - 1); - linked_event.stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(linked_event.stream_name, all_streams[i].name, MAX_STREAM_NAME, 0); linked_event.timestamp = timestamp; linked_event.active = motion_detected; linked_event.confidence = 1.0f; - strncpy(linked_event.event_type, "motion", sizeof(linked_event.event_type) - 1); + safe_strcpy(linked_event.event_type, "motion", sizeof(linked_event.event_type), 0); if (push_event(&linked_event) != 0) { log_error("Failed to push linked motion event to stream: %s (triggered by: %s)", @@ -999,8 +997,7 @@ int get_current_motion_recording_path(const char *stream_name, char *path, size_ pthread_mutex_lock(&ctx->mutex); if (ctx->current_file_path[0] != '\0') { - strncpy(path, ctx->current_file_path, path_size - 1); - path[path_size - 1] = '\0'; + safe_strcpy(path, ctx->current_file_path, path_size, 0); pthread_mutex_unlock(&ctx->mutex); return 0; } diff --git a/src/video/onvif_ptz.c b/src/video/onvif_ptz.c index 2f654a34..bc63154b 100644 --- a/src/video/onvif_ptz.c +++ b/src/video/onvif_ptz.c @@ -1,13 +1,15 @@ -#include "video/onvif_ptz.h" -#include "video/onvif_soap.h" -#include "core/logger.h" -#include "core/url_utils.h" #include #include #include #include #include "ezxml.h" +#include "video/onvif_ptz.h" +#include "video/onvif_soap.h" +#include "core/logger.h" +#include "core/url_utils.h" +#include "utils/strings.h" + // Structure to store memory for CURL responses typedef struct { char *memory; @@ -393,12 +395,10 @@ int onvif_ptz_get_presets(const char *ptz_url, const char *profile_token, if (!name_elem) name_elem = ezxml_child(preset, "Name"); if (token) { - strncpy(presets[count].token, token, sizeof(presets[count].token) - 1); - presets[count].token[sizeof(presets[count].token) - 1] = '\0'; + safe_strcpy(presets[count].token, token, sizeof(presets[count].token), 0); } if (name_elem && name_elem->txt) { - strncpy(presets[count].name, name_elem->txt, sizeof(presets[count].name) - 1); - presets[count].name[sizeof(presets[count].name) - 1] = '\0'; + safe_strcpy(presets[count].name, name_elem->txt, sizeof(presets[count].name), 0); } count++; } @@ -494,8 +494,7 @@ int onvif_ptz_set_preset(const char *ptz_url, const char *profile_token, ezxml_t token_elem = ezxml_child(set_preset_response, "tptz:PresetToken"); if (!token_elem) token_elem = ezxml_child(set_preset_response, "PresetToken"); if (token_elem && token_elem->txt) { - strncpy(preset_token, token_elem->txt, token_size - 1); - preset_token[token_size - 1] = '\0'; + safe_strcpy(preset_token, token_elem->txt, token_size, 0); } } } diff --git a/src/video/onvif_soap.c b/src/video/onvif_soap.c index d426bc08..582674f9 100644 --- a/src/video/onvif_soap.c +++ b/src/video/onvif_soap.c @@ -10,6 +10,7 @@ #include "video/onvif_soap.h" #include "core/logger.h" +#include "utils/strings.h" #include "ezxml.h" #include @@ -155,7 +156,7 @@ static ezxml_t find_with_ns(ezxml_t parent, const char *local_name, return ezxml_child(parent, local_name); } -void onvif_log_soap_fault(char *response, size_t response_len, const char *context) { +void onvif_log_soap_fault(const char *response, size_t response_len, const char *context) { if (!response || response_len == 0) return; const char *ctx = context ? context : "ONVIF"; diff --git a/src/video/packet_buffer.c b/src/video/packet_buffer.c index 5328ca41..a2eff3da 100644 --- a/src/video/packet_buffer.c +++ b/src/video/packet_buffer.c @@ -16,6 +16,7 @@ #include "video/packet_buffer.h" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" // Global buffer pool static packet_buffer_pool_t buffer_pool; @@ -235,7 +236,7 @@ packet_buffer_t* create_packet_buffer(const char *stream_name, int buffer_second } buffer->mutex_initialized = true; - strncpy(buffer->stream_name, stream_name, sizeof(buffer->stream_name) - 1); + safe_strcpy(buffer->stream_name, stream_name, sizeof(buffer->stream_name), 0); buffer->buffer_seconds = buffer_seconds; buffer->mode = mode; @@ -314,8 +315,7 @@ void destroy_packet_buffer(packet_buffer_t *buffer) { pthread_mutex_unlock(&buffer_pool.pool_mutex); char stream_name_copy[256]; - strncpy(stream_name_copy, buffer->stream_name, sizeof(stream_name_copy) - 1); - stream_name_copy[sizeof(stream_name_copy) - 1] = '\0'; + safe_strcpy(stream_name_copy, buffer->stream_name, sizeof(stream_name_copy), 0); buffer->active = false; @@ -662,7 +662,7 @@ int packet_buffer_set_disk_fallback(packet_buffer_t *buffer, bool enable, const if (enable) { buffer->mode = BUFFER_MODE_HYBRID; if (disk_path) { - strncpy(buffer->disk_buffer_path, disk_path, sizeof(buffer->disk_buffer_path) - 1); + safe_strcpy(buffer->disk_buffer_path, disk_path, sizeof(buffer->disk_buffer_path), 0); } log_info("Enabled disk fallback for buffer: %s (path: %s)", buffer->stream_name, buffer->disk_buffer_path); diff --git a/src/video/recording.c b/src/video/recording.c index f216cc8e..8bf2ac7a 100644 --- a/src/video/recording.c +++ b/src/video/recording.c @@ -13,6 +13,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/path_utils.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/streams.h" #include "video/mp4_writer.h" @@ -106,7 +107,7 @@ uint64_t start_recording(const char *stream_name, const char *output_path) { // from memset would make them look critical AND hit the 0-multiplier bug. metadata.retention_tier = RETENTION_TIER_STANDARD; - strncpy(metadata.stream_name, stream_name, sizeof(metadata.stream_name) - 1); + safe_strcpy(metadata.stream_name, stream_name, sizeof(metadata.stream_name), 0); // Format paths for the recording - MAKE SURE THIS POINTS TO REAL FILES char mp4_path[MAX_PATH_LENGTH]; @@ -115,19 +116,16 @@ uint64_t start_recording(const char *stream_name, const char *output_path) { const mp4_writer_t *mp4_writer = get_mp4_writer_for_stream(stream_name); if (mp4_writer && mp4_writer->output_path) { // Use the actual MP4 file path from the writer - strncpy(mp4_path, mp4_writer->output_path, sizeof(mp4_path) - 1); - mp4_path[sizeof(mp4_path) - 1] = '\0'; + safe_strcpy(mp4_path, mp4_writer->output_path, sizeof(mp4_path), 0); // Store the actual MP4 path in the metadata - strncpy(metadata.file_path, mp4_path, sizeof(metadata.file_path) - 1); - metadata.file_path[sizeof(metadata.file_path) - 1] = '\0'; + safe_strcpy(metadata.file_path, mp4_path, sizeof(metadata.file_path), 0); log_info("Using actual MP4 path for recording: %s", mp4_path); } else { // Fallback to a default path if no writer is available snprintf(mp4_path, sizeof(mp4_path), "%s/recording.mp4", output_path); - strncpy(metadata.file_path, mp4_path, sizeof(metadata.file_path) - 1); - metadata.file_path[sizeof(metadata.file_path) - 1] = '\0'; + safe_strcpy(metadata.file_path, mp4_path, sizeof(metadata.file_path), 0); log_warn("No MP4 writer found for stream %s, using default path: %s", stream_name, mp4_path); } @@ -140,7 +138,7 @@ uint64_t start_recording(const char *stream_name, const char *output_path) { metadata.width = config.width; metadata.height = config.height; metadata.fps = config.fps; - strncpy(metadata.codec, config.codec, sizeof(metadata.codec) - 1); + safe_strcpy(metadata.codec, config.codec, sizeof(metadata.codec), 0); metadata.is_complete = false; // Add recording to database with detailed error handling @@ -156,8 +154,8 @@ uint64_t start_recording(const char *stream_name, const char *output_path) { for (int i = 0; i < g_config.max_streams; i++) { if (active_recordings[i].recording_id == 0) { active_recordings[i].recording_id = recording_id; - strncpy(active_recordings[i].stream_name, stream_name, MAX_STREAM_NAME - 1); - strncpy(active_recordings[i].output_path, output_path, MAX_PATH_LENGTH - 1); + safe_strcpy(active_recordings[i].stream_name, stream_name, MAX_STREAM_NAME, 0); + safe_strcpy(active_recordings[i].output_path, output_path, MAX_PATH_LENGTH, 0); active_recordings[i].start_time = metadata.start_time; log_info("Started recording for stream %s with ID %llu", @@ -185,8 +183,7 @@ void update_recording(const char *stream_name) { uint64_t recording_id = active_recordings[i].recording_id; char output_path[MAX_PATH_LENGTH]; - strncpy(output_path, active_recordings[i].output_path, MAX_PATH_LENGTH - 1); - output_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(output_path, active_recordings[i].output_path, MAX_PATH_LENGTH, 0); // Calculate total size of all segments uint64_t total_size = 0; @@ -230,8 +227,7 @@ void stop_recording(const char *stream_name) { uint64_t recording_id = active_recordings[i].recording_id; char output_path[MAX_PATH_LENGTH]; - strncpy(output_path, active_recordings[i].output_path, MAX_PATH_LENGTH - 1); - output_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(output_path, active_recordings[i].output_path, MAX_PATH_LENGTH, 0); time_t start_time = active_recordings[i].start_time; // Clear the active recording slot @@ -265,8 +261,7 @@ void stop_recording(const char *stream_name) { recording_metadata_t metadata; if (get_recording_metadata_by_id(recording_id, &metadata) == 0) { if (mp4_writer->output_path && mp4_writer->output_path[0] != '\0') { - strncpy(metadata.file_path, mp4_writer->output_path, sizeof(metadata.file_path) - 1); - metadata.file_path[sizeof(metadata.file_path) - 1] = '\0'; + safe_strcpy(metadata.file_path, mp4_writer->output_path, sizeof(metadata.file_path), 0); update_recording_metadata(recording_id, end_time, total_size, true); log_info("Updated recording %llu with actual MP4 path: %s", (unsigned long long)recording_id, metadata.file_path); @@ -356,8 +351,7 @@ static bool find_first_mp4_in_dir(const char *dir_path, const char *prefix, /* Keep the lexicographically smallest name (mirrors "| sort | head -1") */ if (best[0] == '\0' || strcmp(name, best) < 0) { - strncpy(best, name, sizeof(best) - 1); - best[sizeof(best) - 1] = '\0'; + safe_strcpy(best, name, sizeof(best), 0); } } closedir(d); @@ -372,8 +366,7 @@ static bool find_first_mp4_in_dir(const char *dir_path, const char *prefix, struct stat st; if (stat(full, &st) != 0 || st.st_size == 0) return false; - strncpy(out_path, full, out_size - 1); - out_path[out_size - 1] = '\0'; + safe_strcpy(out_path, full, out_size, 0); return true; } @@ -389,7 +382,7 @@ int find_mp4_recording(const char *stream_name, time_t timestamp, char *mp4_path // Get global config for storage paths const config_t *global_config = get_streaming_config(); - char base_path[256]; + char base_path[MAX_PATH_LENGTH]; // Format timestamp for pattern matching char timestamp_str[32]; diff --git a/src/video/sod_detection.c b/src/video/sod_detection.c index a98dd0f4..1ee00ebb 100644 --- a/src/video/sod_detection.c +++ b/src/video/sod_detection.c @@ -16,6 +16,7 @@ #include "core/logger.h" #include "core/config.h" // For MAX_PATH_LENGTH +#include "utils/strings.h" #include "video/detection_result.h" #include "video/detection_model.h" #include "video/detection_model_internal.h" @@ -264,13 +265,12 @@ detection_model_t load_sod_model(const char *model_path, float threshold) { } // Initialize model structure - strncpy(model->type, MODEL_TYPE_SOD, sizeof(model->type) - 1); + safe_strcpy(model->type, MODEL_TYPE_SOD, sizeof(model->type), 0); model->sod = sod_model; model->threshold = threshold; // Store the model path in the model structure - strncpy(model->path, model_path, MAX_PATH_LENGTH - 1); - model->path[MAX_PATH_LENGTH - 1] = '\0'; // Ensure null termination + safe_strcpy(model->path, model_path, MAX_PATH_LENGTH, 0); log_info("SOD model loaded: %s with threshold %.2f", model_path, threshold); return model; @@ -492,11 +492,10 @@ int detect_with_sod_model(detection_model_t model, const unsigned char *frame_da // Extra safety check for name string if (name && strlen(name) > 0) { - strncpy(label, name, MAX_LABEL_LENGTH - 1); + safe_strcpy(label, name, MAX_LABEL_LENGTH, 0); } else { - strncpy(label, "object", MAX_LABEL_LENGTH - 1); + safe_strcpy(label, "object", MAX_LABEL_LENGTH, 0); } - label[MAX_LABEL_LENGTH - 1] = '\0'; // Clamp confidence to valid range [0.0, 1.0] float confidence = box->score; @@ -523,7 +522,7 @@ int detect_with_sod_model(detection_model_t model, const unsigned char *frame_da } // Add valid detection to result - strncpy(result->detections[valid_count].label, label, MAX_LABEL_LENGTH - 1); + safe_strcpy(result->detections[valid_count].label, label, MAX_LABEL_LENGTH, 0); result->detections[valid_count].confidence = confidence; result->detections[valid_count].x = x; result->detections[valid_count].y = y; diff --git a/src/video/sod_integration.c b/src/video/sod_integration.c index 19370108..d351ad29 100644 --- a/src/video/sod_integration.c +++ b/src/video/sod_integration.c @@ -12,6 +12,7 @@ #include "../../include/core/logger.h" #include "../../include/core/config.h" +#include "../../include/utils/strings.h" #include "../../include/video/detection.h" #include "../../include/video/detection_result.h" #include "../../include/video/sod_integration.h" @@ -100,7 +101,7 @@ void* load_sod_model_for_detection(const char *model_path, float threshold, model_path); if (file_exists(alt_path)) { log_info("Found model at alternative location: %s", alt_path); - strncpy(full_model_path, alt_path, max_path_length - 1); + safe_strcpy(full_model_path, alt_path, max_path_length, 0); break; } } @@ -108,7 +109,7 @@ void* load_sod_model_for_detection(const char *model_path, float threshold, } } else { // Already an absolute path - strncpy(full_model_path, model_path, max_path_length - 1); + safe_strcpy(full_model_path, model_path, max_path_length, 0); } log_info("Using model path: %s", full_model_path); diff --git a/src/video/sod_realnet.c b/src/video/sod_realnet.c index f2508589..d78e368b 100644 --- a/src/video/sod_realnet.c +++ b/src/video/sod_realnet.c @@ -6,6 +6,7 @@ #include "video/detection.h" #include "core/logger.h" +#include "utils/strings.h" // SOD RealNet function pointers for dynamic loading typedef struct { @@ -245,9 +246,8 @@ int detect_with_sod_realnet(void *model, const unsigned char *frame_data, } // Copy detection data - strncpy(result->detections[valid_count].label, - name ? name : "face", - MAX_LABEL_LENGTH - 1); + safe_strcpy(result->detections[valid_count].label, + name ? name : "face", MAX_LABEL_LENGTH, 0); // Convert confidence from SOD score to 0.0-1.0 range // SOD RealNet typically uses a score > 5.0 for good detections diff --git a/src/video/stream_manager.c b/src/video/stream_manager.c index ec7d0f23..167f083d 100644 --- a/src/video/stream_manager.c +++ b/src/video/stream_manager.c @@ -8,6 +8,7 @@ #include "video/stream_manager.h" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" #include "video/streams.h" #include "video/detection.h" #include "video/stream_reader.h" @@ -295,8 +296,7 @@ void shutdown_stream_manager(void) { for (int i = 0; i < streams_capacity; i++) { if (streams[i].config.name[0] != '\0' && streams[i].status == STREAM_STATUS_RUNNING) { char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, streams[i].config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, streams[i].config.name, MAX_STREAM_NAME, 0); bool streaming_enabled = streams[i].config.streaming_enabled; bool recording_enabled = streams[i].config.record; @@ -457,15 +457,13 @@ int set_stream_detection_recording(stream_handle_t stream, bool enabled, const c // Get stream name for potential thread stopping/starting char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, s->config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, s->config.name, MAX_STREAM_NAME, 0); // Update configuration s->config.detection_based_recording = enabled; if (model_path) { - strncpy(s->config.detection_model, model_path, MAX_PATH_LENGTH - 1); - s->config.detection_model[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(s->config.detection_model, model_path, MAX_PATH_LENGTH, 0); } // Get a copy of the config for database update @@ -670,8 +668,7 @@ int remove_stream(stream_handle_t handle) { // Save stream name for logging char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, s->config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, s->config.name, MAX_STREAM_NAME, 0); // Note: Database deletion and go2rtc unregistration are handled by the caller // (handle_delete_stream) which calls delete_stream_config_internal() and @@ -682,8 +679,7 @@ int remove_stream(stream_handle_t handle) { // Save stream name for timestamp tracker cleanup char stream_name_for_cleanup[MAX_STREAM_NAME]; - strncpy(stream_name_for_cleanup, s->config.name, MAX_STREAM_NAME - 1); - stream_name_for_cleanup[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name_for_cleanup, s->config.name, MAX_STREAM_NAME, 0); memset(&s->config, 0, sizeof(stream_config_t)); s->status = STREAM_STATUS_STOPPED; @@ -710,8 +706,7 @@ int start_stream(stream_handle_t handle) { // Get stream name for logging char stream_name[MAX_STREAM_NAME]; pthread_mutex_lock(&s->mutex); - strncpy(stream_name, s->config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, s->config.name, MAX_STREAM_NAME, 0); pthread_mutex_unlock(&s->mutex); // First try to use the new state management system @@ -834,8 +829,7 @@ int stop_stream(stream_handle_t handle) { // Get stream name for logging char stream_name[MAX_STREAM_NAME]; pthread_mutex_lock(&s->mutex); - strncpy(stream_name, s->config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, s->config.name, MAX_STREAM_NAME, 0); pthread_mutex_unlock(&s->mutex); // First try to use the new state management system @@ -1047,8 +1041,7 @@ int set_stream_recording(stream_handle_t handle, bool enable) { // Get stream name for logging char stream_name[MAX_STREAM_NAME]; pthread_mutex_lock(&s->mutex); - strncpy(stream_name, s->config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, s->config.name, MAX_STREAM_NAME, 0); pthread_mutex_unlock(&s->mutex); // First try to use the new state management system @@ -1136,8 +1129,7 @@ int set_stream_streaming_enabled(stream_handle_t handle, bool enabled) { // Get stream name for logging char stream_name[MAX_STREAM_NAME]; pthread_mutex_lock(&s->mutex); - strncpy(stream_name, s->config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, s->config.name, MAX_STREAM_NAME, 0); pthread_mutex_unlock(&s->mutex); // First try to use the new state management system @@ -1203,8 +1195,7 @@ int set_stream_onvif_flag(stream_handle_t handle, bool is_onvif) { // Get stream name for logging char stream_name[MAX_STREAM_NAME]; pthread_mutex_lock(&s->mutex); - strncpy(stream_name, s->config.name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, s->config.name, MAX_STREAM_NAME, 0); pthread_mutex_unlock(&s->mutex); log_info("Setting ONVIF flag for stream '%s' to %s", stream_name, is_onvif ? "true" : "false"); diff --git a/src/video/stream_protocol.c b/src/video/stream_protocol.c index 164fc66c..d3a5f128 100644 --- a/src/video/stream_protocol.c +++ b/src/video/stream_protocol.c @@ -1,9 +1,3 @@ -#include "video/stream_protocol.h" -#include "core/url_utils.h" -#include "core/logger.h" -#include "core/shutdown_coordinator.h" -#include "video/ffmpeg_utils.h" -#include "video/ffmpeg_leak_detector.h" #include #include #include @@ -19,6 +13,14 @@ #include #include +#include "video/stream_protocol.h" +#include "core/url_utils.h" +#include "core/logger.h" +#include "core/shutdown_coordinator.h" +#include "utils/strings.h" +#include "video/ffmpeg_utils.h" +#include "video/ffmpeg_leak_detector.h" + /** * Interrupt callback for FFmpeg operations * This allows blocking FFmpeg operations (like av_read_frame) to be interrupted during shutdown @@ -61,8 +63,7 @@ bool is_multicast_url(const char *url) { } if (url_redact_for_logging(url, safe_url, sizeof(safe_url)) != 0) { - strncpy(safe_url, "[invalid-url]", sizeof(safe_url) - 1); - safe_url[sizeof(safe_url) - 1] = '\0'; + safe_strcpy(safe_url, "[invalid-url]", sizeof(safe_url), 0); } // Extract IP address from URL with more robust parsing @@ -85,8 +86,7 @@ bool is_multicast_url(const char *url) { // Make a copy of the IP part to avoid modifying the original char ip_buffer[256]; - strncpy(ip_buffer, ip_start, sizeof(ip_buffer) - 1); - ip_buffer[sizeof(ip_buffer) - 1] = '\0'; + safe_strcpy(ip_buffer, ip_start, sizeof(ip_buffer), 0); // Remove port and path information char *colon = strchr(ip_buffer, ':'); @@ -135,8 +135,7 @@ static bool check_rtsp_stream_exists(const char *url) { } if (url_redact_for_logging(url, safe_url, sizeof(safe_url)) != 0) { - strncpy(safe_url, "[invalid-url]", sizeof(safe_url) - 1); - safe_url[sizeof(safe_url) - 1] = '\0'; + safe_strcpy(safe_url, "[invalid-url]", sizeof(safe_url), 0); } // Extract the host and port from the URL @@ -273,8 +272,7 @@ int open_input_stream(AVFormatContext **input_ctx, const char *url, int protocol if (is_shutdown_initiated()) { char shutdown_safe_url[MAX_URL_LENGTH] = {0}; if (url_redact_for_logging(url, shutdown_safe_url, sizeof(shutdown_safe_url)) != 0) { - strncpy(shutdown_safe_url, "[invalid-url]", sizeof(shutdown_safe_url) - 1); - shutdown_safe_url[sizeof(shutdown_safe_url) - 1] = '\0'; + safe_strcpy(shutdown_safe_url, "[invalid-url]", sizeof(shutdown_safe_url), 0); } log_info("Skipping input stream open for %s during shutdown", shutdown_safe_url); return AVERROR(EINTR); // Interrupted system call @@ -303,13 +301,11 @@ int open_input_stream(AVFormatContext **input_ctx, const char *url, int protocol } // Copy URL to local buffer - strncpy(local_url, url, sizeof(local_url) - 1); - local_url[sizeof(local_url) - 1] = '\0'; + safe_strcpy(local_url, url, sizeof(local_url), 0); char safe_url[MAX_URL_LENGTH] = {0}; if (url_redact_for_logging(local_url, safe_url, sizeof(safe_url)) != 0) { - strncpy(safe_url, "[invalid-url]", sizeof(safe_url) - 1); - safe_url[sizeof(safe_url) - 1] = '\0'; + safe_strcpy(safe_url, "[invalid-url]", sizeof(safe_url), 0); } // Use local_url instead of url from this point forward @@ -632,7 +628,7 @@ int open_input_stream(AVFormatContext **input_ctx, const char *url, int protocol if (*input_ctx && (*input_ctx)->nb_streams > 0) { // CRITICAL FIX: Sanitize the URL before logging to prevent displaying non-printable characters // This prevents potential issues with corrupted stream names - char sanitized_url[1024] = {0}; + char sanitized_url[1024]; size_t i; // Copy and sanitize the redacted URL @@ -704,8 +700,7 @@ bool is_onvif_stream(const char *url) { if (strstr(url, "onvif") != NULL) { char safe_url[MAX_URL_LENGTH] = {0}; if (url_redact_for_logging(url, safe_url, sizeof(safe_url)) != 0) { - strncpy(safe_url, "[invalid-url]", sizeof(safe_url) - 1); - safe_url[sizeof(safe_url) - 1] = '\0'; + safe_strcpy(safe_url, "[invalid-url]", sizeof(safe_url), 0); } log_info("Detected ONVIF stream URL: %s", safe_url); return true; diff --git a/src/video/stream_state.c b/src/video/stream_state.c index 076c9d99..a86cf4e7 100644 --- a/src/video/stream_state.c +++ b/src/video/stream_state.c @@ -9,6 +9,7 @@ #include "video/stream_state.h" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" #include "video/stream_reader.h" #include "video/hls_streaming.h" #include "video/mp4_recording.h" @@ -76,8 +77,7 @@ void shutdown_stream_state_manager(void) { if (stream_states[i]) { // Make a local copy of the stream name for logging char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, stream_states[i]->name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, stream_states[i]->name, MAX_STREAM_NAME, 0); // Stop the stream if it's active if (stream_states[i]->state == STREAM_STATE_ACTIVE || @@ -170,8 +170,7 @@ stream_state_manager_t *create_stream_state(const stream_config_t *config) { memcpy(&state->config, config, sizeof(stream_config_t)); // Initialize name - strncpy(state->name, config->name, MAX_STREAM_NAME - 1); - state->name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(state->name, config->name, MAX_STREAM_NAME, 0); // Initialize state state->state = STREAM_STATE_INACTIVE; @@ -621,8 +620,7 @@ int stop_stream_with_state(stream_state_manager_t *state, bool wait_for_completi // Get stream name for logging char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, state->name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, state->name, MAX_STREAM_NAME, 0); // Only disable callbacks if we're stopping the entire stream // If we're just disabling recording, we still want to keep callbacks enabled for HLS streaming @@ -875,8 +873,7 @@ int remove_stream_state(stream_state_manager_t *state) { // Save stream name for logging char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, state->name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, state->name, MAX_STREAM_NAME, 0); // Check reference count pthread_mutex_lock(&state->mutex); diff --git a/src/video/stream_state_adapter.c b/src/video/stream_state_adapter.c index b32b3778..25233a61 100644 --- a/src/video/stream_state_adapter.c +++ b/src/video/stream_state_adapter.c @@ -7,6 +7,7 @@ #include "video/stream_state.h" #include "video/stream_state_adapter.h" #include "core/logger.h" +#include "utils/strings.h" // Mapping between old stream handles and new state managers typedef struct { @@ -258,8 +259,7 @@ int remove_stream_adapter(stream_handle_t handle) { // Save stream name for logging char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, state->name, MAX_STREAM_NAME - 1); - stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(stream_name, state->name, MAX_STREAM_NAME, 0); // Remove mapping remove_handle_mapping_by_handle(handle); @@ -457,8 +457,7 @@ int set_stream_detection_recording_adapter(stream_handle_t handle, bool enable, memcpy(&updated_config, &state->config, sizeof(stream_config_t)); pthread_mutex_unlock(&state->mutex); - strncpy(updated_config.detection_model, model_path, MAX_PATH_LENGTH - 1); - updated_config.detection_model[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(updated_config.detection_model, model_path, MAX_PATH_LENGTH, 0); update_stream_state_config(state, &updated_config); } diff --git a/src/video/timestamp_manager.c b/src/video/timestamp_manager.c index 61ad70ab..0a4971d5 100644 --- a/src/video/timestamp_manager.c +++ b/src/video/timestamp_manager.c @@ -2,9 +2,6 @@ #define _XOPEN_SOURCE 700 #define _GNU_SOURCE -#include "video/timestamp_manager.h" -#include "core/logger.h" -#include "core/config.h" #include #include #include @@ -12,6 +9,11 @@ #include // For usleep #include +#include "video/timestamp_manager.h" +#include "core/logger.h" +#include "core/config.h" +#include "utils/strings.h" + // Structure to track timestamp information per stream typedef struct { char stream_name[MAX_STREAM_NAME]; @@ -40,9 +42,7 @@ void *get_timestamp_tracker(const char *stream_name) { // Make a local copy of the stream name to avoid issues with concurrent access char local_stream_name[MAX_STREAM_NAME]; - strncpy(local_stream_name, stream_name, MAX_STREAM_NAME - 1); - local_stream_name[MAX_STREAM_NAME - 1] = '\0'; - + safe_strcpy(local_stream_name, stream_name, MAX_STREAM_NAME, 0); // Look for existing tracker for (int i = 0; i < MAX_TIMESTAMP_TRACKERS; i++) { @@ -55,8 +55,7 @@ void *get_timestamp_tracker(const char *stream_name) { // Create new tracker for (int i = 0; i < MAX_TIMESTAMP_TRACKERS; i++) { if (!timestamp_trackers[i].initialized) { - strncpy(timestamp_trackers[i].stream_name, local_stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[i].stream_name, local_stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[i].last_pts = AV_NOPTS_VALUE; timestamp_trackers[i].last_dts = AV_NOPTS_VALUE; timestamp_trackers[i].pts_discontinuity_count = 0; @@ -93,8 +92,7 @@ void *get_timestamp_tracker(const char *stream_name) { (long)(current_time - timestamp_trackers[i].last_keyframe_time)); // Reset the tracker for the new stream - strncpy(timestamp_trackers[i].stream_name, local_stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[i].stream_name, local_stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[i].last_pts = AV_NOPTS_VALUE; timestamp_trackers[i].last_dts = AV_NOPTS_VALUE; timestamp_trackers[i].pts_discontinuity_count = 0; @@ -145,8 +143,7 @@ void set_timestamp_tracker_udp_flag(const char *stream_name, bool is_udp) { // Make a local copy of the stream name to avoid issues with concurrent access char local_stream_name[MAX_STREAM_NAME]; - strncpy(local_stream_name, stream_name, MAX_STREAM_NAME - 1); - local_stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(local_stream_name, stream_name, MAX_STREAM_NAME, 0); // Look for existing tracker int found = 0; @@ -167,8 +164,7 @@ void set_timestamp_tracker_udp_flag(const char *stream_name, bool is_udp) { for (int i = 0; i < MAX_TIMESTAMP_TRACKERS; i++) { if (!timestamp_trackers[i].initialized) { // Initialize the new tracker - strncpy(timestamp_trackers[i].stream_name, local_stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[i].stream_name, local_stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[i].last_pts = AV_NOPTS_VALUE; timestamp_trackers[i].last_dts = AV_NOPTS_VALUE; timestamp_trackers[i].pts_discontinuity_count = 0; @@ -320,8 +316,7 @@ void update_keyframe_time(const char *stream_name) { bool created = false; for (int i = 0; i < MAX_TIMESTAMP_TRACKERS; i++) { if (!timestamp_trackers[i].initialized) { - strncpy(timestamp_trackers[i].stream_name, stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[i].stream_name, stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[i].last_pts = AV_NOPTS_VALUE; timestamp_trackers[i].last_dts = AV_NOPTS_VALUE; timestamp_trackers[i].pts_discontinuity_count = 0; @@ -359,8 +354,7 @@ void update_keyframe_time(const char *stream_name) { stream_name); // Reset the tracker for the new stream - strncpy(timestamp_trackers[oldest_idx].stream_name, stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[oldest_idx].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[oldest_idx].stream_name, stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[oldest_idx].last_pts = AV_NOPTS_VALUE; timestamp_trackers[oldest_idx].last_dts = AV_NOPTS_VALUE; timestamp_trackers[oldest_idx].pts_discontinuity_count = 0; @@ -429,8 +423,7 @@ int last_keyframe_received(const char *stream_name, time_t *keyframe_time) { if (!timestamp_trackers[i].initialized) { log_info("Creating new timestamp tracker for stream %s during keyframe check", stream_name); - strncpy(timestamp_trackers[i].stream_name, stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[i].stream_name, stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[i].last_pts = AV_NOPTS_VALUE; timestamp_trackers[i].last_dts = AV_NOPTS_VALUE; timestamp_trackers[i].pts_discontinuity_count = 0; @@ -491,8 +484,7 @@ time_t get_last_detection_time(const char *stream_name) { if (!timestamp_trackers[i].initialized) { log_info("Creating new timestamp tracker for stream %s during get_last_detection_time", stream_name); - strncpy(timestamp_trackers[i].stream_name, stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[i].stream_name, stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[i].last_pts = AV_NOPTS_VALUE; timestamp_trackers[i].last_dts = AV_NOPTS_VALUE; timestamp_trackers[i].pts_discontinuity_count = 0; @@ -547,8 +539,7 @@ void update_last_detection_time(const char *stream_name, time_t detection_time) bool created = false; for (int i = 0; i < MAX_TIMESTAMP_TRACKERS; i++) { if (!timestamp_trackers[i].initialized) { - strncpy(timestamp_trackers[i].stream_name, stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[i].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[i].stream_name, stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[i].last_pts = AV_NOPTS_VALUE; timestamp_trackers[i].last_dts = AV_NOPTS_VALUE; timestamp_trackers[i].pts_discontinuity_count = 0; @@ -586,8 +577,7 @@ void update_last_detection_time(const char *stream_name, time_t detection_time) stream_name); // Reset the tracker for the new stream - strncpy(timestamp_trackers[oldest_idx].stream_name, stream_name, MAX_STREAM_NAME - 1); - timestamp_trackers[oldest_idx].stream_name[MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(timestamp_trackers[oldest_idx].stream_name, stream_name, MAX_STREAM_NAME, 0); timestamp_trackers[oldest_idx].last_pts = AV_NOPTS_VALUE; timestamp_trackers[oldest_idx].last_dts = AV_NOPTS_VALUE; timestamp_trackers[oldest_idx].pts_discontinuity_count = 0; diff --git a/src/video/unified_detection_thread.c b/src/video/unified_detection_thread.c index 98cce1aa..e2168a91 100644 --- a/src/video/unified_detection_thread.c +++ b/src/video/unified_detection_thread.c @@ -37,6 +37,7 @@ #include "core/config.h" #include "core/path_utils.h" #include "core/shutdown_coordinator.h" +#include "utils/strings.h" #include "video/unified_detection_thread.h" #include "video/packet_buffer.h" #include "video/detection.h" @@ -468,8 +469,8 @@ int start_unified_detection_thread(const char *stream_name, const char *model_pa config_t *global_cfg = get_streaming_config(); // Initialize context - strncpy(ctx->stream_name, stream_name, sizeof(ctx->stream_name) - 1); - strncpy(ctx->model_path, model_path, sizeof(ctx->model_path) - 1); + safe_strcpy(ctx->stream_name, stream_name, sizeof(ctx->stream_name), 0); + safe_strcpy(ctx->model_path, model_path, sizeof(ctx->model_path), 0); ctx->detection_threshold = threshold; ctx->pre_buffer_seconds = pre_buffer_seconds > 0 ? pre_buffer_seconds : 10; ctx->post_buffer_seconds = post_buffer_seconds > 0 ? post_buffer_seconds : 5; @@ -494,7 +495,7 @@ int start_unified_detection_thread(const char *stream_name, const char *model_pa config.onvif_username[0] ? config.onvif_username : NULL, config.onvif_password[0] ? config.onvif_password : NULL, ctx->rtsp_url, sizeof(ctx->rtsp_url)) != 0) { - strncpy(ctx->rtsp_url, config.url, sizeof(ctx->rtsp_url) - 1); + safe_strcpy(ctx->rtsp_url, config.url, sizeof(ctx->rtsp_url), 0); } } @@ -988,8 +989,7 @@ static void *unified_detection_thread_func(void *arg) { } char stream_name[MAX_STREAM_NAME]; - strncpy(stream_name, ctx->stream_name, sizeof(stream_name) - 1); - stream_name[sizeof(stream_name) - 1] = '\0'; + safe_strcpy(stream_name, ctx->stream_name, sizeof(stream_name), 0); log_set_thread_context("Detection", stream_name); log_info("[%s] Unified detection thread started", stream_name); @@ -1580,7 +1580,7 @@ static int udt_start_recording(unified_detection_ctx_t *ctx) { } // Set trigger type to detection - strncpy(ctx->mp4_writer->trigger_type, "detection", sizeof(ctx->mp4_writer->trigger_type) - 1); + safe_strcpy(ctx->mp4_writer->trigger_type, "detection", sizeof(ctx->mp4_writer->trigger_type), 0); // Store recording start time ctx->mp4_writer->creation_time = now; @@ -1588,13 +1588,13 @@ static int udt_start_recording(unified_detection_ctx_t *ctx) { // Add recording to database at START (so it appears in recordings list immediately) // It will be updated with end_time, size, and is_complete=true when recording stops recording_metadata_t metadata = {0}; - strncpy(metadata.file_path, ctx->current_recording_path, sizeof(metadata.file_path) - 1); - strncpy(metadata.stream_name, ctx->stream_name, sizeof(metadata.stream_name) - 1); + safe_strcpy(metadata.file_path, ctx->current_recording_path, sizeof(metadata.file_path), 0); + safe_strcpy(metadata.stream_name, ctx->stream_name, sizeof(metadata.stream_name), 0); metadata.start_time = now; metadata.end_time = 0; // Will be set when recording stops metadata.size_bytes = 0; // Will be set when recording stops metadata.is_complete = false; // Will be set to true when recording stops - strncpy(metadata.trigger_type, "detection", sizeof(metadata.trigger_type) - 1); + safe_strcpy(metadata.trigger_type, "detection", sizeof(metadata.trigger_type), 0); ctx->current_recording_id = add_recording_metadata(&metadata); if (ctx->current_recording_id > 0) { @@ -1660,13 +1660,13 @@ static int udt_stop_recording(unified_detection_ctx_t *ctx) { // Fallback: if no recording_id, try to add a new record (shouldn't happen normally) log_warn("[%s] No recording ID found, creating new database entry", ctx->stream_name); recording_metadata_t metadata = {0}; - strncpy(metadata.file_path, ctx->current_recording_path, sizeof(metadata.file_path) - 1); - strncpy(metadata.stream_name, ctx->stream_name, sizeof(metadata.stream_name) - 1); + safe_strcpy(metadata.file_path, ctx->current_recording_path, sizeof(metadata.file_path), 0); + safe_strcpy(metadata.stream_name, ctx->stream_name, sizeof(metadata.stream_name), 0); metadata.start_time = start_time; metadata.end_time = end_time; metadata.size_bytes = file_size; metadata.is_complete = true; - strncpy(metadata.trigger_type, "detection", sizeof(metadata.trigger_type) - 1); + safe_strcpy(metadata.trigger_type, "detection", sizeof(metadata.trigger_type), 0); uint64_t recording_id = add_recording_metadata(&metadata); if (recording_id > 0) { diff --git a/src/video/zone_filter.c b/src/video/zone_filter.c index 43f29af3..c4dcdeef 100644 --- a/src/video/zone_filter.c +++ b/src/video/zone_filter.c @@ -1,10 +1,12 @@ +#include +#include + #include "video/zone_filter.h" #include "database/db_zones.h" #include "database/db_streams.h" #include "core/logger.h" #include "core/config.h" -#include -#include +#include "utils/strings.h" /** * Check if a point is inside a polygon using ray casting algorithm @@ -57,8 +59,7 @@ static bool detection_class_matches(const detection_t *detection, const detectio // Check if detection label is in the comma-separated filter list char filter_copy[256]; - strncpy(filter_copy, zone->filter_classes, sizeof(filter_copy) - 1); - filter_copy[sizeof(filter_copy) - 1] = '\0'; + safe_strcpy(filter_copy, zone->filter_classes, sizeof(filter_copy), 0); char *token = strtok(filter_copy, ","); while (token) { @@ -190,9 +191,8 @@ int filter_detections_by_zones(const char *stream_name, detection_result_t *resu // Set the zone_id for this detection if (matched_zone_id) { - strncpy(filtered.detections[filtered.count].zone_id, matched_zone_id, - sizeof(filtered.detections[filtered.count].zone_id) - 1); - filtered.detections[filtered.count].zone_id[sizeof(filtered.detections[filtered.count].zone_id) - 1] = '\0'; + safe_strcpy(filtered.detections[filtered.count].zone_id, matched_zone_id, + sizeof(filtered.detections[filtered.count].zone_id), 0); } filtered.count++; @@ -222,8 +222,7 @@ static bool label_in_list(const char *label, const char *class_list) { } char list_copy[256]; - strncpy(list_copy, class_list, sizeof(list_copy) - 1); - list_copy[sizeof(list_copy) - 1] = '\0'; + safe_strcpy(list_copy, class_list, sizeof(list_copy), 0); char *token = strtok(list_copy, ","); while (token) { diff --git a/src/web/api_handlers_auth_backend_agnostic.c b/src/web/api_handlers_auth_backend_agnostic.c index b1c8b30b..bca3c07a 100644 --- a/src/web/api_handlers_auth_backend_agnostic.c +++ b/src/web/api_handlers_auth_backend_agnostic.c @@ -20,6 +20,7 @@ #define LOG_COMPONENT "AuthAPI" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" #include "database/db_auth.h" /* ========== Login Rate Limiting ========== */ @@ -95,8 +96,7 @@ static void record_failed_attempt(const char *username) { // Add new entry if (rate_limit_count < MAX_RATE_LIMIT_ENTRIES) { - strncpy(rate_limit_table[rate_limit_count].username, username, 63); - rate_limit_table[rate_limit_count].username[63] = '\0'; + safe_strcpy(rate_limit_table[rate_limit_count].username, username, 64, 0); rate_limit_table[rate_limit_count].attempt_count = 1; rate_limit_table[rate_limit_count].window_start = now; rate_limit_count++; @@ -110,8 +110,7 @@ static void record_failed_attempt(const char *username) { oldest_idx = i; } } - strncpy(rate_limit_table[oldest_idx].username, username, 63); - rate_limit_table[oldest_idx].username[63] = '\0'; + safe_strcpy(rate_limit_table[oldest_idx].username, username, 64, 0); rate_limit_table[oldest_idx].attempt_count = 1; rate_limit_table[oldest_idx].window_start = now; } @@ -169,13 +168,13 @@ static int parse_form_credentials(const char *body, size_t body_len, char *usern return -1; } - // Make a copy of the body to work with - char *body_copy = malloc(body_len + 1); + // Make a copy of the body to work with. Note that body may + // not initially be null-terminated: strndup guarantees the + // result *will* be null-terminated. + char *body_copy = strndup(body, body_len); if (!body_copy) { return -1; } - memcpy(body_copy, body, body_len); - body_copy[body_len] = '\0'; int result = -1; @@ -226,8 +225,7 @@ void handle_auth_login(const http_request_t *req, http_response_t *res) { bool totp_verified = false; if (httpd_get_effective_client_ip(req, effective_client_ip, sizeof(effective_client_ip)) != 0) { - strncpy(effective_client_ip, req->client_ip, sizeof(effective_client_ip) - 1); - effective_client_ip[sizeof(effective_client_ip) - 1] = '\0'; + safe_strcpy(effective_client_ip, req->client_ip, sizeof(effective_client_ip), 0); } // Check Content-Type to determine if it's form data or JSON @@ -270,13 +268,13 @@ void handle_auth_login(const http_request_t *req, http_response_t *res) { return; } - strncpy(username, username_json->valuestring, sizeof(username) - 1); - strncpy(password, password_json->valuestring, sizeof(password) - 1); + safe_strcpy(username, username_json->valuestring, sizeof(username), 0); + safe_strcpy(password, password_json->valuestring, sizeof(password), 0); // Extract optional TOTP code (used in force-MFA mode) cJSON *totp_code_json = cJSON_GetObjectItem(login, "totp_code"); if (totp_code_json && cJSON_IsString(totp_code_json)) { - strncpy(totp_code, totp_code_json->valuestring, sizeof(totp_code) - 1); + safe_strcpy(totp_code, totp_code_json->valuestring, sizeof(totp_code), 0); } cJSON *remember_device_json = cJSON_GetObjectItem(login, "remember_device"); diff --git a/src/web/api_handlers_go2rtc_proxy.c b/src/web/api_handlers_go2rtc_proxy.c index 2077e539..57778257 100644 --- a/src/web/api_handlers_go2rtc_proxy.c +++ b/src/web/api_handlers_go2rtc_proxy.c @@ -21,6 +21,7 @@ #define LOG_COMPONENT "go2rtcAPI" #include "core/logger.h" #include "utils/memory.h" +#include "utils/strings.h" #ifdef HTTP_BACKEND_LIBUV #include "web/libuv_connection.h" @@ -245,7 +246,7 @@ void handle_go2rtc_proxy(const http_request_t *req, http_response_t *res) { // Set response res->status_code = (int)ctx.http_code; if (ctx.content_type[0] != '\0') { - strncpy(res->content_type, ctx.content_type, sizeof(res->content_type) - 1); + safe_strcpy(res->content_type, ctx.content_type, sizeof(res->content_type), 0); } // Add CORS headers diff --git a/src/web/api_handlers_metrics.c b/src/web/api_handlers_metrics.c index 6708dd21..743f728d 100644 --- a/src/web/api_handlers_metrics.c +++ b/src/web/api_handlers_metrics.c @@ -22,6 +22,7 @@ #define LOG_COMPONENT "MetricsAPI" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" /* ------------------------------------------------------------------ */ /* Growable buffer for Prometheus output */ @@ -146,7 +147,7 @@ void handle_get_metrics(const http_request_t *req, http_response_t *res) { int max = metrics_get_max_streams(); if (max <= 0) { res->status_code = 503; - strncpy(res->content_type, "text/plain", sizeof(res->content_type)); + safe_strcpy(res->content_type, "text/plain", sizeof(res->content_type), 0); http_response_set_body(res, "# Metrics subsystem not initialized\n"); return; } @@ -274,7 +275,7 @@ void handle_get_metrics(const http_request_t *req, http_response_t *res) { /* Send response */ res->status_code = 200; - strncpy(res->content_type, "text/plain; version=0.0.4; charset=utf-8", sizeof(res->content_type) - 1); + safe_strcpy(res->content_type, "text/plain; version=0.0.4; charset=utf-8", sizeof(res->content_type), 0); http_response_set_body(res, buf.data ? buf.data : ""); prom_buf_free(&buf); @@ -312,11 +313,11 @@ void handle_post_player_telemetry(const http_request_t *req, http_response_t *re cJSON *item; if ((item = cJSON_GetObjectItem(json, "stream_name")) && cJSON_IsString(item)) - strncpy(event.stream_name, item->valuestring, sizeof(event.stream_name) - 1); + safe_strcpy(event.stream_name, item->valuestring, sizeof(event.stream_name), 0); if ((item = cJSON_GetObjectItem(json, "session_id")) && cJSON_IsString(item)) - strncpy(event.session_id, item->valuestring, sizeof(event.session_id) - 1); + safe_strcpy(event.session_id, item->valuestring, sizeof(event.session_id), 0); if ((item = cJSON_GetObjectItem(json, "transport")) && cJSON_IsString(item)) - strncpy(event.transport, item->valuestring, sizeof(event.transport) - 1); + safe_strcpy(event.transport, item->valuestring, sizeof(event.transport), 0); if ((item = cJSON_GetObjectItem(json, "ttff_ms")) && cJSON_IsNumber(item)) event.ttff_ms = item->valuedouble; if ((item = cJSON_GetObjectItem(json, "rebuffer_count")) && cJSON_IsNumber(item)) diff --git a/src/web/api_handlers_motion.c b/src/web/api_handlers_motion.c index df78750d..185812d5 100644 --- a/src/web/api_handlers_motion.c +++ b/src/web/api_handlers_motion.c @@ -13,6 +13,7 @@ #define LOG_COMPONENT "MotionAPI" #include "core/logger.h" #include "database/db_motion_config.h" +#include "utils/strings.h" #include "video/onvif_motion_recording.h" #include "video/motion_storage_manager.h" #include @@ -119,16 +120,16 @@ void handle_post_motion_config(const http_request_t *req, http_response_t *res) cJSON *codec = cJSON_GetObjectItem(json, "codec"); if (codec && cJSON_IsString(codec)) { - strncpy(config.codec, codec->valuestring, sizeof(config.codec) - 1); + safe_strcpy(config.codec, codec->valuestring, sizeof(config.codec), 0); } else { - strncpy(config.codec, "h264", sizeof(config.codec) - 1); + safe_strcpy(config.codec, "h264", sizeof(config.codec), 0); } cJSON *quality = cJSON_GetObjectItem(json, "quality"); if (quality && cJSON_IsString(quality)) { - strncpy(config.quality, quality->valuestring, sizeof(config.quality) - 1); + safe_strcpy(config.quality, quality->valuestring, sizeof(config.quality), 0); } else { - strncpy(config.quality, "medium", sizeof(config.quality) - 1); + safe_strcpy(config.quality, "medium", sizeof(config.quality), 0); } cJSON *retention = cJSON_GetObjectItem(json, "retention_days"); diff --git a/src/web/api_handlers_onvif_backend_agnostic.c b/src/web/api_handlers_onvif_backend_agnostic.c index 9131777a..8db28e65 100644 --- a/src/web/api_handlers_onvif_backend_agnostic.c +++ b/src/web/api_handlers_onvif_backend_agnostic.c @@ -14,6 +14,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/url_utils.h" +#include "utils/strings.h" #include "video/onvif_discovery.h" #include "video/stream_manager.h" #include @@ -265,17 +266,14 @@ void handle_get_onvif_device_profiles(const http_request_t *req, http_response_t char username[64] = {0}; char password[64] = {0}; - strncpy(device_url, device_url_param, sizeof(device_url) - 1); - device_url[sizeof(device_url) - 1] = '\0'; + safe_strcpy(device_url, device_url_param, sizeof(device_url), 0); if (username_param) { - strncpy(username, username_param, sizeof(username) - 1); - username[sizeof(username) - 1] = '\0'; + safe_strcpy(username, username_param, sizeof(username), 0); } if (password_param) { - strncpy(password, password_param, sizeof(password) - 1); - password[sizeof(password) - 1] = '\0'; + safe_strcpy(password, password_param, sizeof(password), 0); } // Get device profiles @@ -323,12 +321,10 @@ void handle_get_onvif_device_profiles(const http_request_t *req, http_response_t char safe_stream_uri[MAX_URL_LENGTH]; if (url_strip_credentials(profiles[i].snapshot_uri, safe_snapshot_uri, sizeof(safe_snapshot_uri)) != 0) { - strncpy(safe_snapshot_uri, profiles[i].snapshot_uri, sizeof(safe_snapshot_uri) - 1); - safe_snapshot_uri[sizeof(safe_snapshot_uri) - 1] = '\0'; + safe_strcpy(safe_snapshot_uri, profiles[i].snapshot_uri, sizeof(safe_snapshot_uri), 0); } if (url_strip_credentials(profiles[i].stream_uri, safe_stream_uri, sizeof(safe_stream_uri)) != 0) { - strncpy(safe_stream_uri, profiles[i].stream_uri, sizeof(safe_stream_uri) - 1); - safe_stream_uri[sizeof(safe_stream_uri) - 1] = '\0'; + safe_strcpy(safe_stream_uri, profiles[i].stream_uri, sizeof(safe_stream_uri), 0); } cJSON_AddStringToObject(profile, "snapshot_uri", safe_snapshot_uri); @@ -435,7 +431,7 @@ void handle_post_add_onvif_device_as_stream(const http_request_t *req, http_resp // Create device info onvif_device_info_t device_info; memset(&device_info, 0, sizeof(device_info)); - strncpy(device_info.device_service, device_url, sizeof(device_info.device_service) - 1); + safe_strcpy(device_info.device_service, device_url, sizeof(device_info.device_service), 0); // Add ONVIF device as stream int result = add_onvif_device_as_stream(&device_info, profile, username, password, stream_name); diff --git a/src/web/api_handlers_ptz.c b/src/web/api_handlers_ptz.c index 77fd4c8b..bc3781fd 100644 --- a/src/web/api_handlers_ptz.c +++ b/src/web/api_handlers_ptz.c @@ -13,6 +13,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/url_utils.h" +#include "utils/strings.h" #include "database/db_streams.h" #include "video/onvif_ptz.h" #include @@ -471,8 +472,7 @@ void handle_ptz_goto_preset(const http_request_t *req, http_response_t *res) { } char preset_token[64]; - strncpy(preset_token, token_json->valuestring, sizeof(preset_token) - 1); - preset_token[sizeof(preset_token) - 1] = '\0'; + safe_strcpy(preset_token, token_json->valuestring, sizeof(preset_token), 0); cJSON_Delete(body); log_info("Handling POST /api/streams/%s/ptz/preset (goto %s)", stream_name, preset_token); diff --git a/src/web/api_handlers_recordings_backend_agnostic.c b/src/web/api_handlers_recordings_backend_agnostic.c index 52fde3a6..fa125652 100644 --- a/src/web/api_handlers_recordings_backend_agnostic.c +++ b/src/web/api_handlers_recordings_backend_agnostic.c @@ -26,6 +26,7 @@ #include "database/db_recordings.h" #include "database/db_detections.h" #include "database/db_auth.h" +#include "utils/strings.h" #include "web/api_handlers_recordings_thumbnail.h" #include "storage/storage_manager_streams_cache.h" @@ -237,9 +238,8 @@ void handle_delete_recording(const http_request_t *req, http_response_t *res) { } // Save file path before deleting from database - char file_path_copy[256]; - strncpy(file_path_copy, recording.file_path, sizeof(file_path_copy) - 1); - file_path_copy[sizeof(file_path_copy) - 1] = '\0'; + char file_path_copy[MAX_PATH_LENGTH]; + safe_strcpy(file_path_copy, recording.file_path, sizeof(file_path_copy), 0); // Delete from database FIRST if (delete_recording_metadata(id) != 0) { @@ -334,9 +334,8 @@ static void *batch_delete_worker_thread(void *arg) { error_count++; } else { // Save file path before deleting from database - char file_path_copy[256]; - strncpy(file_path_copy, recording.file_path, sizeof(file_path_copy) - 1); - file_path_copy[sizeof(file_path_copy) - 1] = '\0'; + char file_path_copy[MAX_PATH_LENGTH]; + safe_strcpy(file_path_copy, recording.file_path, sizeof(file_path_copy), 0); // Delete from database FIRST if (delete_recording_metadata(id) != 0) { @@ -429,7 +428,7 @@ static void *batch_delete_worker_thread(void *arg) { } if (stream && cJSON_IsString(stream)) { - strncpy(stream_name, stream->valuestring, sizeof(stream_name) - 1); + safe_strcpy(stream_name, stream->valuestring, sizeof(stream_name), 0); } if (detection && cJSON_IsNumber(detection)) { @@ -437,15 +436,15 @@ static void *batch_delete_worker_thread(void *arg) { } if (detection_label_item && cJSON_IsString(detection_label_item)) { - strncpy(detection_label, detection_label_item->valuestring, sizeof(detection_label) - 1); + safe_strcpy(detection_label, detection_label_item->valuestring, sizeof(detection_label), 0); } if (tag_item && cJSON_IsString(tag_item)) { - strncpy(tag_filter, tag_item->valuestring, sizeof(tag_filter) - 1); + safe_strcpy(tag_filter, tag_item->valuestring, sizeof(tag_filter), 0); } if (capture_method_item && cJSON_IsString(capture_method_item)) { - strncpy(capture_method_filter, capture_method_item->valuestring, sizeof(capture_method_filter) - 1); + safe_strcpy(capture_method_filter, capture_method_item->valuestring, sizeof(capture_method_filter), 0); } if (detection_label[0] != '\0') { @@ -519,9 +518,8 @@ static void *batch_delete_worker_thread(void *arg) { uint64_t id = recordings[i].id; // Save file path before deleting from database - char file_path_copy[256]; - strncpy(file_path_copy, recordings[i].file_path, sizeof(file_path_copy) - 1); - file_path_copy[sizeof(file_path_copy) - 1] = '\0'; + char file_path_copy[MAX_PATH_LENGTH]; + safe_strcpy(file_path_copy, recordings[i].file_path, sizeof(file_path_copy), 0); // Delete from database FIRST if (delete_recording_metadata(id) != 0) { @@ -646,8 +644,7 @@ void handle_batch_delete_recordings(const http_request_t *req, http_response_t * return; } - strncpy(thread_data->job_id, job_id, sizeof(thread_data->job_id) - 1); - thread_data->job_id[sizeof(thread_data->job_id) - 1] = '\0'; + safe_strcpy(thread_data->job_id, job_id, sizeof(thread_data->job_id), 0); thread_data->json = json; // Transfer ownership to thread // Spawn worker thread diff --git a/src/web/api_handlers_recordings_batch_download.c b/src/web/api_handlers_recordings_batch_download.c index 8a852bb7..4514211a 100644 --- a/src/web/api_handlers_recordings_batch_download.c +++ b/src/web/api_handlers_recordings_batch_download.c @@ -26,6 +26,7 @@ #define LOG_COMPONENT "RecordingsAPI" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" #include "database/db_recordings.h" #include "database/db_auth.h" @@ -143,8 +144,8 @@ typedef enum { DL_PENDING=0, DL_RUNNING, DL_COMPLETE, DL_ERROR } dl_status_t; typedef struct { char job_id[64]; - char zip_filename[256]; - char tmp_path[256]; + char zip_filename[MAX_PATH_LENGTH]; + char tmp_path[MAX_PATH_LENGTH]; uint64_t ids[MAX_DL_IDS]; int id_count; dl_status_t status; @@ -221,7 +222,7 @@ static void *zip_worker(void *arg) { log_set_thread_context("BatchDownload", NULL); dl_thread_arg_t *a = (dl_thread_arg_t *)arg; char job_id[64]; - strncpy(job_id, a->job_id, sizeof(job_id)-1); + safe_strcpy(job_id, a->job_id, sizeof(job_id), 0); free(a); /* Grab job info */ @@ -239,7 +240,11 @@ static void *zip_worker(void *arg) { log_error("zip_worker: mkstemp failed: %s", strerror(errno)); pthread_mutex_lock(&s_dl_mutex); slot = find_dl_job(job_id); - if (slot>=0) { s_dl_jobs[slot].status=DL_ERROR; strncpy(s_dl_jobs[slot].error,"Failed to create temp file",255); s_dl_jobs[slot].updated_at=time(NULL); } + if (slot>=0) { + s_dl_jobs[slot].status = DL_ERROR; + safe_strcpy(s_dl_jobs[slot].error, "Failed to create temp file", 256, 0); + s_dl_jobs[slot].updated_at = time(NULL); + } pthread_mutex_unlock(&s_dl_mutex); return NULL; } @@ -310,7 +315,7 @@ static void *zip_worker(void *arg) { pthread_mutex_lock(&s_dl_mutex); slot = find_dl_job(job_id); if (slot>=0) { - strncpy(s_dl_jobs[slot].tmp_path, tmp_template, sizeof(s_dl_jobs[slot].tmp_path)-1); + safe_strcpy(s_dl_jobs[slot].tmp_path, tmp_template, sizeof(s_dl_jobs[slot].tmp_path), 0); s_dl_jobs[slot].status = DL_COMPLETE; s_dl_jobs[slot].current = job.id_count; s_dl_jobs[slot].updated_at = time(NULL); @@ -372,7 +377,7 @@ void handle_batch_download_recordings(const http_request_t *req, http_response_t batch_dl_job_t *job = &s_dl_jobs[slot]; memset(job, 0, sizeof(*job)); gen_uuid(job->job_id); - strncpy(job->zip_filename, filename_raw, sizeof(job->zip_filename)-1); + safe_strcpy(job->zip_filename, filename_raw, sizeof(job->zip_filename), 0); job->id_count = count; job->total = count; job->status = DL_PENDING; @@ -385,14 +390,14 @@ void handle_batch_download_recordings(const http_request_t *req, http_response_t } char job_id[64]; - strncpy(job_id, job->job_id, sizeof(job_id)-1); + safe_strcpy(job_id, job->job_id, sizeof(job_id), 0); pthread_mutex_unlock(&s_dl_mutex); cJSON_Delete(json); /* Spawn worker thread */ dl_thread_arg_t *targ = malloc(sizeof(dl_thread_arg_t)); if (!targ) { http_response_set_json_error(res, 500, "Out of memory"); return; } - strncpy(targ->job_id, job_id, sizeof(targ->job_id)-1); + safe_strcpy(targ->job_id, job_id, sizeof(targ->job_id), 0); pthread_t tid; pthread_attr_t attr; @@ -484,9 +489,10 @@ void handle_batch_download_result(const http_request_t *req, http_response_t *re http_response_set_json_error(res, 409, err); return; } - char tmp_path[256], zip_filename[256]; - strncpy(tmp_path, s_dl_jobs[slot].tmp_path, sizeof(tmp_path)-1); - strncpy(zip_filename, s_dl_jobs[slot].zip_filename, sizeof(zip_filename)-1); + char tmp_path[MAX_PATH_LENGTH]; + char zip_filename[MAX_PATH_LENGTH]; + safe_strcpy(tmp_path, s_dl_jobs[slot].tmp_path, sizeof(tmp_path), 0); + safe_strcpy(zip_filename, s_dl_jobs[slot].zip_filename, sizeof(zip_filename), 0); /* * Mark the slot as inactive so it can be reused, but do NOT unlink the * temp file here. http_serve_file() uses libuv async I/O: it queues a diff --git a/src/web/api_handlers_recordings_list.c b/src/web/api_handlers_recordings_list.c index c25182a9..3b26fb81 100644 --- a/src/web/api_handlers_recordings_list.c +++ b/src/web/api_handlers_recordings_list.c @@ -20,6 +20,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/shutdown_coordinator.h" +#include "utils/strings.h" #include "database/db_recordings.h" #include "database/db_detections.h" #include "database/db_auth.h" @@ -29,23 +30,6 @@ #define MAX_SELECTED_STREAM_FILTERS 32 #define MAX_SELECTED_STREAM_NAME_LEN 64 -static void trim_filter_value(char *value) { - if (!value) return; - - char *start = value; - while (*start && isspace((unsigned char)*start)) { - start++; - } - if (start != value) { - memmove(value, start, strlen(start) + 1); - } - - size_t len = strlen(value); - while (len > 0 && isspace((unsigned char)value[len - 1])) { - value[--len] = '\0'; - } -} - static int parse_selected_streams(const char *csv, char values[][MAX_SELECTED_STREAM_NAME_LEN], int max_values) { @@ -54,17 +38,13 @@ static int parse_selected_streams(const char *csv, } char buffer[MAX_SELECTED_STREAM_FILTERS * MAX_SELECTED_STREAM_NAME_LEN]; - strncpy(buffer, csv, sizeof(buffer) - 1); - buffer[sizeof(buffer) - 1] = '\0'; + safe_strcpy(buffer, csv, sizeof(buffer), 0); int count = 0; char *saveptr = NULL; char *token = strtok_r(buffer, ",", &saveptr); while (token && count < max_values) { - trim_filter_value(token); - if (*token) { - strncpy(values[count], token, MAX_SELECTED_STREAM_NAME_LEN - 1); - values[count][MAX_SELECTED_STREAM_NAME_LEN - 1] = '\0'; + if (copy_trimmed_value(values[count], MAX_SELECTED_STREAM_NAME_LEN, token, 0)) { count++; } token = strtok_r(NULL, ",", &saveptr); diff --git a/src/web/api_handlers_settings.c b/src/web/api_handlers_settings.c index 93077f77..38ebd36f 100644 --- a/src/web/api_handlers_settings.c +++ b/src/web/api_handlers_settings.c @@ -18,6 +18,7 @@ #define LOG_COMPONENT "SettingsAPI" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_streams.h" #include "database/db_auth.h" @@ -441,23 +442,23 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // Snapshot current MQTT settings before parsing new values bool old_mqtt_enabled = g_config.mqtt_enabled; char old_mqtt_broker_host[256]; - strncpy(old_mqtt_broker_host, g_config.mqtt_broker_host, sizeof(old_mqtt_broker_host)); + safe_strcpy(old_mqtt_broker_host, g_config.mqtt_broker_host, sizeof(old_mqtt_broker_host), 0); int old_mqtt_broker_port = g_config.mqtt_broker_port; char old_mqtt_username[128]; - strncpy(old_mqtt_username, g_config.mqtt_username, sizeof(old_mqtt_username)); + safe_strcpy(old_mqtt_username, g_config.mqtt_username, sizeof(old_mqtt_username), 0); char old_mqtt_password[128]; - strncpy(old_mqtt_password, g_config.mqtt_password, sizeof(old_mqtt_password)); + safe_strcpy(old_mqtt_password, g_config.mqtt_password, sizeof(old_mqtt_password), 0); char old_mqtt_client_id[128]; - strncpy(old_mqtt_client_id, g_config.mqtt_client_id, sizeof(old_mqtt_client_id)); + safe_strcpy(old_mqtt_client_id, g_config.mqtt_client_id, sizeof(old_mqtt_client_id), 0); char old_mqtt_topic_prefix[256]; - strncpy(old_mqtt_topic_prefix, g_config.mqtt_topic_prefix, sizeof(old_mqtt_topic_prefix)); + safe_strcpy(old_mqtt_topic_prefix, g_config.mqtt_topic_prefix, sizeof(old_mqtt_topic_prefix), 0); bool old_mqtt_tls_enabled = g_config.mqtt_tls_enabled; int old_mqtt_keepalive = g_config.mqtt_keepalive; int old_mqtt_qos = g_config.mqtt_qos; bool old_mqtt_retain = g_config.mqtt_retain; bool old_mqtt_ha_discovery = g_config.mqtt_ha_discovery; char old_mqtt_ha_discovery_prefix[128]; - strncpy(old_mqtt_ha_discovery_prefix, g_config.mqtt_ha_discovery_prefix, sizeof(old_mqtt_ha_discovery_prefix)); + safe_strcpy(old_mqtt_ha_discovery_prefix, g_config.mqtt_ha_discovery_prefix, sizeof(old_mqtt_ha_discovery_prefix), 0); int old_mqtt_ha_snapshot_interval = g_config.mqtt_ha_snapshot_interval; // Web thread pool size (requires restart; stored in config only) @@ -499,8 +500,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { if (bind_ip_empty) { log_warn("Rejected empty web_bind_ip update"); } else if (strcmp(g_config.web_bind_ip, new_bind_ip) != 0) { - strncpy(g_config.web_bind_ip, new_bind_ip, sizeof(g_config.web_bind_ip) - 1); - g_config.web_bind_ip[sizeof(g_config.web_bind_ip) - 1] = '\0'; + safe_strcpy(g_config.web_bind_ip, new_bind_ip, sizeof(g_config.web_bind_ip), 0); settings_changed = true; restart_required = true; log_info("Updated web_bind_ip: %s (restart required)", g_config.web_bind_ip); @@ -510,8 +510,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // Web root cJSON *web_root = cJSON_GetObjectItem(settings, "web_root"); if (web_root && cJSON_IsString(web_root)) { - strncpy(g_config.web_root, web_root->valuestring, sizeof(g_config.web_root) - 1); - g_config.web_root[sizeof(g_config.web_root) - 1] = '\0'; + safe_strcpy(g_config.web_root, web_root->valuestring, sizeof(g_config.web_root), 0); settings_changed = true; log_info("Updated web_root: %s", g_config.web_root); } @@ -586,9 +585,8 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { http_response_set_json_error(res, 400, "Invalid trusted_proxy_cidrs: expected comma- or newline-separated IPv4/IPv6 CIDRs"); return; } - strncpy(g_config.trusted_proxy_cidrs, trusted_proxy_cidrs->valuestring, - sizeof(g_config.trusted_proxy_cidrs) - 1); - g_config.trusted_proxy_cidrs[sizeof(g_config.trusted_proxy_cidrs) - 1] = '\0'; + safe_strcpy(g_config.trusted_proxy_cidrs, trusted_proxy_cidrs->valuestring, + sizeof(g_config.trusted_proxy_cidrs), 0); settings_changed = true; log_info("Updated trusted_proxy_cidrs"); } @@ -643,8 +641,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { "Invalid storage_path: must be an absolute path without shell metacharacters"); return; } - strncpy(g_config.storage_path, storage_path->valuestring, sizeof(g_config.storage_path) - 1); - g_config.storage_path[sizeof(g_config.storage_path) - 1] = '\0'; + safe_strcpy(g_config.storage_path, storage_path->valuestring, sizeof(g_config.storage_path), 0); settings_changed = true; log_info("Updated storage_path: %s", g_config.storage_path); } @@ -661,8 +658,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { "Invalid storage_path_hls: must be an absolute path without shell metacharacters"); return; } - strncpy(g_config.storage_path_hls, hls_path_val, sizeof(g_config.storage_path_hls) - 1); - g_config.storage_path_hls[sizeof(g_config.storage_path_hls) - 1] = '\0'; + safe_strcpy(g_config.storage_path_hls, hls_path_val, sizeof(g_config.storage_path_hls), 0); settings_changed = true; log_info("Updated storage_path_hls: %s", hls_path_val[0] ? hls_path_val : "(cleared, will use storage_path)"); @@ -703,8 +699,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // Models path cJSON *models_path = cJSON_GetObjectItem(settings, "models_path"); if (models_path && cJSON_IsString(models_path)) { - strncpy(g_config.models_path, models_path->valuestring, sizeof(g_config.models_path) - 1); - g_config.models_path[sizeof(g_config.models_path) - 1] = '\0'; + safe_strcpy(g_config.models_path, models_path->valuestring, sizeof(g_config.models_path), 0); settings_changed = true; log_info("Updated models_path: %s", g_config.models_path); } @@ -727,8 +722,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // Log file cJSON *log_file = cJSON_GetObjectItem(settings, "log_file"); if (log_file && cJSON_IsString(log_file)) { - strncpy(g_config.log_file, log_file->valuestring, sizeof(g_config.log_file) - 1); - g_config.log_file[sizeof(g_config.log_file) - 1] = '\0'; + safe_strcpy(g_config.log_file, log_file->valuestring, sizeof(g_config.log_file), 0); settings_changed = true; log_info("Updated log_file: %s", g_config.log_file); } @@ -753,8 +747,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // Syslog ident cJSON *syslog_ident = cJSON_GetObjectItem(settings, "syslog_ident"); if (syslog_ident && cJSON_IsString(syslog_ident)) { - strncpy(g_config.syslog_ident, syslog_ident->valuestring, sizeof(g_config.syslog_ident) - 1); - g_config.syslog_ident[sizeof(g_config.syslog_ident) - 1] = '\0'; + safe_strcpy(g_config.syslog_ident, syslog_ident->valuestring, sizeof(g_config.syslog_ident), 0); settings_changed = true; log_info("Updated syslog_ident: %s", g_config.syslog_ident); } @@ -840,8 +833,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // Buffer strategy (default for new streams) cJSON *buffer_strategy = cJSON_GetObjectItem(settings, "buffer_strategy"); if (buffer_strategy && cJSON_IsString(buffer_strategy)) { - strncpy(g_config.default_buffer_strategy, buffer_strategy->valuestring, sizeof(g_config.default_buffer_strategy) - 1); - g_config.default_buffer_strategy[sizeof(g_config.default_buffer_strategy) - 1] = '\0'; + safe_strcpy(g_config.default_buffer_strategy, buffer_strategy->valuestring, sizeof(g_config.default_buffer_strategy), 0); settings_changed = true; log_info("Updated default_buffer_strategy: %s", g_config.default_buffer_strategy); } @@ -849,8 +841,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // API detection URL cJSON *api_detection_url = cJSON_GetObjectItem(settings, "api_detection_url"); if (api_detection_url && cJSON_IsString(api_detection_url)) { - strncpy(g_config.api_detection_url, api_detection_url->valuestring, sizeof(g_config.api_detection_url) - 1); - g_config.api_detection_url[sizeof(g_config.api_detection_url) - 1] = '\0'; + safe_strcpy(g_config.api_detection_url, api_detection_url->valuestring, sizeof(g_config.api_detection_url), 0); settings_changed = true; log_info("Updated api_detection_url: %s", g_config.api_detection_url); } @@ -858,8 +849,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // API detection backend cJSON *api_detection_backend = cJSON_GetObjectItem(settings, "api_detection_backend"); if (api_detection_backend && cJSON_IsString(api_detection_backend)) { - strncpy(g_config.api_detection_backend, api_detection_backend->valuestring, sizeof(g_config.api_detection_backend) - 1); - g_config.api_detection_backend[sizeof(g_config.api_detection_backend) - 1] = '\0'; + safe_strcpy(g_config.api_detection_backend, api_detection_backend->valuestring, sizeof(g_config.api_detection_backend), 0); settings_changed = true; log_info("Updated api_detection_backend: %s", g_config.api_detection_backend); } @@ -884,16 +874,14 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { cJSON *go2rtc_binary_path = cJSON_GetObjectItem(settings, "go2rtc_binary_path"); if (go2rtc_binary_path && cJSON_IsString(go2rtc_binary_path)) { - strncpy(g_config.go2rtc_binary_path, go2rtc_binary_path->valuestring, sizeof(g_config.go2rtc_binary_path) - 1); - g_config.go2rtc_binary_path[sizeof(g_config.go2rtc_binary_path) - 1] = '\0'; + safe_strcpy(g_config.go2rtc_binary_path, go2rtc_binary_path->valuestring, sizeof(g_config.go2rtc_binary_path), 0); settings_changed = true; log_info("Updated go2rtc_binary_path: %s", g_config.go2rtc_binary_path); } cJSON *go2rtc_config_dir = cJSON_GetObjectItem(settings, "go2rtc_config_dir"); if (go2rtc_config_dir && cJSON_IsString(go2rtc_config_dir)) { - strncpy(g_config.go2rtc_config_dir, go2rtc_config_dir->valuestring, sizeof(g_config.go2rtc_config_dir) - 1); - g_config.go2rtc_config_dir[sizeof(g_config.go2rtc_config_dir) - 1] = '\0'; + safe_strcpy(g_config.go2rtc_config_dir, go2rtc_config_dir->valuestring, sizeof(g_config.go2rtc_config_dir), 0); settings_changed = true; log_info("Updated go2rtc_config_dir: %s", g_config.go2rtc_config_dir); } @@ -935,24 +923,21 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { cJSON *go2rtc_stun_server = cJSON_GetObjectItem(settings, "go2rtc_stun_server"); if (go2rtc_stun_server && cJSON_IsString(go2rtc_stun_server)) { - strncpy(g_config.go2rtc_stun_server, go2rtc_stun_server->valuestring, sizeof(g_config.go2rtc_stun_server) - 1); - g_config.go2rtc_stun_server[sizeof(g_config.go2rtc_stun_server) - 1] = '\0'; + safe_strcpy(g_config.go2rtc_stun_server, go2rtc_stun_server->valuestring, sizeof(g_config.go2rtc_stun_server), 0); settings_changed = true; log_info("Updated go2rtc_stun_server: %s", g_config.go2rtc_stun_server); } cJSON *go2rtc_external_ip = cJSON_GetObjectItem(settings, "go2rtc_external_ip"); if (go2rtc_external_ip && cJSON_IsString(go2rtc_external_ip)) { - strncpy(g_config.go2rtc_external_ip, go2rtc_external_ip->valuestring, sizeof(g_config.go2rtc_external_ip) - 1); - g_config.go2rtc_external_ip[sizeof(g_config.go2rtc_external_ip) - 1] = '\0'; + safe_strcpy(g_config.go2rtc_external_ip, go2rtc_external_ip->valuestring, sizeof(g_config.go2rtc_external_ip), 0); settings_changed = true; log_info("Updated go2rtc_external_ip: %s", g_config.go2rtc_external_ip); } cJSON *go2rtc_ice_servers = cJSON_GetObjectItem(settings, "go2rtc_ice_servers"); if (go2rtc_ice_servers && cJSON_IsString(go2rtc_ice_servers)) { - strncpy(g_config.go2rtc_ice_servers, go2rtc_ice_servers->valuestring, sizeof(g_config.go2rtc_ice_servers) - 1); - g_config.go2rtc_ice_servers[sizeof(g_config.go2rtc_ice_servers) - 1] = '\0'; + safe_strcpy(g_config.go2rtc_ice_servers, go2rtc_ice_servers->valuestring, sizeof(g_config.go2rtc_ice_servers), 0); settings_changed = true; log_info("Updated go2rtc_ice_servers: %s", g_config.go2rtc_ice_servers); } @@ -975,8 +960,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // MQTT broker host cJSON *mqtt_broker_host = cJSON_GetObjectItem(settings, "mqtt_broker_host"); if (mqtt_broker_host && cJSON_IsString(mqtt_broker_host)) { - strncpy(g_config.mqtt_broker_host, mqtt_broker_host->valuestring, sizeof(g_config.mqtt_broker_host) - 1); - g_config.mqtt_broker_host[sizeof(g_config.mqtt_broker_host) - 1] = '\0'; + safe_strcpy(g_config.mqtt_broker_host, mqtt_broker_host->valuestring, sizeof(g_config.mqtt_broker_host), 0); settings_changed = true; log_info("Updated mqtt_broker_host: %s", g_config.mqtt_broker_host); } @@ -992,8 +976,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // MQTT username cJSON *mqtt_username = cJSON_GetObjectItem(settings, "mqtt_username"); if (mqtt_username && cJSON_IsString(mqtt_username)) { - strncpy(g_config.mqtt_username, mqtt_username->valuestring, sizeof(g_config.mqtt_username) - 1); - g_config.mqtt_username[sizeof(g_config.mqtt_username) - 1] = '\0'; + safe_strcpy(g_config.mqtt_username, mqtt_username->valuestring, sizeof(g_config.mqtt_username), 0); settings_changed = true; log_info("Updated mqtt_username: %s", g_config.mqtt_username); } @@ -1001,8 +984,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // MQTT password (only update if not masked) cJSON *mqtt_password = cJSON_GetObjectItem(settings, "mqtt_password"); if (mqtt_password && cJSON_IsString(mqtt_password) && strcmp(mqtt_password->valuestring, "********") != 0) { - strncpy(g_config.mqtt_password, mqtt_password->valuestring, sizeof(g_config.mqtt_password) - 1); - g_config.mqtt_password[sizeof(g_config.mqtt_password) - 1] = '\0'; + safe_strcpy(g_config.mqtt_password, mqtt_password->valuestring, sizeof(g_config.mqtt_password), 0); settings_changed = true; log_info("Updated mqtt_password"); } @@ -1010,8 +992,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // MQTT client ID cJSON *mqtt_client_id = cJSON_GetObjectItem(settings, "mqtt_client_id"); if (mqtt_client_id && cJSON_IsString(mqtt_client_id)) { - strncpy(g_config.mqtt_client_id, mqtt_client_id->valuestring, sizeof(g_config.mqtt_client_id) - 1); - g_config.mqtt_client_id[sizeof(g_config.mqtt_client_id) - 1] = '\0'; + safe_strcpy(g_config.mqtt_client_id, mqtt_client_id->valuestring, sizeof(g_config.mqtt_client_id), 0); settings_changed = true; log_info("Updated mqtt_client_id: %s", g_config.mqtt_client_id); } @@ -1019,8 +1000,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // MQTT topic prefix cJSON *mqtt_topic_prefix = cJSON_GetObjectItem(settings, "mqtt_topic_prefix"); if (mqtt_topic_prefix && cJSON_IsString(mqtt_topic_prefix)) { - strncpy(g_config.mqtt_topic_prefix, mqtt_topic_prefix->valuestring, sizeof(g_config.mqtt_topic_prefix) - 1); - g_config.mqtt_topic_prefix[sizeof(g_config.mqtt_topic_prefix) - 1] = '\0'; + safe_strcpy(g_config.mqtt_topic_prefix, mqtt_topic_prefix->valuestring, sizeof(g_config.mqtt_topic_prefix), 0); settings_changed = true; log_info("Updated mqtt_topic_prefix: %s", g_config.mqtt_topic_prefix); } @@ -1071,8 +1051,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // MQTT HA discovery prefix cJSON *mqtt_ha_discovery_prefix = cJSON_GetObjectItem(settings, "mqtt_ha_discovery_prefix"); if (mqtt_ha_discovery_prefix && cJSON_IsString(mqtt_ha_discovery_prefix)) { - strncpy(g_config.mqtt_ha_discovery_prefix, mqtt_ha_discovery_prefix->valuestring, sizeof(g_config.mqtt_ha_discovery_prefix) - 1); - g_config.mqtt_ha_discovery_prefix[sizeof(g_config.mqtt_ha_discovery_prefix) - 1] = '\0'; + safe_strcpy(g_config.mqtt_ha_discovery_prefix, mqtt_ha_discovery_prefix->valuestring, sizeof(g_config.mqtt_ha_discovery_prefix), 0); settings_changed = true; log_info("Updated mqtt_ha_discovery_prefix: %s", g_config.mqtt_ha_discovery_prefix); } @@ -1125,10 +1104,8 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { cJSON *turn_server_url = cJSON_GetObjectItem(settings, "turn_server_url"); if (turn_server_url && cJSON_IsString(turn_server_url)) { char old_turn_server_url[256]; - strncpy(old_turn_server_url, g_config.turn_server_url, sizeof(old_turn_server_url) - 1); - old_turn_server_url[sizeof(old_turn_server_url) - 1] = '\0'; - strncpy(g_config.turn_server_url, turn_server_url->valuestring, sizeof(g_config.turn_server_url) - 1); - g_config.turn_server_url[sizeof(g_config.turn_server_url) - 1] = '\0'; + safe_strcpy(old_turn_server_url, g_config.turn_server_url, sizeof(old_turn_server_url), 0); + safe_strcpy(g_config.turn_server_url, turn_server_url->valuestring, sizeof(g_config.turn_server_url), 0); settings_changed = true; log_info("Updated turn_server_url: %s", g_config.turn_server_url); if (strcmp(old_turn_server_url, g_config.turn_server_url) != 0 && g_config.go2rtc_enabled && g_config.turn_enabled) { @@ -1141,10 +1118,8 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { cJSON *turn_username = cJSON_GetObjectItem(settings, "turn_username"); if (turn_username && cJSON_IsString(turn_username)) { char old_turn_username[128]; - strncpy(old_turn_username, g_config.turn_username, sizeof(old_turn_username) - 1); - old_turn_username[sizeof(old_turn_username) - 1] = '\0'; - strncpy(g_config.turn_username, turn_username->valuestring, sizeof(g_config.turn_username) - 1); - g_config.turn_username[sizeof(g_config.turn_username) - 1] = '\0'; + safe_strcpy(old_turn_username, g_config.turn_username, sizeof(old_turn_username), 0); + safe_strcpy(g_config.turn_username, turn_username->valuestring, sizeof(g_config.turn_username), 0); settings_changed = true; log_info("Updated turn_username: %s", g_config.turn_username); if (strcmp(old_turn_username, g_config.turn_username) != 0 && g_config.go2rtc_enabled && g_config.turn_enabled) { @@ -1159,10 +1134,8 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // Only update if not the masked value if (strcmp(turn_password->valuestring, "********") != 0) { char old_turn_password[128]; - strncpy(old_turn_password, g_config.turn_password, sizeof(old_turn_password) - 1); - old_turn_password[sizeof(old_turn_password) - 1] = '\0'; - strncpy(g_config.turn_password, turn_password->valuestring, sizeof(g_config.turn_password) - 1); - g_config.turn_password[sizeof(g_config.turn_password) - 1] = '\0'; + safe_strcpy(old_turn_password, g_config.turn_password, sizeof(old_turn_password), 0); + safe_strcpy(g_config.turn_password, turn_password->valuestring, sizeof(g_config.turn_password), 0); settings_changed = true; log_info("Updated turn_password"); if (strcmp(old_turn_password, g_config.turn_password) != 0 && g_config.go2rtc_enabled && g_config.turn_enabled) { @@ -1195,8 +1168,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // ONVIF discovery network cJSON *onvif_discovery_network = cJSON_GetObjectItem(settings, "onvif_discovery_network"); if (onvif_discovery_network && cJSON_IsString(onvif_discovery_network)) { - strncpy(g_config.onvif_discovery_network, onvif_discovery_network->valuestring, sizeof(g_config.onvif_discovery_network) - 1); - g_config.onvif_discovery_network[sizeof(g_config.onvif_discovery_network) - 1] = '\0'; + safe_strcpy(g_config.onvif_discovery_network, onvif_discovery_network->valuestring, sizeof(g_config.onvif_discovery_network), 0); settings_changed = true; log_info("Updated onvif_discovery_network: %s", g_config.onvif_discovery_network); } @@ -1230,8 +1202,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { return; } - strncpy(g_config.db_post_backup_script, script_path, sizeof(g_config.db_post_backup_script) - 1); - g_config.db_post_backup_script[sizeof(g_config.db_post_backup_script) - 1] = '\0'; + safe_strcpy(g_config.db_post_backup_script, script_path, sizeof(g_config.db_post_backup_script), 0); settings_changed = true; log_info("Updated db_post_backup_script: %s", g_config.db_post_backup_script[0] ? g_config.db_post_backup_script : "(disabled)"); @@ -1243,12 +1214,10 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { strcmp(g_config.db_path, db_path->valuestring) != 0) { char old_db_path[MAX_PATH_LENGTH]; - strncpy(old_db_path, g_config.db_path, sizeof(old_db_path) - 1); - old_db_path[sizeof(old_db_path) - 1] = '\0'; + safe_strcpy(old_db_path, g_config.db_path, sizeof(old_db_path), 0); // Update the config with the new path - strncpy(g_config.db_path, db_path->valuestring, sizeof(g_config.db_path) - 1); - g_config.db_path[sizeof(g_config.db_path) - 1] = '\0'; + safe_strcpy(g_config.db_path, db_path->valuestring, sizeof(g_config.db_path), 0); settings_changed = true; log_info("Database path changed from %s to %s", old_db_path, g_config.db_path); @@ -1278,8 +1247,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { // Only add enabled streams to the active list if (g_config.streams[i].enabled) { // Copy the stream name for later use - strncpy(active_streams[active_stream_count], g_config.streams[i].name, MAX_STREAM_NAME - 1); - active_streams[active_stream_count][MAX_STREAM_NAME - 1] = '\0'; + safe_strcpy(active_streams[active_stream_count], g_config.streams[i].name, MAX_STREAM_NAME, 0); log_info("Added active stream %d: %s", active_stream_count, active_streams[active_stream_count]); active_stream_count++; @@ -1320,8 +1288,7 @@ void handle_post_settings(const http_request_t *req, http_response_t *res) { log_error("Failed to initialize database with new path, reverting to old path"); // Revert to the old path - strncpy(g_config.db_path, old_db_path, sizeof(g_config.db_path) - 1); - g_config.db_path[sizeof(g_config.db_path) - 1] = '\0'; + safe_strcpy(g_config.db_path, old_db_path, sizeof(g_config.db_path), 0); // Try to reinitialize with the old path if (init_database(g_config.db_path) != 0) { diff --git a/src/web/api_handlers_streaming.c b/src/web/api_handlers_streaming.c index d4f25428..5507c434 100644 --- a/src/web/api_handlers_streaming.c +++ b/src/web/api_handlers_streaming.c @@ -10,6 +10,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/path_utils.h" +#include "utils/strings.h" #include "web/http_server.h" #include "video/streams.h" @@ -56,16 +57,11 @@ void handle_direct_hls_request(const http_request_t *req, http_response_t *res) return; } - if (name_len >= MAX_STREAM_NAME) { - name_len = MAX_STREAM_NAME - 1; - } - - char encoded_stream_name[MAX_STREAM_NAME] = {0}; - char stream_name[MAX_STREAM_NAME] = {0}; - char stream_path[MAX_STREAM_NAME] = {0}; + char encoded_stream_name[MAX_STREAM_NAME]; + char stream_name[MAX_STREAM_NAME]; + char stream_path[MAX_STREAM_NAME]; - strncpy(encoded_stream_name, stream_start, name_len); - encoded_stream_name[name_len] = '\0'; + safe_strcpy(encoded_stream_name, stream_start, MAX_STREAM_NAME, name_len); // URL decode the stream name url_decode(encoded_stream_name, stream_name, MAX_STREAM_NAME); @@ -90,15 +86,13 @@ void handle_direct_hls_request(const http_request_t *req, http_response_t *res) } // Make local copies of the storage paths to avoid race conditions - char storage_path[MAX_PATH_LENGTH] = {0}; - char storage_path_hls[MAX_PATH_LENGTH] = {0}; + char storage_path[MAX_PATH_LENGTH]; + char storage_path_hls[MAX_PATH_LENGTH]; // Copy the storage paths with bounds checking - strncpy(storage_path, global_config->storage_path, MAX_PATH_LENGTH - 1); - storage_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(storage_path, global_config->storage_path, MAX_PATH_LENGTH, 0); - strncpy(storage_path_hls, global_config->storage_path_hls, MAX_PATH_LENGTH - 1); - storage_path_hls[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(storage_path_hls, global_config->storage_path_hls, MAX_PATH_LENGTH, 0); // Construct the full path to the HLS file char hls_file_path[MAX_PATH_LENGTH * 2]; // Double the buffer size to avoid truncation @@ -166,8 +160,8 @@ void handle_direct_hls_request(const http_request_t *req, http_response_t *res) // parse the JSON as m3u8 and fails). With a valid empty playlist, HLS.js will // simply retry the manifest load after its configured retry delay (non-fatal). res->status_code = 200; - strncpy(res->content_type, "application/vnd.apple.mpegurl", - sizeof(res->content_type) - 1); + safe_strcpy(res->content_type, "application/vnd.apple.mpegurl", + sizeof(res->content_type), 0); http_response_add_header(res, "Cache-Control", "no-cache, no-store, must-revalidate"); http_response_add_header(res, "Access-Control-Allow-Origin", "*"); http_response_set_body(res, diff --git a/src/web/api_handlers_streams_get.c b/src/web/api_handlers_streams_get.c index a1223860..470fa0cd 100644 --- a/src/web/api_handlers_streams_get.c +++ b/src/web/api_handlers_streams_get.c @@ -13,6 +13,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/url_utils.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/streams.h" #include "video/stream_state.h" @@ -62,13 +63,10 @@ static void get_stream_api_credentials(const stream_config_t *config, return; } - strncpy(safe_url, config->url, safe_url_size - 1); - safe_url[safe_url_size - 1] = '\0'; + safe_strcpy(safe_url, config->url, safe_url_size, 0); - strncpy(onvif_username, config->onvif_username, onvif_username_size - 1); - onvif_username[onvif_username_size - 1] = '\0'; - strncpy(onvif_password, config->onvif_password, onvif_password_size - 1); - onvif_password[onvif_password_size - 1] = '\0'; + safe_strcpy(onvif_username, config->onvif_username, onvif_username_size, 0); + safe_strcpy(onvif_password, config->onvif_password, onvif_password_size, 0); use_separate_credentials = config->is_onvif || config->onvif_username[0] != '\0' || @@ -82,18 +80,15 @@ static void get_stream_api_credentials(const stream_config_t *config, extracted_username, sizeof(extracted_username), extracted_password, sizeof(extracted_password)) == 0) { if (onvif_username[0] == '\0' && extracted_username[0] != '\0') { - strncpy(onvif_username, extracted_username, onvif_username_size - 1); - onvif_username[onvif_username_size - 1] = '\0'; + safe_strcpy(onvif_username, extracted_username, onvif_username_size, 0); } if (onvif_password[0] == '\0' && extracted_password[0] != '\0') { - strncpy(onvif_password, extracted_password, onvif_password_size - 1); - onvif_password[onvif_password_size - 1] = '\0'; + safe_strcpy(onvif_password, extracted_password, onvif_password_size, 0); } } if (url_strip_credentials(config->url, safe_url, safe_url_size) != 0) { - strncpy(safe_url, config->url, safe_url_size - 1); - safe_url[safe_url_size - 1] = '\0'; + safe_strcpy(safe_url, config->url, safe_url_size, 0); } } diff --git a/src/web/api_handlers_streams_modify.c b/src/web/api_handlers_streams_modify.c index 3a516da9..393368dd 100644 --- a/src/web/api_handlers_streams_modify.c +++ b/src/web/api_handlers_streams_modify.c @@ -15,6 +15,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/url_utils.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/streams.h" #include "video/stream_state.h" @@ -110,18 +111,15 @@ static void normalize_stream_url_credentials(stream_config_t *config) { extracted_username, sizeof(extracted_username), extracted_password, sizeof(extracted_password)) == 0) { if (config->onvif_username[0] == '\0' && extracted_username[0] != '\0') { - strncpy(config->onvif_username, extracted_username, sizeof(config->onvif_username) - 1); - config->onvif_username[sizeof(config->onvif_username) - 1] = '\0'; + safe_strcpy(config->onvif_username, extracted_username, sizeof(config->onvif_username), 0); } if (config->onvif_password[0] == '\0' && extracted_password[0] != '\0') { - strncpy(config->onvif_password, extracted_password, sizeof(config->onvif_password) - 1); - config->onvif_password[sizeof(config->onvif_password) - 1] = '\0'; + safe_strcpy(config->onvif_password, extracted_password, sizeof(config->onvif_password), 0); } } if (url_strip_credentials(config->url, stripped_url, sizeof(stripped_url)) == 0) { - strncpy(config->url, stripped_url, sizeof(config->url) - 1); - config->url[sizeof(config->url) - 1] = '\0'; + safe_strcpy(config->url, stripped_url, sizeof(config->url), 0); } } @@ -136,8 +134,7 @@ static int build_onvif_test_url(const stream_config_t *config, char *out_url, si static void redact_url_for_log(const char *url, char *out_url, size_t out_size) { if (url_redact_for_logging(url, out_url, out_size) != 0) { - strncpy(out_url, "[invalid-url]", out_size - 1); - out_url[out_size - 1] = '\0'; + safe_strcpy(out_url, "[invalid-url]", out_size, 0); } } @@ -436,13 +433,12 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) { } // Copy trimmed name (skip leading whitespace, then strip trailing whitespace) - strncpy(config.name, name_str, sizeof(config.name) - 1); - config.name[sizeof(config.name) - 1] = '\0'; + safe_strcpy(config.name, name_str, sizeof(config.name), 0); size_t name_len = strlen(config.name); while (name_len > 0 && (config.name[name_len - 1] == ' ' || config.name[name_len - 1] == '\t')) { config.name[--name_len] = '\0'; } - strncpy(config.url, url->valuestring, sizeof(config.url) - 1); + safe_strcpy(config.url, url->valuestring, sizeof(config.url), 0); // Optional fields with defaults config.enabled = true; @@ -499,7 +495,7 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) { cJSON *detection_model = cJSON_GetObjectItem(stream_json, "detection_model"); if (detection_model && cJSON_IsString(detection_model)) { - strncpy(config.detection_model, detection_model->valuestring, sizeof(config.detection_model) - 1); + safe_strcpy(config.detection_model, detection_model->valuestring, sizeof(config.detection_model), 0); } cJSON *detection_threshold = cJSON_GetObjectItem(stream_json, "detection_threshold"); @@ -525,14 +521,12 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) { cJSON *detection_object_filter = cJSON_GetObjectItem(stream_json, "detection_object_filter"); if (detection_object_filter && cJSON_IsString(detection_object_filter)) { - strncpy(config.detection_object_filter, detection_object_filter->valuestring, sizeof(config.detection_object_filter) - 1); - config.detection_object_filter[sizeof(config.detection_object_filter) - 1] = '\0'; + safe_strcpy(config.detection_object_filter, detection_object_filter->valuestring, sizeof(config.detection_object_filter), 0); } cJSON *detection_object_filter_list = cJSON_GetObjectItem(stream_json, "detection_object_filter_list"); if (detection_object_filter_list && cJSON_IsString(detection_object_filter_list)) { - strncpy(config.detection_object_filter_list, detection_object_filter_list->valuestring, sizeof(config.detection_object_filter_list) - 1); - config.detection_object_filter_list[sizeof(config.detection_object_filter_list) - 1] = '\0'; + safe_strcpy(config.detection_object_filter_list, detection_object_filter_list->valuestring, sizeof(config.detection_object_filter_list), 0); } cJSON *protocol = cJSON_GetObjectItem(stream_json, "protocol"); @@ -643,16 +637,14 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) { // Parse tags setting (comma-separated list, e.g. "outdoor,critical,entrance") cJSON *tags_post = cJSON_GetObjectItem(stream_json, "tags"); if (tags_post && cJSON_IsString(tags_post)) { - strncpy(config.tags, tags_post->valuestring, sizeof(config.tags) - 1); - config.tags[sizeof(config.tags) - 1] = '\0'; + safe_strcpy(config.tags, tags_post->valuestring, sizeof(config.tags), 0); } else { config.tags[0] = '\0'; } cJSON *admin_url_post = cJSON_GetObjectItem(stream_json, "admin_url"); if (admin_url_post && cJSON_IsString(admin_url_post)) { - strncpy(config.admin_url, admin_url_post->valuestring, sizeof(config.admin_url) - 1); - config.admin_url[sizeof(config.admin_url) - 1] = '\0'; + safe_strcpy(config.admin_url, admin_url_post->valuestring, sizeof(config.admin_url), 0); } else { config.admin_url[0] = '\0'; } @@ -660,9 +652,8 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) { // Parse cross-stream motion trigger source cJSON *motion_trigger_source_post = cJSON_GetObjectItem(stream_json, "motion_trigger_source"); if (motion_trigger_source_post && cJSON_IsString(motion_trigger_source_post)) { - strncpy(config.motion_trigger_source, motion_trigger_source_post->valuestring, - sizeof(config.motion_trigger_source) - 1); - config.motion_trigger_source[sizeof(config.motion_trigger_source) - 1] = '\0'; + safe_strcpy(config.motion_trigger_source, motion_trigger_source_post->valuestring, + sizeof(config.motion_trigger_source), 0); } else { config.motion_trigger_source[0] = '\0'; } @@ -696,13 +687,11 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) { cJSON *onvif_password = cJSON_GetObjectItem(stream_json, "onvif_password"); if (onvif_username && cJSON_IsString(onvif_username)) { - strncpy(config.onvif_username, onvif_username->valuestring, sizeof(config.onvif_username) - 1); - config.onvif_username[sizeof(config.onvif_username) - 1] = '\0'; + safe_strcpy(config.onvif_username, onvif_username->valuestring, sizeof(config.onvif_username), 0); } if (onvif_password && cJSON_IsString(onvif_password)) { - strncpy(config.onvif_password, onvif_password->valuestring, sizeof(config.onvif_password) - 1); - config.onvif_password[sizeof(config.onvif_password) - 1] = '\0'; + safe_strcpy(config.onvif_password, onvif_password->valuestring, sizeof(config.onvif_password), 0); } normalize_stream_url_credentials(&config); @@ -710,8 +699,7 @@ void handle_post_stream(const http_request_t *req, http_response_t *res) { // Build ONVIF device URL, using onvif_port if specified char onvif_device_url[MAX_URL_LENGTH]; if (build_onvif_test_url(&config, onvif_device_url, sizeof(onvif_device_url)) != 0) { - strncpy(onvif_device_url, config.url, sizeof(onvif_device_url) - 1); - onvif_device_url[sizeof(onvif_device_url) - 1] = '\0'; + safe_strcpy(onvif_device_url, config.url, sizeof(onvif_device_url), 0); } // Test ONVIF connection @@ -899,8 +887,7 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { // Save original values for comparison char original_url[MAX_URL_LENGTH]; - strncpy(original_url, config.url, MAX_URL_LENGTH - 1); - original_url[MAX_URL_LENGTH - 1] = '\0'; + safe_strcpy(original_url, config.url, MAX_URL_LENGTH, 0); stream_protocol_t original_protocol = config.protocol; bool original_record_audio = config.record_audio; @@ -908,7 +895,7 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { cJSON *url = cJSON_GetObjectItem(stream_json, "url"); if (url && cJSON_IsString(url)) { if (strcmp(config.url, url->valuestring) != 0) { - strncpy(config.url, url->valuestring, sizeof(config.url) - 1); + safe_strcpy(config.url, url->valuestring, sizeof(config.url), 0); config_changed = true; requires_restart = true; // URL changes always require restart char safe_original_url[MAX_URL_LENGTH]; @@ -978,10 +965,10 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { cJSON *detection_model_json = cJSON_GetObjectItem(stream_json, "detection_model"); bool has_detection_model = false; if (detection_model_json && cJSON_IsString(detection_model_json)) { - char detection_model_value[256] = {0}; - strncpy(detection_model_value, detection_model_json->valuestring, sizeof(detection_model_value) - 1); + char detection_model_value[256]; + safe_strcpy(detection_model_value, detection_model_json->valuestring, sizeof(detection_model_value), 0); has_detection_model = true; - strncpy(config.detection_model, detection_model_value, sizeof(config.detection_model) - 1); + safe_strcpy(config.detection_model, detection_model_value, sizeof(config.detection_model), 0); config_changed = true; non_dynamic_config_changed = true; } @@ -1021,15 +1008,13 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { cJSON *detection_object_filter = cJSON_GetObjectItem(stream_json, "detection_object_filter"); if (detection_object_filter && cJSON_IsString(detection_object_filter)) { - strncpy(config.detection_object_filter, detection_object_filter->valuestring, sizeof(config.detection_object_filter) - 1); - config.detection_object_filter[sizeof(config.detection_object_filter) - 1] = '\0'; + safe_strcpy(config.detection_object_filter, detection_object_filter->valuestring, sizeof(config.detection_object_filter), 0); config_changed = true; } cJSON *detection_object_filter_list = cJSON_GetObjectItem(stream_json, "detection_object_filter_list"); if (detection_object_filter_list && cJSON_IsString(detection_object_filter_list)) { - strncpy(config.detection_object_filter_list, detection_object_filter_list->valuestring, sizeof(config.detection_object_filter_list) - 1); - config.detection_object_filter_list[sizeof(config.detection_object_filter_list) - 1] = '\0'; + safe_strcpy(config.detection_object_filter_list, detection_object_filter_list->valuestring, sizeof(config.detection_object_filter_list), 0); config_changed = true; } @@ -1237,8 +1222,7 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { cJSON *tags_put = cJSON_GetObjectItem(stream_json, "tags"); if (tags_put && cJSON_IsString(tags_put)) { if (strncmp(config.tags, tags_put->valuestring, sizeof(config.tags) - 1) != 0) { - strncpy(config.tags, tags_put->valuestring, sizeof(config.tags) - 1); - config.tags[sizeof(config.tags) - 1] = '\0'; + safe_strcpy(config.tags, tags_put->valuestring, sizeof(config.tags), 0); config_changed = true; log_info("Tags changed to '%s' for stream %s", config.tags, config.name); } @@ -1252,8 +1236,7 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { cJSON *admin_url_put = cJSON_GetObjectItem(stream_json, "admin_url"); if (admin_url_put && cJSON_IsString(admin_url_put)) { if (strncmp(config.admin_url, admin_url_put->valuestring, sizeof(config.admin_url) - 1) != 0) { - strncpy(config.admin_url, admin_url_put->valuestring, sizeof(config.admin_url) - 1); - config.admin_url[sizeof(config.admin_url) - 1] = '\0'; + safe_strcpy(config.admin_url, admin_url_put->valuestring, sizeof(config.admin_url), 0); config_changed = true; log_info("Admin URL changed for stream %s", config.name); } @@ -1269,9 +1252,8 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { if (motion_trigger_source_put && cJSON_IsString(motion_trigger_source_put)) { if (strncmp(config.motion_trigger_source, motion_trigger_source_put->valuestring, sizeof(config.motion_trigger_source) - 1) != 0) { - strncpy(config.motion_trigger_source, motion_trigger_source_put->valuestring, - sizeof(config.motion_trigger_source) - 1); - config.motion_trigger_source[sizeof(config.motion_trigger_source) - 1] = '\0'; + safe_strcpy(config.motion_trigger_source, motion_trigger_source_put->valuestring, + sizeof(config.motion_trigger_source), 0); config_changed = true; log_info("Motion trigger source changed to '%s' for stream %s", config.motion_trigger_source, config.name); @@ -1321,10 +1303,8 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { // Save original credentials before any update so we can detect changes char original_onvif_username[sizeof(config.onvif_username)]; char original_onvif_password[sizeof(config.onvif_password)]; - strncpy(original_onvif_username, config.onvif_username, sizeof(original_onvif_username) - 1); - original_onvif_username[sizeof(original_onvif_username) - 1] = '\0'; - strncpy(original_onvif_password, config.onvif_password, sizeof(original_onvif_password) - 1); - original_onvif_password[sizeof(original_onvif_password) - 1] = '\0'; + safe_strcpy(original_onvif_username, config.onvif_username, sizeof(original_onvif_username), 0); + safe_strcpy(original_onvif_password, config.onvif_password, sizeof(original_onvif_password), 0); if (config.is_onvif) { log_info("Testing ONVIF capabilities for stream %s", config.name); @@ -1335,13 +1315,11 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { cJSON *onvif_password = cJSON_GetObjectItem(stream_json, "onvif_password"); if (onvif_username && cJSON_IsString(onvif_username)) { - strncpy(config.onvif_username, onvif_username->valuestring, sizeof(config.onvif_username) - 1); - config.onvif_username[sizeof(config.onvif_username) - 1] = '\0'; + safe_strcpy(config.onvif_username, onvif_username->valuestring, sizeof(config.onvif_username), 0); } if (onvif_password && cJSON_IsString(onvif_password)) { - strncpy(config.onvif_password, onvif_password->valuestring, sizeof(config.onvif_password) - 1); - config.onvif_password[sizeof(config.onvif_password) - 1] = '\0'; + safe_strcpy(config.onvif_password, onvif_password->valuestring, sizeof(config.onvif_password), 0); } normalize_stream_url_credentials(&config); @@ -1359,8 +1337,7 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { // Build ONVIF device URL, using onvif_port if specified char onvif_device_url[MAX_URL_LENGTH]; if (build_onvif_test_url(&config, onvif_device_url, sizeof(onvif_device_url)) != 0) { - strncpy(onvif_device_url, config.url, sizeof(onvif_device_url) - 1); - onvif_device_url[sizeof(onvif_device_url) - 1] = '\0'; + safe_strcpy(onvif_device_url, config.url, sizeof(onvif_device_url), 0); } // Test ONVIF connection @@ -1519,8 +1496,8 @@ void handle_put_stream(const http_request_t *req, http_response_t *res) { // Populate task with all necessary data task->stream = stream; memcpy(&task->config, &config, sizeof(stream_config_t)); - strncpy(task->stream_id, stream_id, MAX_STREAM_NAME - 1); - strncpy(task->original_url, original_url, MAX_URL_LENGTH - 1); + safe_strcpy(task->stream_id, stream_id, MAX_STREAM_NAME, 0); + safe_strcpy(task->original_url, original_url, MAX_URL_LENGTH, 0); task->original_protocol = original_protocol; task->original_record_audio = original_record_audio; task->config_changed = config_changed; @@ -1724,7 +1701,7 @@ void handle_delete_stream(const http_request_t *req, http_response_t *res) { } task->stream = stream; - strncpy(task->stream_id, stream_id, sizeof(task->stream_id) - 1); + safe_strcpy(task->stream_id, stream_id, sizeof(task->stream_id), 0); task->permanent_delete = permanent_delete; // Send 202 Accepted response immediately so we don't block the event loop @@ -1897,7 +1874,7 @@ void handle_post_stream_refresh(const http_request_t *req, http_response_t *res) http_response_set_json_error(res, 500, "Internal server error"); return; } - strncpy(task->stream_name, stream_name, MAX_STREAM_NAME - 1); + safe_strcpy(task->stream_name, stream_name, MAX_STREAM_NAME, 0); // Send immediate 202 Accepted response cJSON *response = cJSON_CreateObject(); diff --git a/src/web/api_handlers_streams_test.c b/src/web/api_handlers_streams_test.c index efd3b3ba..a847fcb0 100644 --- a/src/web/api_handlers_streams_test.c +++ b/src/web/api_handlers_streams_test.c @@ -13,6 +13,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/url_utils.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/streams.h" #include "video/stream_state.h" @@ -96,11 +97,9 @@ static int test_stream_connection(const char *url, int protocol, // Get codec name const AVCodec *codec = avcodec_find_decoder(codec_params->codec_id); if (codec) { - strncpy(codec_name, codec->name, codec_name_size - 1); - codec_name[codec_name_size - 1] = '\0'; + safe_strcpy(codec_name, codec->name, codec_name_size, 0); } else { - strncpy(codec_name, "unknown", codec_name_size - 1); - codec_name[codec_name_size - 1] = '\0'; + safe_strcpy(codec_name, "unknown", codec_name_size, 0); } // Get dimensions @@ -169,15 +168,13 @@ void handle_test_stream(const http_request_t *req, http_response_t *res) { char credentialed_url[MAX_URL_LENGTH]; if (url_apply_credentials(raw_url, username, password, credentialed_url, sizeof(credentialed_url)) != 0) { - strncpy(credentialed_url, raw_url, sizeof(credentialed_url) - 1); - credentialed_url[sizeof(credentialed_url) - 1] = '\0'; + safe_strcpy(credentialed_url, raw_url, sizeof(credentialed_url), 0); } const char *stream_url = credentialed_url; char safe_url[MAX_URL_LENGTH]; if (url_redact_for_logging(raw_url, safe_url, sizeof(safe_url)) != 0) { - strncpy(safe_url, "[invalid-url]", sizeof(safe_url) - 1); - safe_url[sizeof(safe_url) - 1] = '\0'; + safe_strcpy(safe_url, "[invalid-url]", sizeof(safe_url), 0); } log_info("Testing stream connection: url=%s, protocol=%d", safe_url, stream_protocol); diff --git a/src/web/api_handlers_system.c b/src/web/api_handlers_system.c index a18ad859..d64c5d39 100644 --- a/src/web/api_handlers_system.c +++ b/src/web/api_handlers_system.c @@ -40,6 +40,7 @@ #include "core/config.h" #include "core/version.h" #include "core/shutdown_coordinator.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "database/db_streams.h" #include "database/db_recordings.h" @@ -56,6 +57,7 @@ extern bool get_go2rtc_memory_usage(unsigned long long *memory_usage); // External declarations extern bool daemon_mode; +// Copies the src string after removing whitespace and single- or double-quotes. static void trim_copy_value(char *dest, size_t dest_size, const char *src) { if (!dest || dest_size == 0) { return; @@ -66,26 +68,24 @@ static void trim_copy_value(char *dest, size_t dest_size, const char *src) { return; } - while (*src && isspace((unsigned char)*src)) { - src++; - } - - size_t len = strlen(src); - while (len > 0 && isspace((unsigned char)src[len - 1])) { - len--; - } + const char *start = ltrim_pos(src); + // rtrim_pos returns a pointer into src one *after* the last printing + // character, so we need to subtract 1 to check the last characters + // in the string. + const char *end = rtrim_pos(src, 0) - 1; - if (len >= 2 && ((src[0] == '"' && src[len - 1] == '"') || - (src[0] == '\'' && src[len - 1] == '\''))) { - src++; - len -= 2; + if ((end - start) >= 2 && ((*start == '"' && *end == '"') || + (*start == '\'' && *end == '\''))) { + start++; + end--; } + size_t len = end - start; if (len >= dest_size) { len = dest_size - 1; } - memcpy(dest, src, len); + memcpy(dest, start, len); dest[len] = '\0'; } @@ -1140,13 +1140,13 @@ void handle_get_system_info(const http_request_t *req, http_response_t *res) { if (ioc_sock >= 0) { struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); - strncpy(ifr.ifr_name, safe_name, IFNAMSIZ - 1); + safe_strcpy(ifr.ifr_name, safe_name, IFNAMSIZ, 0); if (ioctl(ioc_sock, SIOCGIFADDR, &ifr) == 0) { struct sockaddr_in *sin = (struct sockaddr_in *)&ifr.ifr_addr; if (inet_ntop(AF_INET, &sin->sin_addr, ip_addr, sizeof(ip_addr)) == NULL) { - strncpy(ip_addr, "Unknown", sizeof(ip_addr) - 1); + safe_strcpy(ip_addr, "Unknown", sizeof(ip_addr), 0); } } close(ioc_sock); diff --git a/src/web/api_handlers_timeline.c b/src/web/api_handlers_timeline.c index 6f09a80d..599173e2 100644 --- a/src/web/api_handlers_timeline.c +++ b/src/web/api_handlers_timeline.c @@ -26,6 +26,7 @@ #include "core/logger.h" #include "core/config.h" #include "core/url_utils.h" +#include "utils/strings.h" #include "database/database_manager.h" #include "database/db_core.h" #include "database/db_recordings.h" @@ -108,10 +109,10 @@ int get_timeline_segments(const char *stream_name, time_t start_time, time_t end segments[count].id = (uint64_t)sqlite3_column_int64(stmt, 0); const char *sname = (const char *)sqlite3_column_text(stmt, 1); - if (sname) strncpy(segments[count].stream_name, sname, sizeof(segments[count].stream_name) - 1); + if (sname) safe_strcpy(segments[count].stream_name, sname, sizeof(segments[count].stream_name), 0); const char *fpath = (const char *)sqlite3_column_text(stmt, 2); - if (fpath) strncpy(segments[count].file_path, fpath, sizeof(segments[count].file_path) - 1); + if (fpath) safe_strcpy(segments[count].file_path, fpath, sizeof(segments[count].file_path), 0); segments[count].start_time = (time_t)sqlite3_column_int64(stmt, 3); segments[count].end_time = (time_t)sqlite3_column_int64(stmt, 4); @@ -138,8 +139,8 @@ static time_t parse_iso8601_time(const char *time_str) { } // URL-decode the time string (replace %3A with :) - char decoded_time[64] = {0}; - strncpy(decoded_time, time_str, sizeof(decoded_time) - 1); + char decoded_time[64]; + safe_strcpy(decoded_time, time_str, sizeof(decoded_time), 0); // Replace %3A with : char *pos = decoded_time; @@ -648,8 +649,7 @@ int create_timeline_manifest(const timeline_segment_t *segments, int segment_cou fclose(manifest); // Copy manifest path to output - strncpy(manifest_path, manifest_filename, MAX_PATH_LENGTH - 1); - manifest_path[MAX_PATH_LENGTH - 1] = '\0'; + safe_strcpy(manifest_path, manifest_filename, MAX_PATH_LENGTH, 0); pthread_mutex_unlock(&manifest_mutex); diff --git a/src/web/api_handlers_users_backend_agnostic.c b/src/web/api_handlers_users_backend_agnostic.c index d5ec1e3b..742f46fe 100644 --- a/src/web/api_handlers_users_backend_agnostic.c +++ b/src/web/api_handlers_users_backend_agnostic.c @@ -14,6 +14,7 @@ #include "web/request_response.h" #define LOG_COMPONENT "UsersAPI" #include "core/logger.h" +#include "utils/strings.h" #include "database/db_auth.h" #include "database/db_core.h" #include "database/db_schema_cache.h" @@ -70,7 +71,7 @@ static int prepare_user_select_stmt(sqlite3 *db, const char *suffix, sqlite3_stm bool has_allowed_tags = cached_column_exists("users", "allowed_tags"); bool has_allowed_login_cidrs = cached_column_exists("users", "allowed_login_cidrs"); - char sql[768] = {0}; + char sql[768]; int written = snprintf(sql, sizeof(sql), "SELECT id, username, email, role, api_key, created_at, " "updated_at, last_login, is_active, password_change_locked, %s, %s, %s " @@ -90,21 +91,18 @@ static void populate_user_from_stmt(sqlite3_stmt *stmt, user_t *user) { memset(user, 0, sizeof(*user)); user->id = sqlite3_column_int64(stmt, 0); - strncpy(user->username, (const char *)sqlite3_column_text(stmt, 1), sizeof(user->username) - 1); - user->username[sizeof(user->username) - 1] = '\0'; + safe_strcpy(user->username, (const char *)sqlite3_column_text(stmt, 1), sizeof(user->username), 0); const char *email = (const char *)sqlite3_column_text(stmt, 2); if (email) { - strncpy(user->email, email, sizeof(user->email) - 1); - user->email[sizeof(user->email) - 1] = '\0'; + safe_strcpy(user->email, email, sizeof(user->email), 0); } user->role = (user_role_t)sqlite3_column_int(stmt, 3); const char *api_key = (const char *)sqlite3_column_text(stmt, 4); if (api_key) { - strncpy(user->api_key, api_key, sizeof(user->api_key) - 1); - user->api_key[sizeof(user->api_key) - 1] = '\0'; + safe_strcpy(user->api_key, api_key, sizeof(user->api_key), 0); } user->created_at = sqlite3_column_int64(stmt, 5); @@ -116,15 +114,13 @@ static void populate_user_from_stmt(sqlite3_stmt *stmt, user_t *user) { const char *allowed_tags = (const char *)sqlite3_column_text(stmt, 11); if (allowed_tags && allowed_tags[0] != '\0') { - strncpy(user->allowed_tags, allowed_tags, sizeof(user->allowed_tags) - 1); - user->allowed_tags[sizeof(user->allowed_tags) - 1] = '\0'; + safe_strcpy(user->allowed_tags, allowed_tags, sizeof(user->allowed_tags), 0); user->has_tag_restriction = true; } const char *allowed_login_cidrs = (const char *)sqlite3_column_text(stmt, 12); if (allowed_login_cidrs && allowed_login_cidrs[0] != '\0') { - strncpy(user->allowed_login_cidrs, allowed_login_cidrs, sizeof(user->allowed_login_cidrs) - 1); - user->allowed_login_cidrs[sizeof(user->allowed_login_cidrs) - 1] = '\0'; + safe_strcpy(user->allowed_login_cidrs, allowed_login_cidrs, sizeof(user->allowed_login_cidrs), 0); user->has_login_cidr_restriction = true; } } @@ -360,9 +356,9 @@ void handle_users_create(const http_request_t *req, http_response_t *res) { } // Make a copy of the username to use after the JSON object is freed - char username_copy[64] = {0}; + char username_copy[64]; const char *username = username_json->valuestring; - strncpy(username_copy, username, sizeof(username_copy) - 1); + safe_strcpy(username_copy, username, sizeof(username_copy), 0); const char *password = password_json->valuestring; const char *email = (email_json && cJSON_IsString(email_json)) ? email_json->valuestring : NULL; @@ -399,7 +395,7 @@ void handle_users_create(const http_request_t *req, http_response_t *res) { has_at_create = true; at_create_is_null = true; } else if (cJSON_IsString(allowed_tags_create_json)) { - strncpy(allowed_tags_buf, allowed_tags_create_json->valuestring, sizeof(allowed_tags_buf) - 1); + safe_strcpy(allowed_tags_buf, allowed_tags_create_json->valuestring, sizeof(allowed_tags_buf), 0); has_at_create = true; } } @@ -412,8 +408,8 @@ void handle_users_create(const http_request_t *req, http_response_t *res) { has_cidr_create = true; cidr_create_is_null = true; } else if (cJSON_IsString(allowed_login_cidrs_create_json)) { - strncpy(allowed_login_cidrs_buf, allowed_login_cidrs_create_json->valuestring, - sizeof(allowed_login_cidrs_buf) - 1); + safe_strcpy(allowed_login_cidrs_buf, allowed_login_cidrs_create_json->valuestring, + sizeof(allowed_login_cidrs_buf), 0); has_cidr_create = true; } else { cJSON_Delete(json_req); diff --git a/src/web/api_handlers_zones.c b/src/web/api_handlers_zones.c index 286aa883..e1cd6b4d 100644 --- a/src/web/api_handlers_zones.c +++ b/src/web/api_handlers_zones.c @@ -1,15 +1,17 @@ +#include +#include +#include + +#define LOG_COMPONENT "ZonesAPI" +#include "core/logger.h" #include "web/api_handlers_zones.h" #include "web/api_handlers.h" #include "web/request_response.h" #include "web/httpd_utils.h" #include "core/config.h" +#include "utils/strings.h" #include "database/db_zones.h" -#define LOG_COMPONENT "ZonesAPI" -#include "core/logger.h" #include -#include -#include -#include /** * Handler for GET /api/streams/:stream_name/zones @@ -185,14 +187,13 @@ void handle_post_zones(const http_request_t *req, http_response_t *res) { } // Copy basic fields - strncpy(zone->id, id->valuestring, MAX_ZONE_ID - 1); - strncpy(zone->stream_name, stream_name, MAX_STREAM_NAME - 1); - strncpy(zone->name, name->valuestring, MAX_ZONE_NAME - 1); + safe_strcpy(zone->id, id->valuestring, MAX_ZONE_ID, 0); + safe_strcpy(zone->stream_name, stream_name, MAX_STREAM_NAME, 0); + safe_strcpy(zone->name, name->valuestring, MAX_ZONE_NAME, 0); zone->enabled = enabled && cJSON_IsTrue(enabled); if (color && cJSON_IsString(color)) { - strncpy(zone->color, color->valuestring, 7); - zone->color[7] = '\0'; + safe_strcpy(zone->color, color->valuestring, 8, 0); } else { strcpy(zone->color, "#3b82f6"); } @@ -220,8 +221,7 @@ void handle_post_zones(const http_request_t *req, http_response_t *res) { // Parse optional fields if (filter_classes && cJSON_IsString(filter_classes)) { - strncpy(zone->filter_classes, filter_classes->valuestring, 255); - zone->filter_classes[255] = '\0'; + safe_strcpy(zone->filter_classes, filter_classes->valuestring, 256, 0); } else { zone->filter_classes[0] = '\0'; } diff --git a/src/web/batch_delete_progress.c b/src/web/batch_delete_progress.c index 03bd61b8..1bc6e607 100644 --- a/src/web/batch_delete_progress.c +++ b/src/web/batch_delete_progress.c @@ -9,9 +9,10 @@ #include #include -#include "web/batch_delete_progress.h" #define LOG_COMPONENT "RecordingsAPI" #include "core/logger.h" +#include "utils/strings.h" +#include "web/batch_delete_progress.h" /** * @brief Generate a simple UUID v4 without external dependencies @@ -192,8 +193,7 @@ int batch_delete_progress_create_job(int total, char *job_id_out) { g_jobs[slot].is_active = true; // Copy job ID to output - strncpy(job_id_out, g_jobs[slot].job_id, 64); - job_id_out[63] = '\0'; + safe_strcpy(job_id_out, g_jobs[slot].job_id, 64, 0); pthread_mutex_unlock(&g_jobs_mutex); @@ -263,8 +263,7 @@ int batch_delete_progress_update(const char *job_id, int current, int succeeded, g_jobs[slot].failed = failed; if (status_message) { - strncpy(g_jobs[slot].status_message, status_message, sizeof(g_jobs[slot].status_message) - 1); - g_jobs[slot].status_message[sizeof(g_jobs[slot].status_message) - 1] = '\0'; + safe_strcpy(g_jobs[slot].status_message, status_message, sizeof(g_jobs[slot].status_message), 0); } g_jobs[slot].updated_at = time(NULL); @@ -339,8 +338,7 @@ int batch_delete_progress_error(const char *job_id, const char *error_message) { g_jobs[slot].status = BATCH_DELETE_STATUS_ERROR; if (error_message) { - strncpy(g_jobs[slot].error_message, error_message, sizeof(g_jobs[slot].error_message) - 1); - g_jobs[slot].error_message[sizeof(g_jobs[slot].error_message) - 1] = '\0'; + safe_strcpy(g_jobs[slot].error_message, error_message, sizeof(g_jobs[slot].error_message), 0); snprintf(g_jobs[slot].status_message, sizeof(g_jobs[slot].status_message), "Error: %s", error_message); diff --git a/src/web/go2rtc_proxy_thread.c b/src/web/go2rtc_proxy_thread.c index f7a28d68..d50e9567 100644 --- a/src/web/go2rtc_proxy_thread.c +++ b/src/web/go2rtc_proxy_thread.c @@ -27,6 +27,7 @@ #define LOG_COMPONENT "go2rtcProxy" #include "core/logger.h" #include "utils/memory.h" +#include "utils/strings.h" // Maximum concurrent proxy threads (independent of UV_THREADPOOL_SIZE) #define MAX_CONCURRENT_PROXY_THREADS 32 @@ -240,8 +241,8 @@ static void proxy_async_cb(uv_async_t *handle) { } else { conn->response.status_code = (int)ctx->http_code; if (ctx->response_content_type[0] != '\0') { - strncpy(conn->response.content_type, ctx->response_content_type, - sizeof(conn->response.content_type) - 1); + safe_strcpy(conn->response.content_type, ctx->response_content_type, + sizeof(conn->response.content_type), 0); } http_response_add_header(&conn->response, "Access-Control-Allow-Origin", "*"); http_response_add_header(&conn->response, "Access-Control-Allow-Methods", @@ -345,8 +346,8 @@ int go2rtc_proxy_thread_submit(libuv_connection_t *conn, write_complete_action_t port, req->path); } - strncpy(ctx->method, req->method_str, sizeof(ctx->method) - 1); - strncpy(ctx->content_type, req->content_type, sizeof(ctx->content_type) - 1); + safe_strcpy(ctx->method, req->method_str, sizeof(ctx->method), 0); + safe_strcpy(ctx->content_type, req->content_type, sizeof(ctx->content_type), 0); ctx->action = action; ctx->conn = conn; diff --git a/src/web/httpd_utils.c b/src/web/httpd_utils.c index 00890832..cfce36a4 100644 --- a/src/web/httpd_utils.c +++ b/src/web/httpd_utils.c @@ -14,6 +14,7 @@ #define LOG_COMPONENT "HTTP" #include "core/logger.h" #include "core/config.h" +#include "utils/strings.h" #include "database/db_auth.h" cJSON* httpd_parse_json_body(const http_request_t *req) { @@ -77,46 +78,6 @@ static int base64_decode(const char *src, size_t src_len, char *dst, size_t dst_ return (int)j; } -static char *trim_ascii_whitespace(char *value) { - if (!value) { - return NULL; - } - - while (*value && isspace((unsigned char)*value)) { - value++; - } - - size_t len = strlen(value); - while (len > 0 && isspace((unsigned char)value[len - 1])) { - value[--len] = '\0'; - } - - return value; -} - -static bool copy_trimmed_value(const char *input, char *output, size_t output_size) { - if (!input || !output || output_size == 0) { - return false; - } - - output[0] = '\0'; - - char buffer[1024] = {0}; - if (strlen(input) >= sizeof(buffer)) { - return false; - } - strncpy(buffer, input, sizeof(buffer) - 1); - - char *trimmed = trim_ascii_whitespace(buffer); - if (!trimmed || trimmed[0] == '\0' || strlen(trimmed) >= output_size) { - return false; - } - - strncpy(output, trimmed, output_size - 1); - output[output_size - 1] = '\0'; - return true; -} - static bool normalize_ip_literal(const char *input, char *normalized, size_t normalized_size) { if (!input || !normalized || normalized_size == 0) { return false; @@ -124,11 +85,11 @@ static bool normalize_ip_literal(const char *input, char *normalized, size_t nor normalized[0] = '\0'; - char candidate_buffer[INET6_ADDRSTRLEN + 8] = {0}; + char candidate_buffer[INET6_ADDRSTRLEN + 8]; if (strlen(input) >= sizeof(candidate_buffer)) { return false; } - strncpy(candidate_buffer, input, sizeof(candidate_buffer) - 1); + safe_strcpy(candidate_buffer, input, sizeof(candidate_buffer), 0); char *candidate = trim_ascii_whitespace(candidate_buffer); if (!candidate || candidate[0] == '\0') { @@ -161,8 +122,7 @@ static bool ip_matches_cidr_list(const char *cidr_list, const char *ip) { user_t rules; memset(&rules, 0, sizeof(rules)); - strncpy(rules.allowed_login_cidrs, cidr_list, sizeof(rules.allowed_login_cidrs) - 1); - rules.allowed_login_cidrs[sizeof(rules.allowed_login_cidrs) - 1] = '\0'; + safe_strcpy(rules.allowed_login_cidrs, cidr_list, sizeof(rules.allowed_login_cidrs), 0); rules.has_login_cidr_restriction = true; return db_auth_ip_allowed_for_user(&rules, ip); } @@ -172,11 +132,11 @@ static int resolve_client_ip_from_forwarded_headers(const http_request_t *req, size_t client_ip_size) { const char *xff = http_request_get_header(req, "X-Forwarded-For"); if (xff && xff[0] != '\0') { - char xff_copy[1024] = {0}; + char xff_copy[1024]; if (strlen(xff) >= sizeof(xff_copy)) { return -1; } - strncpy(xff_copy, xff, sizeof(xff_copy) - 1); + safe_strcpy(xff_copy, xff, sizeof(xff_copy), 0); char normalized_tokens[16][INET6_ADDRSTRLEN] = {{0}}; int token_count = 0; @@ -188,15 +148,14 @@ static int resolve_client_ip_from_forwarded_headers(const http_request_t *req, if (!normalize_ip_literal(token, normalized, sizeof(normalized))) { return -1; } - strncpy(normalized_tokens[token_count], normalized, sizeof(normalized_tokens[token_count]) - 1); + safe_strcpy(normalized_tokens[token_count], normalized, sizeof(normalized_tokens[token_count]), 0); token_count++; } if (token_count > 0) { for (int i = token_count - 1; i >= 0; --i) { if (!ip_matches_cidr_list(g_config.trusted_proxy_cidrs, normalized_tokens[i])) { - strncpy(client_ip, normalized_tokens[i], client_ip_size - 1); - client_ip[client_ip_size - 1] = '\0'; + safe_strcpy(client_ip, normalized_tokens[i], client_ip_size, 0); return 0; } } @@ -209,8 +168,7 @@ static int resolve_client_ip_from_forwarded_headers(const http_request_t *req, if (!normalize_ip_literal(x_real_ip, normalized, sizeof(normalized))) { return -1; } - strncpy(client_ip, normalized, client_ip_size - 1); - client_ip[client_ip_size - 1] = '\0'; + safe_strcpy(client_ip, normalized, client_ip_size, 0); return 0; } @@ -243,10 +201,8 @@ int httpd_get_basic_auth_credentials(const http_request_t *req, if (!colon) return -1; *colon = '\0'; - strncpy(username, decoded, username_size - 1); - username[username_size - 1] = '\0'; - strncpy(password, colon + 1, password_size - 1); - password[password_size - 1] = '\0'; + safe_strcpy(username, decoded, username_size, 0); + safe_strcpy(password, colon + 1, password_size, 0); return 0; } @@ -259,7 +215,7 @@ int httpd_get_api_key(const http_request_t *req, char *api_key, size_t api_key_s api_key[0] = '\0'; const char *header_value = http_request_get_header(req, "X-API-Key"); - if (header_value && copy_trimmed_value(header_value, api_key, api_key_size)) { + if (header_value && copy_trimmed_value(api_key, api_key_size, header_value, 0)) { return 0; } @@ -268,7 +224,7 @@ int httpd_get_api_key(const http_request_t *req, char *api_key, size_t api_key_s return -1; } - return copy_trimmed_value(auth + 7, api_key, api_key_size) ? 0 : -1; + return copy_trimmed_value(api_key, api_key_size, auth + 7, 0) ? 0 : -1; } int httpd_get_effective_client_ip(const http_request_t *req, char *client_ip, size_t client_ip_size) { @@ -283,7 +239,7 @@ int httpd_get_effective_client_ip(const http_request_t *req, char *client_ip, si char peer_ip[INET6_ADDRSTRLEN] = {0}; if (!normalize_ip_literal(req->client_ip, peer_ip, sizeof(peer_ip))) { - return copy_trimmed_value(req->client_ip, client_ip, client_ip_size) ? 0 : -1; + return copy_trimmed_value(client_ip, client_ip_size, req->client_ip, 0) ? 0 : -1; } if (g_config.trusted_proxy_cidrs[0] != '\0' && @@ -292,8 +248,7 @@ int httpd_get_effective_client_ip(const http_request_t *req, char *client_ip, si return 0; } - strncpy(client_ip, peer_ip, client_ip_size - 1); - client_ip[client_ip_size - 1] = '\0'; + safe_strcpy(client_ip, peer_ip, client_ip_size, 0); return 0; } @@ -392,14 +347,13 @@ int httpd_get_authenticated_user(const http_request_t *req, user_t *user) { char effective_client_ip[64] = {0}; if (httpd_get_effective_client_ip(req, effective_client_ip, sizeof(effective_client_ip)) != 0) { - strncpy(effective_client_ip, req->client_ip, sizeof(effective_client_ip) - 1); - effective_client_ip[sizeof(effective_client_ip) - 1] = '\0'; + safe_strcpy(effective_client_ip, req->client_ip, sizeof(effective_client_ip), 0); } // If authentication is disabled, return a dummy admin user if (!g_config.web_auth_enabled) { memset(user, 0, sizeof(user_t)); - strncpy(user->username, "admin", sizeof(user->username) - 1); + safe_strcpy(user->username, "admin", sizeof(user->username), 0); user->role = USER_ROLE_ADMIN; user->is_active = true; return 1; @@ -500,7 +454,7 @@ int httpd_check_viewer_access(const http_request_t *req, user_t *user) { if (!g_config.web_auth_enabled) { // Create a pseudo-user for unauthenticated access memset(user, 0, sizeof(user_t)); - strncpy(user->username, "anonymous", sizeof(user->username) - 1); + safe_strcpy(user->username, "anonymous", sizeof(user->username), 0); user->role = USER_ROLE_VIEWER; user->is_active = true; return 1; @@ -510,7 +464,7 @@ int httpd_check_viewer_access(const http_request_t *req, user_t *user) { if (g_config.demo_mode) { // Create a demo viewer pseudo-user memset(user, 0, sizeof(user_t)); - strncpy(user->username, "demo", sizeof(user->username) - 1); + safe_strcpy(user->username, "demo", sizeof(user->username), 0); user->role = USER_ROLE_VIEWER; user->is_active = true; log_debug("Demo mode: granting viewer access to unauthenticated user"); diff --git a/src/web/libuv_connection.c b/src/web/libuv_connection.c index 94c1a04b..c68d91e1 100644 --- a/src/web/libuv_connection.c +++ b/src/web/libuv_connection.c @@ -20,6 +20,7 @@ #include "web/api_handlers_health.h" #define LOG_COMPONENT "HTTP" #include "core/logger.h" +#include "utils/strings.h" // Forward declaration for MIME type helper defined in libuv_file_serve.c extern const char *libuv_get_mime_type(const char *path); @@ -91,8 +92,8 @@ libuv_connection_t *libuv_connection_create(libuv_server_t *server) { void libuv_connection_reset(libuv_connection_t *conn) { if (!conn) return; - char client_ip[sizeof(conn->request.client_ip)] = {0}; - strncpy(client_ip, conn->request.client_ip, sizeof(client_ip) - 1); + char client_ip[sizeof(conn->request.client_ip)]; + safe_strcpy(client_ip, conn->request.client_ip, sizeof(client_ip), 0); // Free any allocated response body http_response_free(&conn->response); @@ -100,7 +101,7 @@ void libuv_connection_reset(libuv_connection_t *conn) { // Reset request/response http_request_init(&conn->request); http_response_init(&conn->response); - strncpy(conn->request.client_ip, client_ip, sizeof(conn->request.client_ip) - 1); + safe_strcpy(conn->request.client_ip, client_ip, sizeof(conn->request.client_ip), 0); // Reset parser state llhttp_reset(&conn->parser); @@ -287,17 +288,17 @@ static int on_url(llhttp_t *parser, const char *at, size_t length) { memcpy(conn->request.path, conn->request.uri, path_len); conn->request.path[path_len] = '\0'; } - strncpy(conn->request.query_string, query + 1, - sizeof(conn->request.query_string) - 1); + safe_strcpy(conn->request.query_string, query + 1, + sizeof(conn->request.query_string), 0); } else { - strncpy(conn->request.path, conn->request.uri, - sizeof(conn->request.path) - 1); + safe_strcpy(conn->request.path, conn->request.uri, + sizeof(conn->request.path), 0); conn->request.query_string[0] = '\0'; } // Get method from parser const char *method = llhttp_method_name(llhttp_get_method(parser)); - strncpy(conn->request.method_str, method, sizeof(conn->request.method_str) - 1); + safe_strcpy(conn->request.method_str, method, sizeof(conn->request.method_str), 0); // Map to enum (using HTTP_METHOD_ prefix to avoid llhttp conflicts) if (strcmp(method, "GET") == 0) conn->request.method = HTTP_METHOD_GET; @@ -339,8 +340,8 @@ static int on_header_value(llhttp_t *parser, const char *at, size_t length) { // Add to headers array int idx = conn->request.num_headers; - strncpy(conn->request.headers[idx].name, conn->current_header_field, - sizeof(conn->request.headers[idx].name) - 1); + safe_strcpy(conn->request.headers[idx].name, conn->current_header_field, + sizeof(conn->request.headers[idx].name), 0); size_t val_len = length < sizeof(conn->request.headers[idx].value) - 1 ? length : sizeof(conn->request.headers[idx].value) - 1; @@ -350,13 +351,13 @@ static int on_header_value(llhttp_t *parser, const char *at, size_t length) { // Handle special headers if (strcasecmp(conn->current_header_field, "Content-Type") == 0) { - strncpy(conn->request.content_type, conn->request.headers[idx].value, - sizeof(conn->request.content_type) - 1); + safe_strcpy(conn->request.content_type, conn->request.headers[idx].value, + sizeof(conn->request.content_type), 0); } else if (strcasecmp(conn->current_header_field, "Content-Length") == 0) { conn->request.content_length = strtoull(conn->request.headers[idx].value, NULL, 10); } else if (strcasecmp(conn->current_header_field, "User-Agent") == 0) { - strncpy(conn->request.user_agent, conn->request.headers[idx].value, - sizeof(conn->request.user_agent) - 1); + safe_strcpy(conn->request.user_agent, conn->request.headers[idx].value, + sizeof(conn->request.user_agent), 0); } else if (strcasecmp(conn->current_header_field, "Connection") == 0) { conn->keep_alive = (strcasecmp(conn->request.headers[idx].value, "close") != 0); } @@ -540,7 +541,7 @@ static void static_file_resolve_handler(const http_request_t *req, http_response libuv_connection_t *conn = (libuv_connection_t *)req->user_data; const libuv_server_t *server = conn->server; - char file_path[1024]; + char file_path[MAX_PATH_LENGTH]; snprintf(file_path, sizeof(file_path), "%s%s", server->config.web_root, req->path); struct stat st; @@ -555,25 +556,21 @@ static void static_file_resolve_handler(const http_request_t *req, http_response } } log_debug("Serving static file: %s", file_path); - strncpy(conn->deferred_file_path, file_path, sizeof(conn->deferred_file_path) - 1); - conn->deferred_file_path[sizeof(conn->deferred_file_path) - 1] = '\0'; + safe_strcpy(conn->deferred_file_path, file_path, sizeof(conn->deferred_file_path), 0); conn->deferred_content_type[0] = '\0'; conn->deferred_extra_headers[0] = '\0'; conn->deferred_file_serve = true; } else { - char gz_path[1080]; + char gz_path[MAX_PATH_LENGTH]; snprintf(gz_path, sizeof(gz_path), "%s.gz", file_path); if (stat(gz_path, &st) == 0 && S_ISREG(st.st_mode)) { log_debug("Serving gzip static file: %s", gz_path); const char *mime_type = libuv_get_mime_type(file_path); - strncpy(conn->deferred_file_path, gz_path, sizeof(conn->deferred_file_path) - 1); - conn->deferred_file_path[sizeof(conn->deferred_file_path) - 1] = '\0'; - strncpy(conn->deferred_content_type, mime_type, - sizeof(conn->deferred_content_type) - 1); - conn->deferred_content_type[sizeof(conn->deferred_content_type) - 1] = '\0'; - strncpy(conn->deferred_extra_headers, "Content-Encoding: gzip\r\n", - sizeof(conn->deferred_extra_headers) - 1); - conn->deferred_extra_headers[sizeof(conn->deferred_extra_headers) - 1] = '\0'; + safe_strcpy(conn->deferred_file_path, gz_path, sizeof(conn->deferred_file_path), 0); + safe_strcpy(conn->deferred_content_type, mime_type, + sizeof(conn->deferred_content_type), 0); + safe_strcpy(conn->deferred_extra_headers, "Content-Encoding: gzip\r\n", + sizeof(conn->deferred_extra_headers), 0); conn->deferred_file_serve = true; } else { log_debug("Static file not found: %s", file_path); diff --git a/src/web/libuv_file_serve.c b/src/web/libuv_file_serve.c index b151cb63..98f46b19 100644 --- a/src/web/libuv_file_serve.c +++ b/src/web/libuv_file_serve.c @@ -20,6 +20,7 @@ #include "web/libuv_connection.h" #define LOG_COMPONENT "HTTP" #include "core/logger.h" +#include "utils/strings.h" // Forward declaration for response helper defined in libuv_response.c extern int libuv_send_response_ex(libuv_connection_t *conn, const http_response_t *response, @@ -145,16 +146,15 @@ int libuv_serve_file(libuv_connection_t *conn, const char *path, // Set content type if (content_type) { - strncpy(ctx->content_type, content_type, sizeof(ctx->content_type) - 1); + safe_strcpy(ctx->content_type, content_type, sizeof(ctx->content_type), 0); } else { - strncpy(ctx->content_type, libuv_get_mime_type(path), - sizeof(ctx->content_type) - 1); + safe_strcpy(ctx->content_type, libuv_get_mime_type(path), + sizeof(ctx->content_type), 0); } // Store extra headers (CORS, Cache-Control, etc.) if (extra_headers) { - strncpy(ctx->extra_headers, extra_headers, sizeof(ctx->extra_headers) - 1); - ctx->extra_headers[sizeof(ctx->extra_headers) - 1] = '\0'; + safe_strcpy(ctx->extra_headers, extra_headers, sizeof(ctx->extra_headers), 0); } // Check for Range header diff --git a/src/web/libuv_server.c b/src/web/libuv_server.c index 8f881b4f..2de1e0e4 100644 --- a/src/web/libuv_server.c +++ b/src/web/libuv_server.c @@ -23,6 +23,7 @@ #include "core/config.h" #define LOG_COMPONENT "HTTP" #include "core/logger.h" +#include "utils/strings.h" // Initial handler capacity #define INITIAL_HANDLER_CAPACITY 32 @@ -584,9 +585,9 @@ int libuv_server_register_handler(http_server_handle_t handle, const char *path, // Add new handler int idx = server->handler_count; - strncpy(server->handlers[idx].path, path, sizeof(server->handlers[idx].path) - 1); + safe_strcpy(server->handlers[idx].path, path, sizeof(server->handlers[idx].path), 0); if (method) { - strncpy(server->handlers[idx].method, method, sizeof(server->handlers[idx].method) - 1); + safe_strcpy(server->handlers[idx].method, method, sizeof(server->handlers[idx].method), 0); } else { server->handlers[idx].method[0] = '\0'; // Match any method } @@ -636,7 +637,7 @@ static void on_connection(uv_stream_t *listener, int status) { } else if (addr.ss_family == AF_INET6) { uv_ip6_name((struct sockaddr_in6 *)&addr, ip, sizeof(ip)); } - strncpy(conn->request.client_ip, ip, sizeof(conn->request.client_ip) - 1); + safe_strcpy(conn->request.client_ip, ip, sizeof(conn->request.client_ip), 0); log_debug("on_connection: New connection from %s", ip); diff --git a/src/web/request_response.c b/src/web/request_response.c index 553abeab..4fd81287 100644 --- a/src/web/request_response.c +++ b/src/web/request_response.c @@ -6,11 +6,12 @@ #include #include -#include "web/request_response.h" -#include "web/web_server.h" #define LOG_COMPONENT "HTTP" #include "core/logger.h" +#include "utils/strings.h" #include "web/libuv_server.h" +#include "web/request_response.h" +#include "web/web_server.h" #define MAX_HEADER_SIZE 8192 #define RECV_BUFFER_SIZE 4096 @@ -165,10 +166,10 @@ int http_response_add_header(http_response_t *res, const char *name, const char if (!res || !name || !value) return -1; if (res->num_headers >= MAX_HEADERS) return -1; - strncpy(res->headers[res->num_headers].name, name, - sizeof(res->headers[res->num_headers].name) - 1); - strncpy(res->headers[res->num_headers].value, value, - sizeof(res->headers[res->num_headers].value) - 1); + safe_strcpy(res->headers[res->num_headers].name, name, + sizeof(res->headers[res->num_headers].name), 0); + safe_strcpy(res->headers[res->num_headers].value, value, + sizeof(res->headers[res->num_headers].value), 0); res->num_headers++; return 0; } @@ -204,7 +205,7 @@ int http_response_set_json(http_response_t *res, int status_code, const char *js if (!res || !json_str) return -1; res->status_code = status_code; - strncpy(res->content_type, "application/json", sizeof(res->content_type) - 1); + safe_strcpy(res->content_type, "application/json", sizeof(res->content_type), 0); // Add standard headers http_response_add_cors_headers(res); @@ -252,17 +253,14 @@ int http_serve_file(const http_request_t *req, const http_response_t *res, // be called from the loop thread) if (conn->handler_on_worker) { conn->deferred_file_serve = true; - strncpy(conn->deferred_file_path, file_path, sizeof(conn->deferred_file_path) - 1); - conn->deferred_file_path[sizeof(conn->deferred_file_path) - 1] = '\0'; + safe_strcpy(conn->deferred_file_path, file_path, sizeof(conn->deferred_file_path), 0); if (content_type) { - strncpy(conn->deferred_content_type, content_type, sizeof(conn->deferred_content_type) - 1); - conn->deferred_content_type[sizeof(conn->deferred_content_type) - 1] = '\0'; + safe_strcpy(conn->deferred_content_type, content_type, sizeof(conn->deferred_content_type), 0); } else { conn->deferred_content_type[0] = '\0'; } if (extra_headers) { - strncpy(conn->deferred_extra_headers, extra_headers, sizeof(conn->deferred_extra_headers) - 1); - conn->deferred_extra_headers[sizeof(conn->deferred_extra_headers) - 1] = '\0'; + safe_strcpy(conn->deferred_extra_headers, extra_headers, sizeof(conn->deferred_extra_headers), 0); } else { conn->deferred_extra_headers[0] = '\0'; } diff --git a/tests/test_sod_unified.c b/tests/test_sod_unified.c index 89998b5d..fdadd93d 100644 --- a/tests/test_sod_unified.c +++ b/tests/test_sod_unified.c @@ -5,6 +5,7 @@ #include "video/sod_realnet.h" #include "video/sod_integration.h" #include "core/logger.h" +#include "utils/strings.h" #include "sod/sod.h" /** @@ -192,9 +193,9 @@ int main(int argc, char *argv[]) if (cnn_boxes[i].score < 0.3) continue; // Copy detection data - strncpy(result.detections[result.count].label, + safe_strcpy(result.detections[result.count].label, cnn_boxes[i].zName ? cnn_boxes[i].zName : "face", - MAX_LABEL_LENGTH - 1); + MAX_LABEL_LENGTH, 0); result.detections[result.count].confidence = cnn_boxes[i].score; diff --git a/tests/unit/test_api_handlers_system.c b/tests/unit/test_api_handlers_system.c index 40cbacfb..35e63787 100644 --- a/tests/unit/test_api_handlers_system.c +++ b/tests/unit/test_api_handlers_system.c @@ -18,6 +18,7 @@ #include "unity.h" #include "core/config.h" #include "core/logger.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_streams.h" #include "web/api_handlers.h" @@ -28,9 +29,9 @@ extern config_t g_config; -static char g_tmp_root[256]; -static char g_db_path[320]; -static char g_storage_path[320]; +static char g_tmp_root[MAX_PATH_LENGTH]; +static char g_db_path[MAX_PATH_LENGTH]; +static char g_storage_path[MAX_PATH_LENGTH]; static cJSON *parse_response_json(const http_response_t *res) { TEST_ASSERT_NOT_NULL(res); @@ -60,9 +61,9 @@ static void clear_db_streams(void) { static stream_config_t make_test_stream(const char *name) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, name, sizeof(s.name) - 1); - strncpy(s.url, "rtsp://localhost/stream", sizeof(s.url) - 1); - strncpy(s.codec, "h264", sizeof(s.codec) - 1); + safe_strcpy(s.name, name, sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://localhost/stream", sizeof(s.url), 0); + safe_strcpy(s.codec, "h264", sizeof(s.codec), 0); s.enabled = true; s.width = 1920; s.height = 1080; @@ -74,7 +75,7 @@ static stream_config_t make_test_stream(const char *name) { s.tier_important_multiplier = 2.0; s.tier_ephemeral_multiplier = 0.25; s.storage_priority = 5; - strncpy(s.detection_object_filter, "none", sizeof(s.detection_object_filter) - 1); + safe_strcpy(s.detection_object_filter, "none", sizeof(s.detection_object_filter), 0); return s; } @@ -142,7 +143,7 @@ void test_handle_get_streams_includes_motion_trigger_source(void) { clear_db_streams(); stream_config_t ptz = make_test_stream("ptz_cam"); - strncpy(ptz.motion_trigger_source, "fixed_cam", sizeof(ptz.motion_trigger_source) - 1); + safe_strcpy(ptz.motion_trigger_source, "fixed_cam", sizeof(ptz.motion_trigger_source), 0); add_stream_config(&ptz); http_request_t req; @@ -195,7 +196,7 @@ void test_handle_put_stream_parses_motion_trigger_source(void) { http_response_init(&res); /* Set URL path so handler can extract the stream name */ - strncpy(req.path, "/api/streams/cam_put_mts", sizeof(req.path) - 1); + safe_strcpy(req.path, "/api/streams/cam_put_mts", sizeof(req.path), 0); /* JSON body with motion_trigger_source to exercise the new parsing code */ static const char json_body[] = "{\"motion_trigger_source\":\"cam_fixed_src\"}"; @@ -228,7 +229,7 @@ int main(void) { mkdir(g_tmp_root, 0755); mkdir(g_storage_path, 0755); - strncpy(g_config.storage_path, g_storage_path, sizeof(g_config.storage_path) - 1); + safe_strcpy(g_config.storage_path, g_storage_path, sizeof(g_config.storage_path), 0); if (init_database(g_db_path) != 0) { fprintf(stderr, "FATAL: init_database failed\n"); diff --git a/tests/unit/test_config.c b/tests/unit/test_config.c index 3cdcdb47..c60d8e37 100644 --- a/tests/unit/test_config.c +++ b/tests/unit/test_config.c @@ -317,7 +317,7 @@ void test_save_config_accepts_hidden_ini_dotfile(void) { char *dir = mkdtemp(temp_dir); TEST_ASSERT_NOT_NULL(dir); - char config_path[512]; + char config_path[MAX_PATH_LENGTH]; snprintf(config_path, sizeof(config_path), "%s/.ini", dir); load_default_config(&cfg); @@ -349,14 +349,14 @@ void test_env_integer_whitespace_handling(void) { char *dir = mkdtemp(temp_dir); TEST_ASSERT_NOT_NULL(dir); - char config_path[512]; - char storage_path[512]; - char storage_hls_path[512]; - char models_path[512]; - char db_path[512]; - char web_root[512]; - char log_path[512]; - char pid_path[512]; + char config_path[MAX_PATH_LENGTH]; + char storage_path[MAX_PATH_LENGTH]; + char storage_hls_path[MAX_PATH_LENGTH]; + char models_path[MAX_PATH_LENGTH]; + char db_path[MAX_PATH_LENGTH]; + char web_root[MAX_PATH_LENGTH]; + char log_path[MAX_PATH_LENGTH]; + char pid_path[MAX_PATH_LENGTH]; snprintf(config_path, sizeof(config_path), "%s/test.ini", dir); snprintf(storage_path, sizeof(storage_path), "%s/storage", dir); diff --git a/tests/unit/test_cross_stream_motion_trigger.c b/tests/unit/test_cross_stream_motion_trigger.c index 93b2a14e..b38c58d9 100644 --- a/tests/unit/test_cross_stream_motion_trigger.c +++ b/tests/unit/test_cross_stream_motion_trigger.c @@ -26,6 +26,7 @@ #include "video/onvif_motion_recording.h" #include "core/config.h" #include "core/logger.h" +#include "utils/strings.h" #define TEST_DB_PATH "/tmp/lightnvr_unit_cross_stream_trigger_test.db" @@ -36,9 +37,9 @@ extern config_t g_config; static stream_config_t make_stream(const char *name, bool enabled) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, name, sizeof(s.name) - 1); - strncpy(s.url, "rtsp://localhost/stream", sizeof(s.url) - 1); - strncpy(s.codec, "h264", sizeof(s.codec) - 1); + safe_strcpy(s.name, name, sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://localhost/stream", sizeof(s.url), 0); + safe_strcpy(s.codec, "h264", sizeof(s.codec), 0); s.enabled = enabled; s.width = 1920; s.height = 1080; @@ -50,7 +51,7 @@ static stream_config_t make_stream(const char *name, bool enabled) { s.detection_interval = 10; s.pre_detection_buffer = 5; s.post_detection_buffer = 10; - strncpy(s.detection_object_filter, "none", sizeof(s.detection_object_filter) - 1); + safe_strcpy(s.detection_object_filter, "none", sizeof(s.detection_object_filter), 0); s.tier_critical_multiplier = 3.0; s.tier_important_multiplier = 2.0; s.tier_ephemeral_multiplier = 0.25; @@ -109,7 +110,7 @@ void test_process_motion_event_propagates_to_linked_stream(void) { /* Add PTZ stream slaved to cam_fixed */ stream_config_t ptz = make_stream("cam_ptz", true); - strncpy(ptz.motion_trigger_source, "cam_fixed", sizeof(ptz.motion_trigger_source) - 1); + safe_strcpy(ptz.motion_trigger_source, "cam_fixed", sizeof(ptz.motion_trigger_source), 0); add_stream_config(&ptz); /* Trigger a motion-start event on the source stream */ @@ -130,7 +131,7 @@ void test_process_motion_event_end_propagates_to_linked_stream(void) { add_stream_config(&fixed); stream_config_t ptz = make_stream("cam_ptz_end", true); - strncpy(ptz.motion_trigger_source, "cam_fixed_end", sizeof(ptz.motion_trigger_source) - 1); + safe_strcpy(ptz.motion_trigger_source, "cam_fixed_end", sizeof(ptz.motion_trigger_source), 0); add_stream_config(&ptz); /* Trigger motion-end */ @@ -149,7 +150,7 @@ void test_process_motion_event_end_propagates_to_linked_stream(void) { void test_process_motion_event_unregistered_source_no_crash(void) { /* Add a PTZ stream that lists a source not being called */ stream_config_t ptz = make_stream("cam_ptz_orphan", true); - strncpy(ptz.motion_trigger_source, "cam_ghost", sizeof(ptz.motion_trigger_source) - 1); + safe_strcpy(ptz.motion_trigger_source, "cam_ghost", sizeof(ptz.motion_trigger_source), 0); add_stream_config(&ptz); /* Call for a totally different source — no linked streams should match */ diff --git a/tests/unit/test_db_auth.c b/tests/unit/test_db_auth.c index d1508788..8bdf140c 100644 --- a/tests/unit/test_db_auth.c +++ b/tests/unit/test_db_auth.c @@ -17,6 +17,7 @@ #include #include "unity.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_auth.h" @@ -387,8 +388,8 @@ void test_ip_allowed_for_user_accepts_comma_separated_cidrs(void) { user_t user; memset(&user, 0, sizeof(user)); user.has_login_cidr_restriction = true; - strncpy(user.allowed_login_cidrs, "127.0.0.1/32, ::1/128", - sizeof(user.allowed_login_cidrs) - 1); + safe_strcpy(user.allowed_login_cidrs, "127.0.0.1/32, ::1/128", + sizeof(user.allowed_login_cidrs), 0); TEST_ASSERT_TRUE(db_auth_ip_allowed_for_user(&user, "127.0.0.1")); TEST_ASSERT_TRUE(db_auth_ip_allowed_for_user(&user, "::1")); @@ -399,8 +400,8 @@ void test_ip_allowed_for_user_accepts_single_host_ip_entries(void) { user_t user; memset(&user, 0, sizeof(user)); user.has_login_cidr_restriction = true; - strncpy(user.allowed_login_cidrs, "127.0.0.1\n2001:db8::1", - sizeof(user.allowed_login_cidrs) - 1); + safe_strcpy(user.allowed_login_cidrs, "127.0.0.1\n2001:db8::1", + sizeof(user.allowed_login_cidrs), 0); TEST_ASSERT_TRUE(db_auth_ip_allowed_for_user(&user, "127.0.0.1")); TEST_ASSERT_TRUE(db_auth_ip_allowed_for_user(&user, "2001:db8::1")); diff --git a/tests/unit/test_db_detections.c b/tests/unit/test_db_detections.c index 5561fe4e..e9f5c81a 100644 --- a/tests/unit/test_db_detections.c +++ b/tests/unit/test_db_detections.c @@ -18,6 +18,7 @@ #include #include "unity.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_detections.h" #include "database/db_recordings.h" @@ -29,7 +30,7 @@ static detection_result_t make_result(const char *label, float conf) { detection_result_t r; memset(&r, 0, sizeof(r)); r.count = 1; - strncpy(r.detections[0].label, label, MAX_LABEL_LENGTH - 1); + safe_strcpy(r.detections[0].label, label, MAX_LABEL_LENGTH, 0); r.detections[0].confidence = conf; r.detections[0].x = 0.1f; r.detections[0].y = 0.1f; r.detections[0].width = 0.2f; r.detections[0].height = 0.2f; @@ -118,10 +119,10 @@ void test_update_detections_recording_id(void) { /* Create a real recording entry to satisfy the FK constraint */ recording_metadata_t rec; memset(&rec, 0, sizeof(rec)); - strncpy(rec.stream_name, "cam6", sizeof(rec.stream_name) - 1); - strncpy(rec.file_path, "/tmp/t.mp4", sizeof(rec.file_path) - 1); - strncpy(rec.codec, "h264", sizeof(rec.codec) - 1); - strncpy(rec.trigger_type,"detection", sizeof(rec.trigger_type) - 1); + safe_strcpy(rec.stream_name, "cam6", sizeof(rec.stream_name), 0); + safe_strcpy(rec.file_path, "/tmp/t.mp4", sizeof(rec.file_path), 0); + safe_strcpy(rec.codec, "h264", sizeof(rec.codec), 0); + safe_strcpy(rec.trigger_type,"detection", sizeof(rec.trigger_type), 0); rec.start_time = now - 10; rec.end_time = now; rec.size_bytes = 1000; diff --git a/tests/unit/test_db_maintenance.c b/tests/unit/test_db_maintenance.c index ce06e8d8..17199be4 100644 --- a/tests/unit/test_db_maintenance.c +++ b/tests/unit/test_db_maintenance.c @@ -15,6 +15,7 @@ #include #include "unity.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_maintenance.h" #include "database/db_recordings.h" @@ -37,10 +38,10 @@ void test_get_database_size_increases_after_insert(void) { /* Insert some data */ recording_metadata_t m; memset(&m, 0, sizeof(m)); - strncpy(m.stream_name, "cam1", sizeof(m.stream_name) - 1); - strncpy(m.file_path, "/r.mp4", sizeof(m.file_path) - 1); - strncpy(m.codec, "h264", sizeof(m.codec) - 1); - strncpy(m.trigger_type,"scheduled",sizeof(m.trigger_type)- 1); + safe_strcpy(m.stream_name, "cam1", sizeof(m.stream_name), 0); + safe_strcpy(m.file_path, "/r.mp4", sizeof(m.file_path), 0); + safe_strcpy(m.codec, "h264", sizeof(m.codec), 0); + safe_strcpy(m.trigger_type,"scheduled",sizeof(m.trigger_type), 0); m.start_time = time(NULL); m.end_time = m.start_time + 60; m.size_bytes = 1024; diff --git a/tests/unit/test_db_motion_config.c b/tests/unit/test_db_motion_config.c index aea4dec8..81d520a5 100644 --- a/tests/unit/test_db_motion_config.c +++ b/tests/unit/test_db_motion_config.c @@ -16,6 +16,7 @@ #include #include "unity.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_motion_config.h" #include "database/db_streams.h" @@ -32,8 +33,8 @@ static motion_recording_config_t make_config(bool enabled) { cfg.pre_buffer_seconds = 10; cfg.post_buffer_seconds = 20; cfg.max_file_duration = 300; - strncpy(cfg.codec, "h264", sizeof(cfg.codec) - 1); - strncpy(cfg.quality, "medium", sizeof(cfg.quality) - 1); + safe_strcpy(cfg.codec, "h264", sizeof(cfg.codec), 0); + safe_strcpy(cfg.quality, "medium", sizeof(cfg.quality), 0); cfg.retention_days = 7; return cfg; } @@ -48,8 +49,8 @@ static void clear_all(void) { static void ensure_stream(const char *name) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, name, sizeof(s.name) - 1); - strncpy(s.url, "rtsp://localhost/test", sizeof(s.url) - 1); + safe_strcpy(s.name, name, sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://localhost/test", sizeof(s.url), 0); s.enabled = true; s.width = 1920; s.height = 1080; diff --git a/tests/unit/test_db_recordings_extended.c b/tests/unit/test_db_recordings_extended.c index e65bbf96..3c157a5a 100644 --- a/tests/unit/test_db_recordings_extended.c +++ b/tests/unit/test_db_recordings_extended.c @@ -19,16 +19,17 @@ #include "database/db_recording_tags.h" #include "database/db_recordings.h" #include "video/detection_result.h" +#include "utils/strings.h" #define TEST_DB_PATH "/tmp/lightnvr_unit_recordings_ext_test.db" static recording_metadata_t make_rec(const char *stream, const char *path, time_t start) { recording_metadata_t m; memset(&m, 0, sizeof(m)); - strncpy(m.stream_name, stream, sizeof(m.stream_name) - 1); - strncpy(m.file_path, path, sizeof(m.file_path) - 1); - strncpy(m.codec, "h264", sizeof(m.codec) - 1); - strncpy(m.trigger_type, "scheduled", sizeof(m.trigger_type) - 1); + safe_strcpy(m.stream_name, stream, sizeof(m.stream_name), 0); + safe_strcpy(m.file_path, path, sizeof(m.file_path), 0); + safe_strcpy(m.codec, "h264", sizeof(m.codec), 0); + safe_strcpy(m.trigger_type, "scheduled", sizeof(m.trigger_type), 0); m.start_time = start; m.end_time = start + 60; m.size_bytes = 1024 * 1024; @@ -51,7 +52,7 @@ static detection_result_t make_detection_result(const char *label) { detection_result_t result; memset(&result, 0, sizeof(result)); result.count = 1; - strncpy(result.detections[0].label, label, sizeof(result.detections[0].label) - 1); + safe_strcpy(result.detections[0].label, label, sizeof(result.detections[0].label), 0); result.detections[0].confidence = 0.9f; result.detections[0].width = 0.3f; result.detections[0].height = 0.3f; @@ -131,8 +132,8 @@ void test_get_recording_count_supports_multi_value_stream_tag_and_capture_filter recording_metadata_t scheduled = make_rec("cam1", "/rec/multi-1.mp4", now); recording_metadata_t detection = make_rec("cam2", "/rec/multi-2.mp4", now + 60); recording_metadata_t manual = make_rec("cam3", "/rec/multi-3.mp4", now + 120); - strncpy(detection.trigger_type, "detection", sizeof(detection.trigger_type) - 1); - strncpy(manual.trigger_type, "manual", sizeof(manual.trigger_type) - 1); + safe_strcpy(detection.trigger_type, "detection", sizeof(detection.trigger_type), 0); + safe_strcpy(manual.trigger_type, "manual", sizeof(manual.trigger_type), 0); uint64_t scheduled_id = add_recording_metadata(&scheduled); uint64_t detection_id = add_recording_metadata(&detection); diff --git a/tests/unit/test_db_recordings_sync.c b/tests/unit/test_db_recordings_sync.c index 7f07c2d9..7a399764 100644 --- a/tests/unit/test_db_recordings_sync.c +++ b/tests/unit/test_db_recordings_sync.c @@ -22,6 +22,7 @@ #include #include "unity.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_recordings.h" #include "database/db_recordings_sync.h" @@ -35,10 +36,10 @@ static recording_metadata_t make_zero_size_rec(const char *stream, time_t start) { recording_metadata_t m; memset(&m, 0, sizeof(m)); - strncpy(m.stream_name, stream, sizeof(m.stream_name) - 1); - strncpy(m.file_path, path, sizeof(m.file_path) - 1); - strncpy(m.codec, "h264", sizeof(m.codec) - 1); - strncpy(m.trigger_type, "scheduled", sizeof(m.trigger_type) - 1); + safe_strcpy(m.stream_name, stream, sizeof(m.stream_name), 0); + safe_strcpy(m.file_path, path, sizeof(m.file_path), 0); + safe_strcpy(m.codec, "h264", sizeof(m.codec), 0); + safe_strcpy(m.trigger_type, "scheduled", sizeof(m.trigger_type), 0); m.start_time = start; m.end_time = start + 60; m.size_bytes = 0; /* <-- needs sync */ diff --git a/tests/unit/test_db_streams.c b/tests/unit/test_db_streams.c index 60b324ae..76bd56be 100644 --- a/tests/unit/test_db_streams.c +++ b/tests/unit/test_db_streams.c @@ -20,6 +20,7 @@ #include "unity.h" #include "database/db_core.h" #include "database/db_streams.h" +#include "utils/strings.h" #define TEST_DB_PATH "/tmp/lightnvr_unit_streams_test.db" @@ -27,9 +28,9 @@ static stream_config_t make_stream(const char *name, bool enabled) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, name, sizeof(s.name) - 1); - strncpy(s.url, "rtsp://camera/stream", sizeof(s.url) - 1); - strncpy(s.codec, "h264", sizeof(s.codec) - 1); + safe_strcpy(s.name, name, sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://camera/stream", sizeof(s.url), 0); + safe_strcpy(s.codec, "h264", sizeof(s.codec), 0); s.enabled = enabled; s.width = 1920; s.height = 1080; @@ -41,7 +42,7 @@ static stream_config_t make_stream(const char *name, bool enabled) { s.detection_interval = 10; s.pre_detection_buffer = 5; s.post_detection_buffer = 10; - strncpy(s.detection_object_filter, "none", sizeof(s.detection_object_filter) - 1); + safe_strcpy(s.detection_object_filter, "none", sizeof(s.detection_object_filter), 0); s.tier_critical_multiplier = 3.0; s.tier_important_multiplier = 2.0; s.tier_ephemeral_multiplier = 0.25; @@ -91,14 +92,14 @@ void test_get_stream_config_by_name_round_trip(void) { void test_stream_admin_url_round_trip(void) { stream_config_t s = make_stream("cam_admin", true); - strncpy(s.admin_url, "http://camera.local/", sizeof(s.admin_url) - 1); + safe_strcpy(s.admin_url, "http://camera.local/", sizeof(s.admin_url), 0); add_stream_config(&s); stream_config_t got; TEST_ASSERT_EQUAL_INT(0, get_stream_config_by_name("cam_admin", &got)); TEST_ASSERT_EQUAL_STRING("http://camera.local/", got.admin_url); - strncpy(s.admin_url, "https://camera.local/settings", sizeof(s.admin_url) - 1); + safe_strcpy(s.admin_url, "https://camera.local/settings", sizeof(s.admin_url), 0); TEST_ASSERT_EQUAL_INT(0, update_stream_config("cam_admin", &s)); TEST_ASSERT_EQUAL_INT(0, get_stream_config_by_name("cam_admin", &got)); TEST_ASSERT_EQUAL_STRING("https://camera.local/settings", got.admin_url); @@ -112,7 +113,7 @@ void test_update_stream_config_changes_url(void) { stream_config_t s = make_stream("cam_upd", true); add_stream_config(&s); - strncpy(s.url, "rtsp://new/stream", sizeof(s.url) - 1); + safe_strcpy(s.url, "rtsp://new/stream", sizeof(s.url), 0); int rc = update_stream_config("cam_upd", &s); TEST_ASSERT_EQUAL_INT(0, rc); @@ -229,7 +230,7 @@ void test_motion_trigger_source_defaults_empty(void) { void test_motion_trigger_source_round_trip(void) { stream_config_t s = make_stream("cam_ptz", true); - strncpy(s.motion_trigger_source, "cam_fixed", sizeof(s.motion_trigger_source) - 1); + safe_strcpy(s.motion_trigger_source, "cam_fixed", sizeof(s.motion_trigger_source), 0); add_stream_config(&s); stream_config_t got; @@ -239,10 +240,10 @@ void test_motion_trigger_source_round_trip(void) { void test_motion_trigger_source_update(void) { stream_config_t s = make_stream("cam_ptz_upd", true); - strncpy(s.motion_trigger_source, "cam_fixed_old", sizeof(s.motion_trigger_source) - 1); + safe_strcpy(s.motion_trigger_source, "cam_fixed_old", sizeof(s.motion_trigger_source), 0); add_stream_config(&s); - strncpy(s.motion_trigger_source, "cam_fixed_new", sizeof(s.motion_trigger_source) - 1); + safe_strcpy(s.motion_trigger_source, "cam_fixed_new", sizeof(s.motion_trigger_source), 0); TEST_ASSERT_EQUAL_INT(0, update_stream_config("cam_ptz_upd", &s)); stream_config_t got; @@ -253,7 +254,7 @@ void test_motion_trigger_source_update(void) { void test_motion_trigger_source_in_get_all(void) { stream_config_t src = make_stream("cam_src", true); stream_config_t ptz = make_stream("cam_ptz_all", true); - strncpy(ptz.motion_trigger_source, "cam_src", sizeof(ptz.motion_trigger_source) - 1); + safe_strcpy(ptz.motion_trigger_source, "cam_src", sizeof(ptz.motion_trigger_source), 0); add_stream_config(&src); add_stream_config(&ptz); diff --git a/tests/unit/test_db_zones.c b/tests/unit/test_db_zones.c index a6cd2065..eba0e906 100644 --- a/tests/unit/test_db_zones.c +++ b/tests/unit/test_db_zones.c @@ -15,6 +15,7 @@ #include #include "unity.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_zones.h" #include "database/db_streams.h" @@ -25,10 +26,10 @@ static detection_zone_t make_zone(const char *id, const char *stream, const char *name, bool enabled) { detection_zone_t z; memset(&z, 0, sizeof(z)); - strncpy(z.id, id, sizeof(z.id) - 1); - strncpy(z.stream_name, stream, sizeof(z.stream_name) - 1); - strncpy(z.name, name, sizeof(z.name) - 1); - strncpy(z.color, "#ff0000", sizeof(z.color) - 1); + safe_strcpy(z.id, id, sizeof(z.id), 0); + safe_strcpy(z.stream_name, stream, sizeof(z.stream_name), 0); + safe_strcpy(z.name, name, sizeof(z.name), 0); + safe_strcpy(z.color, "#ff0000", sizeof(z.color), 0); z.enabled = enabled; /* Triangle polygon */ z.polygon[0].x = 0.0f; z.polygon[0].y = 0.0f; @@ -36,7 +37,7 @@ static detection_zone_t make_zone(const char *id, const char *stream, z.polygon[2].x = 0.5f; z.polygon[2].y = 1.0f; z.polygon_count = 3; z.min_confidence = 0.5f; - strncpy(z.filter_classes, "person,car", sizeof(z.filter_classes) - 1); + safe_strcpy(z.filter_classes, "person,car", sizeof(z.filter_classes), 0); return z; } @@ -48,8 +49,8 @@ static void clear_zones(void) { static void ensure_test_stream(const char *stream_name) { stream_config_t cfg; memset(&cfg, 0, sizeof(cfg)); - strncpy(cfg.name, stream_name, sizeof(cfg.name) - 1); - strncpy(cfg.url, "rtsp://localhost/test", sizeof(cfg.url) - 1); + safe_strcpy(cfg.name, stream_name, sizeof(cfg.name), 0); + safe_strcpy(cfg.url, "rtsp://localhost/test", sizeof(cfg.url), 0); cfg.enabled = true; cfg.width = 1920; cfg.height = 1080; diff --git a/tests/unit/test_go2rtc_process_detection.c b/tests/unit/test_go2rtc_process_detection.c index 2c31cb11..3b8f9d1e 100644 --- a/tests/unit/test_go2rtc_process_detection.c +++ b/tests/unit/test_go2rtc_process_detection.c @@ -18,6 +18,7 @@ #include #include "unity.h" +#include "utils/strings.h" #include "video/go2rtc/go2rtc_process.h" void setUp(void) {} @@ -79,8 +80,7 @@ static pid_t spawn_path_false_positive(char *dir_out, size_t dir_out_size) { return -1; } - strncpy(dir_out, dir, dir_out_size - 1); - dir_out[dir_out_size - 1] = '\0'; + safe_strcpy(dir_out, dir, dir_out_size, 0); char script_path[PATH_MAX]; snprintf(script_path, sizeof(script_path), "%s/hold.sh", dir_out); diff --git a/tests/unit/test_httpd_utils.c b/tests/unit/test_httpd_utils.c index 164feba5..2f1cc5d1 100644 --- a/tests/unit/test_httpd_utils.c +++ b/tests/unit/test_httpd_utils.c @@ -31,6 +31,7 @@ #include "web/httpd_utils.h" #include "web/request_response.h" #include "core/config.h" +#include "utils/strings.h" #include "database/db_auth.h" #include "database/db_core.h" @@ -42,9 +43,9 @@ extern config_t g_config; /* ---- header helper ---- */ static void add_header(http_request_t *req, const char *name, const char *value) { if (req->num_headers >= MAX_HEADERS) return; - strncpy(req->headers[req->num_headers].name, name, 127); + safe_strcpy(req->headers[req->num_headers].name, name, 128, 0); req->headers[req->num_headers].name[127] = '\0'; - strncpy(req->headers[req->num_headers].value, value, 1023); + safe_strcpy(req->headers[req->num_headers].value, value, 1024, 0); req->headers[req->num_headers].value[1023] = '\0'; req->num_headers++; } @@ -218,7 +219,7 @@ void test_get_effective_client_ip_uses_peer_by_default(void) { http_request_t req; http_request_init(&req); - strncpy(req.client_ip, "198.51.100.10", sizeof(req.client_ip) - 1); + safe_strcpy(req.client_ip, "198.51.100.10", sizeof(req.client_ip), 0); add_header(&req, "X-Forwarded-For", "192.0.2.25"); char client_ip[64] = {0}; @@ -228,11 +229,11 @@ void test_get_effective_client_ip_uses_peer_by_default(void) { } void test_get_effective_client_ip_uses_forwarded_for_from_trusted_proxy(void) { - strncpy(g_config.trusted_proxy_cidrs, "127.0.0.1/32", sizeof(g_config.trusted_proxy_cidrs) - 1); + safe_strcpy(g_config.trusted_proxy_cidrs, "127.0.0.1/32", sizeof(g_config.trusted_proxy_cidrs), 0); http_request_t req; http_request_init(&req); - strncpy(req.client_ip, "127.0.0.1", sizeof(req.client_ip) - 1); + safe_strcpy(req.client_ip, "127.0.0.1", sizeof(req.client_ip), 0); add_header(&req, "X-Forwarded-For", "198.51.100.44, 127.0.0.1"); char client_ip[64] = {0}; @@ -242,12 +243,12 @@ void test_get_effective_client_ip_uses_forwarded_for_from_trusted_proxy(void) { } void test_get_effective_client_ip_uses_forwarded_for_from_comma_separated_trusted_proxies(void) { - strncpy(g_config.trusted_proxy_cidrs, "10.0.0.0/8, 127.0.0.1/32", - sizeof(g_config.trusted_proxy_cidrs) - 1); + safe_strcpy(g_config.trusted_proxy_cidrs, "10.0.0.0/8, 127.0.0.1/32", + sizeof(g_config.trusted_proxy_cidrs), 0); http_request_t req; http_request_init(&req); - strncpy(req.client_ip, "127.0.0.1", sizeof(req.client_ip) - 1); + safe_strcpy(req.client_ip, "127.0.0.1", sizeof(req.client_ip), 0); add_header(&req, "X-Forwarded-For", "198.51.100.44, 127.0.0.1"); char client_ip[64] = {0}; @@ -257,11 +258,11 @@ void test_get_effective_client_ip_uses_forwarded_for_from_comma_separated_truste } void test_get_effective_client_ip_ignores_forwarded_for_from_untrusted_proxy(void) { - strncpy(g_config.trusted_proxy_cidrs, "127.0.0.1/32", sizeof(g_config.trusted_proxy_cidrs) - 1); + safe_strcpy(g_config.trusted_proxy_cidrs, "127.0.0.1/32", sizeof(g_config.trusted_proxy_cidrs), 0); http_request_t req; http_request_init(&req); - strncpy(req.client_ip, "198.51.100.10", sizeof(req.client_ip) - 1); + safe_strcpy(req.client_ip, "198.51.100.10", sizeof(req.client_ip), 0); add_header(&req, "X-Forwarded-For", "192.0.2.25"); char client_ip[64] = {0}; @@ -422,7 +423,7 @@ void test_get_authenticated_user_allows_basic_auth_from_allowed_ip(void) { http_request_t req; http_request_init(&req); add_header(&req, "Authorization", "Basic dGVzdDpwYXNzd29yZDEyMw=="); - strncpy(req.client_ip, "192.0.2.25", sizeof(req.client_ip) - 1); + safe_strcpy(req.client_ip, "192.0.2.25", sizeof(req.client_ip), 0); user_t user; memset(&user, 0, sizeof(user)); @@ -439,7 +440,7 @@ void test_get_authenticated_user_rejects_basic_auth_from_disallowed_ip(void) { http_request_t req; http_request_init(&req); add_header(&req, "Authorization", "Basic dGVzdDpwYXNzd29yZDEyMw=="); - strncpy(req.client_ip, "198.51.100.25", sizeof(req.client_ip) - 1); + safe_strcpy(req.client_ip, "198.51.100.25", sizeof(req.client_ip), 0); user_t user; memset(&user, 0, sizeof(user)); @@ -460,8 +461,8 @@ void test_get_authenticated_user_rejects_session_from_disallowed_ip(void) { char cookie[160] = {0}; snprintf(cookie, sizeof(cookie), "session=%s", token); add_header(&req, "Cookie", cookie); - strncpy(req.client_ip, "198.51.100.10", sizeof(req.client_ip) - 1); - strncpy(req.user_agent, "TestAgent", sizeof(req.user_agent) - 1); + safe_strcpy(req.client_ip, "198.51.100.10", sizeof(req.client_ip), 0); + safe_strcpy(req.user_agent, "TestAgent", sizeof(req.user_agent), 0); user_t user; memset(&user, 0, sizeof(user)); @@ -476,13 +477,13 @@ void test_get_authenticated_user_allows_api_key_from_trusted_forwarded_ip(void) char api_key[64] = {0}; TEST_ASSERT_EQUAL_INT(0, db_auth_generate_api_key(uid, api_key, sizeof(api_key))); - strncpy(g_config.trusted_proxy_cidrs, "127.0.0.1/32", sizeof(g_config.trusted_proxy_cidrs) - 1); + safe_strcpy(g_config.trusted_proxy_cidrs, "127.0.0.1/32", sizeof(g_config.trusted_proxy_cidrs), 0); http_request_t req; http_request_init(&req); add_header(&req, "X-API-Key", api_key); add_header(&req, "X-Forwarded-For", "192.0.2.55, 127.0.0.1"); - strncpy(req.client_ip, "127.0.0.1", sizeof(req.client_ip) - 1); + safe_strcpy(req.client_ip, "127.0.0.1", sizeof(req.client_ip), 0); user_t user; memset(&user, 0, sizeof(user)); @@ -500,13 +501,13 @@ void test_get_authenticated_user_rejects_api_key_with_spoofed_forwarded_ip_from_ char auth_header[96] = {0}; TEST_ASSERT_EQUAL_INT(0, db_auth_generate_api_key(uid, api_key, sizeof(api_key))); snprintf(auth_header, sizeof(auth_header), "Bearer %s", api_key); - strncpy(g_config.trusted_proxy_cidrs, "127.0.0.1/32", sizeof(g_config.trusted_proxy_cidrs) - 1); + safe_strcpy(g_config.trusted_proxy_cidrs, "127.0.0.1/32", sizeof(g_config.trusted_proxy_cidrs), 0); http_request_t req; http_request_init(&req); add_header(&req, "Authorization", auth_header); add_header(&req, "X-Forwarded-For", "192.0.2.55"); - strncpy(req.client_ip, "198.51.100.10", sizeof(req.client_ip) - 1); + safe_strcpy(req.client_ip, "198.51.100.10", sizeof(req.client_ip), 0); user_t user; memset(&user, 0, sizeof(user)); diff --git a/tests/unit/test_logger_json.c b/tests/unit/test_logger_json.c index ccc72016..5c152bea 100644 --- a/tests/unit/test_logger_json.c +++ b/tests/unit/test_logger_json.c @@ -18,11 +18,12 @@ #include #include "unity.h" +#include "core/config.h" #include "core/logger.h" #include "core/logger_json.h" /* Shared temp file path used by setUp/tearDown */ -static char g_tmp_path[256]; +static char g_tmp_path[MAX_PATH_LENGTH]; static int g_initialized = 0; void setUp(void) { diff --git a/tests/unit/test_memory.c b/tests/unit/test_memory.c index 83ca75a0..7af0f75c 100644 --- a/tests/unit/test_memory.c +++ b/tests/unit/test_memory.c @@ -2,8 +2,7 @@ * @file test_memory.c * @brief Layer 2 unit tests — memory utility functions * - * Tests safe_malloc, safe_calloc, safe_strdup, safe_strcpy, safe_strcat, - * secure_zero_memory, and the memory tracking counters. + * Tests safe_malloc, safe_calloc, secure_zero_memory, and the memory tracking counters. */ #define _POSIX_C_SOURCE 200809L @@ -63,70 +62,6 @@ void test_safe_calloc_size_zero_returns_null(void) { TEST_ASSERT_NULL(p); } -/* ================================================================ - * safe_strdup - * ================================================================ */ - -void test_safe_strdup_basic(void) { - char *dup = safe_strdup("hello"); - TEST_ASSERT_NOT_NULL(dup); - TEST_ASSERT_EQUAL_STRING("hello", dup); - free(dup); -} - -void test_safe_strdup_empty_string(void) { - char *dup = safe_strdup(""); - TEST_ASSERT_NOT_NULL(dup); - TEST_ASSERT_EQUAL_STRING("", dup); - free(dup); -} - -void test_safe_strdup_null_returns_null(void) { - char *dup = safe_strdup(NULL); - TEST_ASSERT_NULL(dup); -} - -/* ================================================================ - * safe_strcpy - * ================================================================ */ - -void test_safe_strcpy_success(void) { - char buf[16]; - int rc = safe_strcpy(buf, "hello", sizeof(buf)); - TEST_ASSERT_EQUAL_INT(0, rc); - TEST_ASSERT_EQUAL_STRING("hello", buf); -} - -void test_safe_strcpy_truncation_returns_error(void) { - char buf[4]; - memset(buf, 0, sizeof(buf)); - int rc = safe_strcpy(buf, "hello_world", sizeof(buf)); - TEST_ASSERT_NOT_EQUAL(0, rc); - /* buf should still be null-terminated and contain truncated data */ - TEST_ASSERT_EQUAL_INT('\0', buf[sizeof(buf) - 1]); - TEST_ASSERT_EQUAL_STRING("hel", buf); -} - -/* ================================================================ - * safe_strcat - * ================================================================ */ - -void test_safe_strcat_success(void) { - char buf[32] = "hello"; - int rc = safe_strcat(buf, " world", sizeof(buf)); - TEST_ASSERT_EQUAL_INT(0, rc); - TEST_ASSERT_EQUAL_STRING("hello world", buf); -} - -void test_safe_strcat_overflow_returns_error(void) { - char buf[8] = "hello"; - int rc = safe_strcat(buf, "_overflow", sizeof(buf)); - TEST_ASSERT_NOT_EQUAL(0, rc); - /* Buffer should remain a valid, null-terminated string and keep its safe content */ - TEST_ASSERT_EQUAL_INT('\0', buf[sizeof(buf) - 1] == '\0' ? '\0' : buf[sizeof(buf) - 1]); - TEST_ASSERT_EQUAL_STRING_LEN("hello", buf, 5); -} - /* ================================================================ * secure_zero_memory * ================================================================ */ @@ -208,48 +143,6 @@ void test_safe_free_valid_pointer(void) { TEST_PASS(); } -/* ================================================================ - * safe_strcpy — additional NULL guard cases - * ================================================================ */ - -void test_safe_strcpy_null_dest_returns_error(void) { - int rc = safe_strcpy(NULL, "hello", 10); - TEST_ASSERT_NOT_EQUAL(0, rc); -} - -void test_safe_strcpy_null_src_returns_error(void) { - char buf[16]; - int rc = safe_strcpy(buf, NULL, sizeof(buf)); - TEST_ASSERT_NOT_EQUAL(0, rc); -} - -void test_safe_strcpy_zero_size_returns_error(void) { - char buf[16]; - int rc = safe_strcpy(buf, "hello", 0); - TEST_ASSERT_NOT_EQUAL(0, rc); -} - -/* ================================================================ - * safe_strcat — additional NULL guard cases - * ================================================================ */ - -void test_safe_strcat_null_dest_returns_error(void) { - int rc = safe_strcat(NULL, "world", 10); - TEST_ASSERT_NOT_EQUAL(0, rc); -} - -void test_safe_strcat_null_src_returns_error(void) { - char buf[16] = "hello"; - int rc = safe_strcat(buf, NULL, sizeof(buf)); - TEST_ASSERT_NOT_EQUAL(0, rc); -} - -void test_safe_strcat_zero_size_returns_error(void) { - char buf[16] = "hello"; - int rc = safe_strcat(buf, " world", 0); - TEST_ASSERT_NOT_EQUAL(0, rc); -} - /* ================================================================ * secure_zero_memory — edge cases * ================================================================ */ @@ -308,22 +201,6 @@ int main(void) { RUN_TEST(test_safe_calloc_nmemb_zero_returns_null); RUN_TEST(test_safe_calloc_size_zero_returns_null); - RUN_TEST(test_safe_strdup_basic); - RUN_TEST(test_safe_strdup_empty_string); - RUN_TEST(test_safe_strdup_null_returns_null); - - RUN_TEST(test_safe_strcpy_success); - RUN_TEST(test_safe_strcpy_truncation_returns_error); - RUN_TEST(test_safe_strcpy_null_dest_returns_error); - RUN_TEST(test_safe_strcpy_null_src_returns_error); - RUN_TEST(test_safe_strcpy_zero_size_returns_error); - - RUN_TEST(test_safe_strcat_success); - RUN_TEST(test_safe_strcat_overflow_returns_error); - RUN_TEST(test_safe_strcat_null_dest_returns_error); - RUN_TEST(test_safe_strcat_null_src_returns_error); - RUN_TEST(test_safe_strcat_zero_size_returns_error); - RUN_TEST(test_safe_realloc_grow); RUN_TEST(test_safe_realloc_shrink); RUN_TEST(test_safe_realloc_zero_frees_and_returns_null); diff --git a/tests/unit/test_onvif_soap_fault.c b/tests/unit/test_onvif_soap_fault.c index 18449755..d179857f 100644 --- a/tests/unit/test_onvif_soap_fault.c +++ b/tests/unit/test_onvif_soap_fault.c @@ -38,9 +38,7 @@ void test_soap12_full_fault(void) { "Sender not Authorized" ""; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), "TestFull"); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), "TestFull"); /* No crash = pass */ TEST_PASS(); } @@ -56,9 +54,7 @@ void test_soap12_code_no_subcode(void) { "Internal error" ""; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), "TestNoSubcode"); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), "TestNoSubcode"); TEST_PASS(); } @@ -72,9 +68,7 @@ void test_soap12_reason_only(void) { "Something went wrong" ""; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), "TestReasonOnly"); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), "TestReasonOnly"); TEST_PASS(); } @@ -88,9 +82,7 @@ void test_soap11_faultstring(void) { "Authentication failed" ""; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), "TestSOAP11"); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), "TestSOAP11"); TEST_PASS(); } @@ -102,9 +94,7 @@ void test_no_fault_element(void) { "" "OK"; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), "TestNoFault"); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), "TestNoFault"); TEST_PASS(); } @@ -113,9 +103,7 @@ void test_no_fault_element(void) { * ================================================================ */ void test_unparseable_xml(void) { const char *garbage = "This is not XML at all <><><"; - char *buf = strdup(garbage); - onvif_log_soap_fault(buf, strlen(buf), "TestGarbage"); - free(buf); + onvif_log_soap_fault(garbage, strlen(garbage), "TestGarbage"); TEST_PASS(); } @@ -141,9 +129,7 @@ void test_null_context(void) { "Error" ""; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), NULL); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), NULL); TEST_PASS(); } @@ -155,9 +141,7 @@ void test_empty_fault(void) { "" ""; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), "TestEmptyFault"); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), "TestEmptyFault"); TEST_PASS(); } @@ -174,9 +158,7 @@ void test_soap_env_namespace(void) { "Not supported" ""; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), "TestSOAPENV"); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), "TestSOAPENV"); TEST_PASS(); } @@ -191,9 +173,7 @@ void test_uppercase_s_namespace(void) { "Bad request" ""; - char *buf = strdup(xml); - onvif_log_soap_fault(buf, strlen(buf), "TestUpperS"); - free(buf); + onvif_log_soap_fault(xml, strlen(xml), "TestUpperS"); TEST_PASS(); } diff --git a/tests/unit/test_request_response.c b/tests/unit/test_request_response.c index cbdab54d..25121f54 100644 --- a/tests/unit/test_request_response.c +++ b/tests/unit/test_request_response.c @@ -14,6 +14,7 @@ #include "web/request_response.h" #include "web/libuv_connection.h" #include "core/logger.h" +#include "utils/strings.h" /* ---- Unity boilerplate ---- */ void setUp(void) {} @@ -54,8 +55,8 @@ void test_url_decode_no_encoding(void) { void test_get_header_found(void) { http_request_t req; http_request_init(&req); - strncpy(req.headers[0].name, "Content-Type", sizeof(req.headers[0].name) - 1); - strncpy(req.headers[0].value, "application/json", sizeof(req.headers[0].value) - 1); + safe_strcpy(req.headers[0].name, "Content-Type", sizeof(req.headers[0].name), 0); + safe_strcpy(req.headers[0].value, "application/json", sizeof(req.headers[0].value), 0); req.num_headers = 1; const char *v = http_request_get_header(&req, "Content-Type"); @@ -66,8 +67,8 @@ void test_get_header_found(void) { void test_get_header_case_insensitive(void) { http_request_t req; http_request_init(&req); - strncpy(req.headers[0].name, "content-type", sizeof(req.headers[0].name) - 1); - strncpy(req.headers[0].value, "text/plain", sizeof(req.headers[0].value) - 1); + safe_strcpy(req.headers[0].name, "content-type", sizeof(req.headers[0].name), 0); + safe_strcpy(req.headers[0].value, "text/plain", sizeof(req.headers[0].value), 0); req.num_headers = 1; const char *v = http_request_get_header(&req, "CONTENT-TYPE"); @@ -89,7 +90,7 @@ void test_get_header_not_found(void) { void test_get_query_param_found(void) { http_request_t req; http_request_init(&req); - strncpy(req.query_string, "page=2&limit=10", sizeof(req.query_string) - 1); + safe_strcpy(req.query_string, "page=2&limit=10", sizeof(req.query_string), 0); char val[32]; int rc = http_request_get_query_param(&req, "page", val, sizeof(val)); @@ -100,7 +101,7 @@ void test_get_query_param_found(void) { void test_get_query_param_not_found(void) { http_request_t req; http_request_init(&req); - strncpy(req.query_string, "a=1", sizeof(req.query_string) - 1); + safe_strcpy(req.query_string, "a=1", sizeof(req.query_string), 0); char val[32]; int rc = http_request_get_query_param(&req, "missing", val, sizeof(val)); @@ -114,7 +115,7 @@ void test_get_query_param_not_found(void) { void test_extract_path_param(void) { http_request_t req; http_request_init(&req); - strncpy(req.path, "/api/streams/42", sizeof(req.path) - 1); + safe_strcpy(req.path, "/api/streams/42", sizeof(req.path), 0); char param[32]; int rc = http_request_extract_path_param(&req, "/api/streams/", param, sizeof(param)); @@ -125,7 +126,7 @@ void test_extract_path_param(void) { void test_extract_path_param_encoded(void) { http_request_t req; http_request_init(&req); - strncpy(req.path, "/api/streams/four%20score%20and%202", sizeof(req.path) - 1); + safe_strcpy(req.path, "/api/streams/four%20score%20and%202", sizeof(req.path), 0); char param[32]; int rc = http_request_extract_path_param(&req, "/api/streams/", param, sizeof(param)); @@ -136,7 +137,7 @@ void test_extract_path_param_encoded(void) { void test_extract_path_param_with_query(void) { http_request_t req; http_request_init(&req); - strncpy(req.path, "/api/streams/42?test=yes", sizeof(req.path) - 1); + safe_strcpy(req.path, "/api/streams/42?test=yes", sizeof(req.path), 0); char param[32]; int rc = http_request_extract_path_param(&req, "/api/streams/", param, sizeof(param)); @@ -206,9 +207,9 @@ void test_libuv_connection_reset_preserves_client_ip(void) { libuv_connection_t *conn = libuv_connection_create(&server); TEST_ASSERT_NOT_NULL(conn); - strncpy(conn->request.client_ip, "192.0.2.55", sizeof(conn->request.client_ip) - 1); - strncpy(conn->request.user_agent, "RegressionAgent/1.0", sizeof(conn->request.user_agent) - 1); - strncpy(conn->request.path, "/api/auth/login", sizeof(conn->request.path) - 1); + safe_strcpy(conn->request.client_ip, "192.0.2.55", sizeof(conn->request.client_ip), 0); + safe_strcpy(conn->request.user_agent, "RegressionAgent/1.0", sizeof(conn->request.user_agent), 0); + safe_strcpy(conn->request.path, "/api/auth/login", sizeof(conn->request.path), 0); libuv_connection_reset(conn); diff --git a/tests/unit/test_storage_manager_retention.c b/tests/unit/test_storage_manager_retention.c index 4c35849c..92ed8d8a 100644 --- a/tests/unit/test_storage_manager_retention.c +++ b/tests/unit/test_storage_manager_retention.c @@ -18,6 +18,7 @@ #include "database/db_recordings.h" #include "database/db_streams.h" #include "storage/storage_manager.h" +#include "utils/strings.h" #define TEST_DB_PATH "/tmp/lightnvr_unit_storage_manager_retention.db" @@ -72,9 +73,9 @@ static void create_file(const char *path, size_t size) { static stream_config_t make_stream(const char *name) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, name, sizeof(s.name) - 1); - strncpy(s.url, "rtsp://camera/stream", sizeof(s.url) - 1); - strncpy(s.codec, "h264", sizeof(s.codec) - 1); + safe_strcpy(s.name, name, sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://camera/stream", sizeof(s.url), 0); + safe_strcpy(s.codec, "h264", sizeof(s.codec), 0); s.enabled = true; s.streaming_enabled = true; s.record = true; @@ -95,10 +96,10 @@ static void add_stream_with_quota(const char *name, int max_storage_mb) { static recording_metadata_t make_recording(const char *stream, const char *path, time_t start, uint64_t size_bytes) { recording_metadata_t m; memset(&m, 0, sizeof(m)); - strncpy(m.stream_name, stream, sizeof(m.stream_name) - 1); - strncpy(m.file_path, path, sizeof(m.file_path) - 1); - strncpy(m.codec, "h264", sizeof(m.codec) - 1); - strncpy(m.trigger_type, "scheduled", sizeof(m.trigger_type) - 1); + safe_strcpy(m.stream_name, stream, sizeof(m.stream_name), 0); + safe_strcpy(m.file_path, path, sizeof(m.file_path), 0); + safe_strcpy(m.codec, "h264", sizeof(m.codec), 0); + safe_strcpy(m.trigger_type, "scheduled", sizeof(m.trigger_type), 0); m.start_time = start; m.end_time = start + 60; m.size_bytes = size_bytes; @@ -197,7 +198,8 @@ void test_apply_retention_policy_skips_orphan_cleanup_when_ratio_is_too_high(voi add_stream_with_quota("orphan_cam", 0); for (int i = 0; i < 10; i++) { - char path[PATH_MAX], name[32]; + char path[PATH_MAX]; + char name[32]; recording_metadata_t rec; snprintf(name, sizeof(name), "high-ratio-%02d.mp4", i); mp4_path(path, sizeof(path), name); @@ -212,12 +214,14 @@ void test_apply_retention_policy_skips_orphan_cleanup_when_ratio_is_too_high(voi void test_apply_retention_policy_cleans_low_ratio_orphans_when_storage_is_healthy(void) { time_t now = time(NULL); - char missing_path[PATH_MAX] = "", existing_path[PATH_MAX] = ""; + char missing_path[PATH_MAX] = ""; + char existing_path[PATH_MAX] = ""; create_mp4_dir(); add_stream_with_quota("healthy_cam", 0); for (int i = 0; i < 10; i++) { - char path[PATH_MAX], name[32]; + char path[PATH_MAX]; + char name[32]; recording_metadata_t rec; snprintf(name, sizeof(name), "low-ratio-%02d.mp4", i); mp4_path(path, sizeof(path), name); @@ -240,7 +244,8 @@ void test_apply_retention_policy_skips_orphans_when_mp4_storage_is_inaccessible( add_stream_with_quota("offline_cam", 0); for (int i = 0; i < 10; i++) { - char path[PATH_MAX], name[32]; + char path[PATH_MAX]; + char name[32]; recording_metadata_t rec; snprintf(name, sizeof(name), "offline-%02d.mp4", i); mp4_path(path, sizeof(path), name); diff --git a/tests/unit/test_storage_retention_sqlite.c b/tests/unit/test_storage_retention_sqlite.c index 67dc48cf..2c59759d 100644 --- a/tests/unit/test_storage_retention_sqlite.c +++ b/tests/unit/test_storage_retention_sqlite.c @@ -24,6 +24,7 @@ #include #include "unity.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_recordings.h" @@ -40,10 +41,10 @@ static recording_metadata_t make_recording(const char *stream, bool protected_flag) { recording_metadata_t m; memset(&m, 0, sizeof(m)); - strncpy(m.stream_name, stream, sizeof(m.stream_name) - 1); - strncpy(m.file_path, path, sizeof(m.file_path) - 1); - strncpy(m.codec, "h264", sizeof(m.codec) - 1); - strncpy(m.trigger_type, trigger, sizeof(m.trigger_type) - 1); + safe_strcpy(m.stream_name, stream, sizeof(m.stream_name), 0); + safe_strcpy(m.file_path, path, sizeof(m.file_path), 0); + safe_strcpy(m.codec, "h264", sizeof(m.codec), 0); + safe_strcpy(m.trigger_type, trigger, sizeof(m.trigger_type), 0); m.start_time = start; m.end_time = start + 60; /* 1-minute clip */ m.size_bytes = 1024 * 1024; /* 1 MB */ diff --git a/tests/unit/test_stream_manager.c b/tests/unit/test_stream_manager.c index 511c95ae..32d491d5 100644 --- a/tests/unit/test_stream_manager.c +++ b/tests/unit/test_stream_manager.c @@ -16,14 +16,15 @@ #include "unity.h" #include "core/config.h" +#include "utils/strings.h" #include "video/stream_manager.h" #include "video/stream_state.h" static stream_config_t make_config(const char *name) { stream_config_t cfg; memset(&cfg, 0, sizeof(cfg)); - strncpy(cfg.name, name, sizeof(cfg.name) - 1); - strncpy(cfg.url, "rtsp://localhost/unit_test", sizeof(cfg.url) - 1); + safe_strcpy(cfg.name, name, sizeof(cfg.name), 0); + safe_strcpy(cfg.url, "rtsp://localhost/unit_test", sizeof(cfg.url), 0); cfg.enabled = true; cfg.width = 1920; cfg.height = 1080; diff --git a/tests/unit/test_stream_state.c b/tests/unit/test_stream_state.c index cff552fe..6eab5be1 100644 --- a/tests/unit/test_stream_state.c +++ b/tests/unit/test_stream_state.c @@ -14,14 +14,15 @@ #include "unity.h" #include "core/config.h" +#include "utils/strings.h" #include "video/stream_state.h" /* Build a minimal stream_config_t for testing */ static stream_config_t make_config(const char *name) { stream_config_t cfg; memset(&cfg, 0, sizeof(cfg)); - strncpy(cfg.name, name, sizeof(cfg.name) - 1); - strncpy(cfg.url, "rtsp://localhost/test", sizeof(cfg.url) - 1); + safe_strcpy(cfg.name, name, sizeof(cfg.name), 0); + safe_strcpy(cfg.url, "rtsp://localhost/test", sizeof(cfg.url), 0); cfg.enabled = true; cfg.width = 1280; cfg.height = 720; diff --git a/tests/unit/test_strings.c b/tests/unit/test_strings.c index af0262a0..16ed6c4d 100644 --- a/tests/unit/test_strings.c +++ b/tests/unit/test_strings.c @@ -2,6 +2,8 @@ * @file test_strings.c * @brief Layer 2 unit tests — ends_with() string helper * + * Tests safe_strdup, safe_strcpy, safe_strcat + * * Tests all edge cases for the ends_with() function: * - matching and non-matching suffixes * - empty string inputs @@ -11,6 +13,7 @@ #define _POSIX_C_SOURCE 200809L +#include #include "unity.h" #include "utils/strings.h" @@ -18,6 +21,128 @@ void setUp(void) {} void tearDown(void) {} +/* ================================================================ + * safe_strdup + * ================================================================ */ + +void test_safe_strdup_basic(void) { + char *dup = safe_strdup("hello"); + TEST_ASSERT_NOT_NULL(dup); + TEST_ASSERT_EQUAL_STRING("hello", dup); + free(dup); +} + +void test_safe_strdup_empty_string(void) { + char *dup = safe_strdup(""); + TEST_ASSERT_NOT_NULL(dup); + TEST_ASSERT_EQUAL_STRING("", dup); + free(dup); +} + +void test_safe_strdup_null_returns_null(void) { + char *dup = safe_strdup(NULL); + TEST_ASSERT_NULL(dup); +} + +/* ================================================================ + * safe_strcpy + * ================================================================ */ + +void test_safe_strcpy_success(void) { + char buf[16]; + int rc = safe_strcpy(buf, "hello", sizeof(buf), 0); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_EQUAL_STRING("hello", buf); +} + +void test_safe_strcpy_truncation_returns_success(void) { + char buf[4]; + memset(buf, 0, sizeof(buf)); + int rc = safe_strcpy(buf, "hello_world", sizeof(buf), 0); + TEST_ASSERT_EQUAL_INT(0, rc); + /* buf should still be null-terminated and contain truncated data */ + TEST_ASSERT_EQUAL_INT('\0', buf[sizeof(buf) - 1]); + TEST_ASSERT_EQUAL_STRING("hel", buf); +} + +void test_safe_strcpy_unterminated_source(void) { + char buf[16]; + memset(buf, 0, sizeof(buf)); + int rc = safe_strcpy(buf, "hello_world", sizeof(buf), 5); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_EQUAL_STRING("hello", buf); +} + +void test_safe_strcpy_unterminated_source_truncation(void) { + char buf[4]; + memset(buf, 0, sizeof(buf)); + int rc = safe_strcpy(buf, "hello_world", sizeof(buf), 5); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_EQUAL_STRING("hel", buf); +} + +/* ================================================================ + * safe_strcpy — additional NULL guard cases + * ================================================================ */ + +void test_safe_strcpy_null_dest_returns_error(void) { + int rc = safe_strcpy(NULL, "hello", 10, 0); + TEST_ASSERT_NOT_EQUAL(0, rc); +} + +void test_safe_strcpy_null_src_returns_error(void) { + char buf[16]; + int rc = safe_strcpy(buf, NULL, sizeof(buf), 0); + TEST_ASSERT_NOT_EQUAL(0, rc); +} + +void test_safe_strcpy_zero_size_returns_error(void) { + char buf[16]; + int rc = safe_strcpy(buf, "hello", 0, 0); + TEST_ASSERT_NOT_EQUAL(0, rc); +} + +/* ================================================================ + * safe_strcat + * ================================================================ */ + +void test_safe_strcat_success(void) { + char buf[32] = "hello"; + int rc = safe_strcat(buf, " world", sizeof(buf)); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_EQUAL_STRING("hello world", buf); +} + +void test_safe_strcat_overflow_returns_success(void) { + char buf[8] = "hello"; + int rc = safe_strcat(buf, "_overflow", sizeof(buf)); + TEST_ASSERT_EQUAL_INT(0, rc); + /* Buffer should remain a valid, null-terminated string and keep its safe content */ + TEST_ASSERT_EQUAL_INT('\0', buf[sizeof(buf) - 1] == '\0' ? '\0' : buf[sizeof(buf) - 1]); + TEST_ASSERT_EQUAL_STRING("hello_o", buf); +} + +/* ================================================================ + * safe_strcat — additional NULL guard cases + * ================================================================ */ + +void test_safe_strcat_null_dest_returns_error(void) { + int rc = safe_strcat(NULL, "world", 10); + TEST_ASSERT_NOT_EQUAL(0, rc); +} + +void test_safe_strcat_null_src_returns_error(void) { + char buf[16] = "hello"; + int rc = safe_strcat(buf, NULL, sizeof(buf)); + TEST_ASSERT_NOT_EQUAL(0, rc); +} + +void test_safe_strcat_zero_size_returns_error(void) { + char buf[16] = "hello"; + int rc = safe_strcat(buf, " world", 0); + TEST_ASSERT_NOT_EQUAL(0, rc); +} + /* ================================================================ * ends_with — matching cases * ================================================================ */ @@ -99,6 +224,92 @@ void test_ends_with_suffix_longer_than_str(void) { TEST_ASSERT_FALSE(ends_with("hi", "hello")); } +/* ================================================================ + * trim_ascii_whitespace + * ================================================================ */ + +void test_trim_ascii_whitespace_null(void) { + TEST_ASSERT_NULL(trim_ascii_whitespace(NULL)); +} + +void test_trim_ascii_whitespace_empty(void) { + char msg[] = ""; + char *trim = trim_ascii_whitespace(msg); + TEST_ASSERT_EQUAL_PTR(msg, trim); +} + +void test_trim_ascii_whitespace_spaces(void) { + char msg[] = " \t\t "; + char *trim = trim_ascii_whitespace(msg); + TEST_ASSERT_EQUAL_STRING("", trim); +} + +void test_trim_ascii_whitespace_newline(void) { + char msg[] = "\r\nhello_world\r\n\n"; + char *trim = trim_ascii_whitespace(msg); + TEST_ASSERT_EQUAL_STRING("hello_world", trim); +} + +void test_trim_ascii_whitespace_nonprint(void) { + char msg[] = "\a\ehello world\b\f"; + char *trim = trim_ascii_whitespace(msg); + TEST_ASSERT_EQUAL_STRING("hello world", trim); +} + +/* ================================================================ + * copy_trimmed_value + * ================================================================ */ + +void test_copy_trimmed_value_null_output(void) { + TEST_ASSERT_EQUAL_size_t(0, copy_trimmed_value(NULL, 20, "text", 0)); +} + +void test_copy_trimmed_value_zero_output_size(void) { + char buf[32]; + TEST_ASSERT_EQUAL_size_t(0, copy_trimmed_value(buf, 0, "text", 0)); +} + +void test_copy_trimmed_value_null_input(void) { + char buf[32]; + TEST_ASSERT_EQUAL_size_t(0, copy_trimmed_value(buf, 0, NULL, 0)); +} + +void test_copy_trimmed_value_success(void) { + char buf[32]; + size_t ret = copy_trimmed_value(buf, sizeof(buf), " hello world ", 0); + char expected[] = "hello world"; + TEST_ASSERT_EQUAL_STRING(buf, expected); + TEST_ASSERT_EQUAL_size_t(strlen(expected), ret); +} + +void test_copy_trimmed_value_truncate(void) { + char buf[4]; + size_t ret = copy_trimmed_value(buf, sizeof(buf), " hello world ", 0); + char expected[] = "hel"; + TEST_ASSERT_EQUAL_STRING(buf, expected); + TEST_ASSERT_EQUAL_size_t(strlen(expected), ret); +} + +void test_copy_trimmed_value_unterminated(void) { + char buf[32]; + size_t ret = copy_trimmed_value(buf, sizeof(buf), " hello world ", 8); + // Destination should *not* include the additional whitespace present in the + // unterminated input. + char expected[] = "hello"; + TEST_ASSERT_EQUAL_STRING(buf, expected); + TEST_ASSERT_EQUAL_size_t(strlen(expected), ret); +} + +void test_copy_trimmed_value_truncate_with_whitespace(void) { + char buf[7]; + size_t ret = copy_trimmed_value(buf, sizeof(buf), " hello world ", 0); + // Note that the (truncated) output *does* include whitespace at the end; that's + // because the truncation occurs after trimming the whitespace on the source. + char expected[] = "hello "; + TEST_ASSERT_EQUAL_STRING(buf, expected); + TEST_ASSERT_EQUAL_size_t(strlen(expected), ret); +} + /* ================================================================ * main * ================================================================ */ @@ -106,6 +317,24 @@ void test_ends_with_suffix_longer_than_str(void) { int main(void) { UNITY_BEGIN(); + RUN_TEST(test_safe_strdup_basic); + RUN_TEST(test_safe_strdup_empty_string); + RUN_TEST(test_safe_strdup_null_returns_null); + + RUN_TEST(test_safe_strcpy_success); + RUN_TEST(test_safe_strcpy_truncation_returns_success); + RUN_TEST(test_safe_strcpy_unterminated_source); + RUN_TEST(test_safe_strcpy_unterminated_source_truncation); + RUN_TEST(test_safe_strcpy_null_dest_returns_error); + RUN_TEST(test_safe_strcpy_null_src_returns_error); + RUN_TEST(test_safe_strcpy_zero_size_returns_error); + + RUN_TEST(test_safe_strcat_success); + RUN_TEST(test_safe_strcat_overflow_returns_success); + RUN_TEST(test_safe_strcat_null_dest_returns_error); + RUN_TEST(test_safe_strcat_null_src_returns_error); + RUN_TEST(test_safe_strcat_zero_size_returns_error); + RUN_TEST(test_ends_with_simple_match); RUN_TEST(test_ends_with_exact_match); RUN_TEST(test_ends_with_long_suffix); @@ -125,6 +354,20 @@ int main(void) { RUN_TEST(test_ends_with_suffix_longer_than_str); + RUN_TEST(test_trim_ascii_whitespace_null); + RUN_TEST(test_trim_ascii_whitespace_empty); + RUN_TEST(test_trim_ascii_whitespace_spaces); + RUN_TEST(test_trim_ascii_whitespace_newline); + RUN_TEST(test_trim_ascii_whitespace_nonprint); + + RUN_TEST(test_copy_trimmed_value_null_output); + RUN_TEST(test_copy_trimmed_value_zero_output_size); + RUN_TEST(test_copy_trimmed_value_null_input); + RUN_TEST(test_copy_trimmed_value_success); + RUN_TEST(test_copy_trimmed_value_truncate); + RUN_TEST(test_copy_trimmed_value_unterminated); + RUN_TEST(test_copy_trimmed_value_truncate_with_whitespace); + return UNITY_END(); } diff --git a/tests/unit/test_zone_filter.c b/tests/unit/test_zone_filter.c index cf456753..f530aa1c 100644 --- a/tests/unit/test_zone_filter.c +++ b/tests/unit/test_zone_filter.c @@ -18,12 +18,13 @@ #include #include "unity.h" +#include "core/config.h" +#include "utils/strings.h" #include "database/db_core.h" #include "database/db_zones.h" #include "database/db_streams.h" #include "video/zone_filter.h" #include "video/detection_result.h" -#include "core/config.h" #define TEST_DB_PATH "/tmp/lightnvr_unit_zone_filter_test.db" @@ -34,7 +35,7 @@ static detection_result_t make_result_1det(const char *label, float x, float y, detection_result_t r; memset(&r, 0, sizeof(r)); r.count = 1; - strncpy(r.detections[0].label, label, MAX_LABEL_LENGTH - 1); + safe_strcpy(r.detections[0].label, label, MAX_LABEL_LENGTH, 0); r.detections[0].x = x; r.detections[0].y = y; r.detections[0].width = w; @@ -50,13 +51,13 @@ static detection_zone_t make_square_zone(const char *stream, const char *name, float min_conf) { detection_zone_t z; memset(&z, 0, sizeof(z)); - strncpy(z.id, "zone-test-1", sizeof(z.id) - 1); - strncpy(z.stream_name, stream, sizeof(z.stream_name) - 1); - strncpy(z.name, name, sizeof(z.name) - 1); + safe_strcpy(z.id, "zone-test-1", sizeof(z.id), 0); + safe_strcpy(z.stream_name, stream, sizeof(z.stream_name), 0); + safe_strcpy(z.name, name, sizeof(z.name), 0); z.enabled = enabled; z.min_confidence = min_conf; if (classes) - strncpy(z.filter_classes, classes, sizeof(z.filter_classes) - 1); + safe_strcpy(z.filter_classes, classes, sizeof(z.filter_classes), 0); /* Square polygon: TL→TR→BR→BL */ z.polygon[0] = (zone_point_t){0.0f, 0.0f}; z.polygon[1] = (zone_point_t){0.5f, 0.0f}; @@ -69,8 +70,8 @@ static detection_zone_t make_square_zone(const char *stream, const char *name, static void ensure_stream(const char *name) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, name, sizeof(s.name) - 1); - strncpy(s.url, "rtsp://localhost/test", sizeof(s.url) - 1); + safe_strcpy(s.name, name, sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://localhost/test", sizeof(s.url), 0); s.enabled = true; s.width = 1920; s.height = 1080; @@ -232,12 +233,12 @@ void test_stream_object_include_keeps_matching_label(void) { /* Insert stream with include filter */ stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, "cam_inc", sizeof(s.name) - 1); - strncpy(s.url, "rtsp://localhost/inc", sizeof(s.url) - 1); + safe_strcpy(s.name, "cam_inc", sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://localhost/inc", sizeof(s.url), 0); s.enabled = true; s.width = 1920; s.height = 1080; s.fps = 30; s.protocol = STREAM_PROTOCOL_TCP; - strncpy(s.detection_object_filter, "include", sizeof(s.detection_object_filter) - 1); - strncpy(s.detection_object_filter_list, "person,car", sizeof(s.detection_object_filter_list) - 1); + safe_strcpy(s.detection_object_filter, "include", sizeof(s.detection_object_filter), 0); + safe_strcpy(s.detection_object_filter_list, "person,car", sizeof(s.detection_object_filter_list), 0); add_stream_config(&s); detection_result_t r = make_result_1det("person", 0.1f, 0.1f, 0.1f, 0.1f, 0.9f); @@ -248,12 +249,12 @@ void test_stream_object_include_keeps_matching_label(void) { void test_stream_object_include_drops_unmatched_label(void) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, "cam_inc2", sizeof(s.name) - 1); - strncpy(s.url, "rtsp://localhost/inc2", sizeof(s.url) - 1); + safe_strcpy(s.name, "cam_inc2", sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://localhost/inc2", sizeof(s.url), 0); s.enabled = true; s.width = 1920; s.height = 1080; s.fps = 30; s.protocol = STREAM_PROTOCOL_TCP; - strncpy(s.detection_object_filter, "include", sizeof(s.detection_object_filter) - 1); - strncpy(s.detection_object_filter_list, "person,car", sizeof(s.detection_object_filter_list) - 1); + safe_strcpy(s.detection_object_filter, "include", sizeof(s.detection_object_filter), 0); + safe_strcpy(s.detection_object_filter_list, "person,car", sizeof(s.detection_object_filter_list), 0); add_stream_config(&s); detection_result_t r = make_result_1det("bicycle", 0.1f, 0.1f, 0.1f, 0.1f, 0.9f); @@ -268,12 +269,12 @@ void test_stream_object_include_drops_unmatched_label(void) { void test_stream_object_exclude_drops_matching_label(void) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, "cam_exc", sizeof(s.name) - 1); - strncpy(s.url, "rtsp://localhost/exc", sizeof(s.url) - 1); + safe_strcpy(s.name, "cam_exc", sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://localhost/exc", sizeof(s.url), 0); s.enabled = true; s.width = 1920; s.height = 1080; s.fps = 30; s.protocol = STREAM_PROTOCOL_TCP; - strncpy(s.detection_object_filter, "exclude", sizeof(s.detection_object_filter) - 1); - strncpy(s.detection_object_filter_list, "cat", sizeof(s.detection_object_filter_list) - 1); + safe_strcpy(s.detection_object_filter, "exclude", sizeof(s.detection_object_filter), 0); + safe_strcpy(s.detection_object_filter_list, "cat", sizeof(s.detection_object_filter_list), 0); add_stream_config(&s); detection_result_t r = make_result_1det("cat", 0.1f, 0.1f, 0.1f, 0.1f, 0.9f); @@ -284,12 +285,12 @@ void test_stream_object_exclude_drops_matching_label(void) { void test_stream_object_exclude_keeps_unmatched_label(void) { stream_config_t s; memset(&s, 0, sizeof(s)); - strncpy(s.name, "cam_exc2", sizeof(s.name) - 1); - strncpy(s.url, "rtsp://localhost/exc2", sizeof(s.url) - 1); + safe_strcpy(s.name, "cam_exc2", sizeof(s.name), 0); + safe_strcpy(s.url, "rtsp://localhost/exc2", sizeof(s.url), 0); s.enabled = true; s.width = 1920; s.height = 1080; s.fps = 30; s.protocol = STREAM_PROTOCOL_TCP; - strncpy(s.detection_object_filter, "exclude", sizeof(s.detection_object_filter) - 1); - strncpy(s.detection_object_filter_list, "cat", sizeof(s.detection_object_filter_list) - 1); + safe_strcpy(s.detection_object_filter, "exclude", sizeof(s.detection_object_filter), 0); + safe_strcpy(s.detection_object_filter_list, "cat", sizeof(s.detection_object_filter_list), 0); add_stream_config(&s); detection_result_t r = make_result_1det("dog", 0.1f, 0.1f, 0.1f, 0.1f, 0.9f);