Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

v1.18: [clap-v3-utils] Add functions to parse directly from SignerSource (backport of #34678) #35384

Merged
merged 1 commit into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 267 additions & 6 deletions clap-v3-utils/src/input_parsers/signer.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,162 @@
use {
crate::{
input_parsers::{keypair_of, keypairs_of, pubkey_of, pubkeys_of},
keypair::{
parse_signer_source, pubkey_from_path, resolve_signer_from_path, signer_from_path,
SignerSource, SignerSourceError, SignerSourceKind,
},
keypair::{pubkey_from_path, resolve_signer_from_path, signer_from_path, ASK_KEYWORD},
},
clap::{builder::ValueParser, ArgMatches},
solana_remote_wallet::remote_wallet::RemoteWalletManager,
solana_remote_wallet::{
locator::{Locator as RemoteWalletLocator, LocatorError as RemoteWalletLocatorError},
remote_wallet::RemoteWalletManager,
},
solana_sdk::{
derivation_path::{DerivationPath, DerivationPathError},
pubkey::Pubkey,
signature::{Keypair, Signature, Signer},
},
std::{error, rc::Rc, str::FromStr},
thiserror::Error,
};

const SIGNER_SOURCE_PROMPT: &str = "prompt";
const SIGNER_SOURCE_FILEPATH: &str = "file";
const SIGNER_SOURCE_USB: &str = "usb";
const SIGNER_SOURCE_STDIN: &str = "stdin";
const SIGNER_SOURCE_PUBKEY: &str = "pubkey";

#[derive(Debug, Error)]
pub enum SignerSourceError {
#[error("unrecognized signer source")]
UnrecognizedSource,
#[error(transparent)]
RemoteWalletLocatorError(#[from] RemoteWalletLocatorError),
#[error(transparent)]
DerivationPathError(#[from] DerivationPathError),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error("unsupported source")]
UnsupportedSource,
}

#[derive(Clone)]
pub enum SignerSourceKind {
Prompt,
Filepath(String),
Usb(RemoteWalletLocator),
Stdin,
Pubkey(Pubkey),
}

impl AsRef<str> for SignerSourceKind {
fn as_ref(&self) -> &str {
match self {
Self::Prompt => SIGNER_SOURCE_PROMPT,
Self::Filepath(_) => SIGNER_SOURCE_FILEPATH,
Self::Usb(_) => SIGNER_SOURCE_USB,
Self::Stdin => SIGNER_SOURCE_STDIN,
Self::Pubkey(_) => SIGNER_SOURCE_PUBKEY,
}
}
}

impl std::fmt::Debug for SignerSourceKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s: &str = self.as_ref();
write!(f, "{s}")
}
}

#[derive(Debug, Clone)]
pub struct SignerSource {
pub kind: SignerSourceKind,
pub derivation_path: Option<DerivationPath>,
pub legacy: bool,
}

impl SignerSource {
fn new(kind: SignerSourceKind) -> Self {
Self {
kind,
derivation_path: None,
legacy: false,
}
}

fn new_legacy(kind: SignerSourceKind) -> Self {
Self {
kind,
derivation_path: None,
legacy: true,
}
}

pub(crate) fn parse<S: AsRef<str>>(source: S) -> Result<Self, SignerSourceError> {
let source = source.as_ref();
let source = {
#[cfg(target_family = "windows")]
{
// trim matched single-quotes since cmd.exe won't
let mut source = source;
while let Some(trimmed) = source.strip_prefix('\'') {
source = if let Some(trimmed) = trimmed.strip_suffix('\'') {
trimmed
} else {
break;
}
}
source.replace('\\', "/")
}
#[cfg(not(target_family = "windows"))]
{
source.to_string()
}
};
match uriparse::URIReference::try_from(source.as_str()) {
Err(_) => Err(SignerSourceError::UnrecognizedSource),
Ok(uri) => {
if let Some(scheme) = uri.scheme() {
let scheme = scheme.as_str().to_ascii_lowercase();
match scheme.as_str() {
SIGNER_SOURCE_PROMPT => Ok(SignerSource {
kind: SignerSourceKind::Prompt,
derivation_path: DerivationPath::from_uri_any_query(&uri)?,
legacy: false,
}),
SIGNER_SOURCE_FILEPATH => Ok(SignerSource::new(
SignerSourceKind::Filepath(uri.path().to_string()),
)),
SIGNER_SOURCE_USB => Ok(SignerSource {
kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?),
derivation_path: DerivationPath::from_uri_key_query(&uri)?,
legacy: false,
}),
SIGNER_SOURCE_STDIN => Ok(SignerSource::new(SignerSourceKind::Stdin)),
_ => {
#[cfg(target_family = "windows")]
// On Windows, an absolute path's drive letter will be parsed as the URI
// scheme. Assume a filepath source in case of a single character shceme.
if scheme.len() == 1 {
return Ok(SignerSource::new(SignerSourceKind::Filepath(source)));
}
Err(SignerSourceError::UnrecognizedSource)
}
}
} else {
match source.as_str() {
STDOUT_OUTFILE_TOKEN => Ok(SignerSource::new(SignerSourceKind::Stdin)),
ASK_KEYWORD => Ok(SignerSource::new_legacy(SignerSourceKind::Prompt)),
_ => match Pubkey::from_str(source.as_str()) {
Ok(pubkey) => Ok(SignerSource::new(SignerSourceKind::Pubkey(pubkey))),
Err(_) => std::fs::metadata(source.as_str())
.map(|_| SignerSource::new(SignerSourceKind::Filepath(source)))
.map_err(|err| err.into()),
},
}
}
}
}
}
}

// Sentinel value used to indicate to write to screen instead of file
pub const STDOUT_OUTFILE_TOKEN: &str = "-";

Expand Down Expand Up @@ -72,7 +214,7 @@ impl SignerSourceParserBuilder {
pub fn build(self) -> ValueParser {
ValueParser::from(
move |arg: &str| -> Result<SignerSource, SignerSourceError> {
let signer_source = parse_signer_source(arg)?;
let signer_source = SignerSource::parse(arg)?;
if !self.allow_legacy && signer_source.legacy {
return Err(SignerSourceError::UnsupportedSource);
}
Expand Down Expand Up @@ -240,11 +382,130 @@ mod tests {
super::*,
assert_matches::assert_matches,
clap::{Arg, Command},
solana_remote_wallet::locator::Manufacturer,
solana_sdk::signature::write_keypair_file,
std::fs,
tempfile::NamedTempFile,
};

#[test]
fn test_parse_signer_source() {
assert_matches!(
SignerSource::parse(STDOUT_OUTFILE_TOKEN).unwrap(),
SignerSource {
kind: SignerSourceKind::Stdin,
derivation_path: None,
legacy: false,
}
);
let stdin = "stdin:".to_string();
assert_matches!(
SignerSource::parse(stdin).unwrap(),
SignerSource {
kind: SignerSourceKind::Stdin,
derivation_path: None,
legacy: false,
}
);
assert_matches!(
SignerSource::parse(ASK_KEYWORD).unwrap(),
SignerSource {
kind: SignerSourceKind::Prompt,
derivation_path: None,
legacy: true,
}
);
let pubkey = Pubkey::new_unique();
assert!(
matches!(SignerSource::parse(pubkey.to_string()).unwrap(), SignerSource {
kind: SignerSourceKind::Pubkey(p),
derivation_path: None,
legacy: false,
}
if p == pubkey)
);

// Set up absolute and relative path strs
let file0 = NamedTempFile::new().unwrap();
let path = file0.path();
assert!(path.is_absolute());
let absolute_path_str = path.to_str().unwrap();

let file1 = NamedTempFile::new_in(std::env::current_dir().unwrap()).unwrap();
let path = file1.path().file_name().unwrap().to_str().unwrap();
let path = std::path::Path::new(path);
assert!(path.is_relative());
let relative_path_str = path.to_str().unwrap();

assert!(
matches!(SignerSource::parse(absolute_path_str).unwrap(), SignerSource {
kind: SignerSourceKind::Filepath(p),
derivation_path: None,
legacy: false,
} if p == absolute_path_str)
);
assert!(
matches!(SignerSource::parse(relative_path_str).unwrap(), SignerSource {
kind: SignerSourceKind::Filepath(p),
derivation_path: None,
legacy: false,
} if p == relative_path_str)
);

let usb = "usb://ledger".to_string();
let expected_locator = RemoteWalletLocator {
manufacturer: Manufacturer::Ledger,
pubkey: None,
};
assert_matches!(SignerSource::parse(usb).unwrap(), SignerSource {
kind: SignerSourceKind::Usb(u),
derivation_path: None,
legacy: false,
} if u == expected_locator);
let usb = "usb://ledger?key=0/0".to_string();
let expected_locator = RemoteWalletLocator {
manufacturer: Manufacturer::Ledger,
pubkey: None,
};
let expected_derivation_path = Some(DerivationPath::new_bip44(Some(0), Some(0)));
assert_matches!(SignerSource::parse(usb).unwrap(), SignerSource {
kind: SignerSourceKind::Usb(u),
derivation_path: d,
legacy: false,
} if u == expected_locator && d == expected_derivation_path);
// Catchall into SignerSource::Filepath fails
let junk = "sometextthatisnotapubkeyorfile".to_string();
assert!(Pubkey::from_str(&junk).is_err());
assert_matches!(
SignerSource::parse(&junk),
Err(SignerSourceError::IoError(_))
);

let prompt = "prompt:".to_string();
assert_matches!(
SignerSource::parse(prompt).unwrap(),
SignerSource {
kind: SignerSourceKind::Prompt,
derivation_path: None,
legacy: false,
}
);
assert!(
matches!(SignerSource::parse(format!("file:{absolute_path_str}")).unwrap(), SignerSource {
kind: SignerSourceKind::Filepath(p),
derivation_path: None,
legacy: false,
} if p == absolute_path_str)
);
assert!(
matches!(SignerSource::parse(format!("file:{relative_path_str}")).unwrap(), SignerSource {
kind: SignerSourceKind::Filepath(p),
derivation_path: None,
legacy: false,
} if p == relative_path_str)
);
}

fn app<'ab>() -> Command<'ab> {
Command::new("test")
.arg(
Expand Down
9 changes: 6 additions & 3 deletions clap-v3-utils/src/input_validators.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use {
crate::keypair::{parse_signer_source, SignerSourceKind, ASK_KEYWORD},
crate::{
input_parsers::signer::{SignerSource, SignerSourceKind},
keypair::ASK_KEYWORD,
},
chrono::DateTime,
solana_sdk::{
clock::{Epoch, Slot},
Expand Down Expand Up @@ -119,7 +122,7 @@ pub fn is_prompt_signer_source(string: &str) -> Result<(), String> {
if string == ASK_KEYWORD {
return Ok(());
}
match parse_signer_source(string)
match SignerSource::parse(string)
.map_err(|err| format!("{err}"))?
.kind
{
Expand Down Expand Up @@ -154,7 +157,7 @@ pub fn is_valid_pubkey<T>(string: T) -> Result<(), String>
where
T: AsRef<str> + Display,
{
match parse_signer_source(string.as_ref())
match SignerSource::parse(string.as_ref())
.map_err(|err| format!("{err}"))?
.kind
{
Expand Down
Loading
Loading