fix: sanitize mpv event channel strings for valid UTF-8
This commit is contained in:
@@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user