1010// spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc
1111// spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase
1212// spell-checker:ignore sigquit sigtstp
13- // spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb
13+ // spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb NCCS
1414
1515mod flags;
1616
@@ -30,7 +30,7 @@ use std::num::IntErrorKind;
3030use std:: os:: fd:: { AsFd , BorrowedFd } ;
3131use std:: os:: unix:: fs:: OpenOptionsExt ;
3232use std:: os:: unix:: io:: { AsRawFd , RawFd } ;
33- use uucore:: error:: { UError , UResult , USimpleError } ;
33+ use uucore:: error:: { UError , UResult , USimpleError , UUsageError } ;
3434use uucore:: format_usage;
3535use uucore:: translate;
3636
@@ -150,6 +150,7 @@ enum ArgOptions<'a> {
150150 Mapping ( ( S , u8 ) ) ,
151151 Special ( SpecialSetting ) ,
152152 Print ( PrintSetting ) ,
153+ SavedState ( Vec < u32 > ) ,
153154}
154155
155156impl < ' a > From < AllFlags < ' a > > for ArgOptions < ' a > {
@@ -352,8 +353,12 @@ fn stty(opts: &Options) -> UResult<()> {
352353 valid_args. push ( ArgOptions :: Print ( PrintSetting :: Size ) ) ;
353354 }
354355 _ => {
356+ // Try to parse saved format (hex string like "6d02:5:4bf:8a3b:...")
357+ if let Some ( state) = parse_saved_state ( arg) {
358+ valid_args. push ( ArgOptions :: SavedState ( state) ) ;
359+ }
355360 // control char
356- if let Some ( char_index) = cc_to_index ( arg) {
361+ else if let Some ( char_index) = cc_to_index ( arg) {
357362 if let Some ( mapping) = args_iter. next ( ) {
358363 let cc_mapping = string_to_control_char ( mapping) . map_err ( |e| {
359364 let message = match e {
@@ -370,7 +375,7 @@ fn stty(opts: &Options) -> UResult<()> {
370375 )
371376 }
372377 } ;
373- USimpleError :: new ( 1 , message)
378+ UUsageError :: new ( 1 , message)
374379 } ) ?;
375380 valid_args. push ( ArgOptions :: Mapping ( ( char_index, cc_mapping) ) ) ;
376381 } else {
@@ -418,6 +423,9 @@ fn stty(opts: &Options) -> UResult<()> {
418423 ArgOptions :: Print ( setting) => {
419424 print_special_setting ( setting, opts. file . as_raw_fd ( ) ) ?;
420425 }
426+ ArgOptions :: SavedState ( state) => {
427+ apply_saved_state ( & mut termios, state) ?;
428+ }
421429 }
422430 }
423431 tcsetattr ( opts. file . as_fd ( ) , set_arg, & termios) ?;
@@ -429,8 +437,9 @@ fn stty(opts: &Options) -> UResult<()> {
429437 Ok ( ( ) )
430438}
431439
440+ // The GNU implementation adds the --help message when the args are incorrectly formatted
432441fn missing_arg < T > ( arg : & str ) -> Result < T , Box < dyn UError > > {
433- Err :: < T , Box < dyn UError > > ( USimpleError :: new (
442+ Err ( UUsageError :: new (
434443 1 ,
435444 translate ! (
436445 "stty-error-missing-argument" ,
@@ -440,7 +449,7 @@ fn missing_arg<T>(arg: &str) -> Result<T, Box<dyn UError>> {
440449}
441450
442451fn invalid_arg < T > ( arg : & str ) -> Result < T , Box < dyn UError > > {
443- Err :: < T , Box < dyn UError > > ( USimpleError :: new (
452+ Err ( UUsageError :: new (
444453 1 ,
445454 translate ! (
446455 "stty-error-invalid-argument" ,
@@ -450,7 +459,7 @@ fn invalid_arg<T>(arg: &str) -> Result<T, Box<dyn UError>> {
450459}
451460
452461fn invalid_integer_arg < T > ( arg : & str ) -> Result < T , Box < dyn UError > > {
453- Err :: < T , Box < dyn UError > > ( USimpleError :: new (
462+ Err ( UUsageError :: new (
454463 1 ,
455464 translate ! (
456465 "stty-error-invalid-integer-argument" ,
@@ -478,6 +487,43 @@ fn parse_rows_cols(arg: &str) -> Option<u16> {
478487 None
479488}
480489
490+ /// Parse a saved terminal state string in stty format.
491+ ///
492+ /// The format is colon-separated hexadecimal values:
493+ /// `input_flags:output_flags:control_flags:local_flags:cc0:cc1:cc2:...`
494+ ///
495+ /// - Must have exactly 4 + NCCS parts (4 flags + platform-specific control characters)
496+ /// - All parts must be non-empty valid hex values
497+ /// - Control characters must fit in u8 (0-255)
498+ /// - Returns `None` if format is invalid
499+ fn parse_saved_state ( arg : & str ) -> Option < Vec < u32 > > {
500+ let parts: Vec < & str > = arg. split ( ':' ) . collect ( ) ;
501+ let expected_parts = 4 + nix:: libc:: NCCS ;
502+
503+ // GNU requires exactly the right number of parts for this platform
504+ if parts. len ( ) != expected_parts {
505+ return None ;
506+ }
507+
508+ // Validate all parts are non-empty valid hex
509+ let mut values = Vec :: with_capacity ( expected_parts) ;
510+ for ( i, part) in parts. iter ( ) . enumerate ( ) {
511+ if part. is_empty ( ) {
512+ return None ; // GNU rejects empty hex values
513+ }
514+ let val = u32:: from_str_radix ( part, 16 ) . ok ( ) ?;
515+
516+ // Control characters (indices 4+) must fit in u8
517+ if i >= 4 && val > 255 {
518+ return None ;
519+ }
520+
521+ values. push ( val) ;
522+ }
523+
524+ Some ( values)
525+ }
526+
481527fn check_flag_group < T > ( flag : & Flag < T > , remove : bool ) -> bool {
482528 remove && flag. group . is_some ( )
483529}
@@ -857,6 +903,39 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) {
857903 termios. control_chars [ mapping. 0 as usize ] = mapping. 1 ;
858904}
859905
906+ /// Apply a saved terminal state to the current termios.
907+ ///
908+ /// The state array contains:
909+ /// - `state[0]`: input flags
910+ /// - `state[1]`: output flags
911+ /// - `state[2]`: control flags
912+ /// - `state[3]`: local flags
913+ /// - `state[4..]`: control characters (optional)
914+ ///
915+ /// If state has fewer than 4 elements, no changes are applied. This is a defensive
916+ /// check that should never trigger since `parse_saved_state` rejects such states.
917+ fn apply_saved_state ( termios : & mut Termios , state : & [ u32 ] ) -> nix:: Result < ( ) > {
918+ // Require at least 4 elements for the flags (defensive check)
919+ if state. len ( ) < 4 {
920+ return Ok ( ( ) ) ; // No-op for invalid state (already validated by parser)
921+ }
922+
923+ // Apply the four flag groups, done (as _) for MacOS size compatibility
924+ termios. input_flags = InputFlags :: from_bits_truncate ( state[ 0 ] as _ ) ;
925+ termios. output_flags = OutputFlags :: from_bits_truncate ( state[ 1 ] as _ ) ;
926+ termios. control_flags = ControlFlags :: from_bits_truncate ( state[ 2 ] as _ ) ;
927+ termios. local_flags = LocalFlags :: from_bits_truncate ( state[ 3 ] as _ ) ;
928+
929+ // Apply control characters if present (stored as u32 but used as u8)
930+ for ( i, & cc_val) in state. iter ( ) . skip ( 4 ) . enumerate ( ) {
931+ if i < termios. control_chars . len ( ) {
932+ termios. control_chars [ i] = cc_val as u8 ;
933+ }
934+ }
935+
936+ Ok ( ( ) )
937+ }
938+
860939fn apply_special_setting (
861940 _termios : & mut Termios ,
862941 setting : & SpecialSetting ,
0 commit comments