fix(PR): Multiple fixes by Kaladin

- Auto Open Engine refactor and implemented the engine stop button in the notification card.
- Media downloader stabilization fixes including Batch download fix.
- Implemented a new log filtering menu.
- Cargo Dependency updated to latest stable versions.
- Minor bug fixes.
This commit is contained in:
Sujal Sahu
2026-04-14 14:59:54 +00:00
parent 6f4e746a66
commit 4e3ef029a1
36 changed files with 1209 additions and 1663 deletions

36
native/rust/Cargo.lock generated
View File

@@ -107,9 +107,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.9.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "bzip2"
@@ -563,9 +563,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.1.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
name = "num-traits"
@@ -821,18 +821,28 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -948,22 +958,22 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.44"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde",
"serde_core",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "typenum"

View File

@@ -8,12 +8,17 @@ pub fn native_config() -> NativeConfig {
NATIVE_CONFIG.lock().unwrap().as_ref().expect("NativeConfig not loaded").clone()
}
/// Native configuration structure mirrored from 'NativeConfig.kt'.
///
/// CRITICAL: Fields must maintain 1:1 parity with the Kotlin implementation.
/// Mismatches in field names, types, or order will result in a JNI SIGABRT.
#[derive(Debug, Clone)]
pub(crate) struct NativeConfig {
pub disable_bitmoji: bool,
pub disable_metrics: bool,
pub valdi_hooks: bool,
pub custom_emoji_font_path: Option<String>,
pub debug_font_redirect: bool,
}
impl NativeConfig {
@@ -41,6 +46,7 @@ impl NativeConfig {
disable_metrics: get_boolean!("disableMetrics"),
valdi_hooks: get_boolean!("valdiHooks"),
custom_emoji_font_path: get_string!("customEmojiFontPath"),
debug_font_redirect: get_boolean!("debugFontRedirect"),
})
}
}

View File

@@ -1,7 +1,5 @@
use std::{cell::Cell, ffi::{CStr, CString}};
use nix::libc::{self, c_uint};
use crate::{config, def_hook, dobby_hook_sym};
thread_local! {
@@ -23,6 +21,8 @@ fn should_redirect_font(pathname: &str) -> bool {
file_name.contains("emoji")
|| file_name == "noto_color_emoji.ttf"
|| file_name == "samsungcoloremoji.ttf"
|| file_name == "coloremojifont.ttf"
|| file_name == "coloros_color_emoji.ttf"
)
}
@@ -33,7 +33,7 @@ fn open_custom_font_fd(flags: i32, mode: c_uint) -> Option<i32> {
Ok(c_font_path) => {
let fd = FONT_REDIRECT_IN_PROGRESS.with(|guard| {
let was_active = guard.replace(true);
let fd = unsafe { libc::openat(libc::AT_FDCWD, c_font_path.as_ptr() as *const u8, flags, mode) };
let fd = unsafe { libc::openat(libc::AT_FDCWD, c_font_path.as_ptr() as *const libc::c_char, flags, mode) };
guard.set(was_active);
fd
});
@@ -41,6 +41,9 @@ fn open_custom_font_fd(flags: i32, mode: c_uint) -> Option<i32> {
debug!("redirected emoji font open to {}", font_path);
Some(fd)
} else {
if config::native_config().debug_font_redirect {
panic!("Failed to open custom emoji font: {}", font_path);
}
debug!("failed to open custom emoji font path (fd={}): {}", fd, font_path);
None
}
@@ -61,7 +64,7 @@ def_hook!(
}
if !path.is_null() {
if let Ok(pathname) = CStr::from_ptr(path).to_str() {
if let Ok(pathname) = unsafe { CStr::from_ptr(path as *const libc::c_char) }.to_str() {
if should_redirect_font(pathname) {
if let Some(fd) = open_custom_font_fd(flags, mode) {
return fd;
@@ -74,10 +77,33 @@ def_hook!(
}
);
def_hook!(
openat_hook,
i32,
|dirfd: i32, path: *const u8, flags: i32, mode: c_uint| {
if FONT_REDIRECT_IN_PROGRESS.with(|guard| guard.get()) {
return openat_hook_original.unwrap()(dirfd, path, flags, mode);
}
if !path.is_null() {
if let Ok(pathname) = unsafe { CStr::from_ptr(path as *const libc::c_char) }.to_str() {
if should_redirect_font(pathname) {
if let Some(fd) = open_custom_font_fd(flags, mode) {
return fd;
}
}
}
}
openat_hook_original.unwrap()(dirfd, path, flags, mode)
}
);
pub fn init() {
if config::native_config().custom_emoji_font_path.is_none() {
return;
}
dobby_hook_sym!("libc.so", "open", open_hook);
dobby_hook_sym!("libc.so", "openat", openat_hook);
}

View File

@@ -42,6 +42,9 @@ pub struct ValdiModule {
impl ValdiModule {
pub fn parse(buffer: Vec<u8>) -> Result<ValdiModule, Error> {
if buffer.len() < 8 {
return Err(Error::new(std::io::ErrorKind::InvalidData, "Buffer too small"));
}
let mut offset = 0;
let magic = u32::from_be_bytes([buffer[offset], buffer[offset + 1], buffer[offset + 2], buffer[offset + 3]]);
@@ -62,14 +65,13 @@ impl ValdiModule {
}
fn read_u32(buffer: &Vec<u8>, offset: &mut usize) -> Result<(u32, bool), Error> {
let b1 = buffer[*offset] as u32;
let b2 = buffer[*offset + 1] as u32;
let b3 = buffer[*offset + 2] as u32;
let b4 = (buffer[*offset + 3] & 0x7f) as u32;
let has_padding = (buffer[*offset + 3] & 0x80) != 0;
let bytes = [buffer[*offset], buffer[*offset + 1], buffer[*offset + 2], buffer[*offset + 3]];
let value = u32::from_be_bytes(bytes);
let has_padding = (value & 0x80000000) != 0;
let tag_size = value & 0x7FFFFFFF;
*offset += 4;
Ok((b1 | (b2 << 8) | (b3 << 16) | (b4 << 24), has_padding))
Ok((tag_size, has_padding))
}
let (tag_size, has_padding) = read_u32(&buffer, &mut offset)?;
@@ -98,10 +100,8 @@ impl ValdiModule {
let mut tag_buffer = Vec::new();
fn write_u32(buffer: &mut Vec<u8>, value: u32, has_padding: bool) {
buffer.push(value as u8);
buffer.push(((value >> 8) & 0xff) as u8);
buffer.push(((value >> 16) & 0xff) as u8);
buffer.push(((value >> 24) & 0x7f) as u8 | if has_padding { 0x80 } else { 0x00 });
let encoded_value = (value & 0x7FFFFFFF) | if has_padding { 0x80000000 } else { 0 };
buffer.extend_from_slice(&encoded_value.to_be_bytes());
}
fn write_tag(buffer: &mut Vec<u8>, tag: ModuleTag) {
@@ -125,7 +125,7 @@ impl ValdiModule {
let mut buffer = Vec::new();
buffer.extend_from_slice(&[0x33, 0xc6, 0, 1]);
buffer.extend_from_slice(&(tag_buffer.len() as u32).to_le_bytes());
buffer.extend_from_slice(&(tag_buffer.len() as u32).to_be_bytes());
buffer.extend(tag_buffer);
buffer

View File

@@ -16,7 +16,10 @@ def_hook!(
if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) {
return buffer.len() as i32;
}
aasset_get_length_original.unwrap()(arg0)
if let Some(original) = aasset_get_length_original {
return original(arg0);
}
0
}
);
@@ -27,7 +30,10 @@ def_hook!(
if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) {
return buffer.as_ptr() as *const c_void;
}
aasset_get_buffer_original.unwrap()(arg0)
if let Some(original) = aasset_get_buffer_original {
return original(arg0);
}
std::ptr::null()
}
);
@@ -35,53 +41,89 @@ def_hook!(
aasset_manager_open,
*mut c_void,
|arg0: *mut c_void, arg1: *const u8, arg2: i32| {
let handle = aasset_manager_open_original.unwrap()(arg0, arg1, arg2);
let original_fn = match aasset_manager_open_original {
Some(f) => f,
None => return std::ptr::null_mut(),
};
let path = std::ffi::CStr::from_ptr(arg1).to_str().unwrap_or_default();
if !handle.is_null() && path.starts_with("bridge_observables") {
let asset_buffer = aasset_get_buffer_original.unwrap()(handle);
let asset_length = aasset_get_length_original.unwrap()(handle);
debug!("asset buffer: {:p}, length: {}", asset_buffer, asset_length);
let handle = original_fn(arg0, arg1, arg2);
if handle.is_null() {
return handle;
}
let loader_data = LOADER_DATA.lock().unwrap().clone().expect("No loader data");
let path_cstr = unsafe { std::ffi::CStr::from_ptr(arg1 as *const std::os::raw::c_char) };
let path = path_cstr.to_str().unwrap_or_default();
// Only target compressed Valdi bridge observables
if path.ends_with(".zst") && path.contains("bridge_observables") {
let get_buffer_fn = match aasset_get_buffer_original {
Some(f) => f,
None => return handle,
};
let get_length_fn = match aasset_get_length_original {
Some(f) => f,
None => return handle,
};
let archive_buffer: Vec<u8> = std::slice::from_raw_parts(asset_buffer as *const u8, asset_length as usize).to_vec();
let decompressed = zstd::stream::decode_all(&archive_buffer[..]).expect("Failed to decompress valdi archive");
let mut valdi_module = ValdiModule::parse(decompressed).expect("Failed to parse valdi module");
let mut tags = valdi_module.get_tags();
let mut new_tags = Vec::new();
for (tag1, _) in tags.iter_mut() {
let name = tag1.to_string().unwrap_or_default();
if !name.ends_with("src/utils/converter.js") {
continue;
}
let old_file_name = name.split_once(".").unwrap().0.to_owned() + rand::random::<u32>().to_string().as_str();
tag1.set_buffer((old_file_name.to_owned() + ".js").as_bytes().to_vec());
let original_module_path = path.split_once(".").unwrap().0.to_owned() + "/" + &old_file_name;
let hooked_module = format!("{};module.exports = require(\"{}\");", loader_data, original_module_path);
new_tags.push(
(
ModuleTag::new(true, name.as_bytes().to_vec()),
ModuleTag::new(true, hooked_module.as_bytes().to_vec())
)
);
debug!("Valdi loader injected in {}", name);
break;
let asset_buffer = get_buffer_fn(handle);
let asset_length = get_length_fn(handle);
if asset_buffer.is_null() || asset_length <= 0 {
return handle;
}
tags.extend(new_tags);
valdi_module.set_tags(tags);
let loader_data = match LOADER_DATA.lock().unwrap().clone() {
Some(data) => data,
None => {
warn!("Valdi loader data not yet initialized for {}", path);
return handle;
}
};
let compressed = valdi_module.to_bytes();
let compressed = zstd::stream::encode_all(&compressed[..], 3).expect("Failed to compress");
let archive_buffer: Vec<u8> = unsafe {
std::slice::from_raw_parts(asset_buffer as *const u8, asset_length as usize).to_vec()
};
let decompressed = match zstd::stream::decode_all(&archive_buffer[..]) {
Ok(data) => data,
Err(e) => {
error!("Failed to decompress Valdi archive {}: {}", path, e);
return handle;
}
};
AASSET_MAP.lock().unwrap().insert(handle as usize, compressed);
let valdi_module = match ValdiModule::parse(decompressed) {
Ok(module) => module,
Err(e) => {
error!("Failed to parse Valdi module {}: {}", path, e);
return handle;
}
};
let mut tags = valdi_module.get_tags();
let mut found = false;
for (tag1, tag2) in tags.iter_mut() {
let name = tag1.to_string().unwrap_or_default();
if name.ends_with("src/utils/converter.js") {
let mut hooked_content = loader_data.as_bytes().to_vec();
hooked_content.extend_from_slice(tag2.get_buffer());
*tag2 = ModuleTag::new(true, hooked_content);
found = true;
debug!("Valdi loader prepended to {}", name);
break;
}
}
if found {
let compressed = valdi_module.to_bytes();
match zstd::stream::encode_all(&compressed[..], 3) {
Ok(compressed_data) => {
AASSET_MAP.lock().unwrap().insert(handle as usize, compressed_data);
},
Err(e) => error!("Failed to re-compress Valdi module: {}", e),
}
}
}
handle
}
@@ -89,16 +131,19 @@ def_hook!(
def_hook!(
aasset_close,
c_void,
(),
|handle: *mut c_void| {
AASSET_MAP.lock().unwrap().remove(&(handle as usize));
aasset_close_original.unwrap()(handle)
if let Some(original) = aasset_close_original {
original(handle);
}
}
);
pub fn set_valdi_loader(mut env: JNIEnv, _: *mut c_void, code: JString) {
let new_code = get_jni_string(&mut env, code).expect("Failed to get loader code");
LOADER_DATA.lock().unwrap().replace(new_code);
if let Ok(new_code) = get_jni_string(&mut env, code) {
LOADER_DATA.lock().unwrap().replace(new_code);
}
}
pub fn init() {

View File

@@ -1,5 +1,12 @@
package me.eternal.purrfectsnap.nativelib
/**
* Configuration schema for the native layer.
*
* CRITICAL: This class MUST maintain 1:1 field parity with 'native/rust/src/config.rs'.
* Any modification to field names, types, or order without a corresponding change
* in the Rust implementation will cause a JNI SIGABRT (crash on launch).
*/
data class NativeConfig(
@JvmField
val disableBitmoji: Boolean = false,
@@ -9,4 +16,6 @@ data class NativeConfig(
val valdiHooks: Boolean = false,
@JvmField
val customEmojiFontPath: String? = null,
@JvmField
val debugFontRedirect: Boolean = false,
)