#include "voice_protocol.h" #include #include /* NOTE: Do not use ctype.h (isalnum/isalpha/isdigit/isspace) in this app. Those * functions read the `_ctype_` table, which is resolved from the flashed firmware * at runtime; the firmware's table does not behave correctly for side-loaded ELF * apps, so isalnum('a') can return false. Use explicit ASCII range checks instead. */ static bool is_digit(unsigned char c) { return c >= '0' && c <= '9'; } static bool is_space(unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f'; } static bool is_alnum(unsigned char c) { return is_digit(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } static bool copy_part(char* destination, size_t destination_size, const char* start, size_t length) { if (length == 0 || length >= destination_size) return false; memcpy(destination, start, length); destination[length] = '\0'; return true; } static bool is_loopback(const char* host) { return strcmp(host, "localhost") == 0 || strcmp(host, "::1") == 0 || strncmp(host, "127.", 4) == 0; } static bool valid_identifier(const char* value) { if (value == NULL || *value == '\0') return false; for (const unsigned char* p = (const unsigned char*)value; *p; ++p) { if (!is_alnum(*p) && *p != '-' && *p != '_' && *p != '.') return false; } return true; } bool pv_parse_endpoint(const char* url, PvEndpoint* endpoint) { if (url == NULL || endpoint == NULL || strncmp(url, "ws://", 5) != 0) return false; const char* authority = url + 5; const char* path = strchr(authority, '/'); const char* authority_end = path ? path : authority + strlen(authority); const char* colon = NULL; for (const char* p = authority; p < authority_end; ++p) { if (*p == ':') { if (colon != NULL) return false; colon = p; } if (is_space((unsigned char)*p) || *p == '@' || *p == '?' || *p == '#') return false; } size_t host_length = (size_t)((colon ? colon : authority_end) - authority); if (!copy_part(endpoint->host, sizeof(endpoint->host), authority, host_length) || is_loopback(endpoint->host)) return false; endpoint->port = 80; if (colon != NULL) { unsigned long port = 0; for (const char* p = colon + 1; p < authority_end; ++p) { if (!is_digit((unsigned char)*p)) return false; port = port * 10U + (unsigned long)(*p - '0'); if (port > 65535U) return false; } if (port == 0) return false; endpoint->port = (uint16_t)port; } return path == NULL ? copy_part(endpoint->path, sizeof(endpoint->path), "/", 1) : copy_part(endpoint->path, sizeof(endpoint->path), path, strlen(path)); } bool pv_make_start_json(char* out, size_t out_size, const char* session_id, const char* device_id) { if (out == NULL || !valid_identifier(session_id) || !valid_identifier(device_id)) return false; int written = snprintf(out, out_size, "{\"v\":1,\"event\":\"start\",\"session_id\":\"%s\",\"device_id\":\"%s\",\"audio\":{\"format\":\"pcm_s16le\",\"sample_rate\":16000,\"channels\":1,\"sample_width\":2}}", session_id, device_id); return written > 0 && (size_t)written < out_size; } bool pv_valid_pcm_chunk(size_t bytes) { return bytes > 0 && bytes <= PV_PCM_CHUNK_MAX && (bytes % 2U) == 0; } bool pv_valid_downstream_audio(const char* format, int sample_rate, int channels, int sample_width, size_t byte_length) { return format != NULL && strcmp(format, "pcm_s16le") == 0 && sample_rate > 0 && sample_rate <= 48000 && channels == 1 && sample_width == 2 && byte_length > 0 && byte_length <= PV_DOWNSTREAM_MAX && (byte_length % 2U) == 0; } bool pv_binary_matches_metadata(size_t expected_bytes, size_t received_bytes) { return expected_bytes > 0 && expected_bytes == received_bytes; } uint32_t pv_retry_delay_seconds(unsigned attempt) { uint32_t delay = 1; while (attempt > 0 && delay < 30) { delay *= 2; --attempt; } return delay > 30 ? 30 : delay; } PvState pv_disconnect_state(bool endpoint_valid) { return endpoint_valid ? PV_RECONNECTING : PV_FAILED; } const char* pv_state_label(PvState state) { switch (state) { case PV_CONNECTING: return "CONNECTING"; case PV_STREAMING: return "STREAMING"; case PV_RECONNECTING: return "RECONNECTING"; case PV_FAILED: return "FAILED"; default: return "FAILED"; } }