//! ref: composer/src/Composer/Util/RemoteFilesystem.php use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::preg::{CaptureKey, Preg}; use shirabe_php_shim::{ FILTER_VALIDATE_BOOLEAN, PHP_URL_HOST, PHP_URL_PATH, PHP_VERSION_ID, PhpMixed, RuntimeException, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_PROGRESS, array_replace_recursive, base64_encode, explode, extension_loaded, file_put_contents, filter_var, gethostbyname, http_clear_last_response_headers, http_get_last_response_headers, ini_get, json_decode, parse_url, preg_quote, restore_error_handler, set_error_handler, sprintf, strpos, strtolower, strtr, substr, trim, zlib_decode, }; use crate::config::Config; use crate::downloader::max_file_size_exceeded_exception::MaxFileSizeExceededException; use crate::downloader::transport_exception::TransportException; use crate::io::io_interface::IOInterface; use crate::util::auth_helper::AuthHelper; use crate::util::http::proxy_manager::ProxyManager; use crate::util::http::response::Response; use crate::util::http_downloader::HttpDownloader; use crate::util::platform::Platform; use crate::util::stream_context_factory::StreamContextFactory; use crate::util::url::Url; /// Result of `RemoteFilesystem::get` — string content, `true` (for copy), or `false`. #[derive(Debug, Clone)] pub enum GetResult { False, True, Content(String), } #[derive(Debug)] pub struct RemoteFilesystem { io: Box, config: std::rc::Rc>, scheme: String, bytes_max: i64, origin_url: String, file_url: String, file_name: Option, retry: bool, progress: bool, last_progress: Option, options: IndexMap, disable_tls: bool, last_headers: Vec, store_auth: bool, auth_helper: AuthHelper, degraded_mode: bool, redirects: i64, max_redirects: i64, } impl RemoteFilesystem { pub fn new( io: Box, config: std::rc::Rc>, options: IndexMap, disable_tls: bool, auth_helper: Option, ) -> Self { let (computed_options, disable_tls_set) = if !disable_tls { ( // TODO(phase-b): logger is None placeholder; should pass `&*io` if a Logger view is available. StreamContextFactory::get_tls_defaults(&options, None) .unwrap_or_else(|_| IndexMap::new()), false, ) } else { (IndexMap::new(), true) }; let merged = array_replace_recursive(computed_options, options); let auth_helper = auth_helper.unwrap_or_else(|| AuthHelper::new(io.clone_box(), config.clone())); Self { io, config, scheme: String::new(), bytes_max: 0, origin_url: String::new(), file_url: String::new(), file_name: None, retry: false, progress: false, last_progress: None, options: merged, disable_tls: disable_tls_set, last_headers: Vec::new(), store_auth: false, auth_helper, degraded_mode: false, redirects: 0, max_redirects: 20, } } pub fn copy( &mut self, origin_url: &str, file_url: &str, file_name: &str, progress: bool, options: IndexMap, ) -> anyhow::Result { self.get( origin_url, file_url, options, Some(file_name.to_string()), progress, ) } pub fn get_contents( &mut self, origin_url: &str, file_url: &str, progress: bool, options: IndexMap, ) -> anyhow::Result { self.get(origin_url, file_url, options, None, progress) } pub fn get_options(&self) -> &IndexMap { &self.options } pub fn set_options(&mut self, options: IndexMap) { self.options = array_replace_recursive(self.options.clone(), options); } pub fn is_tls_disabled(&self) -> bool { self.disable_tls } pub fn get_last_headers(&self) -> &[String] { &self.last_headers } pub fn find_status_code(headers: &[String]) -> Option { let mut value: Option = None; for header in headers { let mut m: IndexMap = IndexMap::new(); if Preg::is_match_strict_groups3("{^HTTP/\\S+ (\\d+)}i", header, Some(&mut m)) .unwrap_or(false) { value = m .get(&CaptureKey::ByIndex(1)) .and_then(|s| s.parse().ok()) .or(Some(0)); } } value } pub fn find_status_message(&self, headers: &[String]) -> Option { let mut value: Option = None; for header in headers { if Preg::is_match("{^HTTP/\\S+ \\d+}i", header).unwrap_or(false) { value = Some(header.clone()); } } value } fn get( &mut self, origin_url: &str, file_url: &str, additional_options: IndexMap, file_name: Option, progress: bool, ) -> anyhow::Result { // TODO(phase-b): PHP_URL_SCHEME constant isn't yet in the shim; PHP_URL_HOST stands in. self.scheme = parse_url(&strtr(file_url, "\\", "/"), PHP_URL_HOST) .as_string() .unwrap_or("") .to_string(); self.bytes_max = 0; self.origin_url = origin_url.to_string(); self.file_url = file_url.to_string(); self.file_name = file_name.clone(); self.progress = progress; self.last_progress = None; let mut retry_auth_failure = true; self.last_headers = Vec::new(); self.redirects = 1; // The first request counts. let mut temp_additional_options = additional_options.clone(); if let Some(v) = temp_additional_options.get("retry-auth-failure").cloned() { retry_auth_failure = v.as_bool().unwrap_or(true); temp_additional_options.shift_remove("retry-auth-failure"); } let mut is_redirect = false; if let Some(v) = temp_additional_options.get("redirects").cloned() { self.redirects = v.as_int().unwrap_or(self.redirects); is_redirect = true; temp_additional_options.shift_remove("redirects"); } let mut options = self.get_options_for_url(origin_url, temp_additional_options); let orig_file_url = file_url.to_string(); let mut file_url = file_url.to_string(); if options.contains_key("prevent_ip_access_callable") { return Err(anyhow::anyhow!(RuntimeException { message: "RemoteFilesystem doesn't support the 'prevent_ip_access_callable' config." .to_string(), code: 0, })); } if let Some(token) = options.get("gitlab-token").cloned() { let separator = if strpos(&file_url, "?").is_none() { "?" } else { "&" }; file_url = format!( "{}{}access_token={}", file_url, separator, token.as_string().unwrap_or("") ); options.shift_remove("gitlab-token"); } if let Some(http_opts) = options.get_mut("http") { if let PhpMixed::Array(m) = http_opts { m.insert("ignore_errors".to_string(), Box::new(PhpMixed::Bool(true))); } } let mut degraded_packagist = false; if self.degraded_mode && strpos(&file_url, "http://repo.packagist.org/") == Some(0) { file_url = format!( "http://{}{}", gethostbyname("repo.packagist.org"), substr(&file_url, 20, None) ); degraded_packagist = true; } let mut max_file_size: Option = None; if let Some(v) = options.get("max_file_size").cloned() { max_file_size = v.as_int(); options.shift_remove("max_file_size"); } // TODO(plugin): `Closure::fromCallable([$this, 'callbackGet'])` for stream notification. let ctx = StreamContextFactory::get_context(&file_url, options.clone(), IndexMap::new()) .map_err(|e| anyhow::anyhow!(e))?; let using_proxy = { let proxy_manager_guard = ProxyManager::get_instance().lock().unwrap(); let proxy = proxy_manager_guard .as_ref() .expect("ProxyManager instance") .get_proxy_for_request(&file_url) .map_err(|e| anyhow::anyhow!(e))?; proxy .get_status(Some(" using proxy (%s)")) .unwrap_or_default() }; self.io.write_error3( &format!( "{}{}{}", if strpos(&orig_file_url, "http") == Some(0) { "Downloading " } else { "Reading " }, Url::sanitize(orig_file_url.clone()), using_proxy ), true, crate::io::io_interface::DEBUG, ); if (!Preg::is_match("{^http://(repo\\.)?packagist\\.org/p/}", &file_url).unwrap_or(false) || (strpos(&file_url, "$").is_none() && strpos(&file_url, "%24").is_none())) && !degraded_packagist { let _ = self.config.borrow_mut().prohibit_url_by_config( &file_url, Some(&*self.io), &indexmap::IndexMap::new(), ); } if self.progress && !is_redirect { self.io.write_error3( "Downloading (connecting...)", false, crate::io::io_interface::NORMAL, ); } let mut error_message = String::new(); let error_code = 0_i64; let mut result: Option = None; // TODO(phase-b): set_error_handler with a closure capturing `error_message` by reference. set_error_handler(|_code, _msg, _file, _line| true); let mut http_response_header: Vec = Vec::new(); let inner_result: anyhow::Result<()> = (|| -> anyhow::Result<()> { result = self.get_remote_contents( origin_url, &file_url, &ctx, &mut http_response_header, max_file_size, )?; if !http_response_header.is_empty() && !http_response_header[0].is_empty() { let status_code = Self::find_status_code(&http_response_header); if let Some(code) = status_code { if code >= 300 && Response::find_header_value(&http_response_header, "content-type") .as_deref() == Some("application/json") { let parsed = result .as_deref() .map(|s| json_decode(s, true).unwrap_or(PhpMixed::Null)) .unwrap_or(PhpMixed::Null); let parsed_map: IndexMap = match parsed { PhpMixed::Array(m) => m.into_iter().map(|(k, v)| (k, *v)).collect(), _ => IndexMap::new(), }; let _ = HttpDownloader::output_warnings(&*self.io, origin_url, &parsed_map); } if [401_i64, 403].contains(&code) && retry_auth_failure { let status_message = self.find_status_message(&http_response_header); self.prompt_auth_and_retry( code, status_message, http_response_header.clone(), )?; } } } let content_length = if !http_response_header.is_empty() && !http_response_header[0].is_empty() { Response::find_header_value(&http_response_header, "content-length") } else { None }; if let Some(cl) = content_length { let cl_int: i64 = cl.parse().unwrap_or(0); if cl_int > 0 && Platform::strlen(result.as_deref().unwrap_or("")) < cl_int { let mut e = TransportException::new( format!( "Content-Length mismatch, received {} bytes out of the expected {}", Platform::strlen(result.as_deref().unwrap_or("")), cl_int ), 0, ); e.set_headers(http_response_header.clone()); e.set_status_code(Self::find_status_code(&http_response_header)); let decoded = self .decode_result(result.as_deref(), &http_response_header) .unwrap_or_else(|_| self.normalize_result(result.as_deref())); e.set_response(decoded); self.io.write_error3( &format!( "Content-Length mismatch, received {} out of {} bytes: ({})", Platform::strlen(result.as_deref().unwrap_or("")), cl_int, base64_encode(result.as_deref().unwrap_or("")) ), true, crate::io::io_interface::DEBUG, ); return Err(anyhow::anyhow!(e)); } } Ok(()) })(); let mut caught_e: Option = None; if let Err(mut e) = inner_result { if let Some(te) = e.downcast_mut::() { if !http_response_header.is_empty() && !http_response_header[0].is_empty() { te.set_headers(http_response_header.clone()); te.set_status_code(Self::find_status_code(&http_response_header)); } if result.is_some() { if let Ok(decoded) = self.decode_result(result.as_deref(), &http_response_header) { te.set_response(decoded); } } } caught_e = Some(e); result = None; } if !error_message.is_empty() && !filter_var( &ini_get("allow_url_fopen").unwrap_or_default(), FILTER_VALIDATE_BOOLEAN, ) { error_message = format!( "allow_url_fopen must be enabled in php.ini ({})", error_message ); } restore_error_handler(); if let Some(e) = caught_e { if !self.retry { let msg_owned = format!("{}", e); if !self.degraded_mode && strpos(&msg_owned, "Operation timed out").is_some() { self.degraded_mode = true; self.io .write_error3("", true, crate::io::io_interface::NORMAL); // TODO(phase-b): PHP writeError accepts an array of lines; joined here with newline. self.io.write_error3( &format!( "{}\nRetrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info", msg_owned, ), true, crate::io::io_interface::NORMAL, ); return self.get( &self.origin_url.clone(), &self.file_url.clone(), additional_options, self.file_name.clone(), self.progress, ); } return Err(e); } } let mut status_code: Option = None; let mut content_type: Option = None; let mut location_header: Option = None; if !http_response_header.is_empty() && !http_response_header[0].is_empty() { status_code = Self::find_status_code(&http_response_header); content_type = Response::find_header_value(&http_response_header, "content-type"); location_header = Response::find_header_value(&http_response_header, "location"); } let bitbucket_login_match = origin_url == "bitbucket.org" && !self .auth_helper .is_public_bit_bucket_download(&self.file_url) && substr(&self.file_url, -4, None) == ".zip" && (location_header.is_none() || substr( &parse_url(location_header.as_deref().unwrap_or(""), PHP_URL_PATH) .as_string() .unwrap_or("") .to_string(), -4, None, ) != ".zip") && content_type.is_some() && Preg::is_match("{^text/html\\b}i", content_type.as_deref().unwrap_or("")) .unwrap_or(false); if bitbucket_login_match { result = None; if retry_auth_failure { self.prompt_auth_and_retry(401, None, Vec::new())?; } } let gitlab_domains_value = self.config.borrow_mut().get("gitlab-domains"); let gitlab_domains: Vec = gitlab_domains_value .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect() }) .unwrap_or_default(); if status_code == Some(404) && gitlab_domains.iter().any(|d| d == origin_url) && strpos(&self.file_url, "archive.zip").is_some() { result = None; if retry_auth_failure { self.prompt_auth_and_retry(401, None, Vec::new())?; } } let mut has_followed_redirect = false; if let Some(code) = status_code { if code >= 300 && code <= 399 && code != 304 && self.redirects < self.max_redirects { has_followed_redirect = true; result = self.handle_redirect( &http_response_header, additional_options.clone(), result.clone(), )?; } } if let Some(code) = status_code { if code >= 400 && code <= 599 { if !self.retry { if self.progress && !is_redirect { self.io.overwrite_error4( "Downloading (failed)", false, None, crate::io::io_interface::NORMAL, ); } let mut e = TransportException::new_with_code( format!( "The \"{}\" file could not be downloaded ({})", self.file_url, http_response_header[0] ), code, ); e.set_headers(http_response_header.clone()); let decoded = self .decode_result(result.as_deref(), &http_response_header) .unwrap_or(None); e.set_response(decoded); e.set_status_code(Some(code)); return Err(anyhow::anyhow!(e)); } result = None; } } if self.progress && !self.retry && !is_redirect { self.io.overwrite_error4( &format!( "Downloading ({})", if result.is_none() { "failed" } else { "100%" } ), false, None, crate::io::io_interface::NORMAL, ); } if result.is_some() && extension_loaded("zlib") && strpos(&file_url, "http") == Some(0) && !has_followed_redirect { match self.decode_result(result.as_deref(), &http_response_header) { Ok(decoded) => { result = decoded; } Err(e) => { if self.degraded_mode { return Err(e); } self.degraded_mode = true; // TODO(phase-b): PHP writeError accepts an array of lines; joined here with newline. self.io.write_error3( &format!( "\nFailed to decode response: {}\nRetrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info", e, ), true, crate::io::io_interface::NORMAL, ); return self.get( &self.origin_url.clone(), &self.file_url.clone(), additional_options, self.file_name.clone(), self.progress, ); } } } if result.is_some() && file_name.is_some() && !is_redirect { let result_str = result.as_deref().unwrap(); if result_str.is_empty() { return Err(anyhow::anyhow!(TransportException::new( format!( "\"{}\" appears broken, and returned an empty 200 response", self.file_url ), 0, ))); } let put_error_message = String::new(); // TODO(phase-b): set_error_handler closure that captures `put_error_message` by reference set_error_handler(|_code, _msg, _file, _line| true); let write_result = file_put_contents(file_name.as_deref().unwrap(), result_str.as_bytes()); restore_error_handler(); if write_result.is_none() { return Err(anyhow::anyhow!(TransportException::new( format!( "The \"{}\" file could not be written to {}: {}", self.file_url, file_name.as_deref().unwrap(), put_error_message ), 0, ))); } let _ = put_error_message; } if self.retry { self.retry = false; let new_result = self.get( &self.origin_url.clone(), &self.file_url.clone(), additional_options, self.file_name.clone(), self.progress, )?; if self.store_auth { let _ = self.auth_helper.store_auth( &self.origin_url, crate::util::auth_helper::StoreAuth::Bool(self.store_auth), ); self.store_auth = false; } return Ok(new_result); } if result.is_none() { let mut e = TransportException::new_with_code( format!( "The \"{}\" file could not be downloaded: {}", self.file_url, error_message ), error_code, ); if !http_response_header.is_empty() && !http_response_header[0].is_empty() { e.set_headers(http_response_header.clone()); } let msg_owned = format!("{}", e); if !self.degraded_mode && strpos(&msg_owned, "Operation timed out").is_some() { self.degraded_mode = true; self.io .write_error3("", true, crate::io::io_interface::NORMAL); // TODO(phase-b): PHP writeError accepts an array of lines; joined here with newline. self.io.write_error3( &format!( "{}\nRetrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info", msg_owned, ), true, crate::io::io_interface::NORMAL, ); return self.get( &self.origin_url.clone(), &self.file_url.clone(), additional_options, self.file_name.clone(), self.progress, ); } return Err(anyhow::anyhow!(e)); } if !http_response_header.is_empty() && !http_response_header[0].is_empty() { self.last_headers = http_response_header.clone(); } match result { Some(s) => { if file_name.is_some() && !is_redirect { Ok(GetResult::True) } else { Ok(GetResult::Content(s)) } } None => Ok(GetResult::False), } } fn get_remote_contents( &self, _origin_url: &str, _file_url: &str, _context: &PhpMixed, response_headers: &mut Vec, max_file_size: Option, ) -> anyhow::Result> { let mut result: Option = None; if PHP_VERSION_ID >= 80400 { http_clear_last_response_headers(); } let mut caught_e: Option = None; // TODO(phase-b): wrap PHP's `file_get_contents` with stream context and error capture. let outer: Result, anyhow::Error> = Ok(None); match outer { Ok(v) => result = v, Err(e) => caught_e = Some(e), } if let Some(ref r) = result { if let Some(max) = max_file_size { if Platform::strlen(r) >= max { return Err(anyhow::anyhow!(MaxFileSizeExceededException::new(format!( "Maximum allowed download size reached. Downloaded {} of allowed {} bytes", Platform::strlen(r), max )))); } } } if PHP_VERSION_ID >= 80400 { *response_headers = http_get_last_response_headers().unwrap_or_default(); http_clear_last_response_headers(); } else { // TODO(phase-b): read the magic `$http_response_header` PHP variable. *response_headers = Vec::new(); } if let Some(e) = caught_e { return Err(e); } Ok(result) } fn callback_get( &mut self, notification_code: i64, _severity: i64, message: Option, message_code: i64, bytes_transferred: i64, bytes_max: i64, ) -> anyhow::Result<()> { match notification_code { x if x == STREAM_NOTIFY_FAILURE => { if 400 == message_code { return Err(anyhow::anyhow!(TransportException::new_with_code( format!( "The '{}' URL could not be accessed: {}", self.file_url, message.unwrap_or_default() ), message_code, ))); } } x if x == STREAM_NOTIFY_FILE_SIZE_IS => { self.bytes_max = bytes_max; } x if x == STREAM_NOTIFY_PROGRESS => { if self.bytes_max > 0 && self.progress { let progression = std::cmp::min( 100_i64, ((bytes_transferred as f64 / self.bytes_max as f64) * 100.0).round() as i64, ); if 0 == progression % 5 && 100 != progression && Some(progression) != self.last_progress { self.last_progress = Some(progression); self.io.overwrite_error4( &format!("Downloading ({}%)", progression), false, None, crate::io::io_interface::NORMAL, ); } } } _ => {} } Ok(()) } fn prompt_auth_and_retry( &mut self, http_status: i64, reason: Option, headers: Vec, ) -> anyhow::Result<()> { let file_url = self.file_url.clone(); let origin_url = self.origin_url.clone(); let result = self.auth_helper.prompt_auth_if_needed( &file_url, &origin_url, http_status, reason.as_deref(), headers, 1, None, )?; self.store_auth = matches!( result.store_auth, crate::util::auth_helper::StoreAuth::Bool(true) | crate::util::auth_helper::StoreAuth::Prompt ); self.retry = result.retry; if self.retry { return Err(anyhow::anyhow!(TransportException::new( "RETRY".to_string(), 0, ))); } Ok(()) } fn get_options_for_url( &mut self, origin_url: &str, additional_options: IndexMap, ) -> IndexMap { let tls_options: IndexMap = IndexMap::new(); let mut headers: Vec = Vec::new(); if extension_loaded("zlib") { headers.push("Accept-Encoding: gzip".to_string()); } let mut options = array_replace_recursive(self.options.clone(), tls_options); options = array_replace_recursive(options, additional_options); if !self.degraded_mode { let http_entry = options .entry("http".to_string()) .or_insert_with(|| PhpMixed::Array(IndexMap::new())); if let PhpMixed::Array(m) = http_entry { m.insert( "protocol_version".to_string(), Box::new(PhpMixed::Float(1.1)), ); } headers.push("Connection: close".to_string()); } let header_is_string = options .get("http") .and_then(|v| v.as_array()) .and_then(|m| m.get("header")) .map(|v| matches!(v.as_ref(), PhpMixed::String(_))) .unwrap_or(false); if header_is_string { let header_str = options["http"].as_array().unwrap()["header"] .as_string() .unwrap_or("") .to_string(); let split = explode("\r\n", &trim(&header_str, Some("\r\n"))); if let Some(PhpMixed::Array(m)) = options.get_mut("http") { m.insert( "header".to_string(), Box::new(PhpMixed::List( split .into_iter() .map(|s| Box::new(PhpMixed::String(s))) .collect(), )), ); } } let file_url = self.file_url.clone(); options = self .auth_helper .add_authentication_options(options, origin_url, &file_url) .unwrap_or_else(|_| IndexMap::new()); let http_entry = options .entry("http".to_string()) .or_insert_with(|| PhpMixed::Array(IndexMap::new())); if let PhpMixed::Array(m) = http_entry { m.insert("follow_location".to_string(), Box::new(PhpMixed::Int(0))); } for header in headers { if let Some(PhpMixed::Array(m)) = options.get_mut("http") { let header_list = m .entry("header".to_string()) .or_insert_with(|| Box::new(PhpMixed::List(Vec::new()))); if let PhpMixed::List(l) = header_list.as_mut() { l.push(Box::new(PhpMixed::String(header))); } } } options } fn handle_redirect( &mut self, response_headers: &[String], mut additional_options: IndexMap, result: Option, ) -> anyhow::Result> { let mut target_url: Option = None; if let Some(location_header) = Response::find_header_value(response_headers, "location") { // TODO(phase-b): use PHP_URL_SCHEME once available to detect absolute URLs. if !parse_url(&location_header, PHP_URL_HOST) .as_string() .unwrap_or("") .is_empty() && location_header.contains("://") { target_url = Some(location_header); } else if parse_url(&location_header, PHP_URL_HOST) .as_string() .map(|s| !s.is_empty()) .unwrap_or(false) { target_url = Some(format!("{}:{}", self.scheme, location_header)); } else if location_header.starts_with('/') { let url_host = parse_url(&self.file_url, PHP_URL_HOST) .as_string() .unwrap_or("") .to_string(); target_url = Some( Preg::replace( &format!( "{{^(.+(?://|@){}(?::\\d+)?)(?:[/\\?].*)?$}}", preg_quote(&url_host, None) ), &format!("\\1{}", location_header), &self.file_url, ) .unwrap_or_else(|_| self.file_url.clone()), ); } else { target_url = Some( Preg::replace( "{^(.+/)[^/?]*(?:\\?.*)?$}", &format!("\\1{}", location_header), &self.file_url, ) .unwrap_or_else(|_| self.file_url.clone()), ); } } if let Some(target_url) = target_url { self.redirects += 1; self.io .write_error3("", true, crate::io::io_interface::DEBUG); self.io.write_error3( &sprintf( "Following redirect (%u) %s", &[ PhpMixed::Int(self.redirects), PhpMixed::String(Url::sanitize(target_url.clone())), ], ), true, crate::io::io_interface::DEBUG, ); additional_options.insert("redirects".to_string(), PhpMixed::Int(self.redirects)); let host = parse_url(&target_url, PHP_URL_HOST) .as_string() .unwrap_or("") .to_string(); let res = self.get( &host, &target_url, additional_options, self.file_name.clone(), self.progress, )?; return Ok(match res { GetResult::Content(s) => Some(s), _ => None, }); } if !self.retry { let mut e = TransportException::new( format!( "The \"{}\" file could not be downloaded, got redirect without Location ({})", self.file_url, response_headers.first().map(|s| s.as_str()).unwrap_or("") ), 0, ); e.set_headers(response_headers.to_vec()); let decoded = self.decode_result(result.as_deref(), response_headers)?; e.set_response(decoded); return Err(anyhow::anyhow!(e)); } Ok(None) } fn decode_result( &self, result: Option<&str>, response_headers: &[String], ) -> anyhow::Result> { let mut result = result.map(|s| s.to_string()); if result.is_some() && extension_loaded("zlib") { let content_encoding = Response::find_header_value(response_headers, "content-encoding"); let decode = content_encoding .as_deref() .map(|s| "gzip" == strtolower(s)) .unwrap_or(false); if decode { let decoded = zlib_decode(result.as_deref().unwrap_or("")); result = match decoded { Some(d) => Some(d), None => { return Err(anyhow::anyhow!(TransportException::new( "Failed to decode zlib stream".to_string(), 0, ))); } }; } } Ok(self.normalize_result(result.as_deref())) } fn normalize_result(&self, result: Option<&str>) -> Option { result.map(|s| s.to_string()) } }