use std::net::SocketAddr;
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::Ordering;
use quinn::ClientConfig;
use rand::Rng;
use rcgen::{CertifiedKey, generate_simple_self_signed};
use crate::error::{FastSyncError, Result};
use crate::hash::Blake3Digest;
use crate::summary::human_bytes;
use super::{DEFAULT_BIND_PORT, TEMP_COUNTER, cli::validate_pairing_code};
pub(super) fn server_config() -> Result<quinn::ServerConfig> {
let CertifiedKey { cert, signing_key } =
generate_simple_self_signed(vec!["fastsync.local".to_string(), "localhost".to_string()])
.map_err(|error| other("generate temporary QUIC certificate", error))?;
quinn::ServerConfig::with_single_cert(vec![cert.der().clone()], signing_key.into())
.map_err(|error| other("create QUIC server TLS config", error))
}
pub(super) fn insecure_client_config() -> ClientConfig {
let crypto = quinn::rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(std::sync::Arc::new(
rustls_dangerous::NoCertificateVerification,
))
.with_no_client_auth();
let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(crypto)
.expect("rustls client config must contain a QUIC initial cipher suite");
ClientConfig::new(std::sync::Arc::new(crypto))
}
pub(super) async fn resolve_endpoint(endpoint: &str) -> Result<SocketAddr> {
let raw = endpoint.strip_prefix("quic://").unwrap_or(endpoint);
let (host, port) = match raw.rsplit_once(':') {
Some((host, port)) => {
let port = port
.parse::<u16>()
.map_err(|error| other("parse QUIC endpoint port", error))?;
(host, port)
}
None => (raw, DEFAULT_BIND_PORT),
};
let mut addrs = tokio::net::lookup_host((host, port))
.await
.map_err(|error| other("resolve QUIC endpoint", error))?;
addrs
.next()
.ok_or_else(|| other_message("resolve QUIC endpoint", "no address resolved"))
}
pub(super) fn prompt_code() -> Result<String> {
eprint!("Pairing code: ");
let mut code = String::new();
std::io::stdin()
.read_line(&mut code)
.map_err(|error| other("read pairing code", error))?;
let code = code.trim().to_string();
validate_pairing_code(&code).map_err(|message| other_message("read pairing code", message))?;
Ok(code)
}
pub(super) fn generate_pairing_code() -> String {
let mut rng = rand::rng();
let code: u32 = rng.random_range(0..=999_999);
format!("{code:06}")
}
pub(super) fn safe_join(root: &Path, relative: &Path) -> Result<PathBuf> {
if relative.is_absolute()
|| relative.components().any(|component| {
matches!(
component,
Component::Prefix(_) | Component::RootDir | Component::ParentDir
)
})
{
return Err(FastSyncError::PathOutsideRoot {
path: relative.to_path_buf(),
});
}
Ok(root.join(relative))
}
pub(super) fn unique_temp_path(parent: &Path) -> PathBuf {
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
parent.join(format!(
".fastsync.net.tmp.{}.{}",
std::process::id(),
counter
))
}
pub(super) fn hex_digest(digest: Blake3Digest) -> String {
let mut output = String::with_capacity(digest.len() * 2);
for byte in digest {
use std::fmt::Write as _;
let _ = write!(output, "{byte:02x}");
}
output
}
pub(super) fn throughput_bps(bytes: u64, elapsed_ms: u128) -> u64 {
if elapsed_ms == 0 {
return 0;
}
((bytes as u128).saturating_mul(1000) / elapsed_ms).min(u64::MAX as u128) as u64
}
pub(super) fn throughput_text(bytes: u64, elapsed_ms: u128) -> String {
let bps = throughput_bps(bytes, elapsed_ms);
format!("{}/s", human_bytes(bps))
}
pub(super) fn other(context: impl Into<String>, error: impl std::fmt::Display) -> FastSyncError {
FastSyncError::Io {
context: context.into(),
source: std::io::Error::other(error.to_string()),
}
}
pub(super) fn other_message(
context: impl Into<String>,
message: impl Into<String>,
) -> FastSyncError {
FastSyncError::Io {
context: context.into(),
source: std::io::Error::other(message.into()),
}
}
pub(super) fn io_path(context: &'static str, path: &Path, error: std::io::Error) -> FastSyncError {
FastSyncError::Io {
context: format!("{context}: {}", path.display()),
source: error,
}
}