1use rdrive::{DeviceId, Fdt};
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4enum ConsoleSpec {
5 HardwareSerial(usize),
6 VirtualTty,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ConsoleDeviceIdError {
11 NotSpecified,
12 NoHardwareDevice,
13 DeviceNotFound,
14}
15
16pub fn device_id() -> Result<DeviceId, ConsoleDeviceIdError> {
17 match device_id_from_bootargs(someboot::cmdline()) {
18 Ok(device_id) => Ok(device_id),
19 Err(ConsoleDeviceIdError::NotSpecified) => device_id_from_acpi_spcr()
20 .or_else(device_id_from_fdt_stdout)
21 .ok_or(ConsoleDeviceIdError::NotSpecified),
22 Err(
23 err @ (ConsoleDeviceIdError::NoHardwareDevice | ConsoleDeviceIdError::DeviceNotFound),
24 ) => Err(err),
25 }
26}
27
28fn device_id_from_bootargs(cmdline: Option<&str>) -> Result<DeviceId, ConsoleDeviceIdError> {
29 device_id_from_bootargs_with(cmdline, device_id_from_serial_index)
30}
31
32fn device_id_from_bootargs_with(
33 cmdline: Option<&str>,
34 serial_device_id: impl Fn(usize) -> Option<DeviceId>,
35) -> Result<DeviceId, ConsoleDeviceIdError> {
36 let cmdline = cmdline.ok_or(ConsoleDeviceIdError::NotSpecified)?;
37 let mut has_console_spec = false;
38 let mut last_hardware_serial = None;
39
40 for spec in console_specs(cmdline) {
41 has_console_spec = true;
42 if let ConsoleSpec::HardwareSerial(index) = spec {
43 last_hardware_serial = Some(index);
44 }
45 }
46
47 match last_hardware_serial {
48 Some(index) => serial_device_id(index).ok_or(ConsoleDeviceIdError::DeviceNotFound),
49 None if has_console_spec => Err(ConsoleDeviceIdError::NoHardwareDevice),
50 None => Err(ConsoleDeviceIdError::NotSpecified),
51 }
52}
53
54fn console_specs(cmdline: &str) -> impl Iterator<Item = ConsoleSpec> + '_ {
55 cmdline
56 .split_ascii_whitespace()
57 .filter_map(|arg| arg.strip_prefix("console="))
58 .filter_map(parse_console_spec)
59}
60
61fn parse_console_spec(spec: &str) -> Option<ConsoleSpec> {
62 let name = spec.split(',').next().unwrap_or(spec);
63 if name == "tty"
64 || name == "ttynull"
65 || name
66 .strip_prefix("tty")
67 .is_some_and(|suffix| !suffix.is_empty() && suffix.bytes().all(|c| c.is_ascii_digit()))
68 {
69 return Some(ConsoleSpec::VirtualTty);
70 }
71
72 parse_number_suffix(name, "ttyS")
73 .or_else(|| parse_number_suffix(name, "ttyAMA"))
74 .map(ConsoleSpec::HardwareSerial)
75}
76
77fn parse_number_suffix(name: &str, prefix: &str) -> Option<usize> {
78 name.strip_prefix(prefix)?.parse().ok()
79}
80
81fn device_id_from_serial_index(index: usize) -> Option<DeviceId> {
82 device_id_from_serial_index_with(index, fdt_serial_alias_device_id, device_id_from_acpi_spcr)
83}
84
85fn device_id_from_serial_index_with(
86 index: usize,
87 fdt_device_id: impl FnOnce(usize) -> Option<DeviceId>,
88 spcr_device_id: impl FnOnce() -> Option<DeviceId>,
89) -> Option<DeviceId> {
90 fdt_device_id(index).or_else(|| if index == 0 { spcr_device_id() } else { None })
91}
92
93fn fdt_serial_alias_device_id(index: usize) -> Option<DeviceId> {
94 rdrive::with_fdt(|fdt| {
95 let alias = alloc::format!("serial{index}");
96 let path = alias_path(fdt, &alias)?;
97 rdrive::fdt_path_to_device_id(path)
98 })
99 .flatten()
100}
101
102fn device_id_from_acpi_spcr() -> Option<DeviceId> {
103 rdrive::acpi_spcr_console_device_id()
104}
105
106fn device_id_from_fdt_stdout() -> Option<DeviceId> {
107 rdrive::with_fdt(stdout_device_id).flatten()
108}
109
110fn stdout_device_id(fdt: &Fdt) -> Option<DeviceId> {
111 let chosen = fdt.get_by_path("/chosen")?;
112 ["stdout-path", "linux,stdout-path"]
113 .into_iter()
114 .find_map(|key| {
115 let raw = chosen.as_node().get_property(key)?.as_str()?;
116 let path = split_stdout_options(raw);
117 if path.is_empty() {
118 return None;
119 }
120 if path.starts_with('/') {
121 return rdrive::fdt_path_to_device_id(path);
122 }
123 alias_path(fdt, path).and_then(rdrive::fdt_path_to_device_id)
124 })
125}
126
127fn split_stdout_options(stdout: &str) -> &str {
128 stdout.split(':').next().unwrap_or(stdout)
129}
130
131fn alias_path<'a>(fdt: &'a Fdt, alias: &str) -> Option<&'a str> {
132 fdt.get_by_path("/aliases")?
133 .as_node()
134 .get_property(alias)?
135 .as_str()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn console_specs_keep_command_line_order() {
144 let specs: alloc::vec::Vec<_> =
145 console_specs("console=ttyS2,1500000 console=tty1 console=ttyAMA3,115200").collect();
146
147 assert_eq!(
148 specs,
149 alloc::vec![
150 ConsoleSpec::HardwareSerial(2),
151 ConsoleSpec::VirtualTty,
152 ConsoleSpec::HardwareSerial(3),
153 ]
154 );
155 }
156
157 #[test]
158 fn parses_supported_serial_console_names() {
159 assert_eq!(
160 parse_console_spec("ttyS0,115200n8"),
161 Some(ConsoleSpec::HardwareSerial(0))
162 );
163 assert_eq!(
164 parse_console_spec("ttyAMA1"),
165 Some(ConsoleSpec::HardwareSerial(1))
166 );
167 assert_eq!(parse_console_spec("ttynull"), Some(ConsoleSpec::VirtualTty));
168 assert_eq!(parse_console_spec("tty7"), Some(ConsoleSpec::VirtualTty));
169 assert_eq!(parse_console_spec("ttySx"), None);
170 }
171
172 #[test]
173 fn bootargs_console_spec_suppresses_firmware_fallback() {
174 assert_eq!(
175 device_id_from_bootargs(Some("root=/dev/vda")),
176 Err(ConsoleDeviceIdError::NotSpecified)
177 );
178 assert_eq!(
179 device_id_from_bootargs(Some("console=tty1")),
180 Err(ConsoleDeviceIdError::NoHardwareDevice)
181 );
182 assert_eq!(
183 device_id_from_bootargs(Some("console=ttyS2 console=tty1")),
184 Err(ConsoleDeviceIdError::DeviceNotFound)
185 );
186 }
187
188 #[test]
189 fn later_virtual_console_does_not_hide_hardware_device_id() {
190 let serial2_device = DeviceId::from(42);
191
192 assert_eq!(
193 device_id_from_bootargs_with(Some("console=ttyS2,1500000 console=tty1"), |index| {
194 (index == 2).then_some(serial2_device)
195 }),
196 Ok(serial2_device)
197 );
198 }
199
200 #[test]
201 fn last_available_hardware_console_wins_over_earlier_serial_console() {
202 let serial3_device = DeviceId::from(43);
203
204 assert_eq!(
205 device_id_from_bootargs_with(
206 Some("console=ttyS2,1500000 console=tty1 console=ttyAMA3,115200"),
207 |index| (index == 3).then_some(serial3_device),
208 ),
209 Ok(serial3_device)
210 );
211 }
212
213 #[test]
214 fn later_missing_hardware_console_overrides_earlier_available_serial_console() {
215 let serial2_device = DeviceId::from(42);
216
217 assert_eq!(
218 device_id_from_bootargs_with(
219 Some("console=ttyS2,1500000 console=ttyS3,115200 console=tty1"),
220 |index| (index == 2).then_some(serial2_device),
221 ),
222 Err(ConsoleDeviceIdError::DeviceNotFound)
223 );
224 assert_eq!(
225 device_id_from_bootargs_with(
226 Some("console=ttyS2,1500000 console=ttyS3,115200"),
227 |index| (index == 2).then_some(serial2_device),
228 ),
229 Err(ConsoleDeviceIdError::DeviceNotFound)
230 );
231 }
232
233 #[test]
234 fn serial_index_zero_can_use_acpi_spcr_when_fdt_alias_is_absent() {
235 let spcr_device = DeviceId::from(42);
236
237 assert_eq!(
238 device_id_from_serial_index_with(0, |_| None, || Some(spcr_device)),
239 Some(spcr_device)
240 );
241 }
242
243 #[test]
244 fn non_zero_serial_index_does_not_fallback_to_acpi_spcr() {
245 let spcr_device = DeviceId::from(42);
246
247 assert_eq!(
248 device_id_from_serial_index_with(2, |_| None, || Some(spcr_device)),
249 None
250 );
251 }
252
253 #[test]
254 fn splits_stdout_options() {
255 assert_eq!(
256 split_stdout_options("/soc/serial@1000:115200n8"),
257 "/soc/serial@1000"
258 );
259 assert_eq!(split_stdout_options("serial0"), "serial0");
260 }
261}