use anyhow::{bail, Context, Error, Result};
use lazy_static::lazy_static;
use regex::Regex;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::fmt;
use std::str::FromStr;
const KARG_PREFIX: &str = "console=";
#[derive(Clone, Debug, DeserializeFromStr, SerializeDisplay, PartialEq, Eq)]
pub enum Console {
Graphical(GraphicalConsole),
Serial(SerialConsole),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GraphicalConsole {
device: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerialConsole {
prefix: String,
port: u8,
speed: u32,
data_bits: u8,
parity: Parity,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Parity {
None,
Odd,
Even,
}
impl Parity {
fn for_grub(&self) -> &'static str {
match self {
Self::None => "no",
Self::Odd => "odd",
Self::Even => "even",
}
}
fn for_karg(&self) -> &'static str {
match self {
Self::None => "n",
Self::Odd => "o",
Self::Even => "e",
}
}
}
impl Console {
pub fn grub_terminal(&self) -> &'static str {
match self {
Self::Graphical(_) => "console",
Self::Serial(_) => "serial",
}
}
pub fn grub_command(&self) -> Option<String> {
match self {
Self::Graphical(_) => None,
Self::Serial(c) => Some(format!(
"serial --unit={} --speed={} --word={} --parity={}",
c.port,
c.speed,
c.data_bits,
c.parity.for_grub()
)),
}
}
pub fn karg(&self) -> String {
format!("{KARG_PREFIX}{self}")
}
pub fn maybe_warn_on_kargs(kargs: &[String], karg_option: &str, console_option: &str) {
use textwrap::{fill, Options, WordSplitter};
if let Some(args) = Self::maybe_console_args_from_kargs(kargs) {
const NBSP: &str = "\u{a0}";
let msg = format!(
"Note: consider using \"{}\" instead of \"{}\" to configure both kernel and bootloader consoles.",
args.iter()
.map(|a| format!("{console_option}{NBSP}{a}"))
.collect::<Vec<String>>()
.join(NBSP),
args.iter()
.map(|a| format!("{karg_option}{NBSP}console={a}"))
.collect::<Vec<String>>()
.join(NBSP),
);
let wrapped = fill(
&msg,
Options::new(80)
.break_words(false)
.word_splitter(WordSplitter::NoHyphenation),
)
.replace(NBSP, " ");
eprintln!("\n{wrapped}\n");
}
}
fn maybe_console_args_from_kargs(kargs: &[String]) -> Option<Vec<&str>> {
let (parseable, unparseable): (Vec<&str>, Vec<&str>) = kargs
.iter()
.filter(|a| a.starts_with(KARG_PREFIX))
.map(|a| &a[KARG_PREFIX.len()..])
.partition(|a| Console::from_str(a).is_ok());
if !parseable.is_empty() && unparseable.is_empty() {
Some(parseable)
} else {
None
}
}
}
impl FromStr for Console {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
for prefix in [KARG_PREFIX, "/dev/"] {
if s.starts_with(prefix) {
bail!(r#"spec should not start with "{prefix}""#);
}
}
lazy_static! {
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");
}
if let Some(c) = SERIAL_REGEX.captures(s) {
return Ok(Console::Serial(SerialConsole {
prefix: c
.name("prefix")
.expect("prefix is mandatory")
.as_str()
.to_string(),
port: c
.name("port")
.expect("port is mandatory")
.as_str()
.parse()
.context("couldn't parse port")?,
speed: c
.name("speed")
.map(|v| v.as_str().parse().context("couldn't parse speed"))
.unwrap_or(Ok(9600))?,
data_bits: c
.name("data_bits")
.map(|v| v.as_str().parse().expect("unexpected data bits"))
.unwrap_or(8),
parity: match c.name("parity").map(|v| v.as_str()) {
None => Parity::None,
Some("n") => Parity::None,
Some("e") => Parity::Even,
Some("o") => Parity::Odd,
_ => unreachable!(),
},
}));
}
match s {
"tty0" | "hvc0" | "ttysclp0" => Ok(Console::Graphical(GraphicalConsole {
device: s.to_string(),
})),
_ => bail!("invalid or unsupported console argument"),
}
}
}
impl fmt::Display for Console {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Graphical(c) => write!(f, "{}", c.device),
Self::Serial(c) => write!(
f,
"{}{},{}{}{}",
c.prefix,
c.port,
c.speed,
c.parity.for_karg(),
c.data_bits
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_console_args() {
let cases = vec![
("tty0", "console=tty0", "console", None),
("hvc0", "console=hvc0", "console", None),
("ttysclp0", "console=ttysclp0", "console", None),
(
"ttyS1",
"console=ttyS1,9600n8",
"serial",
Some("serial --unit=1 --speed=9600 --word=8 --parity=no"),
),
(
"ttyAMA1",
"console=ttyAMA1,9600n8",
"serial",
Some("serial --unit=1 --speed=9600 --word=8 --parity=no"),
),
(
"ttyS1,1234567e5",
"console=ttyS1,1234567e5",
"serial",
Some("serial --unit=1 --speed=1234567 --word=5 --parity=even"),
),
(
"ttyS2,5o",
"console=ttyS2,5o8",
"serial",
Some("serial --unit=2 --speed=5 --word=8 --parity=odd"),
),
(
"ttyS3,17",
"console=ttyS3,17n8",
"serial",
Some("serial --unit=3 --speed=17 --word=8 --parity=no"),
),
];
for (input, karg, grub_terminal, grub_command) in cases {
let console = Console::from_str(input).unwrap();
assert_eq!(
console.grub_terminal(),
grub_terminal,
"GRUB terminal for {input}"
);
assert_eq!(
console.grub_command().as_deref(),
grub_command,
"GRUB command for {input}"
);
assert_eq!(console.karg(), karg, "karg for {input}");
}
}
#[test]
fn invalid_console_args() {
let cases = vec![
"foo",
"/dev/tty0",
"/dev/ttyS0",
"console=tty0",
"console=ttyS0",
"ztty0",
"zttyS0",
"tty0z",
"ttyS0z",
"tty1",
"hvc1",
"ttysclp1",
"ttyS0,",
"ttyS0,z",
"ttyS0,115200p8",
"ttyS0,115200n4",
"ttyS0,115200n8r",
"ttyB0",
"ttyS9999999999999999999",
"ttyS0,999999999999999999999",
];
for input in cases {
Console::from_str(input).unwrap_err();
}
}
#[test]
fn maybe_console_args_from_kargs() {
assert_eq!(
Console::maybe_console_args_from_kargs(&[
"foo".into(),
"console=ttyS0".into(),
"bar".into()
]),
Some(vec!["ttyS0"])
);
assert_eq!(
Console::maybe_console_args_from_kargs(&[
"foo".into(),
"console=ttyS0".into(),
"console=tty0".into(),
"console=tty0".into(),
"console=ttyAMA1,115200n8".into(),
"bar".into()
]),
Some(vec!["ttyS0", "tty0", "tty0", "ttyAMA1,115200n8"])
);
assert_eq!(
Console::maybe_console_args_from_kargs(&[
"foo".into(),
"console=ttyS0".into(),
"console=ttyS1z".into(),
"console=tty0".into(),
"bar".into()
]),
None
);
assert_eq!(
Console::maybe_console_args_from_kargs(&["foo".into(), "bar".into()]),
None
);
assert_eq!(Console::maybe_console_args_from_kargs(&[]), None);
}
}