iskra 0.5.0

A safe, modern, Rust-native data transfer tool.
Documentation
use std::ffi::OsString;

const GLOBAL_FLAG_TABLE: &[(&str, Option<char>, bool)] = &[
    ("--timeout", None, true),
    ("--no-decompress", None, false),
    ("--fail", None, false),
    ("--verbose-errors", None, false),
    ("--show-headers", None, false),
    ("--quiet", Some('q'), false),
    ("--verbose", Some('v'), false),
    // ("-h", Some('h'), false), // Removed -h as a global flag so it is only help when used alone
    // -V and --version are handled by clap
];

pub fn extract_show_headers<I, T>(args: I) -> (bool, Vec<OsString>)
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    let mut show_headers = false;
    let mut filtered = Vec::new();
    for arg in args.into_iter() {
        let s = arg.clone().into();
        let s_str = s.to_string_lossy();
        // Remove all --show-headers and --headers, anywhere in the list
        if s_str == "--show-headers" {
            show_headers = true;
            continue;
        }
        filtered.push(s);
    }
    (show_headers, filtered)
}

/// Canonical global flags, their short forms, and whether they take a value
fn is_short_global_flag(c: char) -> bool {
    GLOBAL_FLAG_TABLE.iter().any(|&(_long, short, _)| short == Some(c))
}

fn short_flag_takes_value(c: char) -> bool {
    GLOBAL_FLAG_TABLE.iter().any(|&(_long, short, takes_val)| short == Some(c) && takes_val)
}
/// Flatten all global flag aliases into a set for fast lookup
fn is_global_flag(arg: &str) -> bool {
    // Accept --flag, --flag=val, -h, etc.
    for &(long, short, _takes_val) in GLOBAL_FLAG_TABLE {
        if arg == long || (long.starts_with("--") && arg.starts_with(&format!("{}=", &long[2..]))) || (long.starts_with("--") && arg.starts_with(&format!("{}=", long))) {
            return true;
        }
        if let Some(s) = short {
            if arg == format!("-{}", s) {
                return true;
            }
        }
    }
    // Grouped short flags: -vhf (only if all are global and none take a value)
    if arg.starts_with('-') && !arg.starts_with("--") && arg.len() > 2 {
        let chars: Vec<char> = arg[1..].chars().collect();
        for &c in &chars {
            if !is_short_global_flag(c) || short_flag_takes_value(c) {
                return false;
            }
        }
        return true;
    }
    false
}
/// Preprocess CLI args: move all global options (before and after subcommand) before the subcommand
pub fn reorder_global_options<I, T>(args: I) -> Vec<OsString>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    let mut before_cmd = Vec::new();
    let mut args_vec: Vec<OsString> = args.into_iter().map(|a| a.into()).collect();
    // Skip program name
    if !args_vec.is_empty() {
        before_cmd.push(args_vec[0].clone());
        args_vec.remove(0);
    }
    // GET shortcut: if first non-flag arg looks like a URL, insert "get" as the subcommand
    if !args_vec.is_empty() {
        let first = args_vec[0].to_string_lossy();
        if first.starts_with("http://") || first.starts_with("https://") {
            args_vec.insert(0, OsString::from("get"));
        }
    }

    let mut globals = Vec::new();
    let mut subcmd_and_args = Vec::new();
    let mut i = 0;
    let mut found_subcmd = false;
    while i < args_vec.len() {
        let arg_str = args_vec[i].to_string_lossy();
        if !found_subcmd && !arg_str.starts_with('-') {
            // Subcommand or alias
            let mut subcmd = arg_str.to_string();
            if ["custom", "request", "raw", "http"].contains(&subcmd.as_str()) {
                subcmd = "custom".to_string();
            }
            subcmd_and_args.push(OsString::from(subcmd));
            found_subcmd = true;
        } else if arg_str.starts_with('-') && !arg_str.starts_with("--") && arg_str.len() > 2 {
            // Grouped short flags: -vh
            let chars: Vec<char> = arg_str[1..].chars().collect();
            let mut all_global = true;
            for &c in &chars {
                if !is_short_global_flag(c) || short_flag_takes_value(c) {
                    all_global = false;
                    break;
                }
            }
            if all_global {
                for &c in &chars {
                    globals.push(OsString::from(format!("-{}", c)));
                }
            } else {
                // Not a valid grouped global, treat as normal arg
                if found_subcmd {
                    subcmd_and_args.push(args_vec[i].clone());
                } else {
                    subcmd_and_args.push(args_vec[i].clone());
                }
            }
        } else if is_global_flag(&arg_str) {
            // Handle --flag value (for flags that take values)
            globals.push(args_vec[i].clone());
            // Only --timeout currently takes a value
            if (arg_str == "--timeout" || arg_str.starts_with("--timeout=")) && i + 1 < args_vec.len() && !args_vec[i + 1].to_string_lossy().starts_with('-') {
                globals.push(args_vec[i + 1].clone());
                i += 1;
            }
        } else if arg_str == "-h" && found_subcmd {
            // Treat -h as a header flag for subcommands, not as help
            subcmd_and_args.push(OsString::from("-H"));
        } else {
            if found_subcmd {
                subcmd_and_args.push(args_vec[i].clone());
            } else {
                // Pre-subcommand non-global (shouldn't happen, but preserve order)
                subcmd_and_args.push(args_vec[i].clone());
            }
        }
        i += 1;
    }
    before_cmd.extend(globals);
    before_cmd.extend(subcmd_and_args);
    before_cmd
}

pub fn validate_method(s: &str) -> Result<String, String> {
    let allowed = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE", "CONNECT"];
    let upper = s.to_uppercase();
    if allowed.contains(&upper.as_str()) {
        Ok(upper)
    } else {
        Err(format!(
            "Invalid HTTP method: '{}'. Allowed methods: {}",
            s,
            allowed.join(", ")
        ))
    }
}

pub fn validate_url(s: &str) -> Result<String, String> {
    if s.starts_with("http://") || s.starts_with("https://") {
        Ok(s.to_string())
    } else {
        Err("URL must start with http:// or https://".to_string())
    }
}

pub fn parse_range(s: &str) -> Result<(u64, Option<u64>), String> {
    let parts: Vec<&str> = s.split('-').collect();
    if parts.len() != 2 {
        return Err("Range must be in start-end format (end optional)".to_string());
    }
    let start = parts[0].trim().parse::<u64>().map_err(|_| "Invalid start of range".to_string())?;
    let end = if parts[1].trim().is_empty() {
        None
    } else {
        Some(parts[1].trim().parse::<u64>().map_err(|_| "Invalid end of range".to_string())?)
    };
    Ok((start, end))
}

pub fn parse_header(s: &str) -> Result<(String, String), String> {
    let parts: Vec<&str> = s.splitn(2, ':').collect();
    if parts.len() != 2 {
        return Err("Header must be in key:value format".to_string());
    }
    Ok((parts[0].trim().to_string(), parts[1].trim().to_string()))
}

pub fn parse_query(s: &str) -> Result<(String, String), String> {
    let parts: Vec<&str> = s.splitn(2, '=').collect();
    if parts.len() != 2 {
        return Err("Query must be in key=value format".to_string());
    }
    Ok((parts[0].trim().to_string(), parts[1].trim().to_string()))
}