···11+MIT License
22+33+Copyright (c) 2021 Matt Freitas-Stavola
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
2222+
+15
README.md
···11+# Small Rust
22+33+This repo contains a bunch of really small "scripts" written in Rust.
44+55+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.
66+77+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.
88+99+If you want to play around with these programs, you just need [Rust][rust] and [cargo-eval][cargo-eval].
1010+1111+Be aware that dev work for this repo is done on UNIX-y systems, so they might not work on Windows.
1212+1313+[stb]: https://www.youtube.com/watch?v=eAhWIO1Ra6M
1414+[rust]: https://www.rust-lang.org/tools/install
1515+[cargo-eval]: https://github.com/reitermarkus/cargo-eval
+147
ghoshare
···11+#!/usr/bin/env -S cargo eval --
22+33+//! ```cargo
44+//! [dependencies]
55+//! argh = "0.1"
66+//! reqwest = { version = "0.11.3", features = ["blocking"] }
77+//! ```
88+99+use argh::FromArgs;
1010+use reqwest::blocking::Client;
1111+use reqwest::redirect::Policy;
1212+1313+use std::fs::OpenOptions;
1414+use std::io::{BufReader, Read};
1515+use std::path::PathBuf;
1616+use std::str::FromStr;
1717+1818+const BASE_URL: &str = "https://ghostbin.co";
1919+2020+#[derive(Debug)]
2121+enum ExpirationTime {
2222+ Never,
2323+ TenMinutes,
2424+ OneHour,
2525+ OneDay,
2626+ Fortnight,
2727+}
2828+2929+impl FromStr for ExpirationTime {
3030+ type Err = String;
3131+3232+ fn from_str(value: &str) -> Result<ExpirationTime, String> {
3333+ let expiration = match value {
3434+ "never" => ExpirationTime::Never,
3535+ "10m" | "10 m" | "10minutes" | "10 minutes" | "ten m" | "ten minutes" => {
3636+ ExpirationTime::TenMinutes
3737+ }
3838+ "1h" | "1 h" | "1hour" | "1 hour" | "one h" | "one hour" => ExpirationTime::OneHour,
3939+ "1d" | "1 d" | "1day" | "1 day" | "one d" | "one day" => ExpirationTime::OneDay,
4040+ "14d" | "14 d" | "14days" | "14 days" | "fourteen d" | "fourteen days"
4141+ | "fortnight" => ExpirationTime::Fortnight,
4242+ _ => return Err("unrecoginized duration. try: never, 10m, 1h, 1d, or 14d.".to_string()),
4343+ };
4444+4545+ Ok(expiration)
4646+ }
4747+}
4848+4949+impl Into<String> for ExpirationTime {
5050+ fn into(self) -> String {
5151+ match self {
5252+ ExpirationTime::Never => "-1".to_string(),
5353+ ExpirationTime::TenMinutes => "10m".to_string(),
5454+ ExpirationTime::OneHour => "1h".to_string(),
5555+ ExpirationTime::OneDay => "1d".to_string(),
5656+ ExpirationTime::Fortnight => "14d".to_string(),
5757+ }
5858+ }
5959+}
6060+6161+#[derive(FromArgs)]
6262+#[argh(description = "Copies a local file onto ghostbin.co.")]
6363+struct App {
6464+ #[argh(
6565+ option,
6666+ description = "language to apply syntax highlighting.",
6767+ default = r#""text".to_string()"#
6868+ )]
6969+ language: String,
7070+7171+ #[argh(option, description = "how long to keep the file online.")]
7272+ expiration: Option<ExpirationTime>,
7373+7474+ #[argh(option, description = "title of the uploaded file.")]
7575+ title: Option<String>,
7676+7777+ #[argh(
7878+ option,
7979+ description = "password to access the file.",
8080+ default = r#""".to_string()"#
8181+ )]
8282+ password: String,
8383+8484+ #[argh(positional, description = "file to share.")]
8585+ file: PathBuf,
8686+}
8787+8888+fn main() {
8989+ let app: App = argh::from_env();
9090+9191+ let file = OpenOptions::new()
9292+ .read(true)
9393+ .open(app.file.clone())
9494+ .expect("could not open file");
9595+9696+ let mut reader = BufReader::new(file);
9797+9898+ let mut contents = String::new();
9999+ reader
100100+ .read_to_string(&mut contents)
101101+ .expect("should be able to read file contents");
102102+103103+ let expiration: String = app.expiration.unwrap_or(ExpirationTime::TenMinutes).into();
104104+105105+ let filename = app
106106+ .file
107107+ .file_name()
108108+ .and_then(|name| name.to_str())
109109+ .map(|name| name.to_string());
110110+111111+ let title = app
112112+ .title
113113+ .or_else(|| filename)
114114+ .unwrap_or_else(|| "".to_string());
115115+116116+ println!("Uploading file");
117117+ let client = Client::builder()
118118+ .redirect(Policy::none())
119119+ .build()
120120+ .expect("should be able to build client");
121121+122122+ let res = client
123123+ .post(format!("{}/paste/new", BASE_URL))
124124+ .header("User-Agent", "ghoshare")
125125+ .form(&[
126126+ ("expire", expiration),
127127+ ("password", app.password),
128128+ ("lang", app.language),
129129+ ("title", title),
130130+ ("text", contents),
131131+ ])
132132+ .send()
133133+ .expect("unexpected error while uploading file");
134134+135135+ if res.status() != 303 {
136136+ panic!("Unexpected status code {}", res.status());
137137+ }
138138+139139+ let headers = res.headers();
140140+ let path = headers
141141+ .get("location")
142142+ .expect("we should be redirected!")
143143+ .to_str()
144144+ .expect("should be able to get location header value");
145145+146146+ println!("URL: {}", format!("{}{}", BASE_URL, path));
147147+}
+69
kill_port
···11+#!/usr/bin/env -S cargo eval --
22+33+// cargo-deps: argh = "0.1"
44+55+use argh::FromArgs;
66+77+use std::process::{self, Command};
88+99+#[derive(FromArgs)]
1010+#[argh(description = "Simple program to kill a process listening on a specific port.")]
1111+struct App {
1212+ #[argh(switch, description = "check udp protocol ports.")]
1313+ udp: bool,
1414+1515+ #[argh(
1616+ option,
1717+ description = "which signal to send to the process. defaults to kill.",
1818+ default = "String::from(\"9\")"
1919+ )]
2020+ signal: String,
2121+2222+ #[argh(positional, description = "what port to free.")]
2323+ port: u16,
2424+}
2525+2626+fn main() {
2727+ let app: App = argh::from_env();
2828+2929+ let address = format!("{}:{}", if app.udp { "udp" } else { "tcp" }, app.port);
3030+ let output = Command::new("lsof")
3131+ .arg("-t")
3232+ .arg("-i")
3333+ .arg(address)
3434+ .output()
3535+ .expect("failed to execute lsof");
3636+3737+ if !output.status.success() && !output.stderr.is_empty() {
3838+ let error = String::from_utf8(output.stderr).expect("must be valid utf8");
3939+ eprintln!("Failed to lsof port '{}'. Reason:\n{}", app.port, error);
4040+ process::exit(1);
4141+ }
4242+4343+ let pids = String::from_utf8(output.stdout).expect("must be valid utf8");
4444+ let pids = pids.split_ascii_whitespace().collect::<Vec<_>>();
4545+4646+ if pids.is_empty() {
4747+ println!("Port '{}' is not bound to any process", app.port);
4848+ return;
4949+ }
5050+5151+ println!("Found {} process(es) bound to '{}'", pids.len(), app.port);
5252+5353+ for pid in pids {
5454+ println!("Killing process {}", pid);
5555+5656+ let output = Command::new("kill")
5757+ .arg("-s")
5858+ .arg(&app.signal)
5959+ .arg(pid)
6060+ .output()
6161+ .expect("failed to execute kill");
6262+6363+ if !output.status.success() {
6464+ let error = String::from_utf8(output.stderr).expect("must be valid utf8");
6565+ eprintln!("Failed to kill process '{}'. Reason:\n{}", pid, error);
6666+ process::exit(1);
6767+ }
6868+ }
6969+}
+58
stopwatch
···11+#!/usr/bin/env -S cargo eval --
22+33+// cargo-deps: argh = "0.1", humantime = "2.1.0"
44+55+use argh::FromArgs;
66+77+use std::thread;
88+use std::time::{Duration, Instant};
99+1010+#[derive(FromArgs)]
1111+#[argh(description = "Keep track of how long a task runs or set a timer for some period of time.")]
1212+struct App {
1313+ #[argh(
1414+ option,
1515+ description = "how long to wait before starting the stopwatch."
1616+ )]
1717+ delay: Option<humantime::Duration>,
1818+1919+ #[argh(
2020+ option,
2121+ description = "how long to wait between timer updates.",
2222+ default = "\"1ms\".parse::<humantime::Duration>().unwrap()"
2323+ )]
2424+ pause: humantime::Duration,
2525+2626+ #[argh(option, description = "how long to run the stopwatch.")]
2727+ duration: Option<humantime::Duration>,
2828+}
2929+3030+fn main() {
3131+ let app: App = argh::from_env();
3232+3333+ let delay: Option<Duration> = app.delay.map(|d| d.into());
3434+ let pause = app.pause.into();
3535+ let duration = app
3636+ .duration
3737+ .map(|d| d.into())
3838+ .unwrap_or_else(|| Duration::new(u64::MAX, 0));
3939+4040+ if let Some(delay) = delay {
4141+ println!("Waiting {:?}...", delay);
4242+ thread::sleep(delay);
4343+ }
4444+4545+ let now = Instant::now();
4646+4747+ let mut elapsed = now.elapsed();
4848+ while elapsed < duration {
4949+ print!("\x1B[2K\rElapsed: {}", humantime::format_duration(elapsed));
5050+ thread::sleep(pause);
5151+ elapsed = now.elapsed();
5252+ }
5353+5454+ print!(
5555+ "\x1B[2K\rElapsed: {}\nStopped\x07",
5656+ humantime::format_duration(elapsed)
5757+ );
5858+}
+112
strs
···11+#!/usr/bin/env -S cargo eval --
22+33+// cargo-deps: argh = "0.1", object = "0.24.0"
44+55+use argh::FromArgs;
66+77+use std::fs::OpenOptions;
88+use std::io::{self, BufReader, Read, Write};
99+use std::path::PathBuf;
1010+1111+use object::{File, Object, ObjectSection, SectionKind};
1212+1313+const MIN_STR_LEN: usize = 4;
1414+1515+const ELF_SIG: [u8; 4] = [0x7F, 0x45, 0x4C, 0x46];
1616+const MACH_O_32_SIG: [u8; 4] = [0xFE, 0xED, 0xFA, 0xCE];
1717+const MACH_O_64_SIG: [u8; 4] = [0xFE, 0xED, 0xFA, 0xCF];
1818+const MACH_O_FAT_SIG: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE];
1919+const MACH_O_32_REV_SIG: [u8; 4] = [0xCE, 0xFA, 0xED, 0xFE];
2020+const MACH_O_64_REV_SIG: [u8; 4] = [0xCF, 0xFA, 0xED, 0xFE];
2121+2222+#[derive(FromArgs)]
2323+#[argh(description = "Rust clone of strings(1).")]
2424+struct App {
2525+ #[argh(
2626+ switch,
2727+ description = "ignore heuristics and read all sections of an object file."
2828+ )]
2929+ all: bool,
3030+3131+ #[argh(
3232+ option,
3333+ description = "the minimum length a valid string needs to be in order to be printed.",
3434+ default = "MIN_STR_LEN"
3535+ )]
3636+ min_length: usize,
3737+3838+ #[argh(positional, description = "input file.")]
3939+ file: PathBuf,
4040+}
4141+4242+fn main() {
4343+ let app: App = argh::from_env();
4444+4545+ let file = OpenOptions::new()
4646+ .read(true)
4747+ .open(app.file)
4848+ .expect("could not open source");
4949+5050+ let mut reader = BufReader::new(file);
5151+5252+ // Preallocate 4MB of space since we're usually dealing with files
5353+ let mut contents = Vec::with_capacity(1024 * 1024 * 4);
5454+ reader.read_to_end(&mut contents);
5555+5656+ let stdout = io::stdout();
5757+ let mut handle = stdout.lock();
5858+5959+ let mut magic = [0, 0, 0, 0];
6060+ if contents.len() >= 4 {
6161+ magic.copy_from_slice(&contents[..4]);
6262+ }
6363+6464+ if is_object_file(&magic) {
6565+ print_object_strings(&mut handle, &contents, app.all, app.min_length);
6666+ } else {
6767+ print_strings(&mut handle, &contents, app.min_length);
6868+ }
6969+}
7070+7171+fn is_object_file(magic: &[u8; 4]) -> bool {
7272+ magic == &ELF_SIG
7373+ || magic == &MACH_O_FAT_SIG
7474+ || magic == &MACH_O_32_SIG
7575+ || magic == &MACH_O_64_SIG
7676+ || magic == &MACH_O_32_REV_SIG
7777+ || magic == &MACH_O_64_REV_SIG
7878+}
7979+8080+fn print_object_strings(handle: &mut impl Write, contents: &[u8], all: bool, min_length: usize) {
8181+ // We could also read this in a lazy fashion but... I'm lazy at the moment ;)
8282+ let object = File::parse(contents).expect("Could not parse object file");
8383+ object
8484+ .sections()
8585+ .filter(|section| all || section.kind() == SectionKind::Data)
8686+ .for_each(|section| {
8787+ let contents = section.data().expect("section should have data");
8888+ print_strings(handle, contents, min_length);
8989+ });
9090+}
9191+9292+fn print_strings(handle: &mut impl Write, contents: &[u8], min_length: usize) {
9393+ let mut start = 0;
9494+ let mut end = 0;
9595+9696+ for c in contents.iter() {
9797+ let is_printable = c.is_ascii_graphic() || c.is_ascii_whitespace();
9898+ let has_minimum_len = (end - start) >= min_length;
9999+100100+ if !is_printable && has_minimum_len {
101101+ handle.write(&contents[start..end]);
102102+ handle.write(&[b'\n']);
103103+ start = end + 1;
104104+ } else if !is_printable {
105105+ start = end + 1;
106106+ }
107107+108108+ end += 1;
109109+ }
110110+111111+ handle.flush();
112112+}