From 868d3b8dafec551ad2eb22f74d16f3125826873a Mon Sep 17 00:00:00 2001 From: nerodesu017 <46645625+nerodesu017@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:15:05 +0200 Subject: [PATCH 1/5] make mappers work locally --- src/commands/bundle.rs | 9 +- src/commands/serve.rs | 203 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 203 insertions(+), 9 deletions(-) diff --git a/src/commands/bundle.rs b/src/commands/bundle.rs index 1531be1..c8016d4 100644 --- a/src/commands/bundle.rs +++ b/src/commands/bundle.rs @@ -65,8 +65,13 @@ fn compute_hashes(file_paths: &mut Vec) -> Result let mut hashes: Vec<(String, String)> = Vec::new(); for file_path in file_paths { - let file_content = std::fs::read(file_path.clone())?; - let hash = format!("{:x}", sha2::Sha256::digest(&file_content)); + let file_content = std::fs::read_to_string(file_path.clone())?; + + let mut hasher = sha2::Sha256::new(); + let normalized = file_content.replace("\r\n", "\n"); + hasher.update(normalized.as_bytes()); + let hash = format!("{:x}", hasher.finalize()); + hashes.push((file_path.to_string_lossy().to_string(), hash)); } diff --git a/src/commands/serve.rs b/src/commands/serve.rs index ef5c7c9..d2bf7cd 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -32,6 +32,16 @@ struct FlowQueryV3 { alias: String, } +#[derive(Deserialize)] +struct ResolveMapperAliasQuery { + alias: String, +} + +#[derive(Deserialize)] +struct ResolveMapperFileQuery { + hash: String, +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum LuaScriptOwnerType { @@ -50,6 +60,13 @@ struct FlowResponse { owner_type: LuaScriptOwnerType, } +#[derive(Serialize)] +// #[serde(rename_all = "camelCase")] +struct MapperResponse { + mapper_hash: String, + mapper_url: String, +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct SessionResponse { @@ -186,14 +203,11 @@ static PLATFORM_VECTOR: OnceLock> = OnceLock::new(); static ALIAS_TO_FLOW_MAP_AND_PLATFORM_INDEX: OnceLock> = OnceLock::new(); +static MAPPERS_FOLDER: OnceLock = OnceLock::new(); +static MAPPERS_CONFIG: OnceLock = OnceLock::new(); + pub fn get_platform_vector(config: &Config) -> &Vec { - PLATFORM_VECTOR.get_or_init(|| { - config - .platforms - .iter() - .map(SimplePlatform::from) - .collect() - }) + PLATFORM_VECTOR.get_or_init(|| config.platforms.iter().map(SimplePlatform::from).collect()) } pub fn get_alias_to_platform_index_map(config: &Config) -> &HashMap { @@ -267,6 +281,158 @@ async fn sessions() -> Json { static SHOULD_REBUNDLE: OnceLock = OnceLock::new(); +static MAPPER_TO_HASH: OnceLock> = OnceLock::new(); +static HASH_TO_MAPPER: OnceLock> = OnceLock::new(); + +async fn resolve_mapper_file( + headers: axum::http::HeaderMap, + Query(query): Query, +) -> Response { + let wants_binary = headers + .get(axum::http::header::ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("application/octet-stream")) + .unwrap_or(false); + + match HASH_TO_MAPPER.get() { + None => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "Hash to mapper map not initialized", + ) + .into_response(); + } + Some(hash_to_mapper) => match hash_to_mapper.get(&query.hash) { + None => { + return (axum::http::StatusCode::NOT_FOUND, "Mapper not found").into_response(); + } + Some(mapper) => { + // now we have to get the actual alias back and return the file content + match MAPPERS_FOLDER.get() { + None => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "Mappers folder not initialized", + ) + .into_response(); + } + Some(mappers_folder) => match MAPPERS_CONFIG.get() { + None => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "Mapper config not initialized", + ) + .into_response(); + } + Some(mappers_config) => { + let file_path = PathBuf::from(mappers_folder) + .join(mappers_config.settings.output_directory.clone()) + .join(format!("{}.bundle.luau", mapper)); + + if wants_binary { + let file_bytes = + fs::read(&file_path).map_err(|e| e.to_string()).unwrap(); + return ( + [( + axum::http::header::CONTENT_TYPE, + "application/octet-stream", + )], + file_bytes, + ) + .into_response(); + } else { + let file_content = fs::read_to_string(file_path) + .map_err(|e| e.to_string()) + .unwrap(); + return Json(file_content).into_response(); + } + } + }, + } + } + }, + } +} +async fn resolve_mapper_alias(Query(query): Query) -> Response { + match MAPPER_TO_HASH.get() { + None => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "Mapper to hash map not initialized", + ) + .into_response(); + } + Some(mapper_to_hash) => match mapper_to_hash.get(&query.alias) { + None => { + return (axum::http::StatusCode::NOT_FOUND, "Mapper not found").into_response(); + } + Some(hash) => { + return Json(MapperResponse { + mapper_hash: hash.clone(), + // http://localhost:8080/resolve_mapper_file?hash=... + mapper_url: format!("http://localhost:8080/resolve_mapper_file?hash={}", hash), + }) + .into_response(); + } + }, + } +} + +fn initialize_mappers_folder_config_and_hashmaps( + mappers_location: &str, +) -> Result<(), Box> { + // try to read the file + let file_content = fs::read_to_string(mappers_location).map_err(|e| e.to_string())?; + + // first line of the mappers file + let mappers_folder = file_content + .lines() + .next() + .ok_or("No mappers folder found")? + .to_string(); + + MAPPERS_FOLDER.set(mappers_folder.clone()).unwrap(); // should be initialized only once + MAPPERS_CONFIG + .set( + Config::from_file( + PathBuf::from(mappers_folder.clone()) + .join("opacity.toml") + .to_str() + .unwrap(), + ) + .unwrap(), + ) + .unwrap(); // should be initialized only once + + // initialize the hashmaps + let mut mapper_to_hash = HashMap::new(); + let mut hash_to_mapper = HashMap::new(); + + // so now we have to set the hashmaps + // we first need to read `mapper-index.json` from the mappers folder + let mapper_index_path = PathBuf::from(mappers_folder).join("mapper-index.json"); + let mapper_index_content = fs::read_to_string(mapper_index_path).map_err(|e| e.to_string())?; + + #[derive(Deserialize)] + struct MapperValue { + luau_hash: String, + #[allow(dead_code)] + schema_hash: String, + } + + let mapper_index: HashMap = serde_json::from_str(&mapper_index_content)?; + + for (mapper, value) in mapper_index.iter() { + mapper_to_hash.insert(mapper.clone(), value.luau_hash.clone()); + hash_to_mapper.insert(value.luau_hash.clone(), mapper.clone()); + } + + MAPPER_TO_HASH.set(mapper_to_hash).unwrap(); // should be initialized only once + HASH_TO_MAPPER.set(hash_to_mapper).unwrap(); // should be initialized only once + + Ok(()) +} + pub async fn serve( config_path: &str, should_rebundle: bool, @@ -275,6 +441,27 @@ pub async fn serve( get_alias_to_flow_map_and_platform_index(&Config::from_file(config_path).unwrap()); SHOULD_REBUNDLE.get_or_init(|| should_rebundle); + match initialize_mappers_folder_config_and_hashmaps("./mappers.location") { + Ok(_) => { + info!( + "Mappers folder and config initialized, serving mappers from folder: {}", + MAPPERS_FOLDER.get().unwrap() + ); + } + // if there is no mappers.location file, we are not going to serve mappers + Err(e) => match e.to_string().as_str() { + "File not found" => { + info!("No mappers.location file found, not serving mappers"); + } + "No mappers folder found" => { + info!("No mappers folder found, not serving mappers"); + } + _ => { + return Err(e); + } + }, + } + let port = 8080; let addr = SocketAddr::from(([0, 0, 0, 0], port)); @@ -297,6 +484,8 @@ pub async fn serve( .route("/v2/flows", get(flows)) .route("/v3/flows", get(flowsv3_v2)) .route("/sessions", post(sessions)) + .route("/resolve_mapper_alias", get(resolve_mapper_alias)) // this returns { mapper_hash: String, mapper_url: String } + .route("/resolve_mapper_file", get(resolve_mapper_file)) // this returns the file content .layer(middleware); info!( From c9d029939e9e1e285b5f0f238ba2815f0eda137c Mon Sep 17 00:00:00 2001 From: nerodesu017 <46645625+nerodesu017@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:38:15 +0200 Subject: [PATCH 2/5] serve: fix serving mapper files --- src/commands/serve.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/commands/serve.rs b/src/commands/serve.rs index d2bf7cd..541dbc2 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -344,7 +344,7 @@ async fn resolve_mapper_file( let file_content = fs::read_to_string(file_path) .map_err(|e| e.to_string()) .unwrap(); - return Json(file_content).into_response(); + return (axum::http::StatusCode::OK, file_content).into_response(); } } }, @@ -433,6 +433,19 @@ fn initialize_mappers_folder_config_and_hashmaps( Ok(()) } +async fn received_file(body: String) -> Response { + // The body is a JSON-stringified string (e.g., "\"actual content\"") + // Parse it to get the actual string content + let content: String = serde_json::from_str(&body).unwrap(); + fs::write("received_file", content).unwrap(); + (axum::http::StatusCode::OK, Json(serde_json::json!({ "message": "File received" }))).into_response() +} + +async fn received_binary_file(body: axum::body::Bytes) -> Response { + fs::write("received_binary_file.bin", body).unwrap(); + (axum::http::StatusCode::OK, Json(serde_json::json!({ "message": "Binary file received" }))).into_response() +} + pub async fn serve( config_path: &str, should_rebundle: bool, @@ -486,6 +499,8 @@ pub async fn serve( .route("/sessions", post(sessions)) .route("/resolve_mapper_alias", get(resolve_mapper_alias)) // this returns { mapper_hash: String, mapper_url: String } .route("/resolve_mapper_file", get(resolve_mapper_file)) // this returns the file content + .route("/received_file", post(received_file)) + .route("/received_binary_file", post(received_binary_file)) .layer(middleware); info!( From 548206cc4230b9cbec6e9b31005ae9fc70381dbd Mon Sep 17 00:00:00 2001 From: nerodesu017 <46645625+nerodesu017@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:56:54 +0200 Subject: [PATCH 3/5] cleanup --- src/commands/bundle.rs | 9 ++------- src/commands/serve.rs | 15 --------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/commands/bundle.rs b/src/commands/bundle.rs index c8016d4..1531be1 100644 --- a/src/commands/bundle.rs +++ b/src/commands/bundle.rs @@ -65,13 +65,8 @@ fn compute_hashes(file_paths: &mut Vec) -> Result let mut hashes: Vec<(String, String)> = Vec::new(); for file_path in file_paths { - let file_content = std::fs::read_to_string(file_path.clone())?; - - let mut hasher = sha2::Sha256::new(); - let normalized = file_content.replace("\r\n", "\n"); - hasher.update(normalized.as_bytes()); - let hash = format!("{:x}", hasher.finalize()); - + let file_content = std::fs::read(file_path.clone())?; + let hash = format!("{:x}", sha2::Sha256::digest(&file_content)); hashes.push((file_path.to_string_lossy().to_string(), hash)); } diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 541dbc2..bdd2d4c 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -433,19 +433,6 @@ fn initialize_mappers_folder_config_and_hashmaps( Ok(()) } -async fn received_file(body: String) -> Response { - // The body is a JSON-stringified string (e.g., "\"actual content\"") - // Parse it to get the actual string content - let content: String = serde_json::from_str(&body).unwrap(); - fs::write("received_file", content).unwrap(); - (axum::http::StatusCode::OK, Json(serde_json::json!({ "message": "File received" }))).into_response() -} - -async fn received_binary_file(body: axum::body::Bytes) -> Response { - fs::write("received_binary_file.bin", body).unwrap(); - (axum::http::StatusCode::OK, Json(serde_json::json!({ "message": "Binary file received" }))).into_response() -} - pub async fn serve( config_path: &str, should_rebundle: bool, @@ -499,8 +486,6 @@ pub async fn serve( .route("/sessions", post(sessions)) .route("/resolve_mapper_alias", get(resolve_mapper_alias)) // this returns { mapper_hash: String, mapper_url: String } .route("/resolve_mapper_file", get(resolve_mapper_file)) // this returns the file content - .route("/received_file", post(received_file)) - .route("/received_binary_file", post(received_binary_file)) .layer(middleware); info!( From 0f9f5b6e1e731c3bf41c99bf50d9edd94b028a90 Mon Sep 17 00:00:00 2001 From: nerodesu017 <46645625+nerodesu017@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:58:16 +0200 Subject: [PATCH 4/5] update README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index bf01cdf..6bb7856 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,15 @@ Example: ```bash opacity-cli completions zsh > ~/.oh-my-zsh/completions/_opacity-cli ``` + +### Mapper testing + +To also have the `serve` command serve mappers, you need to create a `mappers.location` file in the root of the project with the following content: + +``` + +``` + +Where `` is the folder containing the mappers, the path has to be ABSOLUTE. + +To get it, just navigate to the mappers folder and run `pwd`. \ No newline at end of file From 25221a89cca1aa8c1b503cc8b695a8a82dcca284 Mon Sep 17 00:00:00 2001 From: nerodesu017 <46645625+nerodesu017@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:01:13 +0200 Subject: [PATCH 5/5] mappers: fix issues --- README.md | 6 +- src/commands/serve.rs | 214 +++++++++++++++++++++++------------------- 2 files changed, 123 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 6bb7856..f92a401 100644 --- a/README.md +++ b/README.md @@ -73,4 +73,8 @@ To also have the `serve` command serve mappers, you need to create a `mappers.lo Where `` is the folder containing the mappers, the path has to be ABSOLUTE. -To get it, just navigate to the mappers folder and run `pwd`. \ No newline at end of file +To get it, just navigate to the mappers folder and run `pwd`. + +Every time you change a mapper, or the first time you want to serve the mappers, make sure you run the `cargo xtask aggregate && opacity-cli bundle` in the mappers folder. + +If you wish to add/remove mappers, you have to shut down the cli http server, modify the opacity.toml file, and then run it again. Changes to the `opacity.toml` are not updated automatically, a server restart is required! \ No newline at end of file diff --git a/src/commands/serve.rs b/src/commands/serve.rs index bdd2d4c..5e924f2 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -12,7 +12,13 @@ use axum::{ use chrono::Utc; use darklua_core::Resources; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, net::SocketAddr, path::PathBuf}; +use std::{ + collections::HashMap, + fs, + net::SocketAddr, + path::PathBuf, + sync::{LazyLock, RwLock}, +}; use tower::ServiceBuilder; use tower_http::trace::{self, TraceLayer}; use tracing::{info, Level}; @@ -281,103 +287,145 @@ async fn sessions() -> Json { static SHOULD_REBUNDLE: OnceLock = OnceLock::new(); -static MAPPER_TO_HASH: OnceLock> = OnceLock::new(); -static HASH_TO_MAPPER: OnceLock> = OnceLock::new(); +static HASH_TO_MAPPER: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); -async fn resolve_mapper_file( +async fn resolve_mapper_alias( headers: axum::http::HeaderMap, - Query(query): Query, + Query(query): Query, ) -> Response { - let wants_binary = headers - .get(axum::http::header::ACCEPT) + let host = headers + .get(axum::http::header::HOST) .and_then(|v| v.to_str().ok()) - .map(|v| v.contains("application/octet-stream")) - .unwrap_or(false); + .unwrap_or("localhost:8080"); - match HASH_TO_MAPPER.get() { + match MAPPERS_CONFIG.get() { None => { return ( axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "Hash to mapper map not initialized", + "Mappers config not initialized", ) .into_response(); } - Some(hash_to_mapper) => match hash_to_mapper.get(&query.hash) { - None => { - return (axum::http::StatusCode::NOT_FOUND, "Mapper not found").into_response(); + Some(mappers_config) => { + // we need to check the bundled folder and see if there's a file with the same name as the alias + match MAPPERS_FOLDER.get() { + None => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "Mappers folder not initialized", + ) + .into_response(); + } + Some(mappers_folder) => { + let file_path = PathBuf::from(mappers_folder) + .join(mappers_config.settings.output_directory.clone()) + .join(format!("{}.bundle.luau", query.alias)); + + match fs::read_to_string(file_path) { + Ok(file_content) => { + let sha256 = get_sha256(&file_content); + HASH_TO_MAPPER + .write() + .unwrap() + .insert(sha256.clone(), query.alias.clone()); + return Json(MapperResponse { + mapper_hash: sha256.clone(), + mapper_url: format!( + "http://{}/resolve_mapper_file?hash={}", + host, sha256 + ), + }) + .into_response(); + } + Err(e) => match e.downcast::() { + Ok(io_error) => { + if io_error.kind() == std::io::ErrorKind::NotFound { + return (axum::http::StatusCode::NOT_FOUND, "Mapper not found") + .into_response(); + } else { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + io_error.to_string(), + ) + .into_response(); + } + } + Err(e) => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + e.to_string(), + ) + .into_response(); + } + }, + } + } } - Some(mapper) => { - // now we have to get the actual alias back and return the file content - match MAPPERS_FOLDER.get() { + } + } +} + +async fn resolve_mapper_file(Query(query): Query) -> Response { + match HASH_TO_MAPPER.read().unwrap().get(&query.hash) { + None => { + return (axum::http::StatusCode::NOT_FOUND, "Mapper not found").into_response(); + } + Some(mapper) => { + // now we have to get the actual alias back and return the file content + match MAPPERS_FOLDER.get() { + None => { + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "Mappers folder not initialized", + ) + .into_response(); + } + Some(mappers_folder) => match MAPPERS_CONFIG.get() { None => { return ( axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "Mappers folder not initialized", + "Mapper config not initialized", ) .into_response(); } - Some(mappers_folder) => match MAPPERS_CONFIG.get() { - None => { - return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "Mapper config not initialized", - ) - .into_response(); - } - Some(mappers_config) => { - let file_path = PathBuf::from(mappers_folder) - .join(mappers_config.settings.output_directory.clone()) - .join(format!("{}.bundle.luau", mapper)); - - if wants_binary { - let file_bytes = - fs::read(&file_path).map_err(|e| e.to_string()).unwrap(); + Some(mappers_config) => { + let file_path = PathBuf::from(mappers_folder) + .join(mappers_config.settings.output_directory.clone()) + .join(format!("{}.bundle.luau", mapper)); + + match fs::read_to_string(file_path) { + Ok(file_content) => { + let rehashed = get_sha256(&file_content); + if rehashed != query.hash { + return (axum::http::StatusCode::NOT_FOUND, "Mapper file content does not match hash, file might've been modified, try resolving the mapper alias again").into_response(); + } + return (axum::http::StatusCode::OK, file_content).into_response(); + } + Err(e) => { return ( - [( - axum::http::header::CONTENT_TYPE, - "application/octet-stream", - )], - file_bytes, + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + e.to_string(), ) .into_response(); - } else { - let file_content = fs::read_to_string(file_path) - .map_err(|e| e.to_string()) - .unwrap(); - return (axum::http::StatusCode::OK, file_content).into_response(); } } - }, - } + } + }, } - }, - } -} -async fn resolve_mapper_alias(Query(query): Query) -> Response { - match MAPPER_TO_HASH.get() { - None => { - return ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "Mapper to hash map not initialized", - ) - .into_response(); } - Some(mapper_to_hash) => match mapper_to_hash.get(&query.alias) { - None => { - return (axum::http::StatusCode::NOT_FOUND, "Mapper not found").into_response(); - } - Some(hash) => { - return Json(MapperResponse { - mapper_hash: hash.clone(), - // http://localhost:8080/resolve_mapper_file?hash=... - mapper_url: format!("http://localhost:8080/resolve_mapper_file?hash={}", hash), - }) - .into_response(); - } - }, } } +// sha256 function used when hashing mapper files in the opacity sdk +pub fn get_sha256(payload: &str) -> String { + use sha2::Digest; + let mut hasher = sha2::Sha256::new(); + let normalized = payload.replace("\r\n", "\n"); + hasher.update(normalized.as_bytes()); + format!("{:x}", hasher.finalize()) +} + fn initialize_mappers_folder_config_and_hashmaps( mappers_location: &str, ) -> Result<(), Box> { @@ -404,32 +452,6 @@ fn initialize_mappers_folder_config_and_hashmaps( ) .unwrap(); // should be initialized only once - // initialize the hashmaps - let mut mapper_to_hash = HashMap::new(); - let mut hash_to_mapper = HashMap::new(); - - // so now we have to set the hashmaps - // we first need to read `mapper-index.json` from the mappers folder - let mapper_index_path = PathBuf::from(mappers_folder).join("mapper-index.json"); - let mapper_index_content = fs::read_to_string(mapper_index_path).map_err(|e| e.to_string())?; - - #[derive(Deserialize)] - struct MapperValue { - luau_hash: String, - #[allow(dead_code)] - schema_hash: String, - } - - let mapper_index: HashMap = serde_json::from_str(&mapper_index_content)?; - - for (mapper, value) in mapper_index.iter() { - mapper_to_hash.insert(mapper.clone(), value.luau_hash.clone()); - hash_to_mapper.insert(value.luau_hash.clone(), mapper.clone()); - } - - MAPPER_TO_HASH.set(mapper_to_hash).unwrap(); // should be initialized only once - HASH_TO_MAPPER.set(hash_to_mapper).unwrap(); // should be initialized only once - Ok(()) }