commit bcec1f5651e9c9c5971bef9f7f02aef481418198
parent bc9af4105c60c4b7a5a4a6ed0e1ac0c583a35890
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun, 28 Jul 2024 20:13:27 +0200

fix(native): composer loader hook

Diffstat:
Mcomposer/src/main/ts/main.ts | 26+++++++++++---------------
Mcomposer/src/main/ts/types.ts | 3+++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/Feature.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt | 32+++++++++++++++++++++++---------
Mnative/rust/Cargo.lock | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mnative/rust/Cargo.toml | 1+
Mnative/rust/src/modules/composer_hook.rs | 239+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mnative/rust/src/modules/mod.rs | 1+
Anative/rust/src/modules/util/composer_utils.rs | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anative/rust/src/modules/util/mod.rs | 2++
10 files changed, 380 insertions(+), 115 deletions(-)

diff --git a/composer/src/main/ts/main.ts b/composer/src/main/ts/main.ts @@ -1,9 +1,9 @@ import { getConfig, log } from "./imports"; -import { Module } from "./types"; +import { Module, modules } from "./types"; -import operaDownloadButton from "./modules/operaDownloadButton"; -import firstCreatedUsername from "./modules/firstCreatedUsername"; -import bypassCameraRollSelectionLimit from "./modules/bypassCameraRollSelectionLimit"; +import "./modules/operaDownloadButton"; +import "./modules/firstCreatedUsername"; +import "./modules/bypassCameraRollSelectionLimit"; try { @@ -15,22 +15,18 @@ try { }) } - const modules: Module[] = [ - operaDownloadButton, - firstCreatedUsername, - bypassCameraRollSelectionLimit - ]; - - modules.forEach(module => { - if (!module.enabled(config)) return + modules.forEach(m => { + if (!m.enabled(config)) { + return + } try { - module.init(); + m.init(); } catch (e) { - console.error(`failed to initialize module ${module.name}`, e, e.stack); + console.error(`failed to initialize module ${m.name}`, e, e.stack); } }); - console.log("composer modules loaded!"); + console.log("modules loaded!"); } catch (e) { log("error", "Failed to load composer modules\n" + e + "\n" + e.stack) } diff --git a/composer/src/main/ts/types.ts b/composer/src/main/ts/types.ts @@ -39,6 +39,9 @@ export interface Module { init: () => void } +export const modules: Module[] = [] + export function defineModule<T extends Module>(module: T & Record<string, any>): T { + modules.push(module) return module } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/Feature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/Feature.kt @@ -15,7 +15,7 @@ abstract class Feature( runCatching { block() }.onFailure { - context.log.error("Failed to run onNextActivityCreate callback", it) + context.log.error("Failed to run defer callback", it) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt @@ -1,14 +1,22 @@ package me.rhunk.snapenhance.core.features.impl.experiments -import android.os.ParcelFileDescriptor +import android.app.Activity import android.view.View import android.widget.FrameLayout -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material3.* +import androidx.compose.material3.Button +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -48,7 +56,7 @@ class ComposerHooks: Feature("ComposerHooks") { verticalArrangement = Arrangement.spacedBy(16.dp) ) { var result by remember { mutableStateOf("") } - var codeContent by remember { mutableStateOf("") } + var codeContent by remember { mutableStateOf("return 1 + 2") } Text("Composer Console", fontSize = 18.sp, fontWeight = FontWeight.Bold) @@ -95,8 +103,11 @@ class ComposerHooks: Feature("ComposerHooks") { private val composerConsoleTag = Random.nextLong().toString() - private fun injectConsole() { - val root = context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content) + private fun injectConsole(activity: Activity) { + val root = activity.findViewById<FrameLayout>(android.R.id.content) ?: run { + context.log.warn("Unable to find root view. Can't inject console.") + return + } root.post { if (root.findViewWithTag<View>(composerConsoleTag) != null) return@post root.addView(createComposeView(root.context) { @@ -202,16 +213,20 @@ class ComposerHooks: Feature("ComposerHooks") { context.native.setComposerLoader(""" (() => { const _getImportsFunctionName = "$getImportsFunctionName"; $loaderScript })(); """.trimIndent().trim()) + } + + loadHooks() + onNextActivityCreate { activity -> if (config.composerConsole.get()) { - injectConsole() + injectConsole(activity) } } findClass("com.snapchat.client.composer.NativeBridge").apply { hook("registerNativeModuleFactory", HookStage.BEFORE) { param -> val moduleFactory = param.argNullable<Any>(1) ?: return@hook - if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString() != "DeviceBridge") return@hook + if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString()?.contains("DeviceBridge") != true) return@hook Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam -> val result = methodParam.getResult() as? MutableMap<String, Any?> ?: return@ephemeralHookObjectMethod result[getImportsFunctionName] = newComposerFunction { @@ -219,7 +234,6 @@ class ComposerHooks: Feature("ComposerHooks") { true } } - loadHooks() } } } diff --git a/native/rust/Cargo.lock b/native/rust/Cargo.lock @@ -78,6 +78,10 @@ name = "cc" version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +dependencies = [ + "jobserver", + "libc", +] [[package]] name = "cesu8" @@ -238,6 +242,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -319,6 +332,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -463,6 +482,7 @@ dependencies = [ "paste", "procfs", "serde_json", + "zstd", ] [[package]] @@ -722,3 +742,31 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.12+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/native/rust/Cargo.toml b/native/rust/Cargo.toml @@ -17,3 +17,4 @@ once_cell = "1.19.0" paste = "1.0.15" procfs = "0.16.0" serde_json = "1.0.120" +zstd = "0.13.2" diff --git a/native/rust/src/modules/composer_hook.rs b/native/rust/src/modules/composer_hook.rs @@ -1,8 +1,10 @@ -#![allow(dead_code, unused_mut)] +#![allow(dead_code, unused_imports)] -use std::{cell::Cell, ffi::{c_void, CStr}, sync::Mutex}; +use super::util::composer_utils::ComposerModule; +use std::{collections::HashMap, ffi::{c_void, CStr}, sync::Mutex}; use jni::{objects::JString, sys::jobject, JNIEnv}; -use crate::{common, config, def_hook, dobby_hook, sig, util::get_jni_string}; +use once_cell::sync::Lazy; +use crate::{common, config, def_hook, dobby_hook, dobby_hook_sym, sig, util::get_jni_string}; const JS_TAG_BIG_DECIMAL: i64 = -11; const JS_TAG_BIG_INT: i64 = -10; @@ -61,124 +63,181 @@ struct JsValue { tag: i64, } +static AASSET_MAP: Lazy<Mutex<HashMap<usize, Vec<u8>>>> = Lazy::new(|| Mutex::new(HashMap::new())); +static COMPOSER_LOADER_DATA: Mutex<Option<String>> = Mutex::new(None); + +def_hook!( + aasset_get_length, + i32, + |arg0: *mut c_void| { + if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) { + return buffer.len() as i32; + } + aasset_get_length_original.unwrap()(arg0) + } +); + +def_hook!( + aasset_get_buffer, + *const c_void, + |arg0: *mut c_void| { + 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) + } +); + +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); + + if !handle.is_null() && CStr::from_ptr(arg1).to_str().unwrap().ends_with("blizzard.composermodule") { + 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 composer_loader = COMPOSER_LOADER_DATA.lock().unwrap().clone().expect("No composer loader data"); + + 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 composer archive"); + let mut composer_module = ComposerModule::parse(decompressed).expect("Failed to parse composer module"); + + let mut tags = composer_module.get_tags(); + + for (tag1, tag2) in tags.iter_mut() { + if tag1.to_string().unwrap().ends_with("BlizzardEventLogger.js") { + let mut buffer = Vec::new(); + buffer.extend(composer_loader.as_bytes()); + buffer.extend(tag2.get_buffer()); + tag2.set_buffer(buffer); + debug!("composer loader injected in {}", tag1.to_string().unwrap()); + } + } + + composer_module.set_tags(tags); + + let compressed = composer_module.to_bytes(); + let compressed = zstd::stream::encode_all(&compressed[..], 3).expect("Failed to compress"); + + AASSET_MAP.lock().unwrap().insert(handle as usize, compressed); + } + handle + } +); + +def_hook!( + aasset_close, + c_void, + |handle: *mut c_void| { + AASSET_MAP.lock().unwrap().remove(&(handle as usize)); + aasset_close_original.unwrap()(handle) + } +); + +#[cfg(target_arch = "aarch64")] static mut GLOBAL_INSTANCE: Option<*mut c_void> = None; +#[cfg(target_arch = "aarch64")] static mut GLOBAL_CTX: Option<*mut c_void> = None; -static COMPOSER_LOADER_DATA: Mutex<Cell<Option<Box<String>>>> = Mutex::new(Cell::new(None)); +#[cfg(target_arch = "aarch64")] static mut JS_EVAL_ORIGINAL2: Option<unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut u8, usize, *const u8, u32) -> JsValue> = None; def_hook!( js_eval, *mut c_void, |arg0: *mut c_void, arg1: *mut c_void, arg2: *mut c_void, arg3: *const u8, arg4: *const u8, arg5: *const u8, arg6: *mut c_void, arg7: u32| { - let mut arg3 = arg3; - let mut arg4 = arg4; - let mut arg5 = arg5; - - if GLOBAL_INSTANCE.is_none() || GLOBAL_CTX.is_none() { - GLOBAL_INSTANCE = Some(arg0); - GLOBAL_CTX = Some(arg1); - - let mut loader_data = COMPOSER_LOADER_DATA.lock().unwrap(); - let mut loader_data = loader_data.get_mut(); - - let loader_data = loader_data.as_mut().unwrap(); - loader_data.push_str("\n"); - #[cfg(target_arch = "aarch64")] - { - loader_data.push_str(CStr::from_ptr(arg3).to_str().unwrap()); - arg3 = loader_data.as_mut_ptr(); - arg4 = loader_data.len() as *const u8; - } - - // On arm the original JS_Eval function is inlined so the arguments are shifted - #[cfg(target_arch = "arm")] - { - loader_data.push_str(CStr::from_ptr(arg4).to_str().unwrap()); - arg4 = loader_data.as_mut_ptr(); - arg5 = loader_data.len() as *const u8; + #[cfg(target_arch = "aarch64")] + { + if GLOBAL_INSTANCE.is_none() || GLOBAL_CTX.is_none() { + GLOBAL_INSTANCE = Some(arg0); + GLOBAL_CTX = Some(arg1); } - - debug!("injected composer loader!"); - } else { - COMPOSER_LOADER_DATA.lock().unwrap().take(); } - js_eval_original.unwrap()(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) } ); pub fn set_composer_loader(mut env: JNIEnv, _: *mut c_void, code: JString) { - let new_code = get_jni_string(&mut env, code).expect("Failed to get code"); - - COMPOSER_LOADER_DATA.lock().unwrap().replace(Some(Box::new(new_code))); + let new_code = get_jni_string(&mut env, code).expect("Failed to get composer loader code"); + COMPOSER_LOADER_DATA.lock().unwrap().replace(new_code); } #[allow(unreachable_code, unused_variables)] pub unsafe fn composer_eval(env: JNIEnv, _: *mut c_void, script: JString) -> jobject { - #[cfg(not(target_arch = "aarch64"))] + #[cfg(target_arch = "aarch64")] { - return env.new_string("Architecture not supported").unwrap().into_raw(); - } + let mut env = env; - let mut env = env; - - let script_str = get_jni_string(&mut env, script).expect("Failed to get script"); - let script_length = script_str.len(); - - let js_value = JS_EVAL_ORIGINAL2.expect("No js eval found")( - GLOBAL_INSTANCE.expect("No global instance found"), - GLOBAL_CTX.expect("No global context found"), - std::ptr::null_mut(), - (script_str + "\0").as_ptr() as *mut u8, - script_length, - "<eval>\0".as_ptr(), - 0 - ); - - let result: String = if js_value.tag == JS_TAG_STRING { - let string = js_value.u.ptr as *mut JsString; - CStr::from_ptr((*string).str8.as_ptr() as *const u8).to_str().unwrap().into() - } else if js_value.tag == JS_TAG_INT { - js_value.u.int32.to_string() - } else if js_value.tag == JS_TAG_BOOL { - if js_value.u.int32 == 1 { "true" } else { "false" }.into() - } else if js_value.tag == JS_TAG_NULL { - "null".into() - } else if js_value.tag == JS_TAG_UNDEFINED { - "undefined".into() - } else if js_value.tag == JS_TAG_OBJECT { - "[object]".into() - } else if js_value.tag == JS_TAG_FLOAT64 { - js_value.u.float64.to_string() - } else if js_value.tag == JS_TAG_EXCEPTION { - "Failed to evaluate script".into() - } else { - "[unknown tag ".to_owned() + &js_value.tag.to_string() + "]".into() - }; + let script_str = get_jni_string(&mut env, script).expect("Failed to get script"); + let script_length = script_str.len(); - env.new_string(result).unwrap().into_raw() + let js_value = JS_EVAL_ORIGINAL2.expect("No js eval found")( + GLOBAL_INSTANCE.expect("No global instance found"), + GLOBAL_CTX.expect("No global context found"), + std::ptr::null_mut(), + (script_str + "\0").as_ptr() as *mut u8, + script_length, + "<eval>\0".as_ptr(), + 0 + ); + + let result: String = if js_value.tag == JS_TAG_STRING { + let string = js_value.u.ptr as *mut JsString; + CStr::from_ptr((*string).str8.as_ptr() as *const u8).to_str().unwrap().into() + } else if js_value.tag == JS_TAG_INT { + js_value.u.int32.to_string() + } else if js_value.tag == JS_TAG_BOOL { + if js_value.u.int32 == 1 { "true" } else { "false" }.into() + } else if js_value.tag == JS_TAG_NULL { + "null".into() + } else if js_value.tag == JS_TAG_UNDEFINED { + "undefined".into() + } else if js_value.tag == JS_TAG_OBJECT { + "[object]".into() + } else if js_value.tag == JS_TAG_FLOAT64 { + js_value.u.float64.to_string() + } else if js_value.tag == JS_TAG_EXCEPTION { + "Failed to evaluate script".into() + } else { + "[unknown tag ".to_owned() + &js_value.tag.to_string() + "]".into() + }; + + return env.new_string(result).unwrap().into_raw() + } + + return env.new_string("Architecture not supported").unwrap().into_raw(); } pub fn init() { if !config::native_config().composer_hooks { return } + + dobby_hook_sym!("libandroid.so", "AAsset_getBuffer", aasset_get_buffer); + dobby_hook_sym!("libandroid.so", "AAsset_getLength", aasset_get_length); + dobby_hook_sym!("libandroid.so", "AAsset_close", aasset_close); + dobby_hook_sym!("libandroid.so", "AAssetManager_open", aasset_manager_open); - if let Some(signature) = sig::find_signature( - &common::CLIENT_MODULE, - "00 E4 00 6F 29 00 80 52 76 00 04 8B", -0x28, - "A1 B0 07 92 81 46", -0x7 - ) { - dobby_hook!(signature as *mut c_void, js_eval); - - unsafe { - JS_EVAL_ORIGINAL2 = Some(std::mem::transmute(js_eval_original.unwrap())); + #[cfg(target_arch = "aarch64")] + { + if let Some(signature) = sig::find_signature( + &common::CLIENT_MODULE, + "00 E4 00 6F 29 00 80 52 76 00 04 8B", -0x28, + "A1 B0 07 92 81 46", -0x7 + ) { + dobby_hook!(signature as *mut c_void, js_eval); + + unsafe { + JS_EVAL_ORIGINAL2 = Some(std::mem::transmute(js_eval_original.unwrap())); + } + + debug!("js_eval {:#x}", signature); + } else { + warn!("Unable to find js_eval signature"); } - - debug!("js_eval {:#x}", signature); - } else { - warn!("Unable to find js_eval signature"); } } diff --git a/native/rust/src/modules/mod.rs b/native/rust/src/modules/mod.rs @@ -1,3 +1,4 @@ +pub mod util; pub mod linker_hook; pub mod duplex_hook; pub mod sqlite_hook; diff --git a/native/rust/src/modules/util/composer_utils.rs b/native/rust/src/modules/util/composer_utils.rs @@ -0,0 +1,141 @@ +use std::{io::Error, string::FromUtf8Error}; + +#[derive(Debug, Clone)] +pub struct ModuleTag { + tag_type: u8, + buffer: Vec<u8>, +} + +impl ModuleTag { + pub fn new(module_type: u8, buffer: Vec<u8>) -> ModuleTag { + ModuleTag { + tag_type: module_type, + buffer, + } + } + + pub fn to_string(&self) -> Result<String, FromUtf8Error> { + Ok(String::from_utf8(self.buffer.clone())?) + } + + pub fn get_tag_type(&self) -> u8 { + self.tag_type + } + + pub fn get_size(&self) -> usize { + self.buffer.len() + } + + pub fn get_buffer(&self) -> &Vec<u8> { + &self.buffer + } + + pub fn set_buffer(&mut self, buffer: Vec<u8>) { + self.buffer = buffer; + } +} + +#[derive(Debug, Clone)] +pub struct ComposerModule { + tags: Vec<(ModuleTag, ModuleTag)>, // file name => file content +} + +impl ComposerModule { + pub fn parse(buffer: Vec<u8>) -> Result<ComposerModule, Error> { + let mut offset = 0; + let magic = u32::from_be_bytes([buffer[offset], buffer[offset + 1], buffer[offset + 2], buffer[offset + 3]]); + + offset += 4; + + if magic != 0x33c60001 { + return Err(Error::new(std::io::ErrorKind::InvalidData, "Invalid magic")); + } + + // skip content length + offset += 4; + + let mut tags = Vec::new(); + + loop { + if offset >= buffer.len() { + break; + } + + fn read_u24(buffer: &Vec<u8>, offset: &mut usize) -> Result<u32, Error> { + let b1 = buffer[*offset] as u32; + let b2 = buffer[*offset + 1] as u32; + let b3 = buffer[*offset + 2] as u32; + *offset += 3; + Ok(b1 | (b2 << 8) | (b3 << 16)) + } + + let tag_size = read_u24(&buffer, &mut offset)?; + let tag_type = buffer[offset]; + offset += 1; + let tag_buffer = buffer[offset..offset + tag_size as usize].to_vec(); + offset += tag_size as usize; + + let padding = 4 - (tag_size % 4); + + if padding != 4 { + offset += padding as usize; + } + + tags.push(ModuleTag::new(tag_type, tag_buffer)); + } + + let tags = tags.chunks(2).map(|chunk| { + (chunk[0].clone(), chunk[1].clone()) + }).collect(); + + Ok(ComposerModule { + tags, + }) + } + + pub fn to_bytes(&self) -> Vec<u8> { + let mut tag_buffer = Vec::new(); + + fn write_u24(buffer: &mut Vec<u8>, value: u32) { + buffer.push((value & 0xff) as u8); + buffer.push(((value >> 8) & 0xff) as u8); + buffer.push(((value >> 16) & 0xff) as u8); + } + + fn write_tag(buffer: &mut Vec<u8>, tag: ModuleTag) { + write_u24(buffer, tag.get_size() as u32); + buffer.push(tag.get_tag_type()); + buffer.extend(tag.get_buffer()); + + let padding = 4 - (tag.get_size() % 4); + + if padding != 4 { + for _ in 0..padding { + buffer.push(0); + } + } + } + + for (tag1, tag2) in &self.tags { + write_tag(&mut tag_buffer, tag1.clone()); + write_tag(&mut tag_buffer, tag2.clone()); + } + + 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(tag_buffer); + + buffer + } + + pub fn get_tags(&self) -> Vec<(ModuleTag, ModuleTag)> { + self.tags.clone() + } + + pub fn set_tags(&mut self, tags: Vec<(ModuleTag, ModuleTag)>) { + self.tags = tags; + } +} + diff --git a/native/rust/src/modules/util/mod.rs b/native/rust/src/modules/util/mod.rs @@ -0,0 +1 @@ +pub mod composer_utils;+ \ No newline at end of file