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:
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