use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OptionsException {
pub message: String,
}
impl OptionsException {
pub fn new(message: impl Into<String>) -> Self {
OptionsException {
message: message.into(),
}
}
}
impl fmt::Display for OptionsException {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for OptionsException {}
pub struct OptionsParser {
params: Vec<String>,
index: usize,
put_back_index: usize,
last_option_value: Option<String>,
put_back_last_option_value: Option<String>,
last_option_original_form: Option<String>,
put_back_last_option_original_form: Option<String>,
}
impl OptionsParser {
pub fn new(params: &[String]) -> Self {
OptionsParser {
params: params.to_vec(),
index: 0,
put_back_index: 0,
last_option_value: None,
put_back_last_option_value: None,
last_option_original_form: None,
put_back_last_option_original_form: None,
}
}
#[allow(clippy::manual_strip)] pub fn next_option(&mut self) -> Result<Option<String>, OptionsException> {
if self.index >= self.params.len() {
return Ok(None);
}
let param = self.params[self.index].clone();
if !param.starts_with('-') {
return Ok(None);
}
self.put_back_index = self.index;
self.index += 1;
self.put_back_last_option_original_form = self.last_option_original_form.clone();
self.last_option_original_form = Some(param.clone());
self.put_back_last_option_value = self.last_option_value.clone();
self.last_option_value = None;
if param.starts_with("--") {
if param == "--" {
return Ok(None);
}
match param.find('=') {
Some(value_delimiter_index) => {
self.last_option_value = Some(param[value_delimiter_index + 1..].to_string());
self.last_option_original_form =
Some(param[..value_delimiter_index].to_string());
Ok(Some(param["--".len()..value_delimiter_index].to_string()))
}
None => Ok(Some(param["--".len()..].to_string())),
}
} else {
Ok(Some(param["-".len()..].to_string()))
}
}
pub fn put_option(&mut self) {
self.index = self.put_back_index;
self.last_option_original_form = self.put_back_last_option_original_form.clone();
self.last_option_value = self.put_back_last_option_value.clone();
}
pub fn get_option_original_form(&self) -> Option<String> {
self.last_option_original_form.clone()
}
fn original_form_for_message(&self) -> String {
match &self.last_option_original_form {
Some(form) => form.clone(),
None => "null".to_string(),
}
}
pub fn get_required_value(
&mut self,
value_description: &str,
) -> Result<String, OptionsException> {
if let Some(value) = self.last_option_value.take() {
return Ok(value);
}
if self.index >= self.params.len() {
return Err(OptionsException::new(format!(
"{} missing after {}",
value_description,
self.original_form_for_message()
)));
}
let param = self.params[self.index].clone();
if param == "--" {
return Err(OptionsException::new(format!(
"{} missing after {}",
value_description,
self.original_form_for_message()
)));
}
self.index += 1;
Ok(param)
}
pub fn get_required_int_value(
&mut self,
value_description: &str,
) -> Result<i32, OptionsException> {
let value = self.get_required_value(value_description)?;
match parse_java_int(&value) {
Some(parsed) => Ok(parsed),
None => Err(OptionsException::new(format!(
"{} ({}) must be a decimal number: {}",
value_description,
self.original_form_for_message(),
value
))),
}
}
pub fn get_optional_boolean_value(
&mut self,
default_value: bool,
) -> Result<bool, OptionsException> {
if let Some(string_value) = self.last_option_value.take() {
return match string_value.as_str() {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(OptionsException::new(format!(
"Unsupported value for {}: {}. Only true or false supported.",
self.original_form_for_message(),
string_value
))),
};
}
if self.index >= self.params.len() {
return Ok(default_value);
}
let string_value = self.params[self.index].clone();
if string_value == "true" {
self.index += 1;
Ok(true)
} else if string_value == "false" {
self.index += 1;
Ok(false)
} else {
Ok(default_value)
}
}
pub fn get_remaining_params(&self) -> Vec<String> {
if self.index >= self.params.len() {
return Vec::new();
}
let param = &self.params[self.index];
if param == "--" {
self.params[self.index + 1..].to_vec()
} else {
self.params[self.index..].to_vec()
}
}
}
fn parse_java_int(value: &str) -> Option<i32> {
let bytes = value.as_bytes();
if bytes.is_empty() {
return None;
}
let (negative, digits) = match bytes[0] {
b'-' => (true, &bytes[1..]),
b'+' => (false, &bytes[1..]),
_ => (false, bytes),
};
if digits.is_empty() {
return None;
}
let mut magnitude: u64 = 0;
for &b in digits {
if !b.is_ascii_digit() {
return None;
}
magnitude = magnitude.checked_mul(10)?.checked_add((b - b'0') as u64)?;
}
if negative {
if magnitude > (i32::MAX as u64) + 1 {
return None;
}
if magnitude == (i32::MAX as u64) + 1 {
return Some(i32::MIN);
}
Some(-(magnitude as i64) as i32)
} else {
if magnitude > i32::MAX as u64 {
return None;
}
Some(magnitude as i32)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn params(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
#[test]
fn parses_double_dash_space_form() {
let mut p = OptionsParser::new(¶ms(&["--ks", "release.jks", "app.apk"]));
assert_eq!(p.next_option().unwrap(), Some("ks".to_string()));
assert_eq!(
p.get_required_value("KeyStore file").unwrap(),
"release.jks"
);
assert_eq!(p.get_option_original_form().unwrap(), "--ks");
assert_eq!(p.next_option().unwrap(), None);
assert_eq!(p.get_remaining_params(), params(&["app.apk"]));
}
#[test]
fn parses_double_dash_equals_form() {
let mut p = OptionsParser::new(¶ms(&["--ks=release.jks"]));
assert_eq!(p.next_option().unwrap(), Some("ks".to_string()));
assert_eq!(p.get_option_original_form().unwrap(), "--ks");
assert_eq!(
p.get_required_value("KeyStore file").unwrap(),
"release.jks"
);
}
#[test]
fn parses_single_dash_form() {
let mut p = OptionsParser::new(¶ms(&["-v", "app.apk"]));
assert_eq!(p.next_option().unwrap(), Some("v".to_string()));
assert_eq!(p.get_option_original_form().unwrap(), "-v");
}
#[test]
fn stops_at_first_non_option() {
let mut p = OptionsParser::new(¶ms(&["app.apk", "--ks", "x.jks"]));
assert_eq!(p.next_option().unwrap(), None);
assert_eq!(
p.get_remaining_params(),
params(&["app.apk", "--ks", "x.jks"])
);
}
#[test]
fn double_dash_is_end_of_options_marker() {
let mut p = OptionsParser::new(¶ms(&["--", "app.apk"]));
assert_eq!(p.next_option().unwrap(), None);
assert_eq!(p.get_remaining_params(), params(&["app.apk"]));
}
#[test]
fn missing_value_reports_option_original_form() {
let mut p = OptionsParser::new(¶ms(&["--ks"]));
assert_eq!(p.next_option().unwrap(), Some("ks".to_string()));
let err = p.get_required_value("KeyStore file").unwrap_err();
assert_eq!(err.to_string(), "KeyStore file missing after --ks");
}
#[test]
fn missing_value_before_end_of_options_marker_is_an_error() {
let mut p = OptionsParser::new(¶ms(&["--ks", "--"]));
assert_eq!(p.next_option().unwrap(), Some("ks".to_string()));
let err = p.get_required_value("KeyStore file").unwrap_err();
assert_eq!(err.to_string(), "KeyStore file missing after --ks");
}
#[test]
fn int_value_reports_non_numeric_input() {
let mut p = OptionsParser::new(¶ms(&["--min-sdk-version", "abc"]));
assert_eq!(
p.next_option().unwrap(),
Some("min-sdk-version".to_string())
);
let err = p.get_required_int_value("Mininimum API Level").unwrap_err();
assert_eq!(
err.to_string(),
"Mininimum API Level (--min-sdk-version) must be a decimal number: abc"
);
}
#[test]
fn int_value_accepts_plain_decimal() {
let mut p = OptionsParser::new(¶ms(&["--min-sdk-version", "24"]));
assert_eq!(
p.next_option().unwrap(),
Some("min-sdk-version".to_string())
);
assert_eq!(p.get_required_int_value("Mininimum API Level").unwrap(), 24);
}
#[test]
fn boolean_option_defaults_when_bare() {
let mut p = OptionsParser::new(¶ms(&["--v1-signing-enabled", "--next-signer"]));
assert_eq!(
p.next_option().unwrap(),
Some("v1-signing-enabled".to_string())
);
assert!(p.get_optional_boolean_value(true).unwrap());
assert_eq!(p.next_option().unwrap(), Some("next-signer".to_string()));
}
#[test]
fn boolean_option_accepts_separate_value() {
let mut p = OptionsParser::new(¶ms(&["--v1-signing-enabled", "false"]));
assert_eq!(
p.next_option().unwrap(),
Some("v1-signing-enabled".to_string())
);
assert!(!p.get_optional_boolean_value(true).unwrap());
}
#[test]
fn boolean_option_accepts_equals_value() {
let mut p = OptionsParser::new(¶ms(&["--v2-signing-enabled=false"]));
assert_eq!(
p.next_option().unwrap(),
Some("v2-signing-enabled".to_string())
);
assert!(!p.get_optional_boolean_value(true).unwrap());
}
#[test]
fn boolean_option_rejects_other_values() {
let mut p = OptionsParser::new(¶ms(&["--v2-signing-enabled=maybe"]));
assert_eq!(
p.next_option().unwrap(),
Some("v2-signing-enabled".to_string())
);
let err = p.get_optional_boolean_value(true).unwrap_err();
assert_eq!(
err.to_string(),
"Unsupported value for --v2-signing-enabled: maybe. Only true or false supported."
);
}
#[test]
fn boolean_option_leaves_default_when_next_param_is_not_boolean() {
let mut p = OptionsParser::new(¶ms(&["--verbose", "app.apk"]));
assert_eq!(p.next_option().unwrap(), Some("verbose".to_string()));
assert!(p.get_optional_boolean_value(true).unwrap());
assert_eq!(p.get_remaining_params(), params(&["app.apk"]));
}
#[test]
fn put_option_undoes_the_last_next_option() {
let mut p = OptionsParser::new(¶ms(&["--next-signer", "--ks", "x.jks"]));
assert_eq!(p.next_option().unwrap(), Some("next-signer".to_string()));
p.put_option();
assert_eq!(p.next_option().unwrap(), Some("next-signer".to_string()));
assert_eq!(p.next_option().unwrap(), Some("ks".to_string()));
}
#[test]
fn put_option_restores_the_original_form() {
let mut p = OptionsParser::new(¶ms(&["--ks", "--out"]));
assert_eq!(p.next_option().unwrap(), Some("ks".to_string()));
assert_eq!(p.get_option_original_form().unwrap(), "--ks");
p.put_option();
assert_eq!(p.next_option().unwrap(), Some("ks".to_string()));
assert_eq!(p.get_option_original_form().unwrap(), "--ks");
}
#[test]
fn remaining_params_are_empty_when_exhausted() {
let mut p = OptionsParser::new(¶ms(&["--verbose"]));
assert_eq!(p.next_option().unwrap(), Some("verbose".to_string()));
assert!(p.get_optional_boolean_value(true).unwrap());
assert_eq!(p.next_option().unwrap(), None);
assert!(p.get_remaining_params().is_empty());
}
#[test]
fn java_int_parsing_matches_parse_int_bounds() {
assert_eq!(parse_java_int("0"), Some(0));
assert_eq!(parse_java_int("+7"), Some(7));
assert_eq!(parse_java_int("-7"), Some(-7));
assert_eq!(parse_java_int("2147483647"), Some(i32::MAX));
assert_eq!(parse_java_int("-2147483648"), Some(i32::MIN));
assert_eq!(parse_java_int("2147483648"), None);
assert_eq!(parse_java_int("-2147483649"), None);
assert_eq!(parse_java_int(""), None);
assert_eq!(parse_java_int("-"), None);
assert_eq!(parse_java_int("1.5"), None);
assert_eq!(parse_java_int(" 1"), None);
}
}