1
0
Fork 0

Adding upstream version 0.2.3.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-05 06:17:43 +01:00
parent c7e0ec57a4
commit 7f48381065
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
14 changed files with 647 additions and 0 deletions

6
.cargo_vcs_info.json Normal file
View file

@ -0,0 +1,6 @@
{
"git": {
"sha1": "0fbe095a9b44a8da5b42c972b54af660a340acda"
},
"path_in_vcs": ""
}

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

20
CHANGELOG Normal file
View file

@ -0,0 +1,20 @@
# Version 0.2.3 - 2023-08-09
- [change][patch] Remove use of `IsTerminal` to support rust version 1.66 and up.
- [change][patch] Declare minimum rust version in `Cargo.toml`.
# Version 0.2.2 - 2023-08-08
- [fix][minor] Fix printing of newline in `Terminal::read_input_line()`.
# Version 0.2.1 - 2023-08-04
- [change][patch] Fix category slug in `Cargo.toml`.
# Version 0.2.0 - 2023-08-04
- [rename][major] Rename `TerminalPrompter` to `Terminal`.
- [rename][major] Rename `read_line()` to `read_input_line()`.
- [add][minor] Implement `BufRead` for `Terminal`.
- [change][minor] Try `sterr`, `stdin`, `stdout` and `/dev/tty` in that order on Unix.
- [change][minor] Do not cache the terminal mode, retrieve it every time when needed.
- [add][minor] Add documentation.
# Version 0.1.0 - 2023-08-03
- [add][minor] Initial release.

39
Cargo.lock generated Normal file
View file

@ -0,0 +1,39 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "terminal-prompt"
version = "0.2.3"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

41
Cargo.toml Normal file
View file

@ -0,0 +1,41 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.66"
name = "terminal-prompt"
version = "0.2.3"
authors = ["Maarten de Vries <maarten@de-vri.es>"]
publish = ["crates-io"]
description = "Tiny library for prompting sensitive or non-sensitive data on the terminal"
documentation = "https://docs.rs/terminal-prompt"
readme = "README.md"
keywords = [
"terminal",
"console",
"prompt",
"input",
"tty",
]
categories = [
"command-line-interface",
"os",
]
license = "BSD-2-Clause"
repository = "https://github.com/de-vri-es/terminal-prompt-rs"
[target."cfg(unix)".dependencies.libc]
version = "0.2.147"
[target."cfg(windows)".dependencies.winapi]
version = "0.3.9"
features = ["consoleapi"]

22
Cargo.toml.orig generated Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "terminal-prompt"
version = "0.2.3"
description = "Tiny library for prompting sensitive or non-sensitive data on the terminal"
license = "BSD-2-Clause"
authors = ["Maarten de Vries <maarten@de-vri.es>"]
repository = "https://github.com/de-vri-es/terminal-prompt-rs"
documentation = "https://docs.rs/terminal-prompt"
keywords = ["terminal", "console", "prompt", "input", "tty"]
categories = ["command-line-interface", "os"]
publish = ["crates-io"]
edition = "2021"
rust-version = "1.66"
[target.'cfg(unix)'.dependencies]
libc = "0.2.147"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["consoleapi"] }

24
LICENSE.md Normal file
View file

@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2023, Maarten de Vries <maarten@de-vri.es>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

18
README.md Normal file
View file

@ -0,0 +1,18 @@
# terminal-prompt
Tiny library for prompting sensitive or non-sensitive data on the terminal.
The only dependency is `libc` on Unix and `winapi` on Windows.
See [`Terminal`] for the API documentation.
## Example
Read a username and password from the terminal:
```rust
use terminal_prompt::Terminal;
let mut terminal = Terminal::open()?;
let username = terminal.prompt("Username: ")?;
let password = terminal.prompt_sensitive("Password: ")?;
```
[`Terminal`]: https://docs.rs/terminal-prompt/latest/terminal_prompt/struct.Terminal.html

5
README.tpl Normal file
View file

@ -0,0 +1,5 @@
# {{crate}}
{{readme}}
[`Terminal`]: https://docs.rs/terminal-prompt/latest/terminal_prompt/struct.Terminal.html

View file

@ -0,0 +1,10 @@
use terminal_prompt::Terminal;
fn main() -> std::io::Result<()> {
let mut terminal = Terminal::open()?;
let username = terminal.prompt("Username: ")?;
let password = terminal.prompt_sensitive("Password: ")?;
println!("Username: {username}");
println!("Password: {password}");
Ok(())
}

191
src/lib.rs Normal file
View file

@ -0,0 +1,191 @@
//! Tiny library for prompting sensitive or non-sensitive data on the terminal.
//!
//! The only dependency is `libc` on Unix and `winapi` on Windows.
//!
//! See [`Terminal`] for the API documentation.
//!
//! # Example
//! Read a username and password from the terminal:
//! ```no_run
//! # fn main() -> std::io::Result<()> {
//! use terminal_prompt::Terminal;
//! let mut terminal = Terminal::open()?;
//! let username = terminal.prompt("Username: ")?;
//! let password = terminal.prompt_sensitive("Password: ")?;
//! # Ok(())
//! # }
//! ```
#![warn(missing_docs)]
use std::io::{BufReader, BufRead, Read, Write};
mod sys;
/// A handle to the terminal associated with the current process.
///
/// Once opened, you can use [`Self::prompt()`] to read non-sensitive data from the terminal,
/// and [`Self::prompt_sensitive()`] to read sensitive data like passwords.
///
/// Alternatively, you can manually call [`Self::enable_echo()`] and [`Self::disable_echo()`], and read/write from the terminal directly.
/// The terminal handle implements the standard [`Read`], [`Write`] and [`BufRead`] traits,
/// and it has a [`Self::read_line()`] convenience function that returns a new string.
///
/// # Terminal modes
/// When opened, the terminal will be put in line editing mode.
/// When dropped, the original mode of the terminal will be restored.
/// Note that the terminal is inherently a global resource,
/// so creating multiple terminal objects and dropping them in a different order can cause the terminal to be left in a different mode.
pub struct Terminal {
/// The underlying terminal.
terminal: BufReader<sys::Terminal>,
/// The mode of the terminal when we opened it.
initial_mode: sys::TerminalMode,
}
impl Terminal {
/// Open the terminal associated with the current process.
///
/// The exact behavior is platform dependent.
///
/// On Unix platforms, if one of standard I/O streams is a terminal, that terminal is used.
/// First standard error is tried, then standard input and finally standard output.
/// If none of those work, the function tries to open `/dev/tty`.
/// This means that on Unix platforms, the terminal prompt can still work, even when both standard input and standard output are connected to pipes instead of the terminal.
///
/// On Windows, if both standard input and standard error are connected to a terminal, those streams are used.
///
/// In all cases, if the function fails to find a terminal for the process, an error is returned.
pub fn open() -> std::io::Result<Self> {
// Open the terminal and retrieve the initial mode.
let terminal = sys::Terminal::open()?;
let initial_mode = terminal.get_terminal_mode()?;
// Enable line editing mode.
let mut mode = initial_mode;
mode.enable_line_editing();
terminal.set_terminal_mode(&mode)?;
Ok(Self {
terminal: BufReader::new(terminal),
initial_mode,
})
}
/// Check if the terminal is echoing input.
///
/// If enabled, any text typed on the terminal will be visible.
pub fn is_echo_enabled(&self) -> std::io::Result<bool> {
let mode = self.terminal.get_ref().get_terminal_mode()?;
Ok(mode.is_echo_enabled())
}
/// Disable echoing of terminal input.
///
/// This will prevent text typed on the terminal from being visible.
/// This can be used to hide passwords while they are being typed.
pub fn disable_echo(&self) -> std::io::Result<()> {
let mut mode = self.terminal.get_ref().get_terminal_mode()?;
mode.disable_echo();
self.terminal.get_ref().set_terminal_mode(&mode)?;
Ok(())
}
/// Enable echoing of terminal input.
///
/// This will cause any text typed on the terminal to be visible.
pub fn enable_echo(&mut self) -> std::io::Result<()> {
let mut mode = self.terminal.get_ref().get_terminal_mode()?;
mode.enable_echo();
self.terminal.get_ref().set_terminal_mode(&mode)?;
Ok(())
}
/// Read a line of input from the terminal.
///
/// If echoing is disabled, this will also print a newline character to visually indicate to the user.
/// If this is not desired, use the [`BufRead::read_line()`] function instead.
pub fn read_input_line(&mut self) -> std::io::Result<String> {
let mut buffer = String::new();
self.terminal.read_line(&mut buffer)?;
if self.is_echo_enabled().ok() == Some(false) {
writeln!(self).ok();
}
if buffer.ends_with('\n') {
buffer.pop();
}
Ok(buffer)
}
/// Prompt the user on the terminal.
///
/// This function does not enable or disable echoing and should not normally be used for reading sensitive data like passwords.
/// Consider [`Self::prompt_sensitive()`] instead.
pub fn prompt(&mut self, prompt: impl std::fmt::Display) -> std::io::Result<String> {
write!(self, "{prompt}")?;
self.read_input_line()
}
/// Prompt the user for sensitive data (like passwords) on the terminal.
///
/// This function makes sure that echoing is disabled before the prompt is shown.
/// If echoing was enabled, it is re-enabled after the response is read.
///
/// Use [`Self::prompt()`] to read non-sensitive data.
pub fn prompt_sensitive(&mut self, prompt: impl std::fmt::Display) -> std::io::Result<String> {
let old_mode = self.terminal.get_ref().get_terminal_mode()?;
if old_mode.is_echo_enabled() {
let mut new_mode = old_mode;
new_mode.disable_echo();
self.terminal.get_ref().set_terminal_mode(&new_mode)?;
}
write!(self, "{prompt}")?;
let line = self.read_input_line();
if old_mode.is_echo_enabled() {
self.terminal.get_ref().set_terminal_mode(&old_mode).ok();
}
line
}
}
impl Drop for Terminal {
fn drop(&mut self) {
self.terminal.get_ref().set_terminal_mode(&self.initial_mode).ok();
}
}
impl Read for Terminal {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.terminal.read(buf)
}
fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
self.terminal.read_vectored(bufs)
}
}
impl BufRead for Terminal {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
self.terminal.fill_buf()
}
fn consume(&mut self, amt: usize) {
self.terminal.consume(amt)
}
}
impl Write for Terminal {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.terminal.get_mut().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.terminal.get_mut().flush()
}
fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
self.terminal.get_mut().write_vectored(bufs)
}
}

11
src/sys/mod.rs Normal file
View file

@ -0,0 +1,11 @@
#[cfg(unix)]
mod unix;
#[cfg(unix)]
pub use unix::*;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub use windows::*;

142
src/sys/unix.rs Normal file
View file

@ -0,0 +1,142 @@
use std::fs::File;
use std::io::{Read, Write};
use std::mem::ManuallyDrop;
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, RawFd};
/// Unix handle to an open terminal.
pub enum Terminal {
/// Non-owning file for one of the standard I/O streams.
Stdio(ManuallyDrop<File>),
/// Owned file for `/dev/tty`.
File(File),
}
#[derive(Copy, Clone)]
pub struct TerminalMode {
termios: libc::termios,
}
impl Terminal {
pub fn open() -> std::io::Result<Self> {
if let Some(terminal) = open_fd_terminal(2) {
Ok(terminal)
} else if let Some(terminal) = open_fd_terminal(0) {
Ok(terminal)
} else if let Some(terminal) = open_fd_terminal(1) {
Ok(terminal)
} else {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")?;
if is_terminal(file.as_fd()) {
Ok(Self::File(file))
} else {
Err(std::io::Error::from_raw_os_error(libc::ENOTTY))
}
}
}
pub fn get_terminal_mode(&self) -> std::io::Result<TerminalMode> {
unsafe {
let mut termios = std::mem::zeroed();
check_ret(libc::tcgetattr(self.as_fd().as_raw_fd(), &mut termios))?;
Ok(TerminalMode { termios })
}
}
pub fn set_terminal_mode(&self, mode: &TerminalMode) -> std::io::Result<()> {
unsafe {
check_ret(libc::tcsetattr(
self.as_fd().as_raw_fd(),
libc::TCSANOW,
&mode.termios,
))?;
Ok(())
}
}
fn as_file(&self) -> &File {
match self {
Self::Stdio(io) => io,
Self::File(io) => io,
}
}
}
fn open_fd_terminal(fd: RawFd) -> Option<Terminal> {
let file = unsafe { ManuallyDrop::new(File::from_raw_fd(fd)) };
if is_terminal(file.as_fd()) {
Some(Terminal::Stdio(file))
} else {
None
}
}
impl TerminalMode {
pub fn enable_line_editing(&mut self) {
self.termios.c_lflag |= libc::ICANON;
}
pub fn disable_echo(&mut self) {
self.termios.c_lflag &= !libc::ECHO;
self.termios.c_lflag &= !libc::ICANON;
}
pub fn enable_echo(&mut self) {
self.termios.c_lflag |= libc::ECHO;
self.termios.c_lflag |= !libc::ICANON;
}
pub fn is_echo_enabled(&self) -> bool {
self.termios.c_lflag & libc::ECHO != 0
}
}
fn is_terminal(fd: BorrowedFd) -> bool {
unsafe {
libc::isatty(fd.as_raw_fd()) == 1
}
}
fn check_ret(input: i32) -> std::io::Result<()> {
if input == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
impl AsFd for Terminal {
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
match self {
Self::Stdio(stdin) => stdin.as_fd(),
Self::File(file) => file.as_fd(),
}
}
}
impl Read for Terminal {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.as_file().read(buf)
}
fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
self.as_file().read_vectored(bufs)
}
}
impl Write for Terminal {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.as_file().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.as_file().flush()
}
fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
self.as_file().write_vectored(bufs)
}
}

117
src/sys/windows.rs Normal file
View file

@ -0,0 +1,117 @@
use std::io::{Read, Write};
use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle};
use winapi::um::consoleapi::{
GetConsoleMode,
SetConsoleMode,
};
use winapi::um::wincon::{
ENABLE_LINE_INPUT,
ENABLE_ECHO_INPUT,
};
use winapi::shared::minwindef::{BOOL, DWORD};
pub struct Terminal {
input: std::io::Stdin,
output: std::io::Stderr,
}
#[derive(Copy, Clone)]
pub struct TerminalMode {
input_mode: DWORD,
}
impl Terminal {
pub fn open() -> std::io::Result<Self> {
let input = std::io::stdin();
let output = std::io::stderr();
if !is_terminal(input.as_handle()) {
return Err(std::io::Error::new(std::io::ErrorKind::Other, "stdin is not a terminal"));
}
if !is_terminal(output.as_handle()) {
return Err(std::io::Error::new(std::io::ErrorKind::Other, "stderr is not a terminal"));
}
Ok(Self {
input,
output,
})
}
pub fn get_terminal_mode(&self) -> std::io::Result<TerminalMode> {
unsafe {
let mut input_mode = 0;
check_ret(GetConsoleMode(self.input.as_raw_handle().cast(), &mut input_mode))?;
Ok(TerminalMode {
input_mode,
})
}
}
pub fn set_terminal_mode(&self, mode: &TerminalMode) -> std::io::Result<()> {
unsafe {
check_ret(SetConsoleMode(
self.input.as_raw_handle().cast(),
mode.input_mode,
))?;
Ok(())
}
}
}
impl TerminalMode {
pub fn enable_line_editing(&mut self) {
self.input_mode |= ENABLE_LINE_INPUT;
}
pub fn disable_echo(&mut self) {
self.input_mode &= !ENABLE_ECHO_INPUT;
}
pub fn enable_echo(&mut self) {
self.input_mode |= ENABLE_ECHO_INPUT;
}
pub fn is_echo_enabled(&self) -> bool {
self.input_mode & ENABLE_ECHO_INPUT != 0
}
}
fn is_terminal(handle: BorrowedHandle) -> bool {
unsafe {
let mut mode = 0;
GetConsoleMode(handle.as_raw_handle().cast(), &mut mode) != 0
}
}
fn check_ret(input: BOOL) -> std::io::Result<()> {
if input != 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
impl Read for Terminal {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.input.read(buf)
}
fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
self.input.read_vectored(bufs)
}
}
impl Write for Terminal {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.output.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.output.flush()
}
fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
self.output.write_vectored(bufs)
}
}