use std::borrow::Cow;
use std::env::var_os;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
use anyhow::Result;
use reqwest::blocking::{Request, Response};
use reqwest::header::HeaderValue;
use url::Url;
pub fn unescape(text: &str, special_chars: &'static str) -> String {
let mut out = String::new();
let mut chars = text.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some(next) if special_chars.contains(next) => {
out.push(next);
}
Some(next) => {
out.push(ch);
out.push(next);
}
None => {
out.push(ch);
}
}
} else {
out.push(ch);
}
}
out
}
pub fn clone_request(request: &mut Request) -> Result<Request> {
if let Some(b) = request.body_mut().as_mut() {
b.buffer()?;
}
Ok(request.try_clone().unwrap()) }
pub fn test_mode() -> bool {
cfg!(test) || var_os("XH_TEST_MODE").is_some()
}
pub fn test_pretend_term() -> bool {
var_os("XH_TEST_MODE_TERM").is_some()
}
pub fn test_default_color() -> bool {
var_os("XH_TEST_MODE_COLOR").is_some()
}
#[cfg(test)]
pub fn random_string() -> String {
use rand::Rng;
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(10)
.map(char::from)
.collect()
}
pub fn config_dir() -> Option<PathBuf> {
if let Some(dir) = std::env::var_os("XH_CONFIG_DIR") {
return Some(dir.into());
}
if cfg!(target_os = "macos") {
let legacy_config_dir = dirs::config_dir()?.join("xh");
let config_home = match var_os("XDG_CONFIG_HOME") {
Some(dir) => dir.into(),
None => dirs::home_dir()?.join(".config"),
};
let new_config_dir = config_home.join("xh");
if legacy_config_dir.exists() && !new_config_dir.exists() {
Some(legacy_config_dir)
} else {
Some(new_config_dir)
}
} else {
Some(dirs::config_dir()?.join("xh"))
}
}
pub fn get_home_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
if let Some(path) = std::env::var_os("XH_TEST_MODE_WIN_HOME_DIR") {
return Some(PathBuf::from(path));
}
dirs::home_dir()
}
pub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf {
if let Ok(path) = path.as_ref().strip_prefix("~") {
let mut expanded_path = PathBuf::new();
expanded_path.push(get_home_dir().unwrap_or_else(|| "~".into()));
expanded_path.push(path);
expanded_path
} else {
path.as_ref().into()
}
}
pub fn url_with_query(mut url: Url, query: &[(&str, Cow<str>)]) -> Url {
if !query.is_empty() {
let mut pairs = url.query_pairs_mut();
for (name, value) in query {
pairs.append_pair(name, value);
}
}
url
}
#[macro_export]
macro_rules! vec_of_strings {
($($str:expr),*) => ({
vec![$(String::from($str),)*] as Vec<String>
});
}
pub const BUFFER_SIZE: usize = 128 * 1024;
pub fn copy_largebuf(
reader: &mut impl io::Read,
writer: &mut impl Write,
flush: bool,
) -> io::Result<()> {
let mut buf = vec![0; BUFFER_SIZE];
loop {
match reader.read(&mut buf) {
Ok(0) => return Ok(()),
Ok(len) => {
writer.write_all(&buf[..len])?;
if flush {
writer.flush()?;
}
}
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
}
pub(crate) trait HeaderValueExt {
fn to_utf8_str(&self) -> Result<&str, Utf8Error>;
fn to_ascii_or_latin1(&self) -> Result<&str, BadHeaderValue<'_>>;
}
impl HeaderValueExt for HeaderValue {
fn to_utf8_str(&self) -> Result<&str, Utf8Error> {
std::str::from_utf8(self.as_bytes())
}
fn to_ascii_or_latin1(&self) -> Result<&str, BadHeaderValue<'_>> {
self.to_str().map_err(|_| BadHeaderValue { value: self })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct BadHeaderValue<'a> {
value: &'a HeaderValue,
}
impl<'a> BadHeaderValue<'a> {
pub(crate) fn latin1(self) -> String {
self.value.as_bytes().iter().map(|&b| b as char).collect()
}
pub(crate) fn utf8(self) -> Option<&'a str> {
self.value.to_utf8_str().ok()
}
}
pub(crate) fn reason_phrase(response: &Response) -> Cow<'_, str> {
if let Some(reason) = response.extensions().get::<hyper::ext::ReasonPhrase>() {
String::from_utf8_lossy(reason.as_bytes())
} else if let Some(reason) = response.status().canonical_reason() {
Cow::Borrowed(reason)
} else {
Cow::Borrowed("<unknown status code>")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_latin1() {
let good = HeaderValue::from_static("Rhodes");
let good = good.to_ascii_or_latin1();
assert_eq!(good, Ok("Rhodes"));
let bad = HeaderValue::from_bytes("Ῥόδος".as_bytes()).unwrap();
let bad = bad.to_ascii_or_latin1().unwrap_err();
assert_eq!(bad.latin1(), "ῬÏ\u{8c}δοÏ\u{82}");
assert_eq!(bad.utf8(), Some("Ῥόδος"));
let worse = HeaderValue::from_bytes(b"R\xF3dos").unwrap();
let worse = worse.to_ascii_or_latin1().unwrap_err();
assert_eq!(worse.latin1(), "Ródos");
assert_eq!(worse.utf8(), None);
}
}