diff --git a/Cargo.lock b/Cargo.lock index 3f60a23..43c352e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,9 +212,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.17" +version = "1.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" dependencies = [ "jobserver", "libc", @@ -378,9 +378,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -874,9 +874,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", @@ -997,9 +997,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -1056,9 +1056,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c72f6929239626840b28f919ce8981a317fc5dc63ce25c30d2ab372f94886f" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -1117,9 +1117,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.9.0", "cfg-if", @@ -1149,9 +1149,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -1293,8 +1293,9 @@ dependencies = [ [[package]] name = "rbx_asset" -version = "0.4.2" +version = "0.4.4" dependencies = [ + "bytes", "chrono", "flate2", "reqwest", @@ -1523,9 +1524,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "once_cell", "rustls-pki-types", @@ -1665,9 +1666,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" @@ -1794,9 +1795,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.1" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -2310,9 +2311,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "xml-rs" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "yoke" diff --git a/rbx_asset/Cargo.toml b/rbx_asset/Cargo.toml index e652476..d41d068 100644 --- a/rbx_asset/Cargo.toml +++ b/rbx_asset/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rbx_asset" -version = "0.4.2" +version = "0.4.4" edition = "2021" publish = ["strafesnet"] repository = "https://git.itzana.me/StrafesNET/asset-tool" @@ -10,9 +10,14 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["gzip"] +gzip = ["dep:flate2"] + [dependencies] +bytes = "1.10.1" chrono = { version = "0.4.38", features = ["serde"] } -flate2 = "1.0.29" +flate2 = { version = "1.0.29", optional = true } reqwest = { version = "0.12.4", features = ["json","multipart"] } serde = { version = "1.0.199", features = ["derive"] } serde_json = "1.0.111" diff --git a/rbx_asset/src/cloud.rs b/rbx_asset/src/cloud.rs index f65c7aa..71f4a9e 100644 --- a/rbx_asset/src/cloud.rs +++ b/rbx_asset/src/cloud.rs @@ -1,4 +1,5 @@ -use crate::{ResponseError,ReaderType,maybe_gzip_decode,read_readable}; +use crate::util::{serialize_u64,deserialize_u64,response_ok}; +use crate::types::{ResponseError,MaybeGzippedBytes}; #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] @@ -65,8 +66,8 @@ pub struct UpdateAssetRequest{ #[derive(Clone,Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub enum Creator{ - userId(String),//u64 string - groupId(String),//u64 string + userId(#[serde(deserialize_with="deserialize_u64",serialize_with="serialize_u64")]u64), + groupId(#[serde(deserialize_with="deserialize_u64",serialize_with="serialize_u64")]u64), } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] @@ -146,14 +147,19 @@ pub struct GetAssetLatestRequest{ #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct AssetResponse{ - pub assetId:String,//u64 wrapped in quotes wohoo!! + //u64 wrapped in quotes wohoo!! + #[serde(deserialize_with="deserialize_u64")] + #[serde(serialize_with="serialize_u64")] + pub assetId:u64, pub assetType:AssetType, pub creationContext:CreationContext, pub description:Option<String>, pub displayName:String, pub path:String, pub revisionCreateTime:chrono::DateTime<chrono::Utc>, - pub revisionId:String,//u64 + #[serde(deserialize_with="deserialize_u64")] + #[serde(serialize_with="serialize_u64")] + pub revisionId:u64, pub moderationResult:ModerationResult, pub icon:Option<String>, #[serde(default)] @@ -169,7 +175,6 @@ pub enum GetError{ ParseError(url::ParseError), Response(ResponseError), Reqwest(reqwest::Error), - IO(std::io::Error) } impl std::fmt::Display for GetError{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ @@ -178,22 +183,28 @@ impl std::fmt::Display for GetError{ } impl std::error::Error for GetError{} +#[derive(Debug,serde::Deserialize,serde::Serialize)] +pub struct AssetLocation( + // the location is private so users cannot mutate it + String +); +impl AssetLocation{ + pub fn location(&self)->&str{ + let Self(location)=self; + location + } +} + #[derive(Debug,serde::Deserialize)] #[allow(nonstandard_style,dead_code)] -pub struct AssetLocation{ - // this field is private so users cannot mutate it - location:String, +pub struct AssetLocationInfo{ + pub location:Option<AssetLocation>, pub requestId:String, pub IsHashDynamic:bool, pub IsCopyrightProtected:bool, pub isArchived:bool, pub assetTypeId:u32, } -impl AssetLocation{ - pub fn location(&self)->&str{ - &self.location - } -} pub struct AssetVersionsRequest{ pub asset_id:u64, @@ -369,7 +380,7 @@ impl Context{ .text("request",request_config) .part("fileContent",part); - let operation=crate::response_ok( + let operation=response_ok( self.post_form(url,form).await.map_err(CreateError::Reqwest)? ).await.map_err(CreateError::Response)? .json::<RobloxOperation>().await.map_err(CreateError::Reqwest)?; @@ -388,7 +399,7 @@ impl Context{ .text("request",request_config) .part("fileContent",reqwest::multipart::Part::bytes(body)); - let operation=crate::response_ok( + let operation=response_ok( self.patch_form(url,form).await.map_err(UpdateError::Reqwest)? ).await.map_err(UpdateError::Response)? .json::<RobloxOperation>().await.map_err(UpdateError::Reqwest)?; @@ -401,7 +412,7 @@ impl Context{ let raw_url=format!("https://apis.roblox.com/assets/v1/operations/{}",config.operation_id); let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; - crate::response_ok( + response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .json::<RobloxOperation>().await.map_err(GetError::Reqwest) @@ -410,7 +421,7 @@ impl Context{ let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}",config.asset_id); let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; - crate::response_ok( + response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .json::<AssetResponse>().await.map_err(GetError::Reqwest) @@ -419,48 +430,44 @@ impl Context{ let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}/versions/{}",config.asset_id,config.version); let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; - crate::response_ok( + response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .json::<AssetResponse>().await.map_err(GetError::Reqwest) } - pub async fn get_asset_location(&self,config:GetAssetLatestRequest)->Result<AssetLocation,GetError>{ + pub async fn get_asset_location(&self,config:GetAssetLatestRequest)->Result<AssetLocationInfo,GetError>{ let raw_url=format!("https://apis.roblox.com/asset-delivery-api/v1/assetId/{}",config.asset_id); let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; - crate::response_ok( + response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .json().await.map_err(GetError::Reqwest) } - pub async fn get_asset_version_location(&self,config:GetAssetVersionRequest)->Result<AssetLocation,GetError>{ + pub async fn get_asset_version_location(&self,config:GetAssetVersionRequest)->Result<AssetLocationInfo,GetError>{ let raw_url=format!("https://apis.roblox.com/asset-delivery-api/v1/assetId/{}/version/{}",config.asset_id,config.version); let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; - crate::response_ok( + response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .json().await.map_err(GetError::Reqwest) } - pub async fn get_asset(&self,config:&AssetLocation)->Result<Vec<u8>,GetError>{ - let url=reqwest::Url::parse(config.location.as_str()).map_err(GetError::ParseError)?; + pub async fn get_asset(&self,config:&AssetLocation)->Result<MaybeGzippedBytes,GetError>{ + let url=reqwest::Url::parse(config.location()).map_err(GetError::ParseError)?; - let body=crate::response_ok( + let bytes=response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .bytes().await.map_err(GetError::Reqwest)?; - match maybe_gzip_decode(std::io::Cursor::new(body)){ - Ok(ReaderType::GZip(readable))=>read_readable(readable), - Ok(ReaderType::Raw(readable))=>read_readable(readable), - Err(e)=>Err(e), - }.map_err(GetError::IO) + Ok(MaybeGzippedBytes::new(bytes)) } pub async fn get_asset_versions(&self,config:AssetVersionsRequest)->Result<AssetVersionsResponse,AssetVersionsError>{ let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}/versions",config.asset_id); let url=reqwest::Url::parse(raw_url.as_str()).map_err(AssetVersionsError::ParseError)?; - crate::response_ok( + response_ok( self.get(url).await.map_err(AssetVersionsError::Reqwest)? ).await.map_err(AssetVersionsError::Response)? .json::<AssetVersionsResponse>().await.map_err(AssetVersionsError::Reqwest) @@ -475,7 +482,7 @@ impl Context{ } } - crate::response_ok( + response_ok( self.get(url).await.map_err(InventoryPageError::Reqwest)? ).await.map_err(InventoryPageError::Response)? .json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest) @@ -489,7 +496,7 @@ impl Context{ query.append_pair("versionType","Published"); } - crate::response_ok( + response_ok( self.post(url,body).await.map_err(UpdateError::Reqwest)? ).await.map_err(UpdateError::Response)? .json::<UpdatePlaceResponse>().await.map_err(UpdateError::Reqwest) diff --git a/rbx_asset/src/cookie.rs b/rbx_asset/src/cookie.rs index 7ad53f2..efee3dc 100644 --- a/rbx_asset/src/cookie.rs +++ b/rbx_asset/src/cookie.rs @@ -1,4 +1,5 @@ -use crate::{ResponseError,ReaderType,maybe_gzip_decode,read_readable}; +use crate::util::response_ok; +use crate::types::{ResponseError,MaybeGzippedBytes}; #[derive(Debug)] pub enum PostError{ @@ -91,7 +92,6 @@ pub enum GetError{ ParseError(url::ParseError), Response(ResponseError), Reqwest(reqwest::Error), - IO(std::io::Error) } impl std::fmt::Display for GetError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -371,7 +371,7 @@ impl Context{ query.append_pair("groupId",group_id.to_string().as_str()); } } - let response=crate::response_ok( + let response=response_ok( self.post(url,body).await.map_err(CreateError::PostError)? ).await.map_err(CreateError::Response)?; @@ -423,7 +423,7 @@ impl Context{ query.append_pair("groupId",group_id.to_string().as_str()); } } - let response=crate::response_ok( + let response=response_ok( self.post(url,body).await.map_err(UploadError::PostError)? ).await.map_err(UploadError::Response)?; @@ -449,7 +449,7 @@ impl Context{ }) } } - pub async fn get_asset(&self,config:GetAssetRequest)->Result<Vec<u8>,GetError>{ + pub async fn get_asset(&self,config:GetAssetRequest)->Result<MaybeGzippedBytes,GetError>{ let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v1/asset/").map_err(GetError::ParseError)?; //url borrow scope { @@ -459,16 +459,13 @@ impl Context{ query.append_pair("version",version.to_string().as_str()); } } - let body=crate::response_ok( + + let bytes=response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .bytes().await.map_err(GetError::Reqwest)?; - match maybe_gzip_decode(std::io::Cursor::new(body)){ - Ok(ReaderType::GZip(readable))=>read_readable(readable), - Ok(ReaderType::Raw(readable))=>read_readable(readable), - Err(e)=>Err(e), - }.map_err(GetError::IO) + Ok(MaybeGzippedBytes::new(bytes)) } pub async fn get_asset_v2(&self,config:GetAssetRequest)->Result<GetAssetV2,GetAssetV2Error>{ let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v2/asset").map_err(GetAssetV2Error::ParseError)?; @@ -480,7 +477,7 @@ impl Context{ query.append_pair("version",version.to_string().as_str()); } } - let response=crate::response_ok( + let response=response_ok( self.get(url).await.map_err(GetAssetV2Error::Reqwest)? ).await.map_err(GetAssetV2Error::Response)?; @@ -500,23 +497,19 @@ impl Context{ info, }) } - pub async fn get_asset_v2_download(&self,config:&GetAssetV2Location)->Result<Vec<u8>,GetError>{ + pub async fn get_asset_v2_download(&self,config:&GetAssetV2Location)->Result<MaybeGzippedBytes,GetError>{ let url=reqwest::Url::parse(config.location.as_str()).map_err(GetError::ParseError)?; - let body=crate::response_ok( + let bytes=response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .bytes().await.map_err(GetError::Reqwest)?; - match maybe_gzip_decode(std::io::Cursor::new(body)){ - Ok(ReaderType::GZip(readable))=>read_readable(readable), - Ok(ReaderType::Raw(readable))=>read_readable(readable), - Err(e)=>Err(e), - }.map_err(GetError::IO) + Ok(MaybeGzippedBytes::new(bytes)) } pub async fn get_asset_details(&self,config:GetAssetDetailsRequest)->Result<AssetDetails,GetError>{ let url=reqwest::Url::parse(format!("https://economy.roblox.com/v2/assets/{}/details",config.asset_id).as_str()).map_err(GetError::ParseError)?; - crate::response_ok( + response_ok( self.get(url).await.map_err(GetError::Reqwest)? ).await.map_err(GetError::Response)? .json().await.map_err(GetError::Reqwest) @@ -533,7 +526,7 @@ impl Context{ query.append_pair("cursor",cursor); } } - crate::response_ok( + response_ok( self.get(url).await.map_err(PageError::Reqwest)? ).await.map_err(PageError::Response)? .json::<AssetVersionsPageResponse>().await.map_err(PageError::Reqwest) @@ -548,7 +541,7 @@ impl Context{ query.append_pair("cursor",cursor); } } - crate::response_ok( + response_ok( self.get(url).await.map_err(PageError::Reqwest)? ).await.map_err(PageError::Response)? .json::<CreationsPageResponse>().await.map_err(PageError::Reqwest) @@ -562,7 +555,7 @@ impl Context{ query.append_pair("cursor",cursor); } } - crate::response_ok( + response_ok( self.get(url).await.map_err(PageError::Reqwest)? ).await.map_err(PageError::Response)? .json::<UserInventoryPageResponse>().await.map_err(PageError::Reqwest) diff --git a/rbx_asset/src/lib.rs b/rbx_asset/src/lib.rs index c41ff74..a6a3487 100644 --- a/rbx_asset/src/lib.rs +++ b/rbx_asset/src/lib.rs @@ -1,56 +1,4 @@ pub mod cloud; pub mod cookie; - -#[allow(dead_code)] -#[derive(Debug)] -pub struct StatusCodeWithUrlAndBody{ - pub status_code:reqwest::StatusCode, - pub url:url::Url, - pub body:String, -} -#[derive(Debug)] -pub enum ResponseError{ - Reqwest(reqwest::Error), - StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody), -} -impl std::fmt::Display for ResponseError{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f,"{self:?}") - } -} -impl std::error::Error for ResponseError{} -// lazy function to draw out meaningful info from http response on failure -pub(crate) async fn response_ok(response:reqwest::Response)->Result<reqwest::Response,ResponseError>{ - let status_code=response.status(); - if status_code.is_success(){ - Ok(response) - }else{ - let url=response.url().to_owned(); - let bytes=response.bytes().await.map_err(ResponseError::Reqwest)?; - let body=String::from_utf8_lossy(&bytes).to_string(); - Err(ResponseError::StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody{ - status_code, - url, - body, - })) - } -} - -//idk how to do this better -pub(crate) enum ReaderType<R:std::io::Read>{ - GZip(flate2::read::GzDecoder<std::io::BufReader<R>>), - Raw(std::io::BufReader<R>), -} -pub(crate) fn maybe_gzip_decode<R:std::io::Read>(input:R)->std::io::Result<ReaderType<R>>{ - let mut buf=std::io::BufReader::new(input); - let peek=std::io::BufRead::fill_buf(&mut buf)?; - match &peek[0..2]{ - b"\x1f\x8b"=>Ok(ReaderType::GZip(flate2::read::GzDecoder::new(buf))), - _=>Ok(ReaderType::Raw(buf)), - } -} -pub(crate) fn read_readable(mut readable:impl std::io::Read)->std::io::Result<Vec<u8>>{ - let mut contents=Vec::new(); - readable.read_to_end(&mut contents)?; - Ok(contents) -} +pub mod types; +mod util; diff --git a/rbx_asset/src/types.rs b/rbx_asset/src/types.rs new file mode 100644 index 0000000..2aa64f5 --- /dev/null +++ b/rbx_asset/src/types.rs @@ -0,0 +1,68 @@ +#[allow(dead_code)] +#[derive(Debug)] +pub struct StatusCodeWithUrlAndBody{ + pub status_code:reqwest::StatusCode, + pub url:url::Url, + pub body:String, +} +#[derive(Debug)] +pub enum ResponseError{ + Reqwest(reqwest::Error), + StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody), +} +impl std::fmt::Display for ResponseError{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f,"{self:?}") + } +} +impl std::error::Error for ResponseError{} + +#[cfg(feature="gzip")] +use std::io::Cursor; +#[cfg(feature="gzip")] +use flate2::read::GzDecoder; + +/// Some bytes that might be gzipped. Use the read_with or to_vec methods to transparently decode gzip. +pub struct MaybeGzippedBytes{ + bytes:bytes::Bytes, +} +impl MaybeGzippedBytes{ + pub(crate) fn new(bytes:bytes::Bytes)->Self{ + Self{bytes} + } + pub fn into_inner(self)->bytes::Bytes{ + self.bytes + } + /// get a reference to the bytes, ignoring gzip decoding + pub fn as_raw_ref(&self)->&[u8]{ + self.bytes.as_ref() + } + /// Transparently decode gzip data, if present (intermediate allocation) + #[cfg(feature="gzip")] + pub fn to_vec(&self)->std::io::Result<Vec<u8>>{ + use std::io::Read; + match self.bytes.get(0..2){ + Some(b"\x1f\x8b")=>{ + let mut buf=Vec::new(); + GzDecoder::new(Cursor::new(self.bytes.as_ref())).read_to_end(&mut buf)?; + Ok(buf) + }, + _=>Ok(self.bytes.to_vec()) + } + } + /// Read the bytes with the provided decoders. + /// The idea is to make a function that is generic over std::io::Read + /// and pass the same function to both closures. + /// This two closure hack must be done because of the different concrete types. + #[cfg(feature="gzip")] + pub fn read_with<'a,ReadGzip,ReadRaw,T>(&'a self,read_gzip:ReadGzip,read_raw:ReadRaw)->T + where + ReadGzip:Fn(GzDecoder<Cursor<&'a [u8]>>)->T, + ReadRaw:Fn(Cursor<&'a [u8]>)->T, + { + match self.bytes.get(0..2){ + Some(b"\x1f\x8b")=>read_gzip(GzDecoder::new(Cursor::new(self.bytes.as_ref()))), + _=>read_raw(Cursor::new(self.bytes.as_ref())) + } + } +} diff --git a/rbx_asset/src/util.rs b/rbx_asset/src/util.rs new file mode 100644 index 0000000..9f8a785 --- /dev/null +++ b/rbx_asset/src/util.rs @@ -0,0 +1,40 @@ +use crate::types::{ResponseError,StatusCodeWithUrlAndBody}; + +// lazy function to draw out meaningful info from http response on failure +pub(crate) async fn response_ok(response:reqwest::Response)->Result<reqwest::Response,ResponseError>{ + let status_code=response.status(); + if status_code.is_success(){ + Ok(response) + }else{ + let url=response.url().to_owned(); + let bytes=response.bytes().await.map_err(ResponseError::Reqwest)?; + let body=String::from_utf8_lossy(&bytes).to_string(); + Err(ResponseError::StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody{ + status_code, + url, + body, + })) + } +} + +use serde::de::{Error,Unexpected}; +use serde::{Deserializer,Serializer}; + +struct U64StringVisitor; +impl serde::de::Visitor<'_> for U64StringVisitor{ + type Value=u64; + fn expecting(&self,formatter:&mut std::fmt::Formatter)->std::fmt::Result{ + write!(formatter,"string value with int") + } + fn visit_str<E:Error>(self,v:&str)->Result<Self::Value,E>{ + v.parse().map_err(|_|E::invalid_value(Unexpected::Str(v),&"u64")) + } +} + +pub(crate) fn deserialize_u64<'de,D:Deserializer<'de>>(deserializer:D)->Result<u64,D::Error>{ + deserializer.deserialize_any(U64StringVisitor) +} + +pub(crate) fn serialize_u64<S:Serializer>(v:&u64,serializer:S)->Result<S::Ok,S::Error>{ + serializer.serialize_str(v.to_string().as_str()) +} diff --git a/src/main.rs b/src/main.rs index ba89009..e8703bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -563,8 +563,8 @@ async fn main()->AResult<()>{ subcommand.api_key_file, ).await?, creator:match (subcommand.creator_user_id,subcommand.creator_group_id){ - (Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()), - (None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()), + (Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id), + (None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id), other=>Err(anyhow!("Invalid creator {other:?}"))?, }, input_file:subcommand.input_file, @@ -585,8 +585,8 @@ async fn main()->AResult<()>{ subcommand.cookie_file, ).await?, creator:match (subcommand.creator_user_id,subcommand.creator_group_id){ - (Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()), - (None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()), + (Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id), + (None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id), other=>Err(anyhow!("Invalid creator {other:?}"))?, }, description:subcommand.description.unwrap_or_else(||String::with_capacity(0)), @@ -903,11 +903,11 @@ async fn create_asset_medias(config:CreateAssetMediasConfig)->AResult<()>{ async move{(path, async move{ let asset_response=asset_response_result.map_err(DownloadDecalError::PollOperation)?; - let file=cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{ - asset_id:asset_response.assetId.parse().map_err(DownloadDecalError::ParseInt)?, + let maybe_gzip=cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{ + asset_id:asset_response.assetId, version:None, }).await.map_err(DownloadDecalError::Get)?; - let dom=load_dom(std::io::Cursor::new(file)).map_err(DownloadDecalError::LoadDom)?; + let dom=maybe_gzip.read_with(load_dom,load_dom).map_err(DownloadDecalError::LoadDom)?; let instance=dom.get_by_ref( *dom.root().children().first().ok_or(DownloadDecalError::NoFirstInstance)? ).ok_or(DownloadDecalError::NoFirstInstance)?; @@ -993,8 +993,8 @@ async fn asset_details(cookie:Cookie,asset_id:AssetID)->AResult<()>{ async fn download_version(cookie:Cookie,asset_id:AssetID,version:Option<u64>,dest:PathBuf)->AResult<()>{ let context=CookieContext::new(cookie); - let data=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version}).await?; - tokio::fs::write(dest,data).await?; + let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version}).await?; + tokio::fs::write(dest,maybe_gzip.to_vec()?).await?; Ok(()) } @@ -1006,9 +1006,9 @@ async fn download_version_v2(cookie:Cookie,asset_id:AssetID,version:Option<u64>, println!("version:{}",info.version); let location=info.info.locations.first().ok_or(anyhow::Error::msg("No locations"))?; - let data=context.get_asset_v2_download(location).await?; + let maybe_gzip=context.get_asset_v2_download(location).await?; - tokio::fs::write(dest,data).await?; + tokio::fs::write(dest,maybe_gzip.to_vec()?).await?; Ok(()) } @@ -1024,7 +1024,7 @@ async fn download_list(cookie:Cookie,asset_id_file_map:AssetIDFileMap)->AResult< .buffer_unordered(CONCURRENT_REQUESTS) .for_each(|b:AResult<_>|async{ match b{ - Ok((dest,data))=>if let Err(e)=tokio::fs::write(dest,data).await{ + Ok((dest,maybe_gzip))=>if let Err(e)=(async||{tokio::fs::write(dest,maybe_gzip.to_vec()?).await})().await{ eprintln!("fs error: {}",e); }, Err(e)=>eprintln!("dl error: {}",e), @@ -1228,9 +1228,9 @@ async fn download_history(mut config:DownloadHistoryConfig)->AResult<()>{ let mut path=output_folder.clone(); path.push(format!("{}_v{}.rbxl",config.asset_id,version_number)); join_set.spawn(async move{ - let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:Some(version_number)}).await?; + let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:Some(version_number)}).await?; - tokio::fs::write(path,file).await?; + tokio::fs::write(path,maybe_gzip.to_vec()?).await?; Ok::<_,anyhow::Error>(()) }); @@ -1350,9 +1350,9 @@ struct DownloadDecompileConfig{ async fn download_decompile(config:DownloadDecompileConfig)->AResult<()>{ let context=CookieContext::new(config.cookie); - let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:None}).await?; + let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:None}).await?; - let dom=load_dom(std::io::Cursor::new(file))?; + let dom=maybe_gzip.read_with(load_dom,load_dom)?; let context=rox_compiler::DecompiledContext::from_dom(dom); context.write_files(rox_compiler::WriteConfig{ @@ -1532,8 +1532,8 @@ async fn download_and_decompile_history_into_git(config:DownloadAndDecompileHist .map(|asset_version|{ let context=context.clone(); tokio::task::spawn(async move{ - let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:Some(asset_version.assetVersionNumber)}).await?; - let dom=load_dom(std::io::Cursor::new(file))?; + let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:Some(asset_version.assetVersionNumber)}).await?; + let dom=maybe_gzip.read_with(load_dom,load_dom)?; Ok::<_,anyhow::Error>((asset_version,rox_compiler::DecompiledContext::from_dom(dom))) }) }))