diff --git a/.gitmodules b/.gitmodules index 46789ed1..90ebae43 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "plugin/Packages/Highlighter"] path = plugin/Packages/Highlighter url = https://github.com/boatbomber/highlighter.git +[submodule "plugin/Packages/msgpack-luau"] + path = plugin/Packages/msgpack-luau + url = https://github.com/cipharius/msgpack-luau/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 765f668b..36d08fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +* `inf` and `nan` values in properties are now synced ([#1176]) * Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179]) * Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192]) * Added actors and bindable/remote event/function variants to be synced back as JSON files. ([#1199]) @@ -38,6 +39,7 @@ Making a new release? Simply add the new header with the version and date undern * Fixed a bug where the notification timeout thread would fail to cancel on unmount ([#1211]) * Added a "Forget" option to the sync reminder notification to avoid being reminded for that place in the future ([#1215]) +[#1176]: https://github.com/rojo-rbx/rojo/pull/1176 [#1179]: https://github.com/rojo-rbx/rojo/pull/1179 [#1192]: https://github.com/rojo-rbx/rojo/pull/1192 [#1199]: https://github.com/rojo-rbx/rojo/pull/1199 diff --git a/Cargo.lock b/Cargo.lock index 8ac0676b..4451b3bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2078,10 +2078,12 @@ dependencies = [ "rbx_xml", "reqwest", "ritz", + "rmp-serde", "roblox_install", "rojo-insta-ext", "semver", "serde", + "serde_bytes", "serde_json", "serde_yaml", "strum", @@ -2222,6 +2224,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_cbor" version = "0.11.2" diff --git a/Cargo.toml b/Cargo.toml index d8f34311..08879ab6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,8 @@ data-encoding = "2.8.0" blake3 = "1.5.0" float-cmp = "0.9.0" indexmap = { version = "2.10.0", features = ["serde"] } +rmp-serde = "1.3.0" +serde_bytes = "0.11.19" [target.'cfg(windows)'.dependencies] winreg = "0.10.1" diff --git a/build.rs b/build.rs index 6bfd8e55..0f52585b 100644 --- a/build.rs +++ b/build.rs @@ -30,6 +30,11 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result { continue; } + // Ignore images in msgpack-luau because they aren't UTF-8 encoded. + if file_name.ends_with(".png") { + continue; + } + let child_snapshot = snapshot_from_fs_path(&entry.path())?; children.push((file_name, child_snapshot)); } diff --git a/plugin/Packages/msgpack-luau b/plugin/Packages/msgpack-luau new file mode 160000 index 00000000..40f67fc0 --- /dev/null +++ b/plugin/Packages/msgpack-luau @@ -0,0 +1 @@ +Subproject commit 40f67fc0f6eab20f8db9d3488d3d9f0928d1031b diff --git a/plugin/http/Response.lua b/plugin/http/Response.lua index 1230a1c9..b02ff644 100644 --- a/plugin/http/Response.lua +++ b/plugin/http/Response.lua @@ -1,5 +1,7 @@ local HttpService = game:GetService("HttpService") +local msgpack = require(script.Parent.Parent.msgpack) + local stringTemplate = [[ Http.Response { code: %d @@ -31,4 +33,8 @@ function Response:json() return HttpService:JSONDecode(self.body) end +function Response:msgpack() + return msgpack.decode(self.body) +end + return Response diff --git a/plugin/http/init.lua b/plugin/http/init.lua index cac0ebc4..106ebc45 100644 --- a/plugin/http/init.lua +++ b/plugin/http/init.lua @@ -1,7 +1,8 @@ local HttpService = game:GetService("HttpService") -local Promise = require(script.Parent.Promise) local Log = require(script.Parent.Log) +local msgpack = require(script.Parent.msgpack) +local Promise = require(script.Parent.Promise) local HttpError = require(script.Error) local HttpResponse = require(script.Response) @@ -68,4 +69,12 @@ function Http.jsonDecode(source) return HttpService:JSONDecode(source) end +function Http.msgpackEncode(object) + return msgpack.encode(object) +end + +function Http.msgpackDecode(source) + return msgpack.decode(source) +end + return Http diff --git a/plugin/src/ApiContext.lua b/plugin/src/ApiContext.lua index 2d2459ec..2ce6ea14 100644 --- a/plugin/src/ApiContext.lua +++ b/plugin/src/ApiContext.lua @@ -145,7 +145,7 @@ function ApiContext:connect() return Http.get(url) :andThen(rejectFailedRequests) - :andThen(Http.Response.json) + :andThen(Http.Response.msgpack) :andThen(rejectWrongProtocolVersion) :andThen(function(body) assert(validateApiInfo(body)) @@ -163,7 +163,7 @@ end function ApiContext:read(ids) local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ",")) - return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) + return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body) if body.sessionId ~= self.__sessionId then return Promise.reject("Server changed ID") end @@ -191,9 +191,9 @@ function ApiContext:write(patch) table.insert(updated, fixedUpdate) end - -- Only add the 'added' field if the table is non-empty, or else Roblox's - -- JSON implementation will turn the table into an array instead of an - -- object, causing API validation to fail. + -- Only add the 'added' field if the table is non-empty, or else the msgpack + -- encode implementation will turn the table into an array instead of a map, + -- causing API validation to fail. local added if next(patch.added) ~= nil then added = patch.added @@ -206,13 +206,16 @@ function ApiContext:write(patch) added = added, } - body = Http.jsonEncode(body) + body = Http.msgpackEncode(body) - return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(responseBody) - Log.info("Write response: {:?}", responseBody) + return Http.post(url, body) + :andThen(rejectFailedRequests) + :andThen(Http.Response.msgpack) + :andThen(function(responseBody) + Log.info("Write response: {:?}", responseBody) - return responseBody - end) + return responseBody + end) end function ApiContext:connectWebSocket(packetHandlers) @@ -234,7 +237,7 @@ function ApiContext:connectWebSocket(packetHandlers) local closed, errored, received received = self.__wsClient.MessageReceived:Connect(function(msg) - local data = Http.jsonDecode(msg) + local data = Http.msgpackDecode(msg) if data.sessionId ~= self.__sessionId then Log.warn("Received message with wrong session ID; ignoring") return @@ -280,7 +283,7 @@ end function ApiContext:open(id) local url = ("%s/api/open/%s"):format(self.__baseUrl, id) - return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) + return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body) if body.sessionId ~= self.__sessionId then return Promise.reject("Server changed ID") end @@ -291,11 +294,11 @@ end function ApiContext:serialize(ids: { string }) local url = ("%s/api/serialize"):format(self.__baseUrl) - local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids }) + local request_body = Http.msgpackEncode({ sessionId = self.__sessionId, ids = ids }) return Http.post(url, request_body) :andThen(rejectFailedRequests) - :andThen(Http.Response.json) + :andThen(Http.Response.msgpack) :andThen(function(response_body) if response_body.sessionId ~= self.__sessionId then return Promise.reject("Server changed ID") @@ -309,11 +312,11 @@ end function ApiContext:refPatch(ids: { string }) local url = ("%s/api/ref-patch"):format(self.__baseUrl) - local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids }) + local request_body = Http.msgpackEncode({ sessionId = self.__sessionId, ids = ids }) return Http.post(url, request_body) :andThen(rejectFailedRequests) - :andThen(Http.Response.json) + :andThen(Http.Response.msgpack) :andThen(function(response_body) if response_body.sessionId ~= self.__sessionId then return Promise.reject("Server changed ID") diff --git a/plugin/src/Reconciler/diff.lua b/plugin/src/Reconciler/diff.lua index fb4c79d0..c8da0939 100644 --- a/plugin/src/Reconciler/diff.lua +++ b/plugin/src/Reconciler/diff.lua @@ -54,6 +54,10 @@ local function trueEquals(a, b): boolean end return true + -- For NaN, check if both values are not equal to themselves + elseif a ~= a and b ~= b then + return true + -- For numbers, compare with epsilon of 0.0001 to avoid floating point inequality elseif typeA == "number" and typeB == "number" then return fuzzyEq(a, b, 0.0001) diff --git a/src/snapshot/patch_compute.rs b/src/snapshot/patch_compute.rs index cc3fbbbb..412c677a 100644 --- a/src/snapshot/patch_compute.rs +++ b/src/snapshot/patch_compute.rs @@ -8,7 +8,7 @@ use rbx_dom_weak::{ ustr, HashMapExt as _, UstrMap, UstrSet, }; -use crate::{RojoRef, REF_POINTER_ATTRIBUTE_PREFIX}; +use crate::{variant_eq::variant_eq, RojoRef, REF_POINTER_ATTRIBUTE_PREFIX}; use super::{ patch::{PatchAdd, PatchSet, PatchUpdate}, @@ -127,7 +127,7 @@ fn compute_property_patches( match instance.properties().get(&name) { Some(instance_value) => { - if &snapshot_value != instance_value { + if !variant_eq(&snapshot_value, instance_value) { changed_properties.insert(name, Some(snapshot_value)); } } diff --git a/src/web/api.rs b/src/web/api.rs index 5fbd7b6d..38163bb9 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -13,7 +13,6 @@ use rbx_dom_weak::{ }; use crate::{ - json, serve_session::ServeSession, snapshot::{InstanceWithMeta, PatchSet, PatchUpdate}, web::{ @@ -22,11 +21,10 @@ use crate::{ ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage, WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION, }, - util::{json, json_ok}, + util::{deserialize_msgpack, msgpack, msgpack_ok, serialize_msgpack}, }, web_api::{ - BufferEncode, InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest, - SerializeResponse, + InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest, SerializeResponse, }, }; @@ -42,7 +40,7 @@ pub async fn call(serve_session: Arc, mut request: Request) if is_upgrade_request(&request) { service.handle_api_socket(&mut request).await } else { - json( + msgpack( ErrorResponse::bad_request( "/api/socket must be called as a websocket upgrade request", ), @@ -58,7 +56,7 @@ pub async fn call(serve_session: Arc, mut request: Request) } (&Method::POST, "/api/write") => service.handle_api_write(request).await, - (_method, path) => json( + (_method, path) => msgpack( ErrorResponse::not_found(format!("Route not found: {}", path)), StatusCode::NOT_FOUND, ), @@ -79,7 +77,7 @@ impl ApiService { let tree = self.serve_session.tree(); let root_instance_id = tree.get_root_id(); - json_ok(&ServerInfoResponse { + msgpack_ok(&ServerInfoResponse { server_version: SERVER_VERSION.to_owned(), protocol_version: PROTOCOL_VERSION, session_id: self.serve_session.session_id(), @@ -98,7 +96,7 @@ impl ApiService { let input_cursor: u32 = match argument.parse() { Ok(v) => v, Err(err) => { - return json( + return msgpack( ErrorResponse::bad_request(format!("Malformed message cursor: {}", err)), StatusCode::BAD_REQUEST, ); @@ -109,7 +107,7 @@ impl ApiService { let (response, websocket) = match upgrade(request, None) { Ok(result) => result, Err(err) => { - return json( + return msgpack( ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)), StatusCode::INTERNAL_SERVER_ERROR, ); @@ -136,10 +134,10 @@ impl ApiService { let body = body::to_bytes(request.into_body()).await.unwrap(); - let request: WriteRequest = match json::from_slice(&body) { + let request: WriteRequest = match deserialize_msgpack(&body) { Ok(request) => request, Err(err) => { - return json( + return msgpack( ErrorResponse::bad_request(format!("Invalid body: {}", err)), StatusCode::BAD_REQUEST, ); @@ -147,7 +145,7 @@ impl ApiService { }; if request.session_id != session_id { - return json( + return msgpack( ErrorResponse::bad_request("Wrong session ID"), StatusCode::BAD_REQUEST, ); @@ -173,7 +171,7 @@ impl ApiService { }) .unwrap(); - json_ok(WriteResponse { session_id }) + msgpack_ok(WriteResponse { session_id }) } async fn handle_api_read(&self, request: Request) -> Response { @@ -183,7 +181,7 @@ impl ApiService { let requested_ids = match requested_ids { Ok(ids) => ids, Err(_) => { - return json( + return msgpack( ErrorResponse::bad_request("Malformed ID list"), StatusCode::BAD_REQUEST, ); @@ -207,7 +205,7 @@ impl ApiService { } } - json_ok(ReadResponse { + msgpack_ok(ReadResponse { session_id: self.serve_session.session_id(), message_cursor, instances, @@ -225,10 +223,10 @@ impl ApiService { let session_id = self.serve_session.session_id(); let body = body::to_bytes(request.into_body()).await.unwrap(); - let request: SerializeRequest = match json::from_slice(&body) { + let request: SerializeRequest = match deserialize_msgpack(&body) { Ok(request) => request, Err(err) => { - return json( + return msgpack( ErrorResponse::bad_request(format!("Invalid body: {}", err)), StatusCode::BAD_REQUEST, ); @@ -236,7 +234,7 @@ impl ApiService { }; if request.session_id != session_id { - return json( + return msgpack( ErrorResponse::bad_request("Wrong session ID"), StatusCode::BAD_REQUEST, ); @@ -269,7 +267,7 @@ impl ApiService { response_dom.transfer_within(child_ref, object_value); } else { - json( + msgpack( ErrorResponse::bad_request(format!("provided id {id} is not in the tree")), StatusCode::BAD_REQUEST, ); @@ -280,9 +278,9 @@ impl ApiService { let mut source = Vec::new(); rbx_binary::to_writer(&mut source, &response_dom, &[response_dom.root_ref()]).unwrap(); - json_ok(SerializeResponse { + msgpack_ok(SerializeResponse { session_id: self.serve_session.session_id(), - model_contents: BufferEncode::new(source), + model_contents: source, }) } @@ -294,10 +292,10 @@ impl ApiService { let session_id = self.serve_session.session_id(); let body = body::to_bytes(request.into_body()).await.unwrap(); - let request: RefPatchRequest = match json::from_slice(&body) { + let request: RefPatchRequest = match deserialize_msgpack(&body) { Ok(request) => request, Err(err) => { - return json( + return msgpack( ErrorResponse::bad_request(format!("Invalid body: {}", err)), StatusCode::BAD_REQUEST, ); @@ -305,7 +303,7 @@ impl ApiService { }; if request.session_id != session_id { - return json( + return msgpack( ErrorResponse::bad_request("Wrong session ID"), StatusCode::BAD_REQUEST, ); @@ -338,7 +336,7 @@ impl ApiService { } } - json_ok(RefPatchResponse { + msgpack_ok(RefPatchResponse { session_id: self.serve_session.session_id(), patch: SubscribeMessage { added: HashMap::new(), @@ -354,7 +352,7 @@ impl ApiService { let requested_id = match Ref::from_str(argument) { Ok(id) => id, Err(_) => { - return json( + return msgpack( ErrorResponse::bad_request("Invalid instance ID"), StatusCode::BAD_REQUEST, ); @@ -366,7 +364,7 @@ impl ApiService { let instance = match tree.get_instance(requested_id) { Some(instance) => instance, None => { - return json( + return msgpack( ErrorResponse::bad_request("Instance not found"), StatusCode::NOT_FOUND, ); @@ -376,7 +374,7 @@ impl ApiService { let script_path = match pick_script_path(instance) { Some(path) => path, None => { - return json( + return msgpack( ErrorResponse::bad_request( "No appropriate file could be found to open this script", ), @@ -389,7 +387,7 @@ impl ApiService { Ok(()) => {} Err(error) => match error { OpenError::Io(io_error) => { - return json( + return msgpack( ErrorResponse::internal_error(format!( "Attempting to open {} failed because of the following io error: {}", script_path.display(), @@ -403,7 +401,7 @@ impl ApiService { status, stderr, } => { - return json( + return msgpack( ErrorResponse::internal_error(format!( r#"The command '{}' to open '{}' failed with the error code '{}'. Error logs: @@ -419,7 +417,7 @@ impl ApiService { }, }; - json_ok(OpenResponse { + msgpack_ok(OpenResponse { session_id: self.serve_session.session_id(), }) } @@ -483,7 +481,7 @@ async fn handle_websocket_subscription( match result { Ok((new_cursor, messages)) => { if !messages.is_empty() { - let json_message = { + let msgpack_message = { let tree = tree_handle.lock().unwrap(); let api_messages = messages .into_iter() @@ -499,12 +497,12 @@ async fn handle_websocket_subscription( }), }; - serde_json::to_string(&response)? + serialize_msgpack(response)? }; log::debug!("Sending batch of messages over WebSocket subscription"); - if websocket.send(Message::Text(json_message)).await.is_err() { + if websocket.send(Message::Binary(msgpack_message)).await.is_err() { // Client disconnected log::debug!("WebSocket subscription closed by client"); break; diff --git a/src/web/interface.rs b/src/web/interface.rs index 430f13ec..51307b67 100644 --- a/src/web/interface.rs +++ b/src/web/interface.rs @@ -249,31 +249,8 @@ pub struct SerializeRequest { #[serde(rename_all = "camelCase")] pub struct SerializeResponse { pub session_id: SessionId, - pub model_contents: BufferEncode, -} - -/// Using this struct we can force Roblox to JSONDecode this as a buffer. -/// This is what Roblox's serde APIs use, so it saves a step in the plugin. -#[derive(Debug, Serialize, Deserialize)] -pub struct BufferEncode { - m: (), - t: Cow<'static, str>, - base64: String, -} - -impl BufferEncode { - pub fn new(content: Vec) -> Self { - let base64 = data_encoding::BASE64.encode(&content); - Self { - m: (), - t: Cow::Borrowed("buffer"), - base64, - } - } - - pub fn model(&self) -> &str { - &self.base64 - } + #[serde(with = "serde_bytes")] + pub model_contents: Vec, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/web/util.rs b/src/web/util.rs index 2b6882e5..1c1cbb50 100644 --- a/src/web/util.rs +++ b/src/web/util.rs @@ -1,8 +1,48 @@ use hyper::{header::CONTENT_TYPE, Body, Response, StatusCode}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -pub fn json_ok(value: T) -> Response { - json(value, StatusCode::OK) +pub fn msgpack_ok(value: T) -> Response { + msgpack(value, StatusCode::OK) +} + +pub fn msgpack(value: T, code: StatusCode) -> Response { + let mut serialized = Vec::new(); + let mut serializer = rmp_serde::Serializer::new(&mut serialized) + .with_human_readable() + .with_struct_map(); + + if let Err(err) = value.serialize(&mut serializer) { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, "text/plain") + .body(Body::from(err.to_string())) + .unwrap(); + }; + + Response::builder() + .status(code) + .header(CONTENT_TYPE, "application/msgpack") + .body(Body::from(serialized)) + .unwrap() +} + +pub fn serialize_msgpack(value: T) -> anyhow::Result> { + let mut serialized = Vec::new(); + let mut serializer = rmp_serde::Serializer::new(&mut serialized) + .with_human_readable() + .with_struct_map(); + + value.serialize(&mut serializer)?; + + Ok(serialized) +} + +pub fn deserialize_msgpack<'a, T: Deserialize<'a>>( + input: &'a [u8], +) -> Result { + let mut deserializer = rmp_serde::Deserializer::new(input).with_human_readable(); + + T::deserialize(&mut deserializer) } pub fn json(value: T, code: StatusCode) -> Response { diff --git a/tests/rojo_test/serve_util.rs b/tests/rojo_test/serve_util.rs index a5decdbb..4b67a883 100644 --- a/tests/rojo_test/serve_util.rs +++ b/tests/rojo_test/serve_util.rs @@ -10,6 +10,7 @@ use std::{ use hyper_tungstenite::tungstenite::{connect, Message}; use rbx_dom_weak::types::Ref; +use serde::{Deserialize, Serialize}; use tempfile::{tempdir, TempDir}; use librojo::{ @@ -161,22 +162,16 @@ impl TestServeSession { pub fn get_api_rojo(&self) -> Result { let url = format!("http://localhost:{}/api/rojo", self.port); - let body = reqwest::blocking::get(url)?.text()?; + let body = reqwest::blocking::get(url)?.bytes()?; - let value = jsonc_parser::parse_to_serde_value(&body, &Default::default()) - .expect("Failed to parse JSON") - .expect("No JSON value"); - Ok(serde_json::from_value(value).expect("Server returned malformed response")) + Ok(deserialize_msgpack(&body).expect("Server returned malformed response")) } pub fn get_api_read(&self, id: Ref) -> Result, reqwest::Error> { let url = format!("http://localhost:{}/api/read/{}", self.port, id); - let body = reqwest::blocking::get(url)?.text()?; + let body = reqwest::blocking::get(url)?.bytes()?; - let value = jsonc_parser::parse_to_serde_value(&body, &Default::default()) - .expect("Failed to parse JSON") - .expect("No JSON value"); - Ok(serde_json::from_value(value).expect("Server returned malformed response")) + Ok(deserialize_msgpack(&body).expect("Server returned malformed response")) } pub fn get_api_socket_packet( @@ -198,8 +193,8 @@ impl TestServeSession { } match socket.read() { - Ok(Message::Text(text)) => { - let packet: SocketPacket = serde_json::from_str(&text)?; + Ok(Message::Binary(binary)) => { + let packet: SocketPacket = deserialize_msgpack(&binary)?; if packet.packet_type != packet_type { continue; } @@ -212,7 +207,7 @@ impl TestServeSession { return Err("WebSocket closed before receiving messages".into()); } Ok(_) => { - // Ignore other message types (ping, pong, binary) + // Ignore other message types (ping, pong, text) continue; } Err(hyper_tungstenite::tungstenite::Error::Io(e)) @@ -236,15 +231,37 @@ impl TestServeSession { ) -> Result { let client = reqwest::blocking::Client::new(); let url = format!("http://localhost:{}/api/serialize", self.port); - let body = serde_json::to_string(&SerializeRequest { + let body = serialize_msgpack(&SerializeRequest { session_id, ids: ids.to_vec(), - }); + }) + .unwrap(); - client.post(url).body((body).unwrap()).send()?.json() + let body = client.post(url).body(body).send()?.bytes()?; + + Ok(deserialize_msgpack(&body).expect("Server returned malformed response")) } } +fn serialize_msgpack(value: T) -> Result, rmp_serde::encode::Error> { + let mut serialized = Vec::new(); + let mut serializer = rmp_serde::Serializer::new(&mut serialized) + .with_human_readable() + .with_struct_map(); + + value.serialize(&mut serializer)?; + + Ok(serialized) +} + +fn deserialize_msgpack<'a, T: Deserialize<'a>>( + input: &'a [u8], +) -> Result { + let mut deserializer = rmp_serde::Deserializer::new(input).with_human_readable(); + + T::deserialize(&mut deserializer) +} + /// Probably-okay way to generate random enough port numbers for running the /// Rojo live server. /// @@ -262,11 +279,7 @@ fn get_port_number() -> usize { /// Since the provided structure intentionally includes unredacted referents, /// some post-processing is done to ensure they don't show up in the model. pub fn serialize_to_xml_model(response: &SerializeResponse, redactions: &RedactionMap) -> String { - let model_content = data_encoding::BASE64 - .decode(response.model_contents.model().as_bytes()) - .unwrap(); - - let mut dom = rbx_binary::from_reader(model_content.as_slice()).unwrap(); + let mut dom = rbx_binary::from_reader(response.model_contents.as_slice()).unwrap(); // This makes me realize that maybe we need a `descendants_mut` iter. let ref_list: Vec = dom.descendants().map(|inst| inst.referent()).collect(); for referent in ref_list {