1use 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 }
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 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 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 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 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 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 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 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}