Skip to main content

libcoreinst/cmdline/
console.rs

1// Copyright 2022 Red Hat, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Helper types for console argument.
16
17use anyhow::{bail, Context, Error, Result};
18use lazy_static::lazy_static;
19use regex::Regex;
20use serde_with::{DeserializeFromStr, SerializeDisplay};
21use std::fmt;
22use std::str::FromStr;
23
24const KARG_PREFIX: &str = "console=";
25
26#[derive(Clone, Debug, DeserializeFromStr, SerializeDisplay, PartialEq, Eq)]
27pub enum Console {
28    Graphical(GraphicalConsole),
29    Serial(SerialConsole),
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct GraphicalConsole {
34    device: String,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct SerialConsole {
39    prefix: String,
40    port: u8,
41    speed: u32,
42    data_bits: u8,
43    parity: Parity,
44    // Linux console doesn't support stop bits
45    // GRUB doesn't support RTS/CTS flow control
46}
47
48#[derive(Copy, Clone, Debug, PartialEq, Eq)]
49enum Parity {
50    None,
51    Odd,
52    Even,
53}
54
55impl Parity {
56    fn for_grub(&self) -> &'static str {
57        match self {
58            Self::None => "no",
59            Self::Odd => "odd",
60            Self::Even => "even",
61        }
62    }
63
64    fn for_karg(&self) -> &'static str {
65        match self {
66            Self::None => "n",
67            Self::Odd => "o",
68            Self::Even => "e",
69        }
70    }
71}
72
73impl Console {
74    pub fn grub_terminal(&self) -> &'static str {
75        match self {
76            Self::Graphical(_) => "console",
77            Self::Serial(_) => "serial",
78        }
79    }
80
81    pub fn grub_command(&self) -> Option<String> {
82        match self {
83            Self::Graphical(_) => None,
84            Self::Serial(c) => Some(format!(
85                "serial --unit={} --speed={} --word={} --parity={}",
86                c.port,
87                c.speed,
88                c.data_bits,
89                c.parity.for_grub()
90            )),
91        }
92    }
93
94    pub fn karg(&self) -> String {
95        format!("{KARG_PREFIX}{self}")
96    }
97
98    /// Write a warning message to stdout if kargs contains "console="
99    /// arguments we can parse and no "console=" arguments we can't.  The
100    /// warning suggests that users use console_option instead of
101    /// karg_option to specify the desired console.
102    pub fn maybe_warn_on_kargs(kargs: &[String], karg_option: &str, console_option: &str) {
103        use textwrap::{fill, Options, WordSplitter};
104        if let Some(args) = Self::maybe_console_args_from_kargs(kargs) {
105            // automatically wrap the message, but use Unicode non-breaking
106            // spaces to avoid wrapping in the middle of the argument
107            // strings, and then replace the non-breaking spaces afterward
108            const NBSP: &str = "\u{a0}";
109            let msg = format!(
110                "Note: consider using \"{}\" instead of \"{}\" to configure both kernel and bootloader consoles.",
111                args.iter()
112                    .map(|a| format!("{console_option}{NBSP}{a}"))
113                    .collect::<Vec<String>>()
114                    .join(NBSP),
115                args.iter()
116                    .map(|a| format!("{karg_option}{NBSP}console={a}"))
117                    .collect::<Vec<String>>()
118                    .join(NBSP),
119            );
120            let wrapped = fill(
121                &msg,
122                Options::new(80)
123                    .break_words(false)
124                    .word_splitter(WordSplitter::NoHyphenation),
125            )
126            .replace(NBSP, " ");
127            eprintln!("\n{wrapped}\n");
128        }
129    }
130
131    /// If kargs contains at least one console argument and all console
132    /// arguments are parseable as consoles, return a vector of verbatim
133    /// (unparsed) console arguments with the console= prefixes removed.
134    fn maybe_console_args_from_kargs(kargs: &[String]) -> Option<Vec<&str>> {
135        let (parseable, unparseable): (Vec<&str>, Vec<&str>) = kargs
136            .iter()
137            .filter(|a| a.starts_with(KARG_PREFIX))
138            .map(|a| &a[KARG_PREFIX.len()..])
139            .partition(|a| Console::from_str(a).is_ok());
140        if !parseable.is_empty() && unparseable.is_empty() {
141            Some(parseable)
142        } else {
143            None
144        }
145    }
146}
147
148impl FromStr for Console {
149    type Err = Error;
150
151    fn from_str(s: &str) -> Result<Self, Self::Err> {
152        // help the user with possible misunderstandings
153        for prefix in [KARG_PREFIX, "/dev/"] {
154            if s.starts_with(prefix) {
155                bail!(r#"spec should not start with "{prefix}""#);
156            }
157        }
158
159        // first, parse serial console parameters
160        lazy_static! {
161            static ref SERIAL_REGEX: Regex = Regex::new("^(?P<prefix>ttyS|ttyAMA)(?P<port>[0-9]+)(?:,(?P<speed>[0-9]+)(?:(?P<parity>n|o|e)(?P<data_bits>[5-8])?)?)?$").expect("compiling console regex");
162        }
163        if let Some(c) = SERIAL_REGEX.captures(s) {
164            return Ok(Console::Serial(SerialConsole {
165                prefix: c
166                    .name("prefix")
167                    .expect("prefix is mandatory")
168                    .as_str()
169                    .to_string(),
170                port: c
171                    .name("port")
172                    .expect("port is mandatory")
173                    .as_str()
174                    .parse()
175                    .context("couldn't parse port")?,
176                speed: c
177                    .name("speed")
178                    .map(|v| v.as_str().parse().context("couldn't parse speed"))
179                    .unwrap_or(Ok(9600))?,
180                data_bits: c
181                    .name("data_bits")
182                    .map(|v| v.as_str().parse().expect("unexpected data bits"))
183                    .unwrap_or(8),
184                parity: match c.name("parity").map(|v| v.as_str()) {
185                    // default
186                    None => Parity::None,
187                    Some("n") => Parity::None,
188                    Some("e") => Parity::Even,
189                    Some("o") => Parity::Odd,
190                    _ => unreachable!(),
191                },
192            }));
193        }
194
195        // then try hardcoded strings for graphical consoles
196        match s {
197            "tty0" | "hvc0" | "ttysclp0" => Ok(Console::Graphical(GraphicalConsole {
198                device: s.to_string(),
199            })),
200            _ => bail!("invalid or unsupported console argument"),
201        }
202    }
203}
204
205impl fmt::Display for Console {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        match self {
208            Self::Graphical(c) => write!(f, "{}", c.device),
209            Self::Serial(c) => write!(
210                f,
211                "{}{},{}{}{}",
212                c.prefix,
213                c.port,
214                c.speed,
215                c.parity.for_karg(),
216                c.data_bits
217            ),
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn valid_console_args() {
228        let cases = vec![
229            ("tty0", "console=tty0", "console", None),
230            ("hvc0", "console=hvc0", "console", None),
231            ("ttysclp0", "console=ttysclp0", "console", None),
232            (
233                "ttyS1",
234                "console=ttyS1,9600n8",
235                "serial",
236                Some("serial --unit=1 --speed=9600 --word=8 --parity=no"),
237            ),
238            (
239                "ttyAMA1",
240                "console=ttyAMA1,9600n8",
241                "serial",
242                Some("serial --unit=1 --speed=9600 --word=8 --parity=no"),
243            ),
244            (
245                "ttyS1,1234567e5",
246                "console=ttyS1,1234567e5",
247                "serial",
248                Some("serial --unit=1 --speed=1234567 --word=5 --parity=even"),
249            ),
250            (
251                "ttyS2,5o",
252                "console=ttyS2,5o8",
253                "serial",
254                Some("serial --unit=2 --speed=5 --word=8 --parity=odd"),
255            ),
256            (
257                "ttyS3,17",
258                "console=ttyS3,17n8",
259                "serial",
260                Some("serial --unit=3 --speed=17 --word=8 --parity=no"),
261            ),
262        ];
263        for (input, karg, grub_terminal, grub_command) in cases {
264            let console = Console::from_str(input).unwrap();
265            assert_eq!(
266                console.grub_terminal(),
267                grub_terminal,
268                "GRUB terminal for {input}"
269            );
270            assert_eq!(
271                console.grub_command().as_deref(),
272                grub_command,
273                "GRUB command for {input}"
274            );
275            assert_eq!(console.karg(), karg, "karg for {input}");
276        }
277    }
278
279    #[test]
280    fn invalid_console_args() {
281        let cases = vec![
282            "foo",
283            "/dev/tty0",
284            "/dev/ttyS0",
285            "console=tty0",
286            "console=ttyS0",
287            "ztty0",
288            "zttyS0",
289            "tty0z",
290            "ttyS0z",
291            "tty1",
292            "hvc1",
293            "ttysclp1",
294            "ttyS0,",
295            "ttyS0,z",
296            "ttyS0,115200p8",
297            "ttyS0,115200n4",
298            "ttyS0,115200n8r",
299            "ttyB0",
300            "ttyS9999999999999999999",
301            "ttyS0,999999999999999999999",
302        ];
303        for input in cases {
304            Console::from_str(input).unwrap_err();
305        }
306    }
307
308    #[test]
309    fn maybe_console_args_from_kargs() {
310        assert_eq!(
311            Console::maybe_console_args_from_kargs(&[
312                "foo".into(),
313                "console=ttyS0".into(),
314                "bar".into()
315            ]),
316            Some(vec!["ttyS0"])
317        );
318        assert_eq!(
319            Console::maybe_console_args_from_kargs(&[
320                "foo".into(),
321                "console=ttyS0".into(),
322                "console=tty0".into(),
323                "console=tty0".into(),
324                "console=ttyAMA1,115200n8".into(),
325                "bar".into()
326            ]),
327            Some(vec!["ttyS0", "tty0", "tty0", "ttyAMA1,115200n8"])
328        );
329        assert_eq!(
330            Console::maybe_console_args_from_kargs(&[
331                "foo".into(),
332                "console=ttyS0".into(),
333                "console=ttyS1z".into(),
334                "console=tty0".into(),
335                "bar".into()
336            ]),
337            None
338        );
339        assert_eq!(
340            Console::maybe_console_args_from_kargs(&["foo".into(), "bar".into()]),
341            None
342        );
343        assert_eq!(Console::maybe_console_args_from_kargs(&[]), None);
344    }
345}