Verify JNI UTF-8 strings against a real JVM with checked JNI

This commit is contained in:
evgeny committed 2026-10-29 13:35:41 +01:00
1 parent 86f0ac7625
commit c0a4beeec4
3 files changed
+86

No files matched your search

@@ -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 этим тестом не запускаются.
@@ -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()
@@ -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;
}