Skip to main content

rustfs_cli/output/
qr.rs

1//! Printing the server-rendered QR code for TOTP enrollment.
2//!
3//! The server does the encoding, so `rc` carries no QR library and the console
4//! and the CLI show the same symbol from the same source. All that happens here
5//! is deciding whether a terminal can display it and writing it out.
6
7use crate::output::Formatter;
8
9/// Widest terminal the block art is worth attempting on.
10///
11/// A version-2 `otpauth://` QR is 25 modules plus a 4-module quiet zone on each
12/// side, so 33 columns is the practical floor. Below that the symbol wraps and
13/// becomes unscannable, and printing a broken one is worse than saying so.
14const MIN_TERMINAL_COLUMNS: u16 = 33;
15
16/// A version-2 symbol is 25 modules wide plus a 4-module quiet zone each side.
17/// Lowering the floor below that would print a code no phone can read.
18const _: () = assert!(
19    MIN_TERMINAL_COLUMNS >= 25 + 4 + 4,
20    "the QR floor must still fit a version-2 symbol with its quiet zone"
21);
22
23/// Print the QR, or explain why it was skipped.
24///
25/// Returns whether it was printed, so a caller can decide how loudly to point at
26/// the manual key.
27pub fn print_qr(formatter: &Formatter, qr_utf8: &str, suppressed: bool) -> bool {
28    if suppressed || qr_utf8.is_empty() {
29        return false;
30    }
31
32    if let Some((columns, _)) = terminal_size()
33        && columns < MIN_TERMINAL_COLUMNS
34    {
35        formatter.println(&format!(
36            "(Terminal is {columns} columns wide; the QR code needs at least {MIN_TERMINAL_COLUMNS}. Use the setup key below.)"
37        ));
38        return false;
39    }
40
41    formatter.println("");
42    for line in qr_utf8.lines() {
43        // The symbol is server-rendered, so it is escaped like any other server
44        // text before it reaches a terminal. Block-drawing characters survive
45        // untouched; an escape sequence smuggled in alongside them does not.
46        formatter.println(&formatter.sanitize_text(line));
47    }
48    formatter.println("");
49    true
50}
51
52fn terminal_size() -> Option<(u16, u16)> {
53    console::Term::stdout()
54        .size_checked()
55        .map(|(rows, columns)| (columns, rows))
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::output::OutputConfig;
62
63    fn formatter() -> Formatter {
64        Formatter::new(OutputConfig {
65            no_color: true,
66            ..Default::default()
67        })
68    }
69
70    #[test]
71    fn suppressing_the_qr_reports_that_nothing_was_printed() {
72        assert!(!print_qr(&formatter(), "▀▄█", true));
73    }
74
75    #[test]
76    fn an_empty_payload_prints_nothing() {
77        // A server that omitted the field must not produce a blank frame the
78        // user might mistake for an unscannable code.
79        assert!(!print_qr(&formatter(), "", false));
80    }
81}