From 59ce2e1e5f99a45a65b6bcd029e3304b9420043e Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Thu, 27 Jun 2024 19:53:35 -0400 Subject: [PATCH] add util: write --- Cargo.lock | 1 + README.md | 2 +- plib/src/curuser.rs | 54 +++++++++ plib/src/lib.rs | 1 + users/Cargo.toml | 7 +- users/src/logname.rs | 22 +--- users/src/tty.rs | 22 +--- users/src/write.rs | 271 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 338 insertions(+), 42 deletions(-) create mode 100644 plib/src/curuser.rs create mode 100644 users/src/write.rs diff --git a/Cargo.lock b/Cargo.lock index fd746810f..77fd20a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1125,6 +1125,7 @@ name = "posixutils-users" version = "0.1.11" dependencies = [ "atty", + "chrono", "clap", "gettext-rs", "libc", diff --git a/README.md b/README.md index 698144cb9..8596130e8 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Because it is a FAQ, the major differences between this project and uutils are: - [x] touch - [x] tty - [x] unlink + - [x] write ## Stage 1 - Rough draft @@ -216,7 +217,6 @@ Because it is a FAQ, the major differences between this project and uutils are: - [ ] talk - [ ] time - [ ] timeout - - [ ] write ## Testing diff --git a/plib/src/curuser.rs b/plib/src/curuser.rs new file mode 100644 index 000000000..1dc14492c --- /dev/null +++ b/plib/src/curuser.rs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2024 Jeff Garzik +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +extern crate libc; +use std::ffi::CStr; + +pub fn login_name() -> String { + let username = unsafe { + // Call getlogin to get the username as a *mut c_char + let c_str = libc::getlogin(); + + // Check if the pointer is not null + if c_str.is_null() { + panic!("Failed to get login name"); + } + + // Convert the *mut c_char to a &CStr + let c_str = CStr::from_ptr(c_str); + + // Convert the &CStr to a Rust String + match c_str.to_str() { + Ok(s) => s.to_owned(), // Successfully converted CStr to Rust String + Err(e) => panic!("Failed to convert login name to a Rust String: {}", e), + } + }; + + username +} + +pub fn tty() -> String { + // Try to get the tty name from STDIN, STDOUT, STDERR in that order + for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].iter() { + unsafe { + let c_str = libc::ttyname(*fd); + + if !c_str.is_null() { + let c_str = CStr::from_ptr(c_str); + + return match c_str.to_str() { + Ok(s) => s.to_owned(), + Err(e) => panic!("Failed to convert tty name to a Rust String: {}", e), + }; + } + } + } + + panic!("Failed to get tty name from any file descriptor"); +} diff --git a/plib/src/lib.rs b/plib/src/lib.rs index ed273b1a0..c0d84ef90 100644 --- a/plib/src/lib.rs +++ b/plib/src/lib.rs @@ -7,6 +7,7 @@ // SPDX-License-Identifier: MIT // +pub mod curuser; pub mod group; pub mod io; pub mod lzw; diff --git a/users/Cargo.toml b/users/Cargo.toml index 9294b84db..d0c707cf5 100644 --- a/users/Cargo.toml +++ b/users/Cargo.toml @@ -11,8 +11,9 @@ plib = { path = "../plib" } clap.workspace = true gettext-rs.workspace = true libc.workspace = true -syslog = "6.1" atty.workspace = true +chrono.workspace = true +syslog = "6.1" [[bin]] name = "id" @@ -38,3 +39,7 @@ path = "src/pwd.rs" name = "tty" path = "src/tty.rs" +[[bin]] +name = "write" +path = "src/write.rs" + diff --git a/users/src/logname.rs b/users/src/logname.rs index 020455e81..6aba3f01c 100644 --- a/users/src/logname.rs +++ b/users/src/logname.rs @@ -7,28 +7,10 @@ // SPDX-License-Identifier: MIT // -extern crate libc; -use std::ffi::CStr; +extern crate plib; fn main() { - let username = unsafe { - // Call getlogin to get the username as a *mut c_char - let c_str = libc::getlogin(); - - // Check if the pointer is not null - if c_str.is_null() { - panic!("Failed to get login name"); - } - - // Convert the *mut c_char to a &CStr - let c_str = CStr::from_ptr(c_str); - - // Convert the &CStr to a Rust String - match c_str.to_str() { - Ok(s) => s.to_owned(), // Successfully converted CStr to Rust String - Err(e) => panic!("Failed to convert login name to a Rust String: {}", e), - } - }; + let username = plib::curuser::login_name(); println!("{}", username); } diff --git a/users/src/tty.rs b/users/src/tty.rs index 236c1b153..2a1ad6b56 100644 --- a/users/src/tty.rs +++ b/users/src/tty.rs @@ -8,8 +8,7 @@ // extern crate atty; -extern crate libc; -use std::ffi::CStr; +extern crate plib; fn main() { let is_tty = atty::is(atty::Stream::Stdin); @@ -18,24 +17,7 @@ fn main() { std::process::exit(1); } - let ttyname = unsafe { - // Call getlogin to get the username as a *mut c_char - let c_str = libc::ttyname(libc::STDIN_FILENO); - - // Check if the pointer is not null - if c_str.is_null() { - panic!("Failed to get tty name"); - } - - // Convert the *mut c_char to a &CStr - let c_str = CStr::from_ptr(c_str); - - // Convert the &CStr to a Rust String - match c_str.to_str() { - Ok(s) => s.to_owned(), // Successfully converted CStr to Rust String - Err(e) => panic!("Failed to convert tty name to a Rust String: {}", e), - } - }; + let ttyname = plib::curuser::tty(); println!("{}", ttyname); } diff --git a/users/src/write.rs b/users/src/write.rs new file mode 100644 index 000000000..826457eb7 --- /dev/null +++ b/users/src/write.rs @@ -0,0 +1,271 @@ +// +// Copyright (c) 2024 Jeff Garzik +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +extern crate clap; +extern crate plib; + +use chrono::Local; +use clap::Parser; +use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; +use plib::PROJECT_NAME; +use std::fs; +use std::fs::OpenOptions; +use std::io::{self, BufRead, Write}; +use std::os::unix::fs::MetadataExt; +use std::os::unix::fs::PermissionsExt; +use std::process::exit; + +const ALERT_CHAR: char = '\u{07}'; +const INTR_CHAR: char = '\u{03}'; +const EOF_CHAR: char = '\u{04}'; +const ERASE_CHAR: char = '\u{08}'; +const KILL_CHAR: char = '\u{15}'; + +/// cat - concatenate and print files +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] +struct Args { + /// Login name of the person to whom the message shall be written. + username: String, + + /// The terminal to which the message shall be written. + terminal: Option, +} + +// Select terminal in an implementation-defined manner and return terminal +// Print an informational message about the chosen terminal +fn select_terminal(user_name: &str) -> String { + let entries = plib::utmpx::load(); + + // Filter the entries to find terminals for the specified user + let user_entries: Vec<_> = entries + .into_iter() + .filter(|entry| entry.user == user_name && entry.typ == libc::USER_PROCESS) + .collect(); + + if user_entries.is_empty() { + eprintln!("{}: {}", gettext("No terminals found for user"), user_name); + exit(1); + } + + // Avoid selecting "console" and ensure we get a valid terminal + for entry in &user_entries { + if entry.line != "console" { + let terminal = format!("/dev/{}", &entry.line); + return terminal; + } + } + + eprintln!( + "{}: {}", + gettext("No valid terminals found for user"), + user_name + ); + exit(1); +} + +fn check_write_permission(terminal: &str) -> bool { + // Get the metadata for the terminal + match fs::metadata(terminal) { + Ok(metadata) => { + let permissions = metadata.permissions().mode(); + + // Get the current user ID and group ID + let uid = unsafe { libc::getuid() }; + let gid = unsafe { libc::getgid() }; + + // Check if the user is the owner and has write permission + if metadata.uid() == uid && (permissions & 0o200 != 0) { + return true; + } + + // Check if the user's group has write permission + if metadata.gid() == gid && (permissions & 0o020 != 0) { + return true; + } + + // Check if others have write permission + if (permissions & 0o002) != 0 { + return true; + } + + false + } + Err(err) => { + eprintln!("{} {}: {}", gettext("Error checking metadata for terminal"), terminal, err); + false + } + } +} + +// Retrieve the sender's login ID +fn get_login_id() -> String { + plib::curuser::login_name() +} + +fn get_terminal() -> String { + plib::curuser::tty() +} + +fn get_current_date() -> String { + // Retrieve the current date and time in a human-readable format + let now = Local::now(); + now.format("%Y-%m-%d %H:%M:%S").to_string() +} + +fn process_erase_or_kill(line: &str) { + let stdout = io::stdout(); + let mut handle = stdout.lock(); + + for ch in line.chars() { + match ch { + ERASE_CHAR => { + // Handle the erase character by writing backspace to the terminal + handle + .write_all(b"\x08 \x08") + .expect("Failed to write erase character"); + } + KILL_CHAR => { + // Handle the kill character by writing a newline to the terminal + handle + .write_all(b"\n") + .expect("Failed to write kill character"); + } + _ => { + handle + .write_all(ch.to_string().as_bytes()) + .expect("Failed to write character"); + } + } + } + + handle.flush().expect("Failed to flush output"); +} + +fn write_to_terminal(terminal: &str, message: &str) { + // Write the message to the specified terminal + let mut file = OpenOptions::new() + .write(true) + .open(terminal) + .expect("Failed to open terminal"); + file.write_all(message.as_bytes()) + .expect("Failed to write to terminal"); +} + +// Alert the sender's terminal twice +fn alert_sender_terminal() { + let alert_message = format!("{}{}", ALERT_CHAR, ALERT_CHAR); + let stdout = io::stdout(); + let mut handle = stdout.lock(); + handle + .write_all(alert_message.as_bytes()) + .expect("Failed to write alert"); + handle.flush().expect("Failed to flush alert"); +} + +// Check if the line contains interrupt or end-of-file characters +fn is_interrupt_or_eof(line: &str) -> bool { + line.chars().all(|c| c == INTR_CHAR || c == EOF_CHAR) +} + +// Check if the line contains the alert character +fn contains_alert_character(line: &str) -> bool { + line.chars().all(|c| c == ALERT_CHAR) +} + +// Return the alert character +fn get_alert_character() -> String { + ALERT_CHAR.to_string() +} + +// Check if the line contains erase or kill characters +fn contains_erase_or_kill_character(line: &str) -> bool { + line.chars().all(|c| c == ERASE_CHAR || c == KILL_CHAR) +} + +// Check if the line contains printable or space characters +fn contains_printable_or_space(line: &str) -> bool { + line.chars() + .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace()) +} + +// Process non-printable characters to implementation-defined sequences of printable characters +fn process_non_printable(line: &str) -> String { + let mut s = String::with_capacity(line.len()); + + for ch in line.chars() { + if ch.is_ascii_control() { + s.push_str(&format!("^{}", ch.to_ascii_uppercase())); + } else { + s.push(ch); + } + } + + s +} + +fn main() -> Result<(), Box> { + // parse command line arguments + let args = Args::parse(); + + setlocale(LocaleCategory::LcAll, ""); + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + + let user_name = args.username; + let terminal = match args.terminal { + Some(terminal) => { + if terminal.starts_with("/dev/") { + terminal + } else { + format!("/dev/{}", terminal) + } + } + None => select_terminal(&user_name), + }; + + if !check_write_permission(&terminal) { + eprintln!("{}", gettext("Permission denied")); + exit(1); + } + + let sender_login_id = get_login_id(); + let sending_terminal = get_terminal(); + let date = get_current_date(); + + let message = format!( + "Message from {} ({}) [{}]...\n", + sender_login_id, sending_terminal, date + ); + write_to_terminal(&terminal, &message); + + alert_sender_terminal(); + + let stdin = io::stdin(); + for line in stdin.lock().lines() { + let line = line.unwrap(); + if is_interrupt_or_eof(&line) { + write_to_terminal(&terminal, "EOT\n"); + exit(0); + } else if contains_alert_character(&line) { + write_to_terminal(&terminal, &get_alert_character()); + } else if contains_erase_or_kill_character(&line) { + process_erase_or_kill(&line); + } else if contains_printable_or_space(&line) { + write_to_terminal(&terminal, &format!("{}\n", line)); + } else { + write_to_terminal(&terminal, &process_non_printable(&line)); + } + } + + // Add the EOF message before exiting + write_to_terminal(&terminal, "EOF\n"); + + Ok(()) +}