use std::fmt::Arguments;
pub(crate) fn print_info() {
if let Some(run_id) = option_env!("GITHUB_RUN_ID") {
print_to_cli(format_args!(
"{}@{} (#{run_id})\n",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
));
} else {
print_to_cli(format_args!("{}@{}\n", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")));
}
print_to_cli(format_args!("{}", env!("CARGO_PKG_DESCRIPTION")));
print_to_cli(format_args!(
"Repository: {}, License: {}\n",
env!("CARGO_PKG_REPOSITORY"),
env!("CARGO_PKG_LICENSE")
));
print_to_cli(format_args!("For help run --help\n"));
if let (Some(sponsors), Some(contributors)) =
(option_env!("SPONSORS"), option_env!("CONTRIBUTORS"))
{
const SPONSORS_URL: &str = "https://github.com/sponsors/kaleidawave";
print_to_cli(format_args!("With thanks to all supporters of the project including:"));
print_to_cli(format_args!(
" Contributors (join them @ https://github.com/kaleidawave/ezno/issues):"
));
wrap_with_indent(contributors);
print_to_cli(format_args!(" Sponsors (join them @ {SPONSORS_URL}):"));
wrap_with_indent(sponsors);
}
}
pub(crate) fn cli_input_resolver(prompt: &str) -> String {
use std::io;
print!("{prompt}> ");
std::io::Write::flush(&mut io::stdout()).unwrap();
let mut input = String::new();
let std_in = &mut io::stdin();
#[cfg(target_family = "windows")]
let _n = multiline_term_input::read_string(std_in, &mut input);
#[cfg(target_family = "unix")]
let _n = std_in.read_line(&mut input).unwrap();
input
}
fn wrap_with_indent(input: &str) {
const INDENT: &str = " ";
const MAX_WIDTH: usize = 60;
let mut buf = String::new();
let mut iter = input.split(',').peekable();
while let Some(part) = iter.next() {
buf.push_str(part.trim());
if let Some(next) = iter.peek() {
if buf.len() + next.len() > MAX_WIDTH {
buf.push(',');
print_to_cli(format_args!("{INDENT}{buf}"));
buf.clear();
} else {
buf.push_str(", ");
}
}
}
print_to_cli(format_args!(
"{INDENT}{buf}\n",
INDENT = if buf.is_empty() { "" } else { INDENT }
));
}
#[cfg(target_family = "wasm")]
pub(crate) fn print_to_cli(arguments: Arguments) {
super::wasm_bindings::log(&arguments.to_string());
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn print_to_cli(arguments: Arguments) {
use std::io;
println!("{arguments}");
io::Write::flush(&mut io::stdout()).unwrap();
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum MaxDiagnostics {
All,
FixedTo(u16),
}
impl argh::FromArgValue for MaxDiagnostics {
fn from_arg_value(value: &str) -> Result<Self, String> {
if value == "all" {
Ok(Self::All)
} else {
match std::str::FromStr::from_str(value) {
Ok(value) => Ok(Self::FixedTo(value)),
Err(reason) => Err(reason.to_string()),
}
}
}
}
impl Default for MaxDiagnostics {
fn default() -> Self {
Self::FixedTo(30)
}
}
#[cfg(target_family = "wasm")]
pub struct FSFunction(pub js_sys::Function);
#[cfg(target_family = "wasm")]
impl checker::ReadFromFS for FSFunction {
fn read_file(&self, path: &std::path::Path) -> Option<Vec<u8>> {
self.0
.call1(
&wasm_bindgen::JsValue::null(),
&wasm_bindgen::JsValue::from(path.display().to_string()),
)
.ok()
.and_then(|s| s.as_string())
.map(|s| s.into_bytes())
}
}
#[cfg(not(target_family = "wasm"))]
pub struct FSFunction;
#[cfg(not(target_family = "wasm"))]
impl checker::ReadFromFS for FSFunction {
fn read_file(&self, path: &std::path::Path) -> Option<Vec<u8>> {
std::fs::read(path).ok()
}
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn upgrade_self() -> Result<String, Box<dyn std::error::Error>> {
use native_tls::{TlsConnector, TlsStream};
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::net::TcpStream;
fn make_request(
root: &str,
path: &str,
) -> Result<TlsStream<TcpStream>, Box<dyn std::error::Error>> {
let url = format!("{root}:443");
let tcp_stream = TcpStream::connect(url)?;
let connector = TlsConnector::new()?;
let mut tls_stream = connector.connect(root, tcp_stream)?;
let request = format!(
"GET {path} HTTP/1.1\r\n\
Host: {root}\r\n\
Connection: close\r\n\
User-Agent: ezno-self-update\r\n\
\r\n"
);
tls_stream.write_all(request.as_bytes())?;
Ok(tls_stream)
}
let (version_name, assert_url) = {
let mut stream = make_request("api.github.com", "/repos/kaleidawave/ezno/releases/latest")?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
let mut lines = response.lines();
for line in lines.by_ref() {
if line.is_empty() {
break;
}
}
use simple_json_parser::*;
let body = lines.next().ok_or("No body on API request")?;
#[cfg(target_os = "windows")]
const EXPECTED_END: &str = "windows.exe";
#[cfg(target_os = "linux")]
const EXPECTED_END: &str = "linux";
#[cfg(target_os = "macos")]
const EXPECTED_END: &str = "macos";
let mut required_binary = None;
let mut version_name = None;
let result = parse_with_exit_signal(body, |keys, value| {
if let [JSONKey::Slice("name")] = keys {
if let RootJSONValue::String(s) = value {
version_name = Some(s.to_owned());
}
} else if let [JSONKey::Slice("assets"), JSONKey::Index(_), JSONKey::Slice("browser_download_url")] =
keys
{
if let RootJSONValue::String(s) = value {
if s.ends_with(EXPECTED_END) {
required_binary = Some(s.to_owned());
return true;
}
}
}
false
});
if let Err(JSONParseError { at, reason }) = result {
return Err(Box::from(format!("JSON parse error: {reason:?} @ {at}")));
}
(
version_name.unwrap_or_default(),
required_binary.ok_or("could not find binary for platform")?,
)
};
let actual_asset_url = {
let url = assert_url.strip_prefix("https://github.com").ok_or_else(|| {
format!("Assert url {assert_url:?} does not start with 'https://github.com'")
})?;
let response = make_request("github.com", url)?;
let mut reader = BufReader::new(response);
let mut status_line = String::new();
reader.read_line(&mut status_line)?;
if !status_line.contains("302 Found") {
return Err(Box::from(format!("Expected redirect, got {status_line:?}")));
}
let mut location = None;
loop {
let mut line = String::new();
reader.read_line(&mut line)?;
if line == "\r\n" {
break;
}
if let l @ Some(_) = line.strip_prefix("Location: ") {
location = l.map(str::to_string);
break;
}
}
location.ok_or("no location")?
};
let url = actual_asset_url
.strip_prefix("https://objects.githubusercontent.com")
.ok_or_else(|| {
format!("Assert url {assert_url:?} does not start with 'https://objects.githubusercontent.com'")
})?
.trim_end();
let response = make_request("objects.githubusercontent.com", url)?;
let mut reader = BufReader::new(response);
let mut status_line = String::new();
reader.read_line(&mut status_line)?;
if !status_line.contains("200 OK") {
return Err(Box::from(format!("Got status {status_line:?}")));
}
let mut headers = String::new();
loop {
let mut line = String::new();
reader.read_line(&mut line)?;
if line == "\r\n" {
break;
}
headers.push_str(&line);
}
let new_binary = "new-ezno.exe";
let mut file = BufWriter::new(std::fs::File::create(new_binary)?);
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
file.write_all(&buffer)?;
self_replace::self_replace(new_binary)?;
std::fs::remove_file(new_binary)?;
Ok(version_name)
}