use anyhow::{Result, anyhow, bail};
use clap::{Parser, Subcommand, ValueEnum};
use sftool_lib::{AfterOperation, BeforeOperation, ChipType};
use strum::{Display, EnumString};
use crate::config::SfToolConfig;
pub type MergedConfig = (
ChipType,
String,
String,
u32,
BeforeOperation,
AfterOperation,
i8,
bool,
bool,
Option<String>, );
#[derive(EnumString, Display, Debug, Clone, ValueEnum)]
pub enum Memory {
#[clap(name = "nor")]
Nor,
#[clap(name = "nand")]
Nand,
#[clap(name = "sd")]
Sd,
}
#[derive(Parser, Debug)]
#[command(author, version, about = "sftool CLI", long_about = None)]
pub struct Cli {
#[arg(short = 'c', long = "chip", value_enum)]
pub chip: Option<ChipType>,
#[arg(short = 'm', long = "memory", value_enum, ignore_case = true)]
pub memory: Option<Memory>,
#[arg(short = 'p', long = "port")]
pub port: Option<String>,
#[arg(short = 'b', long = "baud")]
pub baud: Option<u32>,
#[arg(long = "before", value_enum)]
pub before: Option<BeforeOperation>,
#[arg(long = "after", value_enum)]
pub after: Option<AfterOperation>,
#[arg(long = "connect-attempts")]
pub connect_attempts: Option<i8>,
#[arg(long = "compat")]
pub compat: Option<bool>,
#[arg(long = "stub")]
pub stub: Option<String>,
#[arg(long = "stub-config", global = true)]
pub stub_config_json: Option<String>,
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
#[command(name = "config")]
Config(ConfigCommand),
#[command(name = "write_flash")]
WriteFlash(WriteFlash),
#[command(name = "read_flash")]
ReadFlash(ReadFlash),
#[command(name = "erase_flash")]
EraseFlash(EraseFlash),
#[command(name = "erase_region")]
EraseRegion(EraseRegion),
#[command(name = "stub")]
Stub(StubCommand),
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Execute a command from a JSON configuration file")]
pub struct ConfigCommand {
#[arg(required = true, value_name = "FILE")]
pub path: String,
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Write a binary blob to flash")]
pub struct WriteFlash {
#[arg(long = "verify", default_value = "true")]
pub verify: bool,
#[arg(short = 'u', long = "no-compress")]
pub no_compress: bool,
#[arg(short = 'e', long = "erase-all")]
pub erase_all: bool,
#[arg(required = true)]
pub files: Vec<String>,
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Read a binary blob from flash")]
pub struct ReadFlash {
#[arg(required = true)]
pub files: Vec<String>,
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Erase flash")]
pub struct EraseFlash {
#[arg(required = true)]
pub address: String,
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Erase a region of the flash")]
pub struct EraseRegion {
#[arg(required = true)]
pub region: Vec<String>,
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Manage stub config in AXF/ELF driver files")]
pub struct StubCommand {
#[command(subcommand)]
pub action: StubAction,
}
#[derive(Subcommand, Debug, Clone)]
pub enum StubAction {
#[command(name = "write")]
Write(StubWrite),
#[command(name = "clear")]
Clear(StubClear),
#[command(name = "read")]
Read(StubRead),
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Write stub config into AXF/ELF driver files")]
pub struct StubWrite {
#[arg(required = true)]
pub files: Vec<String>,
#[arg(long = "stub-config")]
pub stub_config: String,
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Clear stub config in AXF/ELF driver files")]
pub struct StubClear {
#[arg(required = true)]
pub files: Vec<String>,
}
#[derive(Parser, Debug, Clone)]
#[command(about = "Read stub config from AXF/ELF driver files")]
pub struct StubRead {
#[arg(required = true)]
pub files: Vec<String>,
#[arg(long = "output")]
pub output: Option<String>,
}
fn memory_to_string(memory: &Memory) -> String {
match memory {
Memory::Nor => "nor".to_string(),
Memory::Nand => "nand".to_string(),
Memory::Sd => "sd".to_string(),
}
}
fn normalize_memory(memory: &str) -> Result<String> {
let normalized = memory.to_ascii_lowercase();
match normalized.as_str() {
"nor" | "nand" | "sd" => Ok(normalized),
_ => bail!(
"Invalid memory type '{}'. Must be one of: nor, nand, sd",
memory
),
}
}
pub fn merge_config(args: &Cli, config: Option<SfToolConfig>) -> Result<MergedConfig> {
let base_config = config.unwrap_or_else(SfToolConfig::with_defaults);
let chip = match &args.chip {
Some(c) => c.clone(),
None => base_config
.parse_chip_type()
.map_err(|e| anyhow!("Invalid chip type in config: {}", e))?,
};
let memory_raw = args
.memory
.as_ref()
.map(memory_to_string)
.unwrap_or_else(|| base_config.memory.clone());
let memory = normalize_memory(&memory_raw)?;
let port = args
.port
.clone()
.unwrap_or_else(|| base_config.port.clone());
let baud = args.baud.unwrap_or(base_config.baud);
let before = match &args.before {
Some(b) => b.clone(),
None => base_config
.parse_before()
.map_err(|e| anyhow!("Invalid before operation in config: {}", e))?,
};
let after = match &args.after {
Some(a) => a.clone(),
None => base_config
.parse_after()
.map_err(|e| anyhow!("Invalid after operation in config: {}", e))?,
};
let connect_attempts = args
.connect_attempts
.unwrap_or(base_config.connect_attempts);
let compat = args.compat.unwrap_or(base_config.compat);
let quiet = args.quiet || base_config.quiet;
let stub_path = args.stub.clone().or_else(|| base_config.stub_path.clone());
if port.is_empty() {
bail!("Port must be specified either via --port or in config file");
}
Ok((
chip,
memory,
port,
baud,
before,
after,
connect_attempts,
compat,
quiet,
stub_path,
))
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum CommandSource {
Cli(Commands),
Config(SfToolConfig),
}
pub fn get_command_source(args: &Cli, config: Option<SfToolConfig>) -> Result<CommandSource> {
match &args.command {
Some(Commands::Config(_)) => config
.map(CommandSource::Config)
.ok_or_else(|| anyhow!("Config command requires a configuration file")),
Some(cmd) => Ok(CommandSource::Cli(cmd.clone())),
None => bail!("No command specified. Use a subcommand or `config <file>`."),
}
}