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 // Guards MPVLib.create/destroy which share global native state
private val mpvLock = Object() 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 private var surfaceView: SurfaceView? = null
@@ -344,8 +358,9 @@ class MpvPlayerCore(private val activity: Activity) :
} }
override fun eventProperty(property: String, value: String) { override fun eventProperty(property: String, value: String) {
val safe = safeString(value)
activity.runOnUiThread { 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" MPVLib.MPV_LOG_LEVEL_TRACE -> "trace"
else -> "info" else -> "info"
} }
val safePrefix = safeString(prefix)
val safeText = safeString(text)
activity.runOnUiThread { activity.runOnUiThread {
delegate?.onEvent("log-message", mapOf( delegate?.onEvent("log-message", mapOf(
"prefix" to prefix, "prefix" to safePrefix,
"level" to levelStr, "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 /// Core MPV player using Metal rendering for iOS
class MpvPlayerCore: NSObject { class MpvPlayerCore: NSObject {
@@ -523,9 +538,9 @@ class MpvPlayerCore: NSObject {
case MPV_EVENT_LOG_MESSAGE: case MPV_EVENT_LOG_MESSAGE:
if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) { if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) {
let msg = msgPtr.pointee let msg = msgPtr.pointee
let prefix = msg.prefix.map { String(cString: $0) } ?? "" let prefix = msg.prefix.map { safeString($0) } ?? ""
let level = msg.level.map { String(cString: $0) } ?? "" let level = msg.level.map { safeString($0) } ?? ""
let text = msg.text.map { String(cString: $0) } ?? "" let text = msg.text.map { safeString($0) } ?? ""
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.onEvent(name: "log-message", data: [ self.delegate?.onEvent(name: "log-message", data: [
@@ -564,7 +579,7 @@ class MpvPlayerCore: NSObject {
case MPV_FORMAT_STRING: case MPV_FORMAT_STRING:
if let ptr = property.data { if let ptr = property.data {
let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee
value = cstr.map { String(cString: $0) } value = cstr.map { safeString($0) }
} }
default: default:
@@ -611,7 +626,7 @@ class MpvPlayerCore: NSObject {
private func convertNode(_ node: mpv_node) -> Any? { private func convertNode(_ node: mpv_node) -> Any? {
switch node.format { switch node.format {
case MPV_FORMAT_STRING: case MPV_FORMAT_STRING:
return node.u.string.map { String(cString: $0) } return node.u.string.map { safeString($0) }
case MPV_FORMAT_FLAG: case MPV_FORMAT_FLAG:
return node.u.flag != 0 return node.u.flag != 0
@@ -636,7 +651,7 @@ class MpvPlayerCore: NSObject {
guard let list = node.u.list?.pointee else { return nil } guard let list = node.u.list?.pointee else { return nil }
var dict = [String: Any]() var dict = [String: Any]()
for i in 0..<Int(list.num) { 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]) let val = convertNode(list.values[i])
{ {
dict[key] = val dict[key] = val
+11
View File
@@ -50,6 +50,17 @@ endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR}) 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. # System-level dependencies.
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 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). # Find epoxy (OpenGL loader).
pkg_check_modules(EPOXY REQUIRED IMPORTED_TARGET epoxy) 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. # Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::MPV) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::MPV)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::EPOXY) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::EPOXY)
target_link_libraries(${BINARY_NAME} PRIVATE simdutf)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+45 -4
View File
@@ -13,6 +13,47 @@
#include <clocale> #include <clocale>
#include <cstring> #include <cstring>
#include <string> #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. // Flutter on Linux uses EGL (OpenGL ES) for both X11 and Wayland.
static void* get_opengl_proc_address(void* ctx, const char* name) { 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(); FlValue* data = fl_value_new_map();
fl_value_set_string_take(data, "prefix", 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_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_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); SendEvent("log-message", data);
fl_value_unref(data); fl_value_unref(data);
break; break;
@@ -559,7 +600,7 @@ FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) {
switch (node->format) { switch (node->format) {
case MPV_FORMAT_STRING: 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: case MPV_FORMAT_FLAG:
return fl_value_new_bool(node->u.flag != 0); return fl_value_new_bool(node->u.flag != 0);
case MPV_FORMAT_INT64: 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 /// Core MPV player using Metal rendering
class MpvPlayerCore: NSObject { class MpvPlayerCore: NSObject {
@@ -506,9 +521,9 @@ class MpvPlayerCore: NSObject {
case MPV_EVENT_LOG_MESSAGE: case MPV_EVENT_LOG_MESSAGE:
if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) { if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) {
let msg = msgPtr.pointee let msg = msgPtr.pointee
let prefix = msg.prefix.map { String(cString: $0) } ?? "" let prefix = msg.prefix.map { safeString($0) } ?? ""
let level = msg.level.map { String(cString: $0) } ?? "" let level = msg.level.map { safeString($0) } ?? ""
let text = msg.text.map { String(cString: $0) } ?? "" let text = msg.text.map { safeString($0) } ?? ""
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.onEvent(name: "log-message", data: [ self.delegate?.onEvent(name: "log-message", data: [
@@ -547,7 +562,7 @@ class MpvPlayerCore: NSObject {
case MPV_FORMAT_STRING: case MPV_FORMAT_STRING:
if let ptr = property.data { if let ptr = property.data {
let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee
value = cstr.map { String(cString: $0) } value = cstr.map { safeString($0) }
} }
default: default:
@@ -592,7 +607,7 @@ class MpvPlayerCore: NSObject {
private func convertNode(_ node: mpv_node) -> Any? { private func convertNode(_ node: mpv_node) -> Any? {
switch node.format { switch node.format {
case MPV_FORMAT_STRING: case MPV_FORMAT_STRING:
return node.u.string.map { String(cString: $0) } return node.u.string.map { safeString($0) }
case MPV_FORMAT_FLAG: case MPV_FORMAT_FLAG:
return node.u.flag != 0 return node.u.flag != 0
@@ -617,7 +632,7 @@ class MpvPlayerCore: NSObject {
guard let list = node.u.list?.pointee else { return nil } guard let list = node.u.list?.pointee else { return nil }
var dict = [String: Any]() var dict = [String: Any]()
for i in 0..<Int(list.num) { 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]) { let val = convertNode(list.values[i]) {
dict[key] = val dict[key] = val
} }
+11
View File
@@ -50,6 +50,17 @@ else()
set(MPV_LIB_DIR "${mpv_dev_SOURCE_DIR}") set(MPV_LIB_DIR "${mpv_dev_SOURCE_DIR}")
endif() 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 name of the executable created for the application. Change this to change
# the on-disk name of your application. # the on-disk name of your application.
set(BINARY_NAME "plezy") 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. # Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 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 # Add dependency libraries and include directories. Add any application-specific
# dependencies here. # dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app flutter_wrapper_plugin) 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 "dwmapi.lib" "comctl32.lib")
target_link_libraries(${BINARY_NAME} PRIVATE "${MPV_LIB_DIR}/libmpv.dll.a") 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_SOURCE_DIR}")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_include_directories(${BINARY_NAME} PRIVATE "${MPV_INCLUDE_DIR}") target_include_directories(${BINARY_NAME} PRIVATE "${MPV_INCLUDE_DIR}")
+45 -5
View File
@@ -2,6 +2,47 @@
#include <cstring> #include <cstring>
#include <fstream> #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) { static void LogToFile(const char* message) {
std::ofstream log("C:\\Users\\admin\\mpv_debug.log", std::ios::app); 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; flutter::EncodableMap data;
data[flutter::EncodableValue("prefix")] = data[flutter::EncodableValue("prefix")] =
flutter::EncodableValue(msg->prefix ? msg->prefix : ""); flutter::EncodableValue(SanitizeUtf8(msg->prefix));
data[flutter::EncodableValue("level")] = data[flutter::EncodableValue("level")] =
flutter::EncodableValue(msg->level ? msg->level : ""); flutter::EncodableValue(SanitizeUtf8(msg->level));
data[flutter::EncodableValue("text")] = data[flutter::EncodableValue("text")] =
flutter::EncodableValue(msg->text ? msg->text : ""); flutter::EncodableValue(SanitizeUtf8(msg->text));
SendEvent("log-message", data); SendEvent("log-message", data);
break; break;
} }
@@ -435,8 +476,7 @@ void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
if (data) { if (data) {
switch (data->format) { switch (data->format) {
case MPV_FORMAT_STRING: case MPV_FORMAT_STRING:
value = flutter::EncodableValue( value = flutter::EncodableValue(SanitizeUtf8(data->u.string));
data->u.string ? std::string(data->u.string) : std::string());
break; break;
case MPV_FORMAT_FLAG: case MPV_FORMAT_FLAG:
value = flutter::EncodableValue(data->u.flag != 0); value = flutter::EncodableValue(data->u.flag != 0);