From c0a4beeec44d8e824ceaa823f38221c437bbebc2 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 11:58:47 +0100 Subject: [PATCH] Verify JNI UTF-8 strings against a real JVM with checked JNI --- .../chatgui-android/tests/jni_utf8/README.md | 10 +++++ tools/chatgui-android/tests/jni_utf8/run.py | 33 ++++++++++++++ .../tests/jni_utf8/test_jni_utf8.c | 43 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 tools/chatgui-android/tests/jni_utf8/README.md create mode 100644 tools/chatgui-android/tests/jni_utf8/run.py create mode 100644 tools/chatgui-android/tests/jni_utf8/test_jni_utf8.c diff --git a/tools/chatgui-android/tests/jni_utf8/README.md b/tools/chatgui-android/tests/jni_utf8/README.md new file mode 100644 index 00000000..8e7d0862 --- /dev/null +++ b/tools/chatgui-android/tests/jni_utf8/README.md @@ -0,0 +1,10 @@ +# Проверка строк JNI + +`python3 tools/chatgui-android/tests/jni_utf8/run.py` из корня запускает настоящий +код преобразования Android bridge в JVM с `-Xcheck:jni`. Нужны Linux, Python 3, +C99-компилятор, OpenSSL headers и JDK с javac/libjvm. Телефон не требуется. + +1024 преобразования UTF-16 → обычный UTF-8 → UTF-16 проверяют кириллицу, surrogate pair emoji, +кавычки и перевод строки. Проверяются отказ выделения C-буфера с повтором, отклонение +внутреннего NUL, пустая строка, NULL и сохранение уже ожидающего Java exception. +JVM проверяет корректность вызовов JNI; ART и Android SDK этим тестом не запускаются. diff --git a/tools/chatgui-android/tests/jni_utf8/run.py b/tools/chatgui-android/tests/jni_utf8/run.py new file mode 100644 index 00000000..3cbf1138 --- /dev/null +++ b/tools/chatgui-android/tests/jni_utf8/run.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Linux: проверить обычный UTF-8 настоящего Android bridge в JVM с -Xcheck:jni.""" +import os +from pathlib import Path +import shlex +import shutil +import subprocess +import tempfile + + +def main(): + directory = Path(__file__).resolve().parent + root = directory.parents[3] + javac = shutil.which("javac") + if not javac: + raise SystemExit("JDK with javac/libjvm required") + java_home = Path(javac).resolve().parents[1] + server = java_home / "lib/server" + includes = [directory.parent / "jni_log", java_home / "include", java_home / "include/linux", + root / "src", root / "src/transport_layer", root / "src/routing_layer", + root / "src/chat", root / "src/media_async", root / "lib"] + with tempfile.TemporaryDirectory(prefix="utun-jni-utf8-") as output: + binary = Path(output) / "test_jni_utf8" + command = shlex.split(os.environ.get("CC", "cc")) + ["-std=c99", "-D_DEFAULT_SOURCE", "-D__ANDROID__", "-DUSE_OPENSSL", + "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", "-pthread", "-Wno-incompatible-pointer-types"] + command += [f"-I{path}" for path in includes] + command += [str(directory / "test_jni_utf8.c"), f"-L{server}", f"-Wl,-rpath,{server}", "-ljvm", "-o", str(binary)] + subprocess.run(command, check=True, timeout=90) + subprocess.run([str(binary)], check=True, timeout=30) + + +if __name__ == "__main__": + main() diff --git a/tools/chatgui-android/tests/jni_utf8/test_jni_utf8.c b/tools/chatgui-android/tests/jni_utf8/test_jni_utf8.c new file mode 100644 index 00000000..55b75485 --- /dev/null +++ b/tools/chatgui-android/tests/jni_utf8/test_jni_utf8.c @@ -0,0 +1,43 @@ +/* Настоящая JVM с -Xcheck:jni: строки с emoji/кириллицей, NUL и сохранение чужого Java exception. */ +#include "../../jni_bridge/android_jni_bridge.c" + +static int fail_allocation; +void* u_malloc(size_t size) { return fail_allocation ? NULL : malloc(size); } +void u_free(void* pointer) { free(pointer); } +int __android_log_print(int priority, const char* tag, const char* format, ...) { + (void)priority; (void)tag; + va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); fputc('\n', stderr); return 0; +} +#define CHECK(value) do { if (!(value)) { fprintf(stderr, "JNI UTF-8 failed line=%d: %s\n", __LINE__, #value); return 1; } } while (0) + +int main(void) { + JavaVM* vm = NULL; JNIEnv* env = NULL; + JavaVMOption option = {.optionString = "-Xcheck:jni"}; + JavaVMInitArgs args = {.version = JNI_VERSION_1_6, .nOptions = 1, .options = &option}; + CHECK(JNI_CreateJavaVM(&vm, (void**)&env, &args) == JNI_OK); + const jchar characters[] = {0x0418, 0x043c, 0x044f, ' ', 0xd83d, 0xde00, '/', '"', '\n'}; + const char* expected = "Имя 😀/\"\n"; + jstring original = (*env)->NewString(env, characters, sizeof(characters) / sizeof(characters[0])); CHECK(original); + for (int i = 0; i < 1024; i++) { + char* bytes = jni_utf8(env, original); CHECK(bytes && !strcmp(bytes, expected)); + jstring restored = jni_text(env, bytes); u_free(bytes); CHECK(restored); + CHECK((*env)->GetStringLength(env, restored) == sizeof(characters) / sizeof(characters[0])); + const jchar* actual = (*env)->GetStringChars(env, restored, NULL); CHECK(actual && !memcmp(actual, characters, sizeof(characters))); + (*env)->ReleaseStringChars(env, restored, actual); (*env)->DeleteLocalRef(env, restored); + } + fail_allocation = 1; CHECK(jni_utf8(env, original) == NULL); fail_allocation = 0; + char* retried = jni_utf8(env, original); CHECK(retried && !strcmp(retried, expected)); u_free(retried); + const jchar nul[] = {'a', 0, 'b'}; + jstring embedded = (*env)->NewString(env, nul, 3); CHECK(embedded && !jni_utf8(env, embedded)); + CHECK(!jni_utf8(env, NULL) && !jni_text(env, NULL)); + jclass exception = (*env)->FindClass(env, "java/lang/IllegalStateException"); CHECK(exception); + CHECK(!(*env)->ThrowNew(env, exception, "preserved")); + CHECK(!jni_utf8(env, original) && !jni_text(env, expected) && (*env)->ExceptionCheck(env)); + (*env)->ExceptionClear(env); + jstring empty = jni_text(env, ""); CHECK(empty && !(*env)->GetStringLength(env, empty)); + (*env)->DeleteLocalRef(env, empty); (*env)->DeleteLocalRef(env, exception); + (*env)->DeleteLocalRef(env, embedded); (*env)->DeleteLocalRef(env, original); + CHECK(!(*env)->ExceptionCheck(env) && (*vm)->DestroyJavaVM(vm) == JNI_OK); + puts("JNI UTF-8: 1024 real JVM roundtrips, allocation failure/retry, embedded NUL and pending exception PASS"); + return 0; +}