use std::
{
fs,
env,
time::Duration,
path::Path,
};
use ureq::{ Error, Agent };
use serde_json::Value;
use semver::Version;
use crate::consts;
#[cfg(feature = "client_base")]
use tokio::sync::mpsc::Sender;
#[cfg(feature = "client_base")]
use crate::network::client::ClientEvent;
#[cfg(feature = "chat")]
use std::
{
fmt::Write,
path::PathBuf,
};
fn get_dir(dir: &str) -> String
{
dir.replace("{HOME}", dirs::home_dir().expect("Could not determine home directory").to_str().expect("Invalid home directory"))
}
pub fn get_version<'a>() -> &'a str {
env!("CARGO_PKG_VERSION")
}
pub fn get_identifier() -> String {
format!("WHY2/{}", get_version())
}
pub fn fetch_data(url: &str) -> Result<String, Error> {
let agent: Agent = Agent::config_builder()
.timeout_global(Some(Duration::from_millis(consts::FETCH_TIMEOUT)))
.build()
.into();
agent.get(url)
.header("User-Agent", &get_identifier())
.call()?
.body_mut()
.read_to_string()
}
pub async fn check_version(#[cfg(feature = "client_base")] tx: &Sender<ClientEvent>) {
let metadata_raw = match tokio::task::spawn_blocking(|| fetch_data(consts::METADATA_URL)).await
.expect("Fetching versions panicked")
{
Ok(m) => m,
Err(_) =>
{
#[cfg(feature = "client_base")]
{
tx.send(ClientEvent::VersionFailed).await.unwrap();
}
#[cfg(feature = "server")]
{
log::warn!("Fetching versions failed, this release could be unsafe!");
}
return;
}
};
let metadata: Value = serde_json::from_str(&metadata_raw).expect("Parsing versions failed"); let newest_version = metadata.get("crate") .and_then(|c| c.get("newest_version"))
.and_then(|v| v.as_str())
.unwrap();
let current_version = get_version();
if current_version != newest_version
{
let versions = metadata.get("versions").and_then(|v| v.as_array()).unwrap();
let mut newer_versions = 0usize;
let current_version = Version::parse(current_version).expect("Invalid version");
for version in versions
{
if Version::parse(version.get("num").and_then(|n| n.as_str()).unwrap()).unwrap() > current_version
{
newer_versions += 1;
}
}
#[cfg(feature = "client_base")]
{
tx.send(ClientEvent::UnsafeVersion(newer_versions, current_version, newest_version.to_owned())).await.unwrap();
}
#[cfg(feature = "server")]
{
log::warn!("This release could be unsafe! You are {newer_versions} versions behind! ({current_version}/{newest_version})");
}
}
}
pub fn get_why2_dir() -> String {
get_dir(consts::CONFIG_DIR)
}
pub fn check_directory() {
let config = get_why2_dir();
if !Path::new(&config).is_dir()
{
fs::create_dir_all(config).expect("Failed to create WHY2 config directory");
}
}
pub fn image_dimensions(header: &[u8]) -> Option<(u32, u32)>
{
let be = |at: usize| u32::from_be_bytes(header[at..at + 4].try_into().unwrap());
let le = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]) as u32;
if header.len() >= 24 && header.starts_with(b"\x89PNG\r\n\x1a\n") && &header[12..16] == b"IHDR"
{
return Some((be(16), be(20)));
}
if header.len() >= 10 && (header.starts_with(b"GIF87a") || header.starts_with(b"GIF89a"))
{
return Some((le(6), le(8)));
}
None
}
pub fn is_avatar(header: &[u8]) -> bool
{
matches!(image_dimensions(header), Some((width, height))
if width == height && width > 0 && width <= consts::AVATAR_DIMENSION)
}
pub fn is_image(header: &[u8]) -> bool {
const MAGIC: [&[u8]; 10] =
[
b"\x89PNG\r\n\x1a\n", b"\xff\xd8\xff", b"GIF87a", b"GIF89a", b"BM", b"\x00\x00\x01\x00", b"qoif", b"#?", b"farbfeld", b"DDS ", ];
if MAGIC.iter().any(|magic| header.starts_with(magic)) { return true; }
(header.len() >= 12 && header.starts_with(b"RIFF") && &header[8..12] == b"WEBP") ||
(header.len() >= 3 && header[0] == b'P' && (b'1'..=b'7').contains(&header[1]) &&
header[2].is_ascii_whitespace())
}
#[cfg(feature = "chat")]
pub fn is_web_url(url: &str) -> bool {
let scheme = |length: usize, scheme: &str| url.get(..length)
.is_some_and(|head| head.eq_ignore_ascii_case(scheme));
scheme(7, "http://") || scheme(8, "https://")
}
#[cfg(feature = "server")]
pub fn get_upload_dir(username: &str) -> PathBuf {
env::temp_dir().join(consts::UPLOADS_DIR).join(username)
}
#[cfg(feature = "server")]
pub fn get_image_dir() -> PathBuf {
PathBuf::from(get_why2_dir() + consts::SERVER_IMAGES_DIR)
}
#[cfg(feature = "chat")]
pub fn unhex(text: &str) -> Option<[u8; 32]> {
if text.len() != 64 { return None; }
let mut bytes = [0u8; 32];
for (byte, pair) in bytes.iter_mut().zip(text.as_bytes().chunks(2))
{
*byte = u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()?;
}
Some(bytes)
}
#[cfg(feature = "chat")]
pub fn hex(bytes: &[u8]) -> String {
let mut string = String::with_capacity(bytes.len() * 2);
for byte in bytes
{
write!(string, "{byte:02x}").expect("Hex encoding failed");
}
string
}
#[cfg(feature = "client_base")]
pub fn avatar_temp(hash: &[u8; 32], extension: &str) -> PathBuf {
env::temp_dir().join(format!("{}{}.{extension}", consts::AVATAR_TEMP_PREFIX, hex(hash)))
}
#[cfg(feature = "client_base")]
pub fn drop_avatar_temp(path: &Path) {
let ours = path.parent() == Some(env::temp_dir().as_path()) && path.file_name()
.and_then(|name| name.to_str()).is_some_and(|name| name.starts_with(consts::AVATAR_TEMP_PREFIX));
if ours { let _ = fs::remove_file(path); }
}
#[cfg(feature = "client_base")]
pub fn get_image_cache_dir(fingerprint: &str) -> PathBuf {
PathBuf::from(get_why2_dir() + consts::CLIENT_IMAGES_DIR).join(fingerprint)
}
#[cfg(feature = "server")]
pub fn restart() -> !
{
let executable = match env::current_exe()
{
Ok(path) => path,
Err(error) =>
{
log::error!("Restart failed (executable not found): {error}");
std::process::exit(1);
}
};
let arguments: Vec<String> = env::args().skip(1).collect();
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let error = std::process::Command::new(&executable).args(&arguments).exec();
log::error!("Restart failed: {error}");
}
#[cfg(not(unix))]
match std::process::Command::new(&executable).args(&arguments).spawn()
{
Ok(_) => std::process::exit(0),
Err(error) => log::error!("Restart failed: {error}"),
}
std::process::exit(1);
}