Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/uu/stty/src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ use nix::sys::termios::{
SpecialCharacterIndices as S,
};

pub enum AllFlags<'a> {
#[cfg(any(
target_os = "freebsd",
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
))]
Baud(u32),
#[cfg(not(any(
target_os = "freebsd",
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
)))]
Baud(BaudRate),
ControlFlags((&'a Flag<C>, bool)),
InputFlags((&'a Flag<I>, bool)),
LocalFlags((&'a Flag<L>, bool)),
OutputFlags((&'a Flag<O>, bool)),
}

pub const CONTROL_FLAGS: &[Flag<C>] = &[
Flag::new("parenb", C::PARENB),
Flag::new("parodd", C::PARODD),
Expand Down
274 changes: 209 additions & 65 deletions src/uu/stty/src/stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore clocal erange tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed cfsetospeed ushort vmin vtime
// spell-checker:ignore clocal erange tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed cfsetospeed ushort vmin vtime cflag lflag

mod flags;

use crate::flags::AllFlags;
use clap::{Arg, ArgAction, ArgMatches, Command};
use nix::libc::{O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, c_ushort};
use nix::sys::termios::{
Expand All @@ -16,7 +17,6 @@
use nix::{ioctl_read_bad, ioctl_write_ptr_bad};
use std::fs::File;
use std::io::{self, Stdout, stdout};
use std::ops::ControlFlow;
use std::os::fd::{AsFd, BorrowedFd};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsRawFd, RawFd};
Expand All @@ -35,6 +35,8 @@
use flags::BAUD_RATES;
use flags::{CONTROL_CHARS, CONTROL_FLAGS, INPUT_FLAGS, LOCAL_FLAGS, OUTPUT_FLAGS};

const ASCII_DEL: u8 = 127;

#[derive(Clone, Copy, Debug)]
pub struct Flag<T> {
name: &'static str,
Expand Down Expand Up @@ -101,6 +103,22 @@
Stdout(Stdout),
}

enum ControlCharMappingError {
IntOutOfRange,
MultipleChars,
}

enum ArgOptions<'a> {
Flags(AllFlags<'a>),
Mapping((SpecialCharacterIndices, u8)),
}

impl<'a> From<AllFlags<'a>> for ArgOptions<'a> {
fn from(flag: AllFlags<'a>) -> Self {
ArgOptions::Flags(flag)
}

Check warning on line 119 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L117-L119

Added lines #L117 - L119 were not covered by tests
}

impl AsFd for Device {
fn as_fd(&self) -> BorrowedFd<'_> {
match self {
Expand Down Expand Up @@ -208,31 +226,77 @@
));
}

// TODO: Figure out the right error message for when tcgetattr fails
let mut termios = tcgetattr(opts.file.as_fd()).expect("Could not get terminal attributes");

if let Some(settings) = &opts.settings {
for setting in settings {
if let ControlFlow::Break(false) = apply_setting(&mut termios, setting) {
return Err(USimpleError::new(
1,
format!("invalid argument '{setting}'"),
));
let mut valid_args: Vec<ArgOptions> = Vec::new();

if let Some(args) = &opts.settings {
let mut args_iter = args.iter();
// iterate over args: skip to next arg if current one is a control char
while let Some(arg) = args_iter.next() {
// control char
if let Some(char_index) = cc_to_index(arg) {
if let Some(mapping) = args_iter.next() {
let cc_mapping = string_to_control_char(mapping).map_err(|e| {
let message = match e {
ControlCharMappingError::IntOutOfRange => format!(
"invalid integer argument: '{mapping}': Value too large for defined data type"
),
ControlCharMappingError::MultipleChars => {
format!("invalid integer argument: '{mapping}'")
}
};
USimpleError::new(1, message)
})?;
valid_args.push(ArgOptions::Mapping((char_index, cc_mapping)));

Check warning on line 249 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L249

Added line #L249 was not covered by tests
} else {
return Err(USimpleError::new(1, format!("missing argument to '{arg}'")));
}
// non control char flag
} else if let Some(flag) = string_to_flag(arg) {
let remove_group = match flag {
AllFlags::Baud(_) => false,
AllFlags::ControlFlags((flag, remove)) => check_flag_group(flag, remove),
AllFlags::InputFlags((flag, remove)) => check_flag_group(flag, remove),
AllFlags::LocalFlags((flag, remove)) => check_flag_group(flag, remove),
AllFlags::OutputFlags((flag, remove)) => check_flag_group(flag, remove),

Check warning on line 260 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L254-L260

Added lines #L254 - L260 were not covered by tests
};
if remove_group {
return Err(USimpleError::new(1, format!("invalid argument '{arg}'")));
}
valid_args.push(flag.into());

Check warning on line 265 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L262-L265

Added lines #L262 - L265 were not covered by tests
// not a valid control char or flag
} else {
return Err(USimpleError::new(1, format!("invalid argument '{arg}'")));

Check warning on line 268 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L268

Added line #L268 was not covered by tests
}
}

// TODO: Figure out the right error message for when tcgetattr fails
let mut termios = tcgetattr(opts.file.as_fd()).expect("Could not get terminal attributes");

Check warning on line 273 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L273

Added line #L273 was not covered by tests

// iterate over valid_args, match on the arg type, do the matching apply function
for arg in &valid_args {
match arg {
ArgOptions::Mapping(mapping) => apply_char_mapping(&mut termios, mapping),
ArgOptions::Flags(flag) => apply_setting(&mut termios, flag),

Check warning on line 279 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L276-L279

Added lines #L276 - L279 were not covered by tests
}
}
tcsetattr(
opts.file.as_fd(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.expect("Could not write terminal attributes");
} else {
// TODO: Figure out the right error message for when tcgetattr fails
let termios = tcgetattr(opts.file.as_fd()).expect("Could not get terminal attributes");

Check warning on line 290 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L289-L290

Added lines #L289 - L290 were not covered by tests
print_settings(&termios, opts).expect("TODO: make proper error here from nix error");
}
Ok(())
}

fn check_flag_group<T>(flag: &Flag<T>, remove: bool) -> bool {
remove && flag.group.is_some()
}

Check warning on line 298 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L296-L298

Added lines #L296 - L298 were not covered by tests

fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
let speed = cfgetospeed(termios);

Expand Down Expand Up @@ -283,6 +347,70 @@
Ok(())
}

fn cc_to_index(option: &str) -> Option<SpecialCharacterIndices> {
for cc in CONTROL_CHARS {
if option == cc.0 {
return Some(cc.1);
}

Check warning on line 354 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L354

Added line #L354 was not covered by tests
}
None

Check warning on line 356 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L356

Added line #L356 was not covered by tests
}

// return Some(flag) if the input is a valid flag, None if not
fn string_to_flag(option: &str) -> Option<AllFlags> {

Check warning on line 360 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L360

Added line #L360 was not covered by tests
// BSDs use a u32 for the baud rate, so any decimal number applies.
#[cfg(any(
target_os = "freebsd",
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
))]
if let Ok(n) = option.parse::<u32>() {
return Some(AllFlags::Baud(n));
}

#[cfg(not(any(
target_os = "freebsd",
target_os = "dragonfly",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
)))]
for (text, baud_rate) in BAUD_RATES {
if *text == option {
return Some(AllFlags::Baud(*baud_rate));
}

Check warning on line 385 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L382-L385

Added lines #L382 - L385 were not covered by tests
}

let remove = option.starts_with('-');
let name = option.trim_start_matches('-');

Check warning on line 389 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L388-L389

Added lines #L388 - L389 were not covered by tests

for cflag in CONTROL_FLAGS {
if name == cflag.name {
return Some(AllFlags::ControlFlags((cflag, remove)));
}

Check warning on line 394 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L391-L394

Added lines #L391 - L394 were not covered by tests
}
for iflag in INPUT_FLAGS {
if name == iflag.name {
return Some(AllFlags::InputFlags((iflag, remove)));
}

Check warning on line 399 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L396-L399

Added lines #L396 - L399 were not covered by tests
}
for lflag in LOCAL_FLAGS {
if name == lflag.name {
return Some(AllFlags::LocalFlags((lflag, remove)));
}

Check warning on line 404 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L401-L404

Added lines #L401 - L404 were not covered by tests
}
for oflag in OUTPUT_FLAGS {
if name == oflag.name {
return Some(AllFlags::OutputFlags((oflag, remove)));
}

Check warning on line 409 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L406-L409

Added lines #L406 - L409 were not covered by tests
}
None
}

Check warning on line 412 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L411-L412

Added lines #L411 - L412 were not covered by tests

fn control_char_to_string(cc: nix::libc::cc_t) -> nix::Result<String> {
if cc == 0 {
return Ok("<undef>".to_string());
Expand Down Expand Up @@ -390,55 +518,25 @@
}

/// Apply a single setting
///
/// The value inside the `Break` variant of the `ControlFlow` indicates whether
/// the setting has been applied.
fn apply_setting(termios: &mut Termios, s: &str) -> ControlFlow<bool> {
apply_baud_rate_flag(termios, s)?;

let (remove, name) = match s.strip_prefix('-') {
Some(s) => (true, s),
None => (false, s),
};
apply_flag(termios, CONTROL_FLAGS, name, remove)?;
apply_flag(termios, INPUT_FLAGS, name, remove)?;
apply_flag(termios, OUTPUT_FLAGS, name, remove)?;
apply_flag(termios, LOCAL_FLAGS, name, remove)?;
ControlFlow::Break(false)
}

/// Apply a flag to a slice of flags
///
/// The value inside the `Break` variant of the `ControlFlow` indicates whether
/// the setting has been applied.
fn apply_flag<T: TermiosFlag>(
termios: &mut Termios,
flags: &[Flag<T>],
input: &str,
remove: bool,
) -> ControlFlow<bool> {
for Flag {
name, flag, group, ..
} in flags
{
if input == *name {
// Flags with groups cannot be removed
// Since the name matches, we can short circuit and don't have to check the other flags.
if remove && group.is_some() {
return ControlFlow::Break(false);
}
// If there is a group, the bits for that group should be cleared before applying the flag
if let Some(group) = group {
group.apply(termios, false);
}
flag.apply(termios, !remove);
return ControlFlow::Break(true);
fn apply_setting(termios: &mut Termios, setting: &AllFlags) {
match setting {
AllFlags::Baud(_) => apply_baud_rate_flag(termios, setting),
AllFlags::ControlFlags((setting, disable)) => {
setting.flag.apply(termios, !disable);
}
AllFlags::InputFlags((setting, disable)) => {
setting.flag.apply(termios, !disable);
}
AllFlags::LocalFlags((setting, disable)) => {
setting.flag.apply(termios, !disable);
}
AllFlags::OutputFlags((setting, disable)) => {
setting.flag.apply(termios, !disable);

Check warning on line 534 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L521-L534

Added lines #L521 - L534 were not covered by tests
}
}
ControlFlow::Continue(())
}

fn apply_baud_rate_flag(termios: &mut Termios, input: &str) -> ControlFlow<bool> {
fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) {

Check warning on line 539 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L539

Added line #L539 was not covered by tests
// BSDs use a u32 for the baud rate, so any decimal number applies.
#[cfg(any(
target_os = "freebsd",
Expand All @@ -448,9 +546,8 @@
target_os = "netbsd",
target_os = "openbsd"
))]
if let Ok(n) = input.parse::<u32>() {
cfsetospeed(termios, n).expect("Failed to set baud rate");
return ControlFlow::Break(true);
if let AllFlags::Baud(n) = input {
cfsetospeed(termios, *n).expect("Failed to set baud rate");
}

// Other platforms use an enum.
Expand All @@ -462,13 +559,60 @@
target_os = "netbsd",
target_os = "openbsd"
)))]
for (text, baud_rate) in BAUD_RATES {
if *text == input {
cfsetospeed(termios, *baud_rate).expect("Failed to set baud rate");
return ControlFlow::Break(true);
if let AllFlags::Baud(br) = input {
cfsetospeed(termios, *br).expect("Failed to set baud rate");
}
}

Check warning on line 565 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L562-L565

Added lines #L562 - L565 were not covered by tests

fn apply_char_mapping(termios: &mut Termios, mapping: &(SpecialCharacterIndices, u8)) {
termios.control_chars[mapping.0 as usize] = mapping.1;
}

Check warning on line 569 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L567-L569

Added lines #L567 - L569 were not covered by tests

// GNU stty defines some valid values for the control character mappings
// 1. Standard character, can be a a single char (ie 'C') or hat notation (ie '^C')
// 2. Integer
// a. hexadecimal, prefixed by '0x'
// b. octal, prefixed by '0'
// c. decimal, no prefix
// 3. Disabling the control character: '^-' or 'undef'
//
// This function returns the ascii value of valid control chars, or ControlCharMappingError if invalid
fn string_to_control_char(s: &str) -> Result<u8, ControlCharMappingError> {
if s == "undef" || s == "^-" {
return Ok(0);

Check warning on line 582 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L582

Added line #L582 was not covered by tests
}

// try to parse integer (hex, octal, or decimal)
let ascii_num = if let Some(hex) = s.strip_prefix("0x") {
u32::from_str_radix(hex, 16).ok()
} else if let Some(octal) = s.strip_prefix("0") {
u32::from_str_radix(octal, 8).ok()
} else {
s.parse::<u32>().ok()
};

if let Some(val) = ascii_num {
if val > 255 {
return Err(ControlCharMappingError::IntOutOfRange);
} else {
return Ok(val as u8);

Check warning on line 598 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L598

Added line #L598 was not covered by tests
}
}
// try to parse ^<char> or just <char>
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some('^'), Some(c)) => {

Check warning on line 604 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L604

Added line #L604 was not covered by tests
// special case: ascii value of '^?' is greater than '?'
if c == '?' {
return Ok(ASCII_DEL);
}

Check warning on line 608 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L606-L608

Added lines #L606 - L608 were not covered by tests
// subtract by '@' to turn the char into the ascii value of '^<char>'
Ok((c.to_ascii_uppercase() as u8).wrapping_sub(b'@'))

Check warning on line 610 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L610

Added line #L610 was not covered by tests
}
(Some(c), None) => Ok(c as u8),

Check warning on line 612 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L612

Added line #L612 was not covered by tests
(Some(_), Some(_)) => Err(ControlCharMappingError::MultipleChars),
_ => unreachable!("No arguments provided: must have been caught earlier"),

Check warning on line 614 in src/uu/stty/src/stty.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/stty/src/stty.rs#L614

Added line #L614 was not covered by tests
}
ControlFlow::Continue(())
}

pub fn uu_app() -> Command {
Expand Down
Loading
Loading