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}