Small Rust scripts

Init

+423
+22
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2021 Matt Freitas-Stavola 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE. 22 +
+15
README.md
··· 1 + # Small Rust 2 + 3 + This repo contains a bunch of really small "scripts" written in Rust. 4 + 5 + Partially inspired by Sean Barrett's [Advice for Writing Small Programs in C][stb], but mostly stems from my incredible ability to completely lose all Bash knowledge after closing an `.sh` file. 6 + 7 + Some of them do useful things, some of them were just written for fun. Please do not have the expectation that these are written in a super robust fashion; most were written for one-off tasks and experiments. 8 + 9 + If you want to play around with these programs, you just need [Rust][rust] and [cargo-eval][cargo-eval]. 10 + 11 + Be aware that dev work for this repo is done on UNIX-y systems, so they might not work on Windows. 12 + 13 + [stb]: https://www.youtube.com/watch?v=eAhWIO1Ra6M 14 + [rust]: https://www.rust-lang.org/tools/install 15 + [cargo-eval]: https://github.com/reitermarkus/cargo-eval
+147
ghoshare
··· 1 + #!/usr/bin/env -S cargo eval -- 2 + 3 + //! ```cargo 4 + //! [dependencies] 5 + //! argh = "0.1" 6 + //! reqwest = { version = "0.11.3", features = ["blocking"] } 7 + //! ``` 8 + 9 + use argh::FromArgs; 10 + use reqwest::blocking::Client; 11 + use reqwest::redirect::Policy; 12 + 13 + use std::fs::OpenOptions; 14 + use std::io::{BufReader, Read}; 15 + use std::path::PathBuf; 16 + use std::str::FromStr; 17 + 18 + const BASE_URL: &str = "https://ghostbin.co"; 19 + 20 + #[derive(Debug)] 21 + enum ExpirationTime { 22 + Never, 23 + TenMinutes, 24 + OneHour, 25 + OneDay, 26 + Fortnight, 27 + } 28 + 29 + impl FromStr for ExpirationTime { 30 + type Err = String; 31 + 32 + fn from_str(value: &str) -> Result<ExpirationTime, String> { 33 + let expiration = match value { 34 + "never" => ExpirationTime::Never, 35 + "10m" | "10 m" | "10minutes" | "10 minutes" | "ten m" | "ten minutes" => { 36 + ExpirationTime::TenMinutes 37 + } 38 + "1h" | "1 h" | "1hour" | "1 hour" | "one h" | "one hour" => ExpirationTime::OneHour, 39 + "1d" | "1 d" | "1day" | "1 day" | "one d" | "one day" => ExpirationTime::OneDay, 40 + "14d" | "14 d" | "14days" | "14 days" | "fourteen d" | "fourteen days" 41 + | "fortnight" => ExpirationTime::Fortnight, 42 + _ => return Err("unrecoginized duration. try: never, 10m, 1h, 1d, or 14d.".to_string()), 43 + }; 44 + 45 + Ok(expiration) 46 + } 47 + } 48 + 49 + impl Into<String> for ExpirationTime { 50 + fn into(self) -> String { 51 + match self { 52 + ExpirationTime::Never => "-1".to_string(), 53 + ExpirationTime::TenMinutes => "10m".to_string(), 54 + ExpirationTime::OneHour => "1h".to_string(), 55 + ExpirationTime::OneDay => "1d".to_string(), 56 + ExpirationTime::Fortnight => "14d".to_string(), 57 + } 58 + } 59 + } 60 + 61 + #[derive(FromArgs)] 62 + #[argh(description = "Copies a local file onto ghostbin.co.")] 63 + struct App { 64 + #[argh( 65 + option, 66 + description = "language to apply syntax highlighting.", 67 + default = r#""text".to_string()"# 68 + )] 69 + language: String, 70 + 71 + #[argh(option, description = "how long to keep the file online.")] 72 + expiration: Option<ExpirationTime>, 73 + 74 + #[argh(option, description = "title of the uploaded file.")] 75 + title: Option<String>, 76 + 77 + #[argh( 78 + option, 79 + description = "password to access the file.", 80 + default = r#""".to_string()"# 81 + )] 82 + password: String, 83 + 84 + #[argh(positional, description = "file to share.")] 85 + file: PathBuf, 86 + } 87 + 88 + fn main() { 89 + let app: App = argh::from_env(); 90 + 91 + let file = OpenOptions::new() 92 + .read(true) 93 + .open(app.file.clone()) 94 + .expect("could not open file"); 95 + 96 + let mut reader = BufReader::new(file); 97 + 98 + let mut contents = String::new(); 99 + reader 100 + .read_to_string(&mut contents) 101 + .expect("should be able to read file contents"); 102 + 103 + let expiration: String = app.expiration.unwrap_or(ExpirationTime::TenMinutes).into(); 104 + 105 + let filename = app 106 + .file 107 + .file_name() 108 + .and_then(|name| name.to_str()) 109 + .map(|name| name.to_string()); 110 + 111 + let title = app 112 + .title 113 + .or_else(|| filename) 114 + .unwrap_or_else(|| "".to_string()); 115 + 116 + println!("Uploading file"); 117 + let client = Client::builder() 118 + .redirect(Policy::none()) 119 + .build() 120 + .expect("should be able to build client"); 121 + 122 + let res = client 123 + .post(format!("{}/paste/new", BASE_URL)) 124 + .header("User-Agent", "ghoshare") 125 + .form(&[ 126 + ("expire", expiration), 127 + ("password", app.password), 128 + ("lang", app.language), 129 + ("title", title), 130 + ("text", contents), 131 + ]) 132 + .send() 133 + .expect("unexpected error while uploading file"); 134 + 135 + if res.status() != 303 { 136 + panic!("Unexpected status code {}", res.status()); 137 + } 138 + 139 + let headers = res.headers(); 140 + let path = headers 141 + .get("location") 142 + .expect("we should be redirected!") 143 + .to_str() 144 + .expect("should be able to get location header value"); 145 + 146 + println!("URL: {}", format!("{}{}", BASE_URL, path)); 147 + }
+69
kill_port
··· 1 + #!/usr/bin/env -S cargo eval -- 2 + 3 + // cargo-deps: argh = "0.1" 4 + 5 + use argh::FromArgs; 6 + 7 + use std::process::{self, Command}; 8 + 9 + #[derive(FromArgs)] 10 + #[argh(description = "Simple program to kill a process listening on a specific port.")] 11 + struct App { 12 + #[argh(switch, description = "check udp protocol ports.")] 13 + udp: bool, 14 + 15 + #[argh( 16 + option, 17 + description = "which signal to send to the process. defaults to kill.", 18 + default = "String::from(\"9\")" 19 + )] 20 + signal: String, 21 + 22 + #[argh(positional, description = "what port to free.")] 23 + port: u16, 24 + } 25 + 26 + fn main() { 27 + let app: App = argh::from_env(); 28 + 29 + let address = format!("{}:{}", if app.udp { "udp" } else { "tcp" }, app.port); 30 + let output = Command::new("lsof") 31 + .arg("-t") 32 + .arg("-i") 33 + .arg(address) 34 + .output() 35 + .expect("failed to execute lsof"); 36 + 37 + if !output.status.success() && !output.stderr.is_empty() { 38 + let error = String::from_utf8(output.stderr).expect("must be valid utf8"); 39 + eprintln!("Failed to lsof port '{}'. Reason:\n{}", app.port, error); 40 + process::exit(1); 41 + } 42 + 43 + let pids = String::from_utf8(output.stdout).expect("must be valid utf8"); 44 + let pids = pids.split_ascii_whitespace().collect::<Vec<_>>(); 45 + 46 + if pids.is_empty() { 47 + println!("Port '{}' is not bound to any process", app.port); 48 + return; 49 + } 50 + 51 + println!("Found {} process(es) bound to '{}'", pids.len(), app.port); 52 + 53 + for pid in pids { 54 + println!("Killing process {}", pid); 55 + 56 + let output = Command::new("kill") 57 + .arg("-s") 58 + .arg(&app.signal) 59 + .arg(pid) 60 + .output() 61 + .expect("failed to execute kill"); 62 + 63 + if !output.status.success() { 64 + let error = String::from_utf8(output.stderr).expect("must be valid utf8"); 65 + eprintln!("Failed to kill process '{}'. Reason:\n{}", pid, error); 66 + process::exit(1); 67 + } 68 + } 69 + }
+58
stopwatch
··· 1 + #!/usr/bin/env -S cargo eval -- 2 + 3 + // cargo-deps: argh = "0.1", humantime = "2.1.0" 4 + 5 + use argh::FromArgs; 6 + 7 + use std::thread; 8 + use std::time::{Duration, Instant}; 9 + 10 + #[derive(FromArgs)] 11 + #[argh(description = "Keep track of how long a task runs or set a timer for some period of time.")] 12 + struct App { 13 + #[argh( 14 + option, 15 + description = "how long to wait before starting the stopwatch." 16 + )] 17 + delay: Option<humantime::Duration>, 18 + 19 + #[argh( 20 + option, 21 + description = "how long to wait between timer updates.", 22 + default = "\"1ms\".parse::<humantime::Duration>().unwrap()" 23 + )] 24 + pause: humantime::Duration, 25 + 26 + #[argh(option, description = "how long to run the stopwatch.")] 27 + duration: Option<humantime::Duration>, 28 + } 29 + 30 + fn main() { 31 + let app: App = argh::from_env(); 32 + 33 + let delay: Option<Duration> = app.delay.map(|d| d.into()); 34 + let pause = app.pause.into(); 35 + let duration = app 36 + .duration 37 + .map(|d| d.into()) 38 + .unwrap_or_else(|| Duration::new(u64::MAX, 0)); 39 + 40 + if let Some(delay) = delay { 41 + println!("Waiting {:?}...", delay); 42 + thread::sleep(delay); 43 + } 44 + 45 + let now = Instant::now(); 46 + 47 + let mut elapsed = now.elapsed(); 48 + while elapsed < duration { 49 + print!("\x1B[2K\rElapsed: {}", humantime::format_duration(elapsed)); 50 + thread::sleep(pause); 51 + elapsed = now.elapsed(); 52 + } 53 + 54 + print!( 55 + "\x1B[2K\rElapsed: {}\nStopped\x07", 56 + humantime::format_duration(elapsed) 57 + ); 58 + }
+112
strs
··· 1 + #!/usr/bin/env -S cargo eval -- 2 + 3 + // cargo-deps: argh = "0.1", object = "0.24.0" 4 + 5 + use argh::FromArgs; 6 + 7 + use std::fs::OpenOptions; 8 + use std::io::{self, BufReader, Read, Write}; 9 + use std::path::PathBuf; 10 + 11 + use object::{File, Object, ObjectSection, SectionKind}; 12 + 13 + const MIN_STR_LEN: usize = 4; 14 + 15 + const ELF_SIG: [u8; 4] = [0x7F, 0x45, 0x4C, 0x46]; 16 + const MACH_O_32_SIG: [u8; 4] = [0xFE, 0xED, 0xFA, 0xCE]; 17 + const MACH_O_64_SIG: [u8; 4] = [0xFE, 0xED, 0xFA, 0xCF]; 18 + const MACH_O_FAT_SIG: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE]; 19 + const MACH_O_32_REV_SIG: [u8; 4] = [0xCE, 0xFA, 0xED, 0xFE]; 20 + const MACH_O_64_REV_SIG: [u8; 4] = [0xCF, 0xFA, 0xED, 0xFE]; 21 + 22 + #[derive(FromArgs)] 23 + #[argh(description = "Rust clone of strings(1).")] 24 + struct App { 25 + #[argh( 26 + switch, 27 + description = "ignore heuristics and read all sections of an object file." 28 + )] 29 + all: bool, 30 + 31 + #[argh( 32 + option, 33 + description = "the minimum length a valid string needs to be in order to be printed.", 34 + default = "MIN_STR_LEN" 35 + )] 36 + min_length: usize, 37 + 38 + #[argh(positional, description = "input file.")] 39 + file: PathBuf, 40 + } 41 + 42 + fn main() { 43 + let app: App = argh::from_env(); 44 + 45 + let file = OpenOptions::new() 46 + .read(true) 47 + .open(app.file) 48 + .expect("could not open source"); 49 + 50 + let mut reader = BufReader::new(file); 51 + 52 + // Preallocate 4MB of space since we're usually dealing with files 53 + let mut contents = Vec::with_capacity(1024 * 1024 * 4); 54 + reader.read_to_end(&mut contents); 55 + 56 + let stdout = io::stdout(); 57 + let mut handle = stdout.lock(); 58 + 59 + let mut magic = [0, 0, 0, 0]; 60 + if contents.len() >= 4 { 61 + magic.copy_from_slice(&contents[..4]); 62 + } 63 + 64 + if is_object_file(&magic) { 65 + print_object_strings(&mut handle, &contents, app.all, app.min_length); 66 + } else { 67 + print_strings(&mut handle, &contents, app.min_length); 68 + } 69 + } 70 + 71 + fn is_object_file(magic: &[u8; 4]) -> bool { 72 + magic == &ELF_SIG 73 + || magic == &MACH_O_FAT_SIG 74 + || magic == &MACH_O_32_SIG 75 + || magic == &MACH_O_64_SIG 76 + || magic == &MACH_O_32_REV_SIG 77 + || magic == &MACH_O_64_REV_SIG 78 + } 79 + 80 + fn print_object_strings(handle: &mut impl Write, contents: &[u8], all: bool, min_length: usize) { 81 + // We could also read this in a lazy fashion but... I'm lazy at the moment ;) 82 + let object = File::parse(contents).expect("Could not parse object file"); 83 + object 84 + .sections() 85 + .filter(|section| all || section.kind() == SectionKind::Data) 86 + .for_each(|section| { 87 + let contents = section.data().expect("section should have data"); 88 + print_strings(handle, contents, min_length); 89 + }); 90 + } 91 + 92 + fn print_strings(handle: &mut impl Write, contents: &[u8], min_length: usize) { 93 + let mut start = 0; 94 + let mut end = 0; 95 + 96 + for c in contents.iter() { 97 + let is_printable = c.is_ascii_graphic() || c.is_ascii_whitespace(); 98 + let has_minimum_len = (end - start) >= min_length; 99 + 100 + if !is_printable && has_minimum_len { 101 + handle.write(&contents[start..end]); 102 + handle.write(&[b'\n']); 103 + start = end + 1; 104 + } else if !is_printable { 105 + start = end + 1; 106 + } 107 + 108 + end += 1; 109 + } 110 + 111 + handle.flush(); 112 + }