Skip to content

Commit 5ade1ad

Browse files
committed
cargo-credential: reset stdin & stdout to the Console
1 parent 2b3554f commit 5ade1ad

File tree

7 files changed

+221
-30
lines changed

7 files changed

+221
-30
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

credential/cargo-credential-1password/src/lib.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ impl OnePasswordKeychain {
8080
let mut cmd = Command::new("op");
8181
cmd.args(["signin", "--raw"]);
8282
cmd.stdout(Stdio::piped());
83-
cmd.stdin(cargo_credential::tty().map_err(Box::new)?);
8483
let mut child = cmd
8584
.spawn()
8685
.map_err(|e| format!("failed to spawn `op`: {}", e))?;
@@ -210,7 +209,7 @@ impl OnePasswordKeychain {
210209
Some(name) => format!("Cargo registry token for {}", name),
211210
None => "Cargo registry token".to_string(),
212211
};
213-
let mut cmd = self.make_cmd(
212+
let cmd = self.make_cmd(
214213
session,
215214
&[
216215
"item",
@@ -225,10 +224,6 @@ impl OnePasswordKeychain {
225224
CARGO_TAG,
226225
],
227226
);
228-
// For unknown reasons, `op item create` seems to not be happy if
229-
// stdin is not a tty. Otherwise it returns with a 0 exit code without
230-
// doing anything.
231-
cmd.stdin(cargo_credential::tty().map_err(Box::new)?);
232227
self.run_cmd(cmd)?;
233228
Ok(())
234229
}

credential/cargo-credential/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ description = "A library to assist writing Cargo credential helpers."
88

99
[dependencies]
1010
anyhow.workspace = true
11+
libc.workspace = true
1112
serde = { workspace = true, features = ["derive"] }
1213
serde_json.workspace = true
1314
thiserror.workspace = true
1415
time.workspace = true
16+
windows-sys = { workspace = true, features = ["Win32_System_Console", "Win32_Foundation"] }
1517

1618
[dev-dependencies]
1719
snapbox = { workspace = true, features = ["examples"] }
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! Provider used for testing redirection of stdout.
2+
3+
use cargo_credential::{Action, Credential, CredentialResponse, Error, RegistryInfo};
4+
5+
struct MyCredential;
6+
7+
impl Credential for MyCredential {
8+
fn perform(
9+
&self,
10+
_registry: &RegistryInfo,
11+
_action: &Action,
12+
_args: &[&str],
13+
) -> Result<CredentialResponse, Error> {
14+
// Informational messages should be sent on stderr.
15+
eprintln!("message on stderr should be sent the the parent process");
16+
17+
// Reading from stdin and writing to stdout will go to the attached console (tty).
18+
println!("message from test credential provider");
19+
Err(Error::OperationNotSupported)
20+
}
21+
}
22+
23+
fn main() {
24+
cargo_credential::main(MyCredential);
25+
}

credential/cargo-credential/src/lib.rs

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,16 @@
3838
//! ```
3939
4040
use serde::{Deserialize, Serialize};
41-
use std::{
42-
fmt::Display,
43-
fs::File,
44-
io::{self, BufRead, BufReader},
45-
};
41+
use std::{fmt::Display, io};
4642
use time::OffsetDateTime;
4743

4844
mod error;
4945
mod secret;
46+
mod stdio;
47+
5048
pub use error::Error;
5149
pub use secret::Secret;
50+
use stdio::stdin_stdout_to_console;
5251

5352
/// Message sent by the credential helper on startup
5453
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -241,32 +240,20 @@ fn doit(
241240
if request.v != PROTOCOL_VERSION_1 {
242241
return Err(format!("unsupported protocol version {}", request.v).into());
243242
}
244-
serde_json::to_writer(
245-
std::io::stdout(),
246-
&credential.perform(&request.registry, &request.action, &request.args),
247-
)?;
243+
244+
let response = stdin_stdout_to_console(|| {
245+
credential.perform(&request.registry, &request.action, &request.args)
246+
})?;
247+
248+
serde_json::to_writer(std::io::stdout(), &response)?;
248249
println!();
249250
}
250251
}
251252

252-
/// Open stdin from the tty
253-
pub fn tty() -> Result<File, io::Error> {
254-
#[cfg(unix)]
255-
const IN_DEVICE: &str = "/dev/tty";
256-
#[cfg(windows)]
257-
const IN_DEVICE: &str = "CONIN$";
258-
let stdin = std::fs::OpenOptions::new()
259-
.read(true)
260-
.write(true)
261-
.open(IN_DEVICE)?;
262-
Ok(stdin)
263-
}
264-
265253
/// Read a line of text from stdin.
266254
pub fn read_line() -> Result<String, io::Error> {
267-
let mut reader = BufReader::new(tty()?);
268255
let mut buf = String::new();
269-
reader.read_line(&mut buf)?;
256+
io::stdin().read_line(&mut buf)?;
270257
Ok(buf.trim().to_string())
271258
}
272259

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use std::{fs::File, io::Error};
2+
3+
/// Reset stdin and stdout to the attached console / tty for the duration of the closure.
4+
/// If no console is available, stdin and stdout will be redirected to null.
5+
pub fn stdin_stdout_to_console<F, T>(f: F) -> Result<T, Error>
6+
where
7+
F: FnOnce() -> T,
8+
{
9+
let open_write = |f| std::fs::OpenOptions::new().write(true).open(f);
10+
11+
let mut stdin = File::open(imp::IN_DEVICE).or_else(|_| File::open(imp::NULL_DEVICE))?;
12+
let mut stdout = open_write(imp::OUT_DEVICE).or_else(|_| open_write(imp::NULL_DEVICE))?;
13+
14+
let _stdin_guard = imp::ReplacementGuard::new(Stdio::Stdin, &mut stdin)?;
15+
let _stdout_guard = imp::ReplacementGuard::new(Stdio::Stdout, &mut stdout)?;
16+
Ok(f())
17+
}
18+
19+
enum Stdio {
20+
Stdin,
21+
Stdout,
22+
}
23+
24+
#[cfg(windows)]
25+
mod imp {
26+
use super::Stdio;
27+
use std::{fs::File, io::Error, os::windows::prelude::AsRawHandle};
28+
use windows_sys::Win32::{
29+
Foundation::{HANDLE, INVALID_HANDLE_VALUE},
30+
System::Console::{
31+
GetStdHandle, SetStdHandle, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
32+
},
33+
};
34+
pub const OUT_DEVICE: &str = "CONOUT$";
35+
pub const IN_DEVICE: &str = "CONIN$";
36+
pub const NULL_DEVICE: &str = "NUL";
37+
38+
/// Restores previous stdio when dropped.
39+
pub struct ReplacementGuard {
40+
std_handle: STD_HANDLE,
41+
previous: HANDLE,
42+
}
43+
44+
impl ReplacementGuard {
45+
pub(super) fn new(stdio: Stdio, replacement: &mut File) -> Result<ReplacementGuard, Error> {
46+
let std_handle = match stdio {
47+
Stdio::Stdin => STD_INPUT_HANDLE,
48+
Stdio::Stdout => STD_OUTPUT_HANDLE,
49+
};
50+
51+
let previous;
52+
unsafe {
53+
// Make a copy of the current handle
54+
previous = GetStdHandle(std_handle);
55+
if previous == INVALID_HANDLE_VALUE {
56+
return Err(std::io::Error::last_os_error());
57+
}
58+
59+
// Replace stdin with the replacement handle
60+
if SetStdHandle(std_handle, replacement.as_raw_handle() as HANDLE) == 0 {
61+
return Err(std::io::Error::last_os_error());
62+
}
63+
}
64+
65+
Ok(ReplacementGuard {
66+
previous,
67+
std_handle,
68+
})
69+
}
70+
}
71+
72+
impl Drop for ReplacementGuard {
73+
fn drop(&mut self) {
74+
unsafe {
75+
// Put previous handle back in to stdin
76+
SetStdHandle(self.std_handle, self.previous);
77+
}
78+
}
79+
}
80+
}
81+
82+
#[cfg(unix)]
83+
mod imp {
84+
use super::Stdio;
85+
use libc::{close, dup, dup2, STDIN_FILENO, STDOUT_FILENO};
86+
use std::{fs::File, io::Error, os::fd::AsRawFd};
87+
pub const IN_DEVICE: &str = "/dev/tty";
88+
pub const OUT_DEVICE: &str = "/dev/tty";
89+
pub const NULL_DEVICE: &str = "/dev/null";
90+
91+
/// Restores previous stdio when dropped.
92+
pub struct ReplacementGuard {
93+
std_fileno: i32,
94+
previous: i32,
95+
}
96+
97+
impl ReplacementGuard {
98+
pub(super) fn new(stdio: Stdio, replacement: &mut File) -> Result<ReplacementGuard, Error> {
99+
let std_fileno = match stdio {
100+
Stdio::Stdin => STDIN_FILENO,
101+
Stdio::Stdout => STDOUT_FILENO,
102+
};
103+
104+
let previous;
105+
unsafe {
106+
// Duplicate the existing stdin file to a new descriptor
107+
previous = dup(std_fileno);
108+
if previous == -1 {
109+
return Err(std::io::Error::last_os_error());
110+
}
111+
// Replace stdin with the replacement file
112+
if dup2(replacement.as_raw_fd(), std_fileno) == -1 {
113+
return Err(std::io::Error::last_os_error());
114+
}
115+
}
116+
117+
Ok(ReplacementGuard {
118+
previous,
119+
std_fileno,
120+
})
121+
}
122+
}
123+
124+
impl Drop for ReplacementGuard {
125+
fn drop(&mut self) {
126+
unsafe {
127+
// Put previous file back in to stdin
128+
dup2(self.previous, self.std_fileno);
129+
// Close the file descriptor we used as a backup
130+
close(self.previous);
131+
}
132+
}
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod test {
138+
use std::fs::OpenOptions;
139+
use std::io::{Seek, Write};
140+
141+
use super::imp::ReplacementGuard;
142+
use super::Stdio;
143+
144+
#[test]
145+
fn stdin() {
146+
let tempdir = snapbox::path::PathFixture::mutable_temp().unwrap();
147+
let file = tempdir.path().unwrap().join("stdin");
148+
let mut file = OpenOptions::new()
149+
.read(true)
150+
.write(true)
151+
.create(true)
152+
.open(file)
153+
.unwrap();
154+
155+
writeln!(&mut file, "hello").unwrap();
156+
file.seek(std::io::SeekFrom::Start(0)).unwrap();
157+
{
158+
let _guard = ReplacementGuard::new(Stdio::Stdin, &mut file).unwrap();
159+
let line = std::io::stdin().lines().next().unwrap().unwrap();
160+
assert_eq!(line, "hello");
161+
}
162+
}
163+
}

credential/cargo-credential/tests/examples.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ use std::path::Path;
22

33
use snapbox::cmd::Command;
44

5+
#[test]
6+
fn stdout_redirected() {
7+
let bin = snapbox::cmd::compile_example("stdout-redirected", []).unwrap();
8+
9+
let hello = r#"{"v":[1]}"#;
10+
let get_request = r#"{"v": 1, "registry": {"index-url":"sparse+https://test/","name":"alternative"},"kind": "get","operation": "read","args": []}"#;
11+
let err_not_supported = r#"{"Err":{"kind":"operation-not-supported"}}"#;
12+
13+
Command::new(bin)
14+
.stdin(format!("{get_request}\n"))
15+
.arg("--cargo-plugin")
16+
.assert()
17+
.stdout_eq(format!("{hello}\n{err_not_supported}\n"))
18+
.stderr_eq("message on stderr should be sent the the parent process\n")
19+
.success();
20+
}
21+
522
#[test]
623
fn file_provider() {
724
let bin = snapbox::cmd::compile_example("file-provider", []).unwrap();

0 commit comments

Comments
 (0)