/* Регрессии native bridge: config callbacks, сохранение идентичности и батчи настоящего Opus-декодера. */ #include #include #include #include #include #include #include #include #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../src/media_async/voice_file.h" #include "../src/config_parser.h" #include "../src/transport_layer/secure_channel.h" #include "../tools/chatgui-android/jni_bridge/android_jni_bridge.h" #include "../tools/chatgui-android/libutun_lite/instance_lite.h" #include "../tools/chatgui-android/libutun_lite/utun_config_api.h" #include "test_utils.h" #define REQUIRE(value) do { if (!(value)) { \ DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Android bridge check failed line=%d: %s", __LINE__, #value); return 1; \ } } while (0) static const char* config_string(const char* key, const char* def) { return !strcmp(key, CFG_KEY_NODE_NAME) ? "Android config" : def; } static int64_t config_int64(const char* key, int64_t def) { return !strcmp(key, "wide") ? INT64_C(0x123456789abcdef) : def; } static int config_int(const char* key, int def) { return !strcmp(key, CFG_KEY_CONTROL_PORT) ? 4321 : def; } static int config_bool(const char* key, int def) { return !strcmp(key, "enabled") ? 3 : def; } /* Таблица caller меняется после регистрации: сохранённые функции не должны зависеть от её памяти. */ static int check_provider_copy(void) { utun_config_provider_t provider = {config_string, config_int64, config_int, config_bool}; utun_config_set_provider(&provider); memset(&provider, 0, sizeof(provider)); REQUIRE(!strcmp(cfg_get_string(CFG_KEY_NODE_NAME, "fallback"), "Android config")); REQUIRE(cfg_get_int64("wide", -1) == INT64_C(0x123456789abcdef)); REQUIRE(cfg_get_int(CFG_KEY_CONTROL_PORT, -1) == 4321); REQUIRE(cfg_get_bool("enabled", -1) == 3); REQUIRE(!strcmp(cfg_get_string("missing", "fallback"), "fallback")); REQUIRE(cfg_get_int64("missing", 123) == 123 && cfg_get_int("missing", 456) == 456); REQUIRE(cfg_get_bool("missing", 789) == 789); provider.get_int = config_int; utun_config_set_provider(&provider); REQUIRE(cfg_get_int(CFG_KEY_CONTROL_PORT, -1) == 4321); REQUIRE(!strcmp(cfg_get_string(CFG_KEY_NODE_NAME, "fallback"), "fallback")); REQUIRE(cfg_get_int64("wide", 123) == 123 && cfg_get_bool("enabled", 456) == 456); utun_config_set_provider(NULL); REQUIRE(cfg_get_string(CFG_KEY_NODE_NAME, NULL) == NULL); REQUIRE(cfg_get_int(CFG_KEY_CONTROL_PORT, 9999) == 9999); printf("[PASS] config provider: owned table, optional callbacks and reset\n"); return 0; } static const char* bridge_string(const char* key) { return config_string(key, NULL); } static int64_t bridge_int64(const char* key) { return config_int64(key, 0); } static int bridge_int(const char* key) { return config_int(key, 0); } /* Рабочая регистрация bridge переживает возврат init, повторная регистрация и destroy снимают прежние callbacks. */ static int check_bridge_config(void) { struct utun_bridge_config config = {bridge_string, bridge_int64, bridge_int}; REQUIRE(utun_bridge_init(NULL, 0, NULL, NULL, &config)); memset(&config, 0, sizeof(config)); REQUIRE(!strcmp(cfg_get_string(CFG_KEY_NODE_NAME, NULL), "Android config")); REQUIRE(!strcmp(cfg_get_string("missing", "fallback"), "fallback")); REQUIRE(cfg_get_int64("wide", -1) == INT64_C(0x123456789abcdef)); REQUIRE(cfg_get_int(CFG_KEY_CONTROL_PORT, -1) == 4321 && cfg_get_bool("enabled", 99) == 99); REQUIRE(utun_bridge_init(NULL, 0, NULL, NULL, NULL)); REQUIRE(!strcmp(cfg_get_string(CFG_KEY_NODE_NAME, "fallback"), "fallback")); REQUIRE(cfg_get_int64("wide", 123) == 123 && cfg_get_int(CFG_KEY_CONTROL_PORT, 456) == 456); config.get_int = bridge_int; REQUIRE(utun_bridge_init(NULL, 0, NULL, NULL, &config)); REQUIRE(cfg_get_int(CFG_KEY_CONTROL_PORT, -1) == 4321); REQUIRE(cfg_get_string(CFG_KEY_NODE_NAME, NULL) == NULL && cfg_get_int64("wide", 789) == 789); REQUIRE(!utun_bridge_destroy()); REQUIRE(cfg_get_int(CFG_KEY_CONTROL_PORT, 9999) == 9999); REQUIRE(utun_bridge_init(NULL, 0, NULL, NULL, NULL)); REQUIRE(cfg_get_int(CFG_KEY_CONTROL_PORT, 9998) == 9998); REQUIRE(!utun_bridge_destroy()); printf("[PASS] bridge config: init, replacement, destroy and reinit\n"); return 0; } /* Настоящий unlink: все три файла, повторное удаление, ошибка одного пути и восстановление после неё. */ static int check_database_cleanup(const char* directory) { REQUIRE(utun_bridge_clear_database(NULL) == -1); REQUIRE(utun_bridge_clear_database("") == -1); REQUIRE(!utun_bridge_clear_database(directory)); const char* names[] = {"chats.db", "chats.db-wal", "chats.db-shm"}; char path[1024]; for (unsigned i = 0; i < 3; ++i) { snprintf(path, sizeof(path), "%s/%s", directory, names[i]); FILE* file = fopen(path, "wb"); REQUIRE(file); int written = fputs("fixture", file); int closed = fclose(file); REQUIRE(written >= 0 && !closed); } REQUIRE(!utun_bridge_clear_database(directory)); for (unsigned i = 0; i < 3; ++i) { snprintf(path, sizeof(path), "%s/%s", directory, names[i]); REQUIRE(test_unlink(path) == -1 && errno == ENOENT); } REQUIRE(!utun_bridge_clear_database(directory)); snprintf(path, sizeof(path), "%s/chats.db-wal", directory); REQUIRE(!utun_mkdir(path, 0700)); for (unsigned i = 0; i < 3; i += 2) { snprintf(path, sizeof(path), "%s/%s", directory, names[i]); FILE* file = fopen(path, "wb"); REQUIRE(file); int written = fputs("fixture", file); int closed = fclose(file); REQUIRE(written >= 0 && !closed); } REQUIRE(utun_bridge_clear_database(directory) == -1); for (unsigned i = 0; i < 3; i += 2) { snprintf(path, sizeof(path), "%s/%s", directory, names[i]); REQUIRE(test_unlink(path) == -1 && errno == ENOENT); } snprintf(path, sizeof(path), "%s/chats.db-wal", directory); REQUIRE(!test_rmdir(path)); REQUIRE(!utun_bridge_clear_database(directory)); printf("[PASS] database cleanup: exact files, missing files, real unlink failure and retry\n"); return 0; } /* Только полностью записанная пара, права 0600, согласованность X25519 и отсутствие оставшихся temp. */ static int check_identity_file(const char* directory, const struct global_config* identity) { char path[1024], text[256], expected[256]; snprintf(path, sizeof(path), "%s/keys.conf", directory); FILE* file = fopen(path, "rb"); REQUIRE(file); size_t length = fread(text, 1, sizeof(text), file); int read_error = ferror(file), closed = fclose(file); int expected_length = snprintf(expected, sizeof(expected), "public_key=%s\nprivate_key=%s\n", identity->my_public_key_hex, identity->my_private_key_hex); REQUIRE(!read_error && !closed && length == (size_t)expected_length && !memcmp(text, expected, length)); struct stat info; REQUIRE(!stat(path, &info) && (info.st_mode & 0777) == 0600); struct SC_MYKEYS keys; uint8_t derived[SC_PUBKEY_SIZE]; REQUIRE(sc_init_local_keys(&keys, identity->my_public_key_hex, identity->my_private_key_hex) == SC_OK); REQUIRE(sc_compute_public_key_from_private(keys.private_key, derived) == SC_OK); REQUIRE(!memcmp(keys.public_key, derived, sizeof(derived))); REQUIRE(sc_derive_node_id_from_pubkey(derived) == identity->my_node_id); DIR* entries = opendir(directory); REQUIRE(entries); int unexpected = 0; struct dirent* entry; while ((entry = readdir(entries))) unexpected += strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..") && strcmp(entry->d_name, "keys.conf"); REQUIRE(!closedir(entries) && !unexpected); return 0; } /* Настоящие ошибки open, flush и publish не меняют config и не разрушают прежний keys.conf. */ static int check_identity_persistence(const char* directory) { struct utun_config* config = u_calloc(1, sizeof(*config)); REQUIRE(config); REQUIRE(instance_lite_generate_keys(NULL) == -1); REQUIRE(instance_lite_generate_keys(config) == -1); /* Выбранный общим INI-парсером каталог: пробелы и последнее значение должны сохранять смысл. */ char ini[1024]; int length = snprintf(ini, sizeof(ini), "[global]\ndb_path=/nonexistent/utun-identity\n db_path = %s \n", directory); struct utun_config* parsed = parse_config_from_buf(ini, (size_t)length, "android-identity-test"); REQUIRE(parsed); config->global = parsed->global; free_config(parsed); REQUIRE(!instance_lite_generate_keys(config)); REQUIRE(!check_identity_file(directory, &config->global)); struct global_config previous = config->global; REQUIRE(!instance_lite_generate_keys(config)); REQUIRE(strcmp(previous.my_public_key_hex, config->global.my_public_key_hex)); REQUIRE(!check_identity_file(directory, &config->global)); previous = config->global; struct rlimit original, limited; REQUIRE(!getrlimit(RLIMIT_FSIZE, &original)); limited = original; limited.rlim_cur = 64; struct sigaction ignore = {0}, old_signal; ignore.sa_handler = SIG_IGN; REQUIRE(!sigemptyset(&ignore.sa_mask) && !sigaction(SIGXFSZ, &ignore, &old_signal)); int limit_result = setrlimit(RLIMIT_FSIZE, &limited); int result = limit_result ? 0 : instance_lite_generate_keys(config); int restored = setrlimit(RLIMIT_FSIZE, &original); int signal_restored = sigaction(SIGXFSZ, &old_signal, NULL); REQUIRE(!limit_result && !restored && !signal_restored && result == -1); REQUIRE(!memcmp(&previous, &config->global, sizeof(previous))); REQUIRE(!check_identity_file(directory, &previous)); char path[1024], backup[1024]; snprintf(path, sizeof(path), "%s/keys.conf", directory); snprintf(backup, sizeof(backup), "%s/identity-backup", directory); REQUIRE(!rename(path, backup) && !utun_mkdir(path, 0700)); REQUIRE(instance_lite_generate_keys(config) == -1); REQUIRE(!memcmp(&previous, &config->global, sizeof(previous))); REQUIRE(!test_rmdir(path) && !rename(backup, path)); REQUIRE(!check_identity_file(directory, &previous)); snprintf(config->global.db_path, sizeof(config->global.db_path), "%s/missing", directory); struct global_config missing = config->global; REQUIRE(instance_lite_generate_keys(config) == -1); REQUIRE(!memcmp(&missing, &config->global, sizeof(missing))); REQUIRE(!check_identity_file(directory, &previous)); config->global = previous; REQUIRE(!instance_lite_generate_keys(config) && !check_identity_file(directory, &config->global)); REQUIRE(!test_unlink(path)); u_free(config); printf("[PASS] identity persistence: coherent keys, private mode, parsed path, flush/open/publish failures and retry\n"); return 0; } /* Один и тот же файл декодируется покадрово и батчами: PCM и количество отсчётов должны совпасть. */ static int check_voice_batches(const char* path, int rate, int channels) { int frame = rate / 50 * channels, count = frame * 27; int16_t* input = u_calloc((size_t)count, sizeof(*input)); int16_t* reference = u_calloc((size_t)count, sizeof(*reference)); int16_t* output = u_calloc((size_t)(count + 8 * frame), sizeof(*output)); REQUIRE(input && reference && output); for (int i = 0; i < count; i++) input[i] = (int16_t)((i % 97 - 48) * 200); struct attachment_info info = {0}; REQUIRE(!voice_file_encode(input, (size_t)count, rate, channels, 1, path, &info)); int decoded_rate, decoded_channels, total; void* decoder = utun_bridge_voice_decode_open(path, &decoded_rate, &decoded_channels, &total); REQUIRE(decoder && decoded_rate == rate && decoded_channels == channels && total == count); for (int position = 0; position < count; position += frame) REQUIRE(utun_bridge_voice_decode_read(decoder, reference + position, frame) == frame); REQUIRE(!utun_bridge_voice_decode_read(decoder, output, frame)); utun_bridge_voice_decode_close(decoder); decoder = utun_bridge_voice_decode_open(path, &decoded_rate, &decoded_channels, &total); REQUIRE(decoder); REQUIRE(!utun_bridge_voice_decode_read(decoder, output, frame - 1)); int position = 0; while (position < count) { int expected = count - position < 7 * frame ? count - position : 7 * frame; output[position + expected] = 12345; int read = utun_bridge_voice_decode_read(decoder, output + position, 7 * frame + frame / 2); REQUIRE(read == expected && output[position + expected] == 12345); position += read; } REQUIRE(position == count && !memcmp(output, reference, (size_t)count * sizeof(*output))); REQUIRE(!utun_bridge_voice_decode_read(decoder, output, count + frame)); REQUIRE(!utun_bridge_voice_decode_read(decoder, output, count + frame)); utun_bridge_voice_decode_close(decoder); decoder = utun_bridge_voice_decode_open(path, &decoded_rate, &decoded_channels, &total); REQUIRE(decoder); output[count] = 12345; REQUIRE(utun_bridge_voice_decode_read(decoder, output, count + frame) == count); REQUIRE(output[count] == 12345 && !memcmp(output, reference, (size_t)count * sizeof(*output))); REQUIRE(!utun_bridge_voice_decode_read(decoder, output, count + frame)); utun_bridge_voice_decode_close(decoder); REQUIRE(!test_unlink(path)); u_free(input); u_free(reference); u_free(output); printf("[PASS] voice decode: rate=%d channels=%d, partial batch, oversized read, exact PCM and EOF\n", rate, channels); return 0; } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_DEBUG); int failed = check_provider_copy(); /* Проверки bridge требуют исправного владения таблицей провайдера. */ if (!failed) failed += check_bridge_config(); utun_config_set_provider(NULL); char directory[512], path[1024]; const char* temporary = getenv("TMPDIR"); int length = snprintf(directory, sizeof(directory), "%s/utun_android_bridge_XXXXXX", temporary ? temporary : "/tmp"); REQUIRE(length > 0 && length < (int)sizeof(directory)); REQUIRE(!test_mkdtemp(directory)); failed += check_database_cleanup(directory); failed += check_identity_persistence(directory); snprintf(path, sizeof(path), "%s/voice.opus", directory); const int rates[] = {8000, 12000, 16000, 24000, 48000}; for (unsigned i = 0; i < sizeof(rates) / sizeof(rates[0]); i++) for (int channels = 1; channels <= 2; channels++) failed += check_voice_batches(path, rates[i], channels); if (failed) return 1; REQUIRE(!test_rmdir(directory)); printf("test_android_bridge: PASS\n"); return 0; }