Skip to content

Commit c8c7821

Browse files
committed
make visudo response reader use BufRead::Lines
and accept "y" as a universal affirmative response
1 parent 57f225e commit c8c7821

File tree

3 files changed

+41
-40
lines changed

3 files changed

+41
-40
lines changed

po/sudo-rs.pot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,9 @@ msgstr ""
315315
msgid "failed to remove temporary file {path}: {error}"
316316
msgstr ""
317317

318-
#. TRANSLATORS: the initial letters of 'yes' and 'no' responses, in that order
318+
#. TRANSLATORS: the initial letters of 'no' and 'yes' responses, in that order
319319
#: src/sudo/edit.rs:302
320-
msgid "yn"
320+
msgid "ny"
321321
msgstr ""
322322

323323
#: src/sudo/edit.rs:306

src/sudo/edit.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,18 +298,19 @@ fn handle_child_inner(editor: &Path, mut files: Vec<ChildFileInfo<'_>>) -> Resul
298298

299299
// If the file has been changed to be empty, ask the user what to do.
300300
if new_data.is_empty() && new_data != file.old_data {
301-
// TRANSLATORS: the initial letters of 'yes' and 'no' responses, in that order
302-
let answers = xlat!("yn").as_bytes().get(..2).unwrap_or(b"yn");
301+
// TRANSLATORS: the initial letters of 'no' and 'yes' responses, in that order
302+
let answers = xlat!("ny");
303303

304304
match crate::visudo::ask_response(
305-
xlat!(
305+
&xlat!(
306306
"sudoedit: truncate {path} to zero? (y/n) [n] ",
307307
path = file.path.display()
308-
)
309-
.as_bytes(),
308+
),
310309
answers,
311310
) {
312-
Ok(val) if val == answers[0] => {}
311+
Ok(val) if answers.starts_with(val) => {}
312+
// a fallback: also accept 'yes' based on Debian's apt behaviour
313+
Ok('y') => if !answers.contains('y') {},
313314
_ => {
314315
user_info!("not overwriting {path}", path = file.path.display());
315316

src/visudo/mod.rs

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod help;
66
use std::{
77
env, ffi,
88
fs::{File, Permissions},
9-
io::{self, Read, Seek, Write},
9+
io::{self, BufRead, Read, Seek, Write},
1010
os::unix::prelude::{MetadataExt, PermissionsExt},
1111
path::{Path, PathBuf},
1212
process::Command,
@@ -318,8 +318,8 @@ fn edit_sudoers_file(
318318

319319
writeln!(stderr)?;
320320

321-
match ask_response(b"What now? e(x)it without saving / (e)dit again: ", b"xe")? {
322-
b'x' => return Ok(()),
321+
match ask_response("What now? e(x)it without saving / (e)dit again: ", "xe")? {
322+
'x' => return Ok(()),
323323
_ => continue,
324324
}
325325
} else {
@@ -329,11 +329,11 @@ fn edit_sudoers_file(
329329
"It looks like you have removed your ability to run 'sudo visudo' again.\n"
330330
)?;
331331
match ask_response(
332-
b"What now? e(x)it without saving / (e)dit again / lock me out and (S)ave: ",
333-
b"xeS",
332+
"What now? e(x)it without saving / (e)dit again / lock me out and (S)ave: ",
333+
"xeS",
334334
)? {
335-
b'x' => return Ok(()),
336-
b'S' => {}
335+
'x' => return Ok(()),
336+
'S' => {}
337337
_ => continue,
338338
}
339339
}
@@ -385,41 +385,41 @@ fn sudo_visudo_is_allowed(mut sudoers: Sudoers, host_name: &Hostname) -> Option<
385385
}
386386

387387
// Make sure that the first valid response is the "safest" choice
388-
pub(crate) fn ask_response(prompt: &[u8], valid_responses: &[u8]) -> io::Result<u8> {
388+
// This will panic if valid_responses is empty.
389+
pub(crate) fn ask_response(prompt: &str, valid_responses: &str) -> io::Result<char> {
389390
let stdin = io::stdin();
390391
let stdout = io::stdout();
391392
let mut stderr = io::stderr();
392393

393-
let mut stdin_handle = stdin.lock();
394+
let stdin_handle = stdin.lock();
394395
let mut stdout_handle = stdout.lock();
395396

397+
let mut lines = stdin_handle.lines();
398+
396399
loop {
397-
stdout_handle.write_all(prompt)?;
400+
stdout_handle.write_all(prompt.as_bytes())?;
398401
stdout_handle.flush()?;
399402

400-
let mut input = [0u8];
401-
if let Err(err) = stdin_handle.read_exact(&mut input) {
402-
writeln!(stderr, "visudo: cannot read user input: {err}")?;
403-
return Ok(valid_responses[0]);
404-
}
405-
406-
// read the trailing newline
407-
loop {
408-
let mut skipped = [0u8];
409-
match stdin_handle.read_exact(&mut skipped) {
410-
Ok(()) if &skipped != b"\n" => continue,
411-
_ => break,
403+
match lines.next() {
404+
Some(Ok(answer))
405+
if answer
406+
.chars()
407+
.next()
408+
.is_some_and(|input| valid_responses.contains(input)) =>
409+
{
410+
return Ok(answer.chars().next().unwrap());
411+
}
412+
Some(Ok(answer)) => writeln!(stderr, "Invalid option: '{answer}'\n",)?,
413+
Some(Err(err)) => writeln!(stderr, "Invalid response: {err}\n",)?,
414+
None => {
415+
let safe_choice = valid_responses
416+
.chars()
417+
.next()
418+
.expect("at least one response");
419+
420+
writeln!(stderr, "visudo: cannot read user input")?;
421+
return Ok(safe_choice);
412422
}
413-
}
414-
415-
if valid_responses.contains(&input[0]) {
416-
return Ok(input[0]);
417-
} else {
418-
writeln!(
419-
stderr,
420-
"Invalid option: '{}'\n",
421-
str::from_utf8(&input).unwrap_or("<INVALID>")
422-
)?;
423423
}
424424
}
425425
}

0 commit comments

Comments
 (0)