fix: sanitize mpv event channel strings for valid UTF-8

This commit is contained in:
edde746
2026-03-04 14:59:11 +01:00
parent 551bd83ffe
commit 4121d568c8
9 changed files with 190 additions and 24 deletions
@@ -35,6 +35,20 @@ class MpvPlayerCore(private val activity: Activity) :
// Guards MPVLib.create/destroy which share global native state
private val mpvLock = Object()
// JNI's NewStringUTF may produce Java Strings with lone surrogates when
// mpv passes invalid UTF-8 (log messages, system-encoded paths, etc.).
// Lone surrogates cause FormatException in Flutter's StandardMessageCodec
// when it encodes them as UTF-8. Re-encoding strips these invalid chars.
private fun safeString(s: String): String {
for (c in s) {
if (c.isSurrogate()) {
// Slow path: re-encode through UTF-8 to replace lone surrogates with U+FFFD
return s.encodeToByteArray().decodeToString()
}
}
return s // Fast path: no surrogates, string is safe
}
}
private var surfaceView: SurfaceView? = null
@@ -344,8 +358,9 @@ class MpvPlayerCore(private val activity: Activity) :
}
override fun eventProperty(property: String, value: String) {
val safe = safeString(value)
activity.runOnUiThread {
delegate?.onPropertyChange(property, value)
delegate?.onPropertyChange(property, safe)
}
}
@@ -376,11 +391,13 @@ class MpvPlayerCore(private val activity: Activity) :
MPVLib.MPV_LOG_LEVEL_TRACE -> "trace"
else -> "info"
}
val safePrefix = safeString(prefix)
val safeText = safeString(text)
activity.runOnUiThread {
delegate?.onEvent("log-message", mapOf(
"prefix" to prefix,
"prefix" to safePrefix,
"level" to levelStr,
"text" to text
"text" to safeText
))
}
}
+21 -6
View File
@@ -35,6 +35,21 @@ private class MetalLayer: CAMetalLayer {
}
}
/// Safely convert a C string to Swift String with UTF-8 validation.
/// Falls back to Latin-1 decoding if the bytes are not valid UTF-8.
/// mpv does not guarantee UTF-8 for log messages, error strings, or
/// system-encoded paths sending invalid UTF-8 through Flutter's
/// StandardMessageCodec causes FormatException crashes.
private func safeString(_ cstr: UnsafePointer<CChar>) -> String {
if let s = String(validatingUTF8: cstr) {
return s
}
// Latin-1 fallback: interpret each byte as its Unicode scalar
let len = strlen(cstr)
let buf = UnsafeBufferPointer(start: UnsafeRawPointer(cstr).assumingMemoryBound(to: UInt8.self), count: len)
return String(buf.map { Character(Unicode.Scalar($0)) })
}
/// Core MPV player using Metal rendering for iOS
class MpvPlayerCore: NSObject {
@@ -523,9 +538,9 @@ class MpvPlayerCore: NSObject {
case MPV_EVENT_LOG_MESSAGE:
if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) {
let msg = msgPtr.pointee
let prefix = msg.prefix.map { String(cString: $0) } ?? ""
let level = msg.level.map { String(cString: $0) } ?? ""
let text = msg.text.map { String(cString: $0) } ?? ""
let prefix = msg.prefix.map { safeString($0) } ?? ""
let level = msg.level.map { safeString($0) } ?? ""
let text = msg.text.map { safeString($0) } ?? ""
DispatchQueue.main.async {
self.delegate?.onEvent(name: "log-message", data: [
@@ -564,7 +579,7 @@ class MpvPlayerCore: NSObject {
case MPV_FORMAT_STRING:
if let ptr = property.data {
let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee
value = cstr.map { String(cString: $0) }
value = cstr.map { safeString($0) }
}
default:
@@ -611,7 +626,7 @@ class MpvPlayerCore: NSObject {
private func convertNode(_ node: mpv_node) -> Any? {
switch node.format {
case MPV_FORMAT_STRING:
return node.u.string.map { String(cString: $0) }
return node.u.string.map { safeString($0) }
case MPV_FORMAT_FLAG:
return node.u.flag != 0
@@ -636,7 +651,7 @@ class MpvPlayerCore: NSObject {
guard let list = node.u.list?.pointee else { return nil }
var dict = [String: Any]()
for i in 0..<Int(list.num) {
if let key = list.keys?[i].map({ String(cString: $0) }),
if let key = list.keys?[i].map({ safeString($0) }),
let val = convertNode(list.values[i])
{
dict[key] = val
+11
View File
@@ -50,6 +50,17 @@ endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Fetch simdutf for SIMD-accelerated UTF-8 validation.
# mpv strings are not guaranteed to be valid UTF-8; invalid bytes sent through
# Flutter's StandardMessageCodec cause FormatException crashes.
include(FetchContent)
FetchContent_Declare(
simdutf
URL https://github.com/simdutf/simdutf/releases/download/v6.4.2/singleheader.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(simdutf)
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+8
View File
@@ -28,10 +28,18 @@ pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
# Find epoxy (OpenGL loader).
pkg_check_modules(EPOXY REQUIRED IMPORTED_TARGET epoxy)
# Build simdutf as a static library from the single-header amalgamation.
add_library(simdutf STATIC "${simdutf_SOURCE_DIR}/simdutf.cpp")
target_include_directories(simdutf PUBLIC "${simdutf_SOURCE_DIR}")
target_compile_features(simdutf PUBLIC cxx_std_17)
# Suppress warnings in third-party code
target_compile_options(simdutf PRIVATE -w)
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::MPV)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::EPOXY)
target_link_libraries(${BINARY_NAME} PRIVATE simdutf)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+45 -4
View File
@@ -13,6 +13,47 @@
#include <clocale>
#include <cstring>
#include <string>
#include <simdutf.h>
// Sanitize a C string that may contain invalid UTF-8 sequences.
// Uses simdutf for SIMD-accelerated validation (fast path for valid strings),
// then falls back to iterative replacement with U+FFFD on the rare invalid case.
// mpv does not guarantee UTF-8 for log messages, error strings, or
// system-encoded paths — sending these unsanitized through Flutter's
// StandardMessageCodec causes FormatException crashes.
static std::string SanitizeUtf8(const char* input) {
if (!input) return std::string();
size_t len = strlen(input);
if (len == 0) return std::string();
// Fast path: SIMD-accelerated validation — almost all strings pass this
if (simdutf::validate_utf8(input, len)) {
return std::string(input, len);
}
// Slow path: find each invalid position, copy valid prefix, insert U+FFFD,
// skip the bad byte, and repeat.
std::string result;
result.reserve(len);
size_t pos = 0;
while (pos < len) {
auto r = simdutf::validate_utf8_with_errors(input + pos, len - pos);
// Copy the valid prefix up to the error
if (r.count > 0) {
result.append(input + pos, r.count);
}
pos += r.count;
if (r.error == simdutf::error_code::SUCCESS) {
break; // remaining tail is valid
}
// Replace the invalid byte with U+FFFD and skip it
result.append("\xEF\xBF\xBD");
pos++;
}
return result;
}
// Flutter on Linux uses EGL (OpenGL ES) for both X11 and Wayland.
static void* get_opengl_proc_address(void* ctx, const char* name) {
@@ -483,11 +524,11 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
FlValue* data = fl_value_new_map();
fl_value_set_string_take(data, "prefix",
fl_value_new_string(msg->prefix ? msg->prefix : ""));
fl_value_new_string(SanitizeUtf8(msg->prefix).c_str()));
fl_value_set_string_take(data, "level",
fl_value_new_string(msg->level ? msg->level : ""));
fl_value_new_string(SanitizeUtf8(msg->level).c_str()));
fl_value_set_string_take(data, "text",
fl_value_new_string(msg->text ? msg->text : ""));
fl_value_new_string(SanitizeUtf8(msg->text).c_str()));
SendEvent("log-message", data);
fl_value_unref(data);
break;
@@ -559,7 +600,7 @@ FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) {
switch (node->format) {
case MPV_FORMAT_STRING:
return fl_value_new_string(node->u.string ? node->u.string : "");
return fl_value_new_string(SanitizeUtf8(node->u.string).c_str());
case MPV_FORMAT_FLAG:
return fl_value_new_bool(node->u.flag != 0);
case MPV_FORMAT_INT64:
+21 -6
View File
@@ -35,6 +35,21 @@ private class MetalLayer: CAMetalLayer {
}
}
/// Safely convert a C string to Swift String with UTF-8 validation.
/// Falls back to Latin-1 decoding if the bytes are not valid UTF-8.
/// mpv does not guarantee UTF-8 for log messages, error strings, or
/// system-encoded paths sending invalid UTF-8 through Flutter's
/// StandardMessageCodec causes FormatException crashes.
private func safeString(_ cstr: UnsafePointer<CChar>) -> String {
if let s = String(validatingUTF8: cstr) {
return s
}
// Latin-1 fallback: interpret each byte as its Unicode scalar
let len = strlen(cstr)
let buf = UnsafeBufferPointer(start: UnsafeRawPointer(cstr).assumingMemoryBound(to: UInt8.self), count: len)
return String(buf.map { Character(Unicode.Scalar($0)) })
}
/// Core MPV player using Metal rendering
class MpvPlayerCore: NSObject {
@@ -506,9 +521,9 @@ class MpvPlayerCore: NSObject {
case MPV_EVENT_LOG_MESSAGE:
if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) {
let msg = msgPtr.pointee
let prefix = msg.prefix.map { String(cString: $0) } ?? ""
let level = msg.level.map { String(cString: $0) } ?? ""
let text = msg.text.map { String(cString: $0) } ?? ""
let prefix = msg.prefix.map { safeString($0) } ?? ""
let level = msg.level.map { safeString($0) } ?? ""
let text = msg.text.map { safeString($0) } ?? ""
DispatchQueue.main.async {
self.delegate?.onEvent(name: "log-message", data: [
@@ -547,7 +562,7 @@ class MpvPlayerCore: NSObject {
case MPV_FORMAT_STRING:
if let ptr = property.data {
let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee
value = cstr.map { String(cString: $0) }
value = cstr.map { safeString($0) }
}
default:
@@ -592,7 +607,7 @@ class MpvPlayerCore: NSObject {
private func convertNode(_ node: mpv_node) -> Any? {
switch node.format {
case MPV_FORMAT_STRING:
return node.u.string.map { String(cString: $0) }
return node.u.string.map { safeString($0) }
case MPV_FORMAT_FLAG:
return node.u.flag != 0
@@ -617,7 +632,7 @@ class MpvPlayerCore: NSObject {
guard let list = node.u.list?.pointee else { return nil }
var dict = [String: Any]()
for i in 0..<Int(list.num) {
if let key = list.keys?[i].map({ String(cString: $0) }),
if let key = list.keys?[i].map({ safeString($0) }),
let val = convertNode(list.values[i]) {
dict[key] = val
}
+11
View File
@@ -50,6 +50,17 @@ else()
set(MPV_LIB_DIR "${mpv_dev_SOURCE_DIR}")
endif()
# Fetch simdutf for SIMD-accelerated UTF-8 validation.
# mpv strings are not guaranteed to be valid UTF-8; invalid bytes sent through
# Flutter's StandardMessageCodec cause FormatException crashes.
include(FetchContent)
FetchContent_Declare(
simdutf
URL https://github.com/simdutf/simdutf/releases/download/v6.4.2/singleheader.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(simdutf)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "plezy")
+8
View File
@@ -35,11 +35,19 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTT
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Build simdutf as a static library from the single-header amalgamation.
add_library(simdutf STATIC "${simdutf_SOURCE_DIR}/simdutf.cpp")
target_include_directories(simdutf PUBLIC "${simdutf_SOURCE_DIR}")
target_compile_features(simdutf PUBLIC cxx_std_17)
# Suppress warnings in third-party code
target_compile_options(simdutf PRIVATE /W0)
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app flutter_wrapper_plugin)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib" "comctl32.lib")
target_link_libraries(${BINARY_NAME} PRIVATE "${MPV_LIB_DIR}/libmpv.dll.a")
target_link_libraries(${BINARY_NAME} PRIVATE simdutf)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_include_directories(${BINARY_NAME} PRIVATE "${MPV_INCLUDE_DIR}")
+45 -5
View File
@@ -2,6 +2,47 @@
#include <cstring>
#include <fstream>
#include <simdutf.h>
// Sanitize a C string that may contain invalid UTF-8 sequences.
// Uses simdutf for SIMD-accelerated validation (fast path for valid strings),
// then falls back to iterative replacement with U+FFFD on the rare invalid case.
// mpv does not guarantee UTF-8 for log messages, error strings, or
// system-encoded paths — sending these unsanitized through Flutter's
// StandardMessageCodec causes FormatException crashes.
static std::string SanitizeUtf8(const char* input) {
if (!input) return std::string();
size_t len = strlen(input);
if (len == 0) return std::string();
// Fast path: SIMD-accelerated validation — almost all strings pass this
if (simdutf::validate_utf8(input, len)) {
return std::string(input, len);
}
// Slow path: find each invalid position, copy valid prefix, insert U+FFFD,
// skip the bad byte, and repeat.
std::string result;
result.reserve(len);
size_t pos = 0;
while (pos < len) {
auto r = simdutf::validate_utf8_with_errors(input + pos, len - pos);
// Copy the valid prefix up to the error
if (r.count > 0) {
result.append(input + pos, r.count);
}
pos += r.count;
if (r.error == simdutf::error_code::SUCCESS) {
break; // remaining tail is valid
}
// Replace the invalid byte with U+FFFD and skip it
result.append("\xEF\xBF\xBD");
pos++;
}
return result;
}
static void LogToFile(const char* message) {
std::ofstream log("C:\\Users\\admin\\mpv_debug.log", std::ios::app);
@@ -349,11 +390,11 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
flutter::EncodableMap data;
data[flutter::EncodableValue("prefix")] =
flutter::EncodableValue(msg->prefix ? msg->prefix : "");
flutter::EncodableValue(SanitizeUtf8(msg->prefix));
data[flutter::EncodableValue("level")] =
flutter::EncodableValue(msg->level ? msg->level : "");
flutter::EncodableValue(SanitizeUtf8(msg->level));
data[flutter::EncodableValue("text")] =
flutter::EncodableValue(msg->text ? msg->text : "");
flutter::EncodableValue(SanitizeUtf8(msg->text));
SendEvent("log-message", data);
break;
}
@@ -435,8 +476,7 @@ void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
if (data) {
switch (data->format) {
case MPV_FORMAT_STRING:
value = flutter::EncodableValue(
data->u.string ? std::string(data->u.string) : std::string());
value = flutter::EncodableValue(SanitizeUtf8(data->u.string));
break;
case MPV_FORMAT_FLAG:
value = flutter::EncodableValue(data->u.flag != 0);