From 5bd9bdfa359f7857d6312e299cedaef19d47816f Mon Sep 17 00:00:00 2001 From: Diego Giagio Date: Tue, 9 Oct 2018 14:21:06 -0400 Subject: [PATCH] Initial commit --- Cargo.toml | 2 + warp-packer/Cargo.toml | 12 ++ warp-packer/src/main.rs | 219 +++++++++++++++++++++++++++++++++++ warp-runner/Cargo.toml | 13 +++ warp-runner/src/executor.rs | 33 ++++++ warp-runner/src/extractor.rs | 99 ++++++++++++++++ warp-runner/src/main.rs | 73 ++++++++++++ warp.iml | 25 ++++ 8 files changed, 476 insertions(+) create mode 100644 Cargo.toml create mode 100644 warp-packer/Cargo.toml create mode 100644 warp-packer/src/main.rs create mode 100644 warp-runner/Cargo.toml create mode 100644 warp-runner/src/executor.rs create mode 100644 warp-runner/src/extractor.rs create mode 100644 warp-runner/src/main.rs create mode 100644 warp.iml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..85fe686 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["warp-runner", "warp-packer"] diff --git a/warp-packer/Cargo.toml b/warp-packer/Cargo.toml new file mode 100644 index 0000000..a69823f --- /dev/null +++ b/warp-packer/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "warp-packer" +version = "0.1.0" +authors = ["Diego Giagio "] + +[dependencies] +clap = "2.32.0" +dirs = "1.0.4" +reqwest = "0.9.2" +tempdir = "0.3.7" +flate2 = "1.0" +tar = "0.4" diff --git a/warp-packer/src/main.rs b/warp-packer/src/main.rs new file mode 100644 index 0000000..617455c --- /dev/null +++ b/warp-packer/src/main.rs @@ -0,0 +1,219 @@ +extern crate clap; +extern crate dirs; +extern crate reqwest; +extern crate tempdir; +extern crate tar; +extern crate flate2; + +use clap::{App, AppSettings, Arg}; +use std::process; +use std::path::PathBuf; +use std::fs; +use std::io::copy; +use tempdir::TempDir; +use std::path::Path; +use std::io; +use std::error::Error; +use std::io::Write; +use std::io::Read; +use std::fs::Metadata; +use flate2::write::GzEncoder; +use flate2::Compression; + +const APP_NAME: &str = env!("CARGO_PKG_NAME"); +const AUTHOR: &str = env!("CARGO_PKG_AUTHORS"); +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +const SUPPORTED_ARCHS: &[&str] = &["linux-x64", "windows-x64", "macos-x64"]; +const RUNNER_URL_TEMPLATE: &str = "https://s3.amazonaws.com/dgiagio-public/warp/$VERSION$/$ARCH$/warp-runner"; +const RUNNER_MAGIC: &[u8] = b"tVQhhsFFlGGD3oWV4lEPST8I8FEPP54IM0q7daes4E1y3p2U2wlJRYmWmjPYfkhZ0PlT14Ls0j8fdDkoj33f2BlRJavLj3mWGibJsGt5uLAtrCDtvxikZ8UX2mQDCrgE\0"; + +/// Print a message to stderr and exit with error code 1 +macro_rules! bail { + () => (process::exit(1)); + ($($arg:tt)*) => ({ + eprint!("{}\n", format_args!($($arg)*)); + process::exit(1); + }) +} + +fn runners_dir() -> PathBuf { + dirs::data_local_dir() + .expect("No data local dir found") + .join("warp") + .join("runners") +} + +fn runner_url(arch: &str) -> String { + let mut ext = ""; + if cfg!(target_family = "windows") { + ext = ".exe"; + } + + RUNNER_URL_TEMPLATE + .replace("$VERSION$", VERSION) + .replace("$ARCH$", arch) + ext +} + +fn patch_runner(runner_exec: &Path, new_runner_exec: &Path, exec_name: &str) -> io::Result<()> { + // Read runner executable in memory + let mut buf = vec![]; + fs::File::open(runner_exec)? + .read_to_end(&mut buf)?; + + // Set the correct target executable name into the local magic buffer + let magic_len = RUNNER_MAGIC.len(); + let mut new_magic = vec![0; magic_len]; + new_magic[..exec_name.len()].clone_from_slice(exec_name.as_bytes()); + + // Find the magic buffer offset inside the runner executable + let mut offs_opt = None; + for (i, chunk) in buf.windows(magic_len).enumerate() { + if chunk == RUNNER_MAGIC { + offs_opt = Some(i); + break; + } + } + + if offs_opt.is_none() { + return Err(io::Error::new(io::ErrorKind::Other, "no magic found inside runner")) + } + + // Replace the magic with the new one that points to the target executable + let offs = offs_opt.unwrap(); + buf[offs..offs + magic_len].clone_from_slice(&new_magic); + + // Write patched runner to disk + fs::File::create(&new_runner_exec)? + .write_all(&buf)?; + + Ok(()) +} + +#[cfg(target_family = "windows")] +fn is_executable(path: &Path, _: &Metadata) -> bool { + if let Some(ext) = path.extension() { + ext == "exe" + } else { + false + } +} + +#[cfg(target_family = "unix")] +fn is_executable(_: &Path, metadata: &Metadata) -> bool { + use std::os::unix::fs::PermissionsExt; + const S_IXUSR: u32 = 0o100; + return metadata.permissions().mode() & S_IXUSR == S_IXUSR +} + +fn create_tgz(dir: &Path, out: &Path) -> io::Result<()> { + let f = fs::File::create(out)?; + let gz = GzEncoder::new(f, Compression::best()); + let mut tar = tar::Builder::new(gz); + tar.append_dir_all(".", dir)?; + Ok(()) +} + +fn create_app(runner_exec: &Path, tgz_path: &Path, out: &Path) -> io::Result<()> { + let mut outf = fs::File::create(out)?; + let mut runnerf = fs::File::open(runner_exec)?; + let mut tgzf = fs::File::open(tgz_path)?; + copy(&mut runnerf, &mut outf)?; + copy(&mut tgzf, &mut outf)?; + Ok(()) +} + +fn main() -> Result<(), Box> { + let args = App::new(APP_NAME) + .settings(&[AppSettings::ArgRequiredElseHelp, AppSettings::ColoredHelp]) + .version(VERSION) + .author(AUTHOR) + .about("Create self-contained single binary application") + .arg(Arg::with_name("arch") + .short("a") + .long("arch") + .value_name("arch") + .help(&format!("Sets the architecture. Supported: {:?}", SUPPORTED_ARCHS)) + .display_order(1) + .takes_value(true) + .required(true)) + .arg(Arg::with_name("input_dir") + .short("i") + .long("input_dir") + .value_name("input_dir") + .help("Sets the input directory containing the application and dependencies") + .display_order(2) + .takes_value(true) + .required(true)) + .arg(Arg::with_name("exec") + .short("e") + .long("exec") + .value_name("exec") + .help("Sets the application executable file name") + .display_order(3) + .takes_value(true) + .required(true)) + .arg(Arg::with_name("output") + .short("o") + .long("output") + .value_name("output") + .help("Sets the resulting self-contained application file name") + .display_order(4) + .takes_value(true) + .required(true)) + .get_matches(); + + let arch = args.value_of("arch").unwrap(); + if !SUPPORTED_ARCHS.contains(&arch) { + bail!("Unknown architecture specified: {}, supported: {:?}", arch, SUPPORTED_ARCHS); + } + + let input_dir = Path::new(args.value_of("input_dir").unwrap()); + if fs::metadata(input_dir).is_err() { + bail!("Cannot access specified input directory {:?}", input_dir); + } + + let exec_name = args.value_of("exec").unwrap(); + if exec_name.len() >= RUNNER_MAGIC.len() { + bail!("Executable name is too long, please consider using a shorter name"); + } + + let exec_path = Path::new(input_dir).join(exec_name); + match fs::metadata(&exec_path) { + Err(_) => { + bail!("Cannot find executable {} inside directory {:?}", exec_name, input_dir); + }, + Ok(metadata) => { + if !is_executable(&exec_path, &metadata) { + bail!("File {} inside directory {:?} isn't executable", exec_name, input_dir); + } + } + } + + let runners_dir = runners_dir(); + fs::create_dir_all(&runners_dir)?; + + let runner_exec = runners_dir.join(arch); + if !runner_exec.exists() { + let url = runner_url(arch); + println!("Downloading runner from {}...", url); + let mut response = reqwest::get(&url)?.error_for_status()?; + let mut f = fs::File::create(&runner_exec)?; + copy(&mut response, &mut f)?; + } + + let tmp_dir = TempDir::new(APP_NAME)?; + let new_runner_exec = tmp_dir.path().join("runner"); + patch_runner(&runner_exec, &new_runner_exec, &exec_name)?; + + println!("Compressing input directory {:?}...", input_dir); + let tgz_path = tmp_dir.path().join("input.tgz"); + create_tgz(&input_dir, &tgz_path)?; + + let exec_name = Path::new(args.value_of("output").unwrap()); + println!("Creating self-contained application binary {:?}...", exec_name); + create_app(&new_runner_exec, &tgz_path, &exec_name)?; + + println!("All done"); + Ok(()) +} diff --git a/warp-runner/Cargo.toml b/warp-runner/Cargo.toml new file mode 100644 index 0000000..17e9ad5 --- /dev/null +++ b/warp-runner/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "warp-runner" +version = "0.1.0" +authors = ["Diego"] + +[dependencies] +memmem = "0.1.1" +flate2 = "1.0" +tar = "0.4" +dirs = "1.0.4" +winapi = "0.3.6" +log = "0.4.5" +simple_logger = "1.0.1" diff --git a/warp-runner/src/executor.rs b/warp-runner/src/executor.rs new file mode 100644 index 0000000..a12ca1c --- /dev/null +++ b/warp-runner/src/executor.rs @@ -0,0 +1,33 @@ +use std::env; +use std::process::Command; +use std::process::Stdio; +use std::path::Path; + +#[cfg(target_family = "windows")] +const PATH_SEPARATOR: char = ';'; + +#[cfg(target_family = "unix")] +const PATH_SEPARATOR: char = ':'; + +pub fn execute(path: &Path, prog: &str) { + let path_str = path.as_os_str().to_os_string().into_string().unwrap(); + let path_env = match env::var("PATH") { + Ok(p) => format!("{}{}{}", &path_str, PATH_SEPARATOR, &p), + _ => path_str + }; + + let mut args: Vec = env::args().collect(); + args[0] = prog.to_owned(); + + trace!("PATH={:?} prog={:?} args={:?}", path_env, prog, args); + Command::new(prog) + .env("PATH", path_env) + .args(args) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap_or_else(|_| panic!("{} failed to start", prog)) + .wait() + .unwrap_or_else(|_| panic!("{} failed to wait", prog)); +} diff --git a/warp-runner/src/extractor.rs b/warp-runner/src/extractor.rs new file mode 100644 index 0000000..6a71f76 --- /dev/null +++ b/warp-runner/src/extractor.rs @@ -0,0 +1,99 @@ +extern crate flate2; +extern crate memmem; +extern crate tar; + +use self::flate2::read::GzDecoder; +use self::memmem::{Searcher, TwoWaySearcher}; +use self::tar::Archive; +use std::fs::File; +use std::io; +use std::io::{BufReader, Read, Seek, SeekFrom}; +use std::path::Path; + +struct FileSearcher<'a> { + buf_reader: BufReader, + searcher: TwoWaySearcher<'a>, + offs: usize, +} + +impl<'a> FileSearcher<'a> { + fn new(path: &'a Path, magic: &'a [u8]) -> io::Result> { + let file = File::open(path)?; + Ok(FileSearcher { + buf_reader: BufReader::new(file), + searcher: TwoWaySearcher::new(magic), + offs: 0, + }) + } +} + +impl<'a> Iterator for FileSearcher<'a> { + type Item = io::Result; + + fn next(&mut self) -> Option> { + let mut buf = [0; 32 * 1024]; + let ret; + + match self.buf_reader.seek(SeekFrom::Start(self.offs as u64)) { + Ok(_) => {} + Err(e) => return Some(Err(e)) + } + + loop { + match self.buf_reader.read(&mut buf[..]) { + Ok(0) => { + ret = None; + break; + } + Ok(n) => { + match self.searcher.search_in(&buf) { + Some(pos) => { + self.offs += pos; + ret = Some(Ok(self.offs)); + self.offs += 1; // one past the match so we can try again if necessary + break; + } + None => self.offs += n + } + } + Err(e) => { + ret = Some(Err(e)); + break; + } + } + } + ret + } +} + +const GZIP_MAGIC: &[u8] = b"\x1f\x8b\x08"; + +pub fn extract_to(src: &Path, dst: &Path) -> io::Result<()> { + let mut found = false; + + let searcher = FileSearcher::new(src, GZIP_MAGIC)?; + for result in searcher { + let offs = result?; + if extract_at_offset(src, offs, dst).is_ok() { + trace!("tarball found at offset {} was extracted successfully", offs); + found = true; + break; + } + } + + if found { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::Other, "no tarball found inside binary")) + } +} + +fn extract_at_offset(src: &Path, offs: usize, dst: &Path) -> io::Result<()> { + let mut f = File::open(src)?; + f.seek(SeekFrom::Start(offs as u64))?; + + let gz = GzDecoder::new(f); + let mut tar = Archive::new(gz); + tar.unpack(dst)?; + Ok(()) +} diff --git a/warp-runner/src/main.rs b/warp-runner/src/main.rs new file mode 100644 index 0000000..f198199 --- /dev/null +++ b/warp-runner/src/main.rs @@ -0,0 +1,73 @@ +extern crate dirs; + +#[macro_use] +extern crate log; +extern crate simple_logger; + +use std::env; +use std::error::Error; +use std::ffi::*; +use std::fs; +use std::io; +use std::path::*; +use log::Level; + +mod extractor; +mod executor; + +static PROG_BUF: &'static [u8] = b"tVQhhsFFlGGD3oWV4lEPST8I8FEPP54IM0q7daes4E1y3p2U2wlJRYmWmjPYfkhZ0PlT14Ls0j8fdDkoj33f2BlRJavLj3mWGibJsGt5uLAtrCDtvxikZ8UX2mQDCrgE\0"; + +fn prog() -> &'static str { + let nul_pos = PROG_BUF.iter() + .position(|elem| *elem == b'\0') + .expect("PROG_BUF has no NUL terminator"); + + let slice = &PROG_BUF[..(nul_pos + 1)]; + CStr::from_bytes_with_nul(slice) + .expect("Can't convert PROG_BUF slice to CStr") + .to_str() + .expect("Can't convert PROG_BUF CStr to str") +} + +fn cache_path(prog: &str) -> PathBuf { + dirs::data_local_dir() + .expect("No data local dir found") + .join("warp") + .join("packages") + .join(prog) +} + +fn extract(exe_path: &Path, cache_path: &Path) -> io::Result<()> { + fs::remove_dir_all(cache_path).ok(); + extractor::extract_to(&exe_path, &cache_path)?; + Ok(()) +} + +fn main() -> Result<(), Box> { + if env::var("WARP_TRACE").is_ok() { + simple_logger::init_with_level(Level::Trace)?; + } + + let prog = prog(); + let cache_path = cache_path(prog); + let exe_path = env::current_exe()?; + trace!("prog={:?}, cache_path={:?}, exe_path={:?}", prog, cache_path, exe_path); + + match fs::metadata(&cache_path) { + Ok(cache) => { + if cache.modified()? >= fs::metadata(&exe_path)?.modified()? { + trace!("cache is up-to-date"); + } else { + trace!("cache is outdated"); + extract(&exe_path, &cache_path)?; + } + } + Err(_) => { + trace!("cache not found"); + extract(&exe_path, &cache_path)?; + } + } + + executor::execute(&cache_path, &prog); + Ok(()) +} diff --git a/warp.iml b/warp.iml new file mode 100644 index 0000000..4d0de19 --- /dev/null +++ b/warp.iml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file