Skip to content

Commit 7547524

Browse files
committed
refactor: remove code duplication, #6194
1 parent 48c57f5 commit 7547524

File tree

1 file changed

+187
-68
lines changed

1 file changed

+187
-68
lines changed

stackslib/src/blockstack_cli.rs

Lines changed: 187 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -333,91 +333,189 @@ fn sign_transaction_single_sig_standard(
333333
.ok_or("TX did not finish signing -- was this a standard single signature transaction?")?)
334334
}
335335

336+
/// Counts how many times a specific flag appears in the argument list.
337+
///
338+
/// # Arguments
339+
///
340+
/// * `args` - A reference to a vector of argument strings.
341+
/// * `flag` - The flag to count occurrences of.
342+
///
343+
/// # Returns
344+
///
345+
/// The number of times `flag` appears in `args`.
346+
fn count_flag(args: &Vec<String>, flag: &str) -> usize {
347+
args.iter().filter(|&arg| arg == flag).count()
348+
}
349+
350+
/// Removes the first occurrence of a flag from the argument list.
351+
///
352+
/// # Arguments
353+
///
354+
/// * `args` - A mutable reference to a vector of argument strings.
355+
/// * `flag` - The flag to remove.
356+
///
357+
/// # Returns
358+
///
359+
/// `true` if the flag was found and removed; `false` otherwise.
360+
fn extract_flag(args: &mut Vec<String>, flag: &str) -> bool {
361+
args.iter()
362+
.position(|arg| arg == flag)
363+
.map(|flag_index| {
364+
args.remove(flag_index);
365+
})
366+
.is_some()
367+
}
368+
369+
/// Removes a flag and its following value from the argument list.
370+
///
371+
/// # Arguments
372+
///
373+
/// * `args` - A mutable reference to a vector of argument strings.
374+
/// * `flag` - The flag whose value to extract and remove.
375+
///
376+
/// # Returns
377+
///
378+
/// An `Option<String>` containing the value following the flag if both were found and removed;
379+
/// returns `None` if the flag was not found or no value follows the flag.
380+
fn extract_flag_with_value(args: &mut Vec<String>, flag: &str) -> Option<String> {
381+
args.iter()
382+
.position(|arg| arg == flag)
383+
.and_then(|flag_index| {
384+
if flag_index + 1 < args.len() {
385+
let value = args.remove(flag_index + 1);
386+
args.remove(flag_index);
387+
Some(value)
388+
} else {
389+
None
390+
}
391+
})
392+
}
393+
394+
/// Parses anchor mode flags from the CLI arguments.
395+
///
396+
/// This function checks for the presence of `--microblock-only` and `--block-only` flags
397+
/// in the provided `args` vector, and returns the corresponding `TransactionAnchorMode`.
398+
///
399+
/// The user may specify **at most one** of these flags:
400+
/// - `--microblock-only` maps to `TransactionAnchorMode::OffChainOnly`
401+
/// - `--block-only` maps to `TransactionAnchorMode::OnChainOnly`
402+
///
403+
/// If **neither flag is provided**, the default mode `TransactionAnchorMode::Any` is returned.
404+
///
405+
/// Both flags are removed from the `args` vector if present.
406+
///
407+
/// # Arguments
408+
///
409+
/// * `args` - A mutable reference to a vector of CLI arguments.
410+
/// * `usage` - A usage string displayed in error messages.
411+
///
412+
/// # Returns
413+
///
414+
/// * `Ok(TransactionAnchorMode)` - On successful parsing or if no anchor mode is specified.
415+
/// * `Err(CliError)` - If either flag is duplicated, or if both are present simultaneously.
416+
///
417+
/// # Errors
418+
///
419+
/// Returns a `CliError::Message` if:
420+
/// - `--microblock-only` or `--block-only` appears more than once.
421+
/// - Both flags are specified together (mutually exclusive).
422+
///
423+
/// # Side Effects
424+
///
425+
/// Removes `--microblock-only` or `--block-only` from the `args` vector if found.
336426
fn parse_anchor_mode(
337427
args: &mut Vec<String>,
338428
usage: &str,
339429
) -> Result<TransactionAnchorMode, CliError> {
340-
let num_args = args.len();
341-
let mut offchain_only = false;
342-
let mut onchain_only = false;
343-
let mut idx = 0;
344-
for i in 0..num_args {
345-
if args[i] == "--microblock-only" {
346-
if idx > 0 {
347-
return Err(CliError::Message(format!(
348-
"Multiple anchor modes detected.\n\nUSAGE:\n{}",
349-
usage,
350-
)));
351-
}
430+
const FLAG_MICROBLOCK: &str = "--microblock-only";
431+
const FLAG_BLOCK: &str = "--block-only";
352432

353-
offchain_only = true;
354-
idx = i;
355-
}
356-
if args[i] == "--block-only" {
357-
if idx > 0 {
358-
return Err(CliError::Message(format!(
359-
"Multiple anchor modes detected.\n\nUSAGE:\n{}",
360-
usage,
361-
)));
362-
}
433+
let count_micro = count_flag(args, FLAG_MICROBLOCK);
434+
let count_block = count_flag(args, FLAG_BLOCK);
363435

364-
onchain_only = true;
365-
idx = i;
366-
}
367-
}
368-
if idx > 0 {
369-
args.remove(idx);
436+
if count_micro > 1 || count_block > 1 {
437+
return Err(CliError::Message(format!(
438+
"Duplicated anchor mode detected.\n\nUSAGE:\n{}",
439+
usage,
440+
)));
370441
}
371-
if onchain_only {
372-
Ok(TransactionAnchorMode::OnChainOnly)
373-
} else if offchain_only {
374-
Ok(TransactionAnchorMode::OffChainOnly)
375-
} else {
376-
Ok(TransactionAnchorMode::Any)
442+
443+
let has_microblock = extract_flag(args, FLAG_MICROBLOCK);
444+
let has_block = extract_flag(args, FLAG_BLOCK);
445+
446+
match (has_microblock, has_block) {
447+
(true, true) => Err(CliError::Message(format!(
448+
"Both anchor modes detected.\n\nUSAGE:\n{}",
449+
usage
450+
))),
451+
(true, false) => Ok(TransactionAnchorMode::OffChainOnly),
452+
(false, true) => Ok(TransactionAnchorMode::OnChainOnly),
453+
(false, false) => Ok(TransactionAnchorMode::Any),
377454
}
378455
}
379456

457+
/// Parses the `--postcondition-mode` flag from the CLI arguments.
458+
///
459+
/// This function looks for the `--postcondition-mode` flag in the provided `args` vector
460+
/// and extracts its associated value. The flag must be specified at most once, and the value
461+
/// must be either `"allow"` or `"deny"`. If the flag is not present, the default mode
462+
/// `TransactionPostConditionMode::Deny` is returned.
463+
///
464+
/// The flag and its value are removed from `args` if found.
465+
///
466+
/// # Arguments
467+
///
468+
/// * `args` - A mutable reference to a vector of CLI arguments.
469+
/// * `usage` - A usage string that is displayed in error messages.
470+
///
471+
/// # Returns
472+
///
473+
/// * `Ok(TransactionPostConditionMode)` - If the flag is parsed successfully or not present (defaults to `Deny`).
474+
/// * `Err(CliError)` - If the flag is duplicated, missing a value, or contains an invalid value.
475+
///
476+
/// # Errors
477+
///
478+
/// Returns a `CliError::Message` if:
479+
/// - The flag appears more than once.
480+
/// - The flag is present but missing a value.
481+
/// - The flag value is not `"allow"` or `"deny"`.
482+
///
483+
/// # Side Effects
484+
///
485+
/// This function modifies the `args` vector by removing the parsed flag and its value if found.
380486
fn parse_postcondition_mode(
381487
args: &mut Vec<String>,
382488
usage: &str,
383489
) -> Result<TransactionPostConditionMode, CliError> {
384-
let mut i = 0;
385-
let mut value = None;
386-
while i < args.len() {
387-
if args[i] == "--postcondition-mode" {
388-
if value.is_some() {
389-
return Err(CliError::Message(format!(
390-
"Duplicated `--postcondition-mode`.\n\nUSAGE:\n{}",
391-
usage
392-
)));
393-
}
394-
if i + 1 >= args.len() {
395-
return Err(CliError::Message(format!(
396-
"Missing value for `--postcondition-mode`.\n\nUSAGE:\n{}",
397-
usage
398-
)));
399-
}
400-
value = Some(args.remove(i + 1));
401-
args.remove(i);
402-
continue; // do not increment i since elements shifted
490+
const FLAG_POSTCONDITION: &str = "--postcondition-mode";
491+
const VALUE_ALLOW: &str = "allow";
492+
const VALUE_DENY: &str = "deny";
493+
494+
match count_flag(args, FLAG_POSTCONDITION) {
495+
0 => return Ok(TransactionPostConditionMode::Deny),
496+
1 => { /* continue below */ }
497+
_ => {
498+
return Err(CliError::Message(format!(
499+
"Duplicated `{}`.\n\nUSAGE:\n{}",
500+
FLAG_POSTCONDITION, usage
501+
)));
403502
}
404-
i += 1;
405503
}
406504

407-
let mode = match value {
408-
Some(mode_str) => match mode_str.as_ref() {
409-
"allow" => TransactionPostConditionMode::Allow,
410-
"deny" => TransactionPostConditionMode::Deny,
411-
_ => {
412-
return Err(CliError::Message(format!(
413-
"Invalid value for `--postcondition-mode`.\n\nUSAGE:\n{}",
414-
usage,
415-
)))
416-
}
505+
match extract_flag_with_value(args, FLAG_POSTCONDITION) {
506+
Some(value) => match value.as_str() {
507+
VALUE_ALLOW => Ok(TransactionPostConditionMode::Allow),
508+
VALUE_DENY => Ok(TransactionPostConditionMode::Deny),
509+
_ => Err(CliError::Message(format!(
510+
"Invalid value for `{}`.\n\nUSAGE:\n{}",
511+
FLAG_POSTCONDITION, usage
512+
))),
417513
},
418-
None => TransactionPostConditionMode::Deny,
419-
};
420-
Ok(mode)
514+
None => Err(CliError::Message(format!(
515+
"Missing value for `{}`.\n\nUSAGE:\n{}",
516+
FLAG_POSTCONDITION, usage
517+
))),
518+
}
421519
}
422520

423521
fn handle_contract_publish(
@@ -1106,7 +1204,28 @@ mod test {
11061204

11071205
let exp_err_msg = format!(
11081206
"{}\n\nUSAGE:\n{}",
1109-
"Multiple anchor modes detected.", PUBLISH_USAGE
1207+
"Both anchor modes detected.", PUBLISH_USAGE
1208+
);
1209+
assert_eq!(exp_err_msg, result.unwrap_err().to_string());
1210+
1211+
// Scenario FAIL using duplicated anchor mode
1212+
let publish_args = [
1213+
"publish",
1214+
"043ff5004e3d695060fa48ac94c96049b8c14ef441c50a184a6a3875d2a000f3",
1215+
"1",
1216+
"0",
1217+
"foo-contract",
1218+
&contract_path,
1219+
"--microblock-only",
1220+
"--microblock-only",
1221+
];
1222+
1223+
let result = main_handler(to_string_vec(&publish_args));
1224+
assert!(result.is_err());
1225+
1226+
let exp_err_msg = format!(
1227+
"{}\n\nUSAGE:\n{}",
1228+
"Duplicated anchor mode detected.", PUBLISH_USAGE
11101229
);
11111230
assert_eq!(exp_err_msg, result.unwrap_err().to_string());
11121231
}

0 commit comments

Comments
 (0)