blsctl 0.2.2

Manages BLS entries and kernel cmdline options
Documentation
// main.rs
//
// Copyright 2019 Alberto Ruiz <aruiz@redhat.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later

use std::env;
use std::process;

mod cmdline;
mod bls;

use cmdline::CmdlineHandler;
use bls::BLSEntry;

fn cmdline_usage() {
  eprintln!("  cmdline ENTRY show                     # shows the kernel cmdline from a bls ENTRY (options)");
  eprintln!("  cmdline ENTRY get PARAM                # displays the value of parameter 'param'");
  eprintln!("  cmdline ENTRY|all set PARAM[=VALUE]    # sets pairs of 'params' to corresponding 'value', you can also set a parameter with no value");
  eprintln!("  cmdline ENTRY|all add PARAM[=VALUE]    # adds an additional 'param' or 'param=value' pair to the cmdline even if it was previously set");
  eprintln!("  cmdline ENTRY|all remove PARAM[=VALUE] # removes a specific param or param=value pair from the cmdline, to remove all pairs or");
  eprintln!("                                         # a parameter with no value use 'clear'");
  eprintln!("  cmdline ENTRY|all clear PARAM          # removes all instances of a specific parameter");
}

fn entry_usage() {
  eprintln!("  entry list                   # lists all Boot Loader Spec *.conf entries in order");
  eprintln!("  entry ENTRY show             # shows the content of a bls ENTRY");
  eprintln!("  entry ENTRY get KEY          # displays the value of a key in a bls ENTRY");
  eprintln!("  entry ENTRY set KEY VALUE    # sets the value for a key in a bls ENTRY");
  eprintln!("  entry ENTRY remove KEY       # removes the key from a bls ENTRY");
  eprintln!("  entry ENTRY create           # creates an empty bls ENTRY");
  eprintln!("  entry ENTRY delete           # deletes the bls ENTRY");
  //FIXME: We need a way to handle the default entry
  //eprintln!("  entry default [ENTRY]        # shows the default BLS entry to boot into or sets it if an argument is given,");
}

fn help_usage() {
  eprintln!("  help [COMMAND]               # prints this usage help or help for specific command");
}

fn log_usage () {
  eprintln!("blscfg usage:");
  cmdline_usage();
  eprintln!("");
  entry_usage();
  help_usage();
}

fn assert_invalid_command(expected_length: usize, args: &[String], max: bool, usage: &dyn Fn()) {
  if args.len() < expected_length {
    eprintln!("ERROR insuficient arguments: `{}`", args.join(" "));
  } else if max && args.len() > expected_length {
    eprintln!("ERROR too many arguments: `{}`", args.join(" "));
  } else {
    return;
  }
  usage();
  process::exit(1);
}

fn handle_cmdline(args: &[String]) {
  let base: usize = 3;
  assert_invalid_command(base, args, false, &cmdline_usage);

  let mut _entry;
  let mut entries = BLSEntry::get_bls_entries().unwrap_or_else (|e| {
        eprintln!("ERROR: {}", e);
        process::exit(1);
      });
  let provider: &mut dyn CmdlineHandler = match &args[base-2] {
    entry if entry.as_str() == "all" => {
      match args[base-1].as_str() {
        arg @ "get" | arg @ "show" => {
          eprintln!("ERROR: cannot {} cmdline for all entries", arg);
          process::exit(1);
        }
        _ => {}
      };
      &mut entries
    },
    entry @ _ => {
      let blsentry = BLSEntry::new(entry);
      match blsentry {
        Ok(ent) => { _entry = ent; &mut _entry }
        Err(_) => {
          eprintln!("ERROR: {} is not a valid bootloader entry", entry);
          process::exit(1);
        }
      }
    }
  };

  macro_rules! exit_error {
    () => { |msg| {
        eprintln!("ERROR: {}", msg);
        process::exit(1);
      }
    }
  };

  match args[base-1].as_str() {
    "show" => {
      assert_invalid_command(base, args, true, &cmdline_usage);
      println!("{}", provider.cmdline_render()
                      .unwrap_or_else(exit_error!()));
    },
    "get" => {
      assert_invalid_command(base+1, args, true, &cmdline_usage);
      match provider.cmdline_get(&args[base]) {
        Ok(params) => {
          for param in params {
            print!("{}", &args[base]);
            if let Some(param) = param  {
              print!("={}", param);
            }
            println!("");
          }
        },
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    "set" => {
      assert_invalid_command(base+1, args, false, &cmdline_usage);
      match provider.cmdline_set(&args[base..]) {
        Ok(()) => {},
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    "add" => {
      assert_invalid_command(base+1, args, false, &cmdline_usage);
      match provider.cmdline_add(&args[base..]) {
        Ok(()) => {},
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    "remove" => {
      assert_invalid_command(base+1, args, false, &cmdline_usage);
      match provider.cmdline_remove(&args[base..]) {
        Ok(()) => {},
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    "clear" => {
      assert_invalid_command(base+1, args, false, &cmdline_usage);
      match provider.cmdline_clear(&args[base..]) {
        Ok(()) => {},
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    _ => {
      eprintln!("ERROR: unrecognized command {}", args.join(" "));
      process::exit(1);
    }
  }
  process::exit(0);
}

fn handle_entry(args: &[String]) {
  assert_invalid_command(2, args, false, &entry_usage);
  match args[1].as_str() {
    "list" => {
      assert_invalid_command(2, args, true, &entry_usage);
      let entries = BLSEntry::get_bls_entries ()
              .unwrap_or_else(|e| { eprintln!("ERROR: could not read bootloader entries from directory {}", e);
                                    process::exit(1); });
      let mut index: u8 = 0;
      for entry in entries {
        println!("{} {}", index, entry);
        index += 1;
      }
      return;
    },
    _ => {}
  }

  let base: usize = 3;
  assert_invalid_command(base, args, false, &entry_usage);

  if args[base-1].as_str() == "create" {
    assert_invalid_command(base, args, true, &entry_usage);
    if let Err(e) = BLSEntry::create(&args[base-2]) {
      eprintln!("ERROR: {}", e);
      process::exit(1);
    } else {
      return;
    }
  }

  let entry = {
    let blsentry = BLSEntry::new(&args[base-2]);
    match blsentry {
      Ok(ent) => ent,
      Err(_) => {
        eprintln!("ERROR: {} is not a valid bootloader entry", &args[base-2]);
        process::exit(1);
      }
    }
  };

  match args[base-1].as_str() {
    "show" => {
      assert_invalid_command(base, args, true, &entry_usage);
      println!("{}", entry);
    },
    "get" => {
      assert_invalid_command(base+1, args, true, &entry_usage);
      match entry.get(&args[base]) {
        Ok(value) => {
          println!("{} {}", &args[base], value.join(" "));
        },
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    "set" => {
      assert_invalid_command(base+2, args, true, &entry_usage);
      match entry.set(&args[base], &[String::from(&args[base+1])]) {
        Ok(()) => {},
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    "remove" => {
      assert_invalid_command(base+1, args, true, &entry_usage);
      match entry.remove(&args[base]) {
        Ok(()) => {},
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    "delete" => {
      assert_invalid_command(base, args, true, &entry_usage);
      match entry.delete() {
        Ok(()) => {},
        Err(error) => {
          eprintln!("ERROR: {}", error);
          process::exit(1);
        }
      }
    },
    _ => {
      eprintln!("ERROR: unrecognized command {}", args.join(" "));
      process::exit(1);
    }
  }
}

fn handle_help(args: &[String]) {
  assert_invalid_command(1, &args[1..], true, &help_usage);
  log_usage();
}

fn main() {
    let args: Vec<_> = env::args().collect();
    if args.len() <= 1 {
      log_usage();
      process::exit(1);
    }

    match args[1].as_str() {
      "cmdline" => handle_cmdline(&args[1..]),
      "entry"   => handle_entry(&args[1..]),
      "help"    => handle_help(&args[1..]),
      _         => {
        eprintln!("ERROR: invalid command {}", args[1]);
        log_usage();
        process::exit(1);
      }
    }
    process::exit(0);
}