1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::{Parser, Subcommand};
6use cols::{OutputMode, Table, WidthHint, print_table};
7use std::{
8 fs::{self, File, OpenOptions},
9 io::{self, Read, Write},
10 process::ExitCode,
11};
12
13const TYPE_ALL: u8 = 0;
15const TYPE_WLAN: u8 = 1;
16const TYPE_BLUETOOTH: u8 = 2;
17const TYPE_UWB: u8 = 3;
18const TYPE_WIMAX: u8 = 4;
19const TYPE_WWAN: u8 = 5;
20const TYPE_GPS: u8 = 6;
21const TYPE_FM: u8 = 7;
22const TYPE_NFC: u8 = 8;
23
24const OP_ADD: u8 = 0;
26const OP_DEL: u8 = 1;
27const OP_CHANGE: u8 = 2;
28const OP_CHANGE_ALL: u8 = 3;
29
30#[derive(Parser)]
31#[command(name = "rfkill", about = "Enable and disable wireless devices")]
32pub struct Args {
33 #[arg(short = 'J', long)]
35 json: bool,
36
37 #[arg(short = 'n', long)]
39 noheadings: bool,
40
41 #[arg(short = 'o', long, value_delimiter = ',')]
43 output: Option<Vec<String>>,
44
45 #[arg(long)]
47 output_all: bool,
48
49 #[arg(short = 'r', long)]
51 raw: bool,
52
53 #[command(subcommand)]
54 command: Option<Cmd>,
55}
56
57#[derive(Subcommand)]
58enum Cmd {
59 Event,
61
62 List {
64 #[arg()]
66 identifiers: Vec<String>,
67 },
68
69 Block {
71 #[arg(required = true)]
73 identifiers: Vec<String>,
74 },
75
76 Unblock {
78 #[arg(required = true)]
80 identifiers: Vec<String>,
81 },
82
83 Toggle {
85 #[arg(required = true)]
87 identifiers: Vec<String>,
88 },
89}
90
91#[derive(Debug)]
92struct Device {
93 id: u32,
94 rf_type: u8,
95 name: String,
96 soft: bool,
97 hard: bool,
98}
99
100impl Device {
101 fn type_name(&self) -> &'static str {
102 type_to_name(self.rf_type)
103 }
104
105 fn type_desc(&self) -> &'static str {
106 type_to_desc(self.rf_type)
107 }
108}
109
110fn type_to_name(t: u8) -> &'static str {
111 match t {
112 TYPE_WLAN => "wlan",
113 TYPE_BLUETOOTH => "bluetooth",
114 TYPE_UWB => "uwb",
115 TYPE_WIMAX => "wimax",
116 TYPE_WWAN => "wwan",
117 TYPE_GPS => "gps",
118 TYPE_FM => "fm",
119 TYPE_NFC => "nfc",
120 _ => "unknown",
121 }
122}
123
124fn type_to_desc(t: u8) -> &'static str {
125 match t {
126 TYPE_WLAN => "Wireless LAN",
127 TYPE_BLUETOOTH => "Bluetooth",
128 TYPE_UWB => "Ultra-Wideband",
129 TYPE_WIMAX => "WiMAX",
130 TYPE_WWAN => "Wireless WAN",
131 TYPE_GPS => "GPS",
132 TYPE_FM => "FM",
133 TYPE_NFC => "NFC",
134 _ => "Unknown",
135 }
136}
137
138fn name_to_type(s: &str) -> Option<u8> {
139 match s {
140 "all" => Some(TYPE_ALL),
141 "wlan" | "wifi" => Some(TYPE_WLAN),
142 "bluetooth" => Some(TYPE_BLUETOOTH),
143 "uwb" | "ultrawideband" => Some(TYPE_UWB),
144 "wimax" => Some(TYPE_WIMAX),
145 "wwan" => Some(TYPE_WWAN),
146 "gps" => Some(TYPE_GPS),
147 "fm" => Some(TYPE_FM),
148 "nfc" => Some(TYPE_NFC),
149 _ => None,
150 }
151}
152
153enum Identifier {
154 Id(u32),
155 Type(u8),
156}
157
158fn parse_identifier(s: &str) -> Option<Identifier> {
159 if let Ok(id) = s.parse::<u32>() {
160 return Some(Identifier::Id(id));
161 }
162 name_to_type(s).map(Identifier::Type)
163}
164
165fn read_devices() -> Vec<Device> {
166 let mut devices = Vec::new();
167
168 let entries = match fs::read_dir("/sys/class/rfkill") {
169 Ok(e) => e,
170 Err(_) => return devices,
171 };
172
173 for entry in entries.flatten() {
174 let fname = entry.file_name();
175 let fname_str = match fname.to_str() {
176 Some(s) => s,
177 None => continue,
178 };
179
180 let id: u32 = match fname_str.strip_prefix("rfkill") {
181 Some(n) => match n.parse() {
182 Ok(v) => v,
183 Err(_) => continue,
184 },
185 None => continue,
186 };
187
188 let base = entry.path();
189
190 let type_str = sysfs_read_str(&base.join("type")).unwrap_or_default();
191 let dev_name = sysfs_read_str(&base.join("name")).unwrap_or_default();
192 let soft = sysfs_read_u8(&base.join("soft")).unwrap_or(0) != 0;
193 let hard = sysfs_read_u8(&base.join("hard")).unwrap_or(0) != 0;
194
195 let rf_type = name_to_type(&type_str).unwrap_or(0);
196
197 devices.push(Device {
198 id,
199 rf_type,
200 name: dev_name,
201 soft,
202 hard,
203 });
204 }
205
206 devices.sort_by_key(|d| d.id);
207 devices
208}
209
210fn sysfs_read_str(path: &std::path::Path) -> Option<String> {
211 fs::read_to_string(path).ok().map(|s| s.trim().to_string())
212}
213
214fn sysfs_read_u8(path: &std::path::Path) -> Option<u8> {
215 sysfs_read_str(path)?.parse().ok()
216}
217
218fn write_event(idx: u32, rf_type: u8, op: u8, soft: u8) -> io::Result<()> {
221 let mut f = OpenOptions::new().write(true).open("/dev/rfkill")?;
222 let mut buf = [0u8; 8];
223 buf[0..4].copy_from_slice(&idx.to_ne_bytes());
224 buf[4] = rf_type;
225 buf[5] = op;
226 buf[6] = soft;
227 buf[7] = 0;
228 f.write_all(&buf)
229}
230
231fn device_matches(dev: &Device, ident: &Identifier) -> bool {
232 match ident {
233 Identifier::Id(id) => dev.id == *id,
234 Identifier::Type(TYPE_ALL) => true,
235 Identifier::Type(t) => dev.rf_type == *t,
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240enum Col {
241 Id,
242 Type,
243 Device,
244 TypeDesc,
245 Soft,
246 Hard,
247}
248
249impl Col {
250 fn name(self) -> &'static str {
251 match self {
252 Col::Id => "ID",
253 Col::Type => "TYPE",
254 Col::Device => "DEVICE",
255 Col::TypeDesc => "TYPE-DESC",
256 Col::Soft => "SOFT",
257 Col::Hard => "HARD",
258 }
259 }
260
261 fn whint(self) -> WidthHint {
262 match self {
263 Col::Id => WidthHint::Fixed(2),
264 Col::Type => WidthHint::Fixed(9),
265 Col::Device => WidthHint::Fixed(10),
266 Col::TypeDesc => WidthHint::Fixed(12),
267 Col::Soft => WidthHint::Fixed(9),
268 Col::Hard => WidthHint::Fixed(9),
269 }
270 }
271
272 fn is_right(self) -> bool {
273 matches!(self, Col::Id)
274 }
275
276 fn from_name(s: &str) -> Option<Self> {
277 match s.to_uppercase().as_str() {
278 "ID" => Some(Col::Id),
279 "TYPE" => Some(Col::Type),
280 "DEVICE" => Some(Col::Device),
281 "TYPE-DESC" => Some(Col::TypeDesc),
282 "SOFT" => Some(Col::Soft),
283 "HARD" => Some(Col::Hard),
284 _ => None,
285 }
286 }
287
288 fn cell_value(self, dev: &Device) -> String {
289 match self {
290 Col::Id => dev.id.to_string(),
291 Col::Type => dev.type_name().to_string(),
292 Col::Device => dev.name.clone(),
293 Col::TypeDesc => dev.type_desc().to_string(),
294 Col::Soft => blocked_str(dev.soft).to_string(),
295 Col::Hard => blocked_str(dev.hard).to_string(),
296 }
297 }
298}
299
300const DEFAULT_COLUMNS: &[Col] =
301 &[Col::Id, Col::Type, Col::Device, Col::Soft, Col::Hard];
302
303const ALL_COLUMNS: &[Col] = &[
304 Col::Id,
305 Col::Type,
306 Col::Device,
307 Col::TypeDesc,
308 Col::Soft,
309 Col::Hard,
310];
311
312fn blocked_str(blocked: bool) -> &'static str {
313 if blocked { "blocked" } else { "unblocked" }
314}
315
316fn print_table_output(args: &Args, devices: &[Device]) -> ExitCode {
317 let columns = if args.output_all {
318 ALL_COLUMNS.to_vec()
319 } else if let Some(ref names) = args.output {
320 let mut cols = Vec::new();
321 for name in names {
322 match Col::from_name(name.trim()) {
323 Some(c) => cols.push(c),
324 None => {
325 eprintln!("rfkill: unknown column: {name}");
326 return ExitCode::FAILURE;
327 }
328 }
329 }
330 cols
331 } else {
332 DEFAULT_COLUMNS.to_vec()
333 };
334
335 let mut table = Table::new();
336 table.name_set("rfkilldevs");
337
338 if args.json {
339 table.output_mode_set(OutputMode::Json);
340 } else if args.raw {
341 table.output_mode_set(OutputMode::Raw);
342 }
343
344 if args.noheadings {
345 table.headings_set(false);
346 }
347
348 for col in &columns {
349 let idx = table.new_column(col.name());
350 table.column_mut(idx).unwrap().width_hint_set(col.whint());
351 if col.is_right() {
352 table.column_mut(idx).unwrap().right_set(true);
353 }
354 }
355
356 for dev in devices {
357 let line_id = table.new_line(None);
358 let line = table.line_mut(line_id);
359 for (ci, col) in columns.iter().enumerate() {
360 line.data_set(ci, &col.cell_value(dev));
361 }
362 }
363
364 let stdout = std::io::stdout();
365 let mut out = stdout.lock();
366 if let Err(e) = print_table(&table, &mut out) {
367 eprintln!("rfkill: {e}");
368 return ExitCode::FAILURE;
369 }
370
371 ExitCode::SUCCESS
372}
373
374fn print_legacy_list(devices: &[Device]) {
375 for dev in devices {
376 println!("{}: {}: {}", dev.id, dev.name, dev.type_desc());
377 println!("\tSoft blocked: {}", if dev.soft { "yes" } else { "no" });
378 println!("\tHard blocked: {}", if dev.hard { "yes" } else { "no" });
379 }
380}
381
382fn filter_devices<'a>(
383 devices: &'a [Device],
384 identifiers: &[String],
385) -> Vec<&'a Device> {
386 if identifiers.is_empty() {
387 return devices.iter().collect();
388 }
389
390 let mut seen = vec![false; devices.len()];
391 let mut result = Vec::new();
392 for ident_str in identifiers {
393 if let Some(ident) = parse_identifier(ident_str) {
394 for (i, dev) in devices.iter().enumerate() {
395 if device_matches(dev, &ident) && !seen[i] {
396 seen[i] = true;
397 result.push(dev);
398 }
399 }
400 }
401 }
402 result
403}
404
405fn do_block_op(identifiers: &[String], soft: u8) -> ExitCode {
406 for ident_str in identifiers {
407 let ident = match parse_identifier(ident_str) {
408 Some(i) => i,
409 None => {
410 eprintln!("rfkill: invalid identifier: {ident_str}");
411 return ExitCode::FAILURE;
412 }
413 };
414
415 let result = match ident {
416 Identifier::Type(t) => write_event(0, t, OP_CHANGE_ALL, soft),
417 Identifier::Id(id) => write_event(id, 0, OP_CHANGE, soft),
418 };
419
420 if let Err(e) = result {
421 eprintln!("rfkill: {e}");
422 return ExitCode::FAILURE;
423 }
424 }
425 ExitCode::SUCCESS
426}
427
428fn do_toggle(identifiers: &[String]) -> ExitCode {
429 let devices = read_devices();
430
431 for ident_str in identifiers {
432 let ident = match parse_identifier(ident_str) {
433 Some(i) => i,
434 None => {
435 eprintln!("rfkill: invalid identifier: {ident_str}");
436 return ExitCode::FAILURE;
437 }
438 };
439
440 for dev in &devices {
441 if device_matches(dev, &ident) {
442 let new_soft: u8 = if dev.soft { 0 } else { 1 };
443 if let Err(e) = write_event(dev.id, 0, OP_CHANGE, new_soft) {
444 eprintln!("rfkill: {e}");
445 return ExitCode::FAILURE;
446 }
447 }
448 }
449 }
450 ExitCode::SUCCESS
451}
452
453fn do_event() -> ExitCode {
454 let mut f = match File::open("/dev/rfkill") {
455 Ok(f) => f,
456 Err(e) => {
457 eprintln!("rfkill: cannot open /dev/rfkill: {e}");
458 return ExitCode::FAILURE;
459 }
460 };
461
462 let mut buf = [0u8; 8];
463 loop {
464 match f.read_exact(&mut buf) {
465 Ok(_) => {}
466 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
467 Err(e) => {
468 eprintln!("rfkill: read error: {e}");
469 return ExitCode::FAILURE;
470 }
471 }
472
473 let idx = u32::from_ne_bytes(buf[0..4].try_into().unwrap());
474 let rf_type = buf[4];
475 let op = buf[5];
476 let soft = buf[6];
477 let hard = buf[7];
478
479 let op_name = match op {
480 OP_ADD => "add",
481 OP_DEL => "remove",
482 OP_CHANGE => "change",
483 OP_CHANGE_ALL => "change-all",
484 _ => "unknown",
485 };
486
487 println!(
488 "{idx}: {}: {op_name} soft={} hard={}",
489 type_to_name(rf_type),
490 blocked_str(soft != 0),
491 blocked_str(hard != 0),
492 );
493 }
494 ExitCode::SUCCESS
495}
496
497pub fn run(args: Args) -> ExitCode {
498 match &args.command {
499 None => {
500 let devices = read_devices();
501 print_table_output(&args, &devices)
502 }
503 Some(Cmd::List { identifiers }) => {
504 let devices = read_devices();
505 if args.output.is_some() || args.output_all || args.json || args.raw
506 {
507 let filtered: Vec<&Device> =
508 filter_devices(&devices, identifiers);
509 let owned: Vec<Device> = filtered
510 .into_iter()
511 .map(|d| Device {
512 id: d.id,
513 rf_type: d.rf_type,
514 name: d.name.clone(),
515 soft: d.soft,
516 hard: d.hard,
517 })
518 .collect();
519 print_table_output(&args, &owned)
520 } else {
521 let filtered = filter_devices(&devices, identifiers);
522 let refs: Vec<&Device> = filtered;
523 let owned: Vec<Device> = refs
524 .into_iter()
525 .map(|d| Device {
526 id: d.id,
527 rf_type: d.rf_type,
528 name: d.name.clone(),
529 soft: d.soft,
530 hard: d.hard,
531 })
532 .collect();
533 print_legacy_list(&owned);
534 ExitCode::SUCCESS
535 }
536 }
537 Some(Cmd::Block { identifiers }) => do_block_op(identifiers, 1),
538 Some(Cmd::Unblock { identifiers }) => do_block_op(identifiers, 0),
539 Some(Cmd::Toggle { identifiers }) => do_toggle(identifiers),
540 Some(Cmd::Event) => do_event(),
541 }
542}