use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct BenchPlan {
#[serde(default, rename = "instruction")]
pub instructions: Vec<InstructionFixture>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstructionFixture {
pub scenario: String,
pub program_id: String,
#[serde(default)]
pub data: String,
#[serde(default, rename = "account")]
pub accounts: Vec<AccountFixture>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AccountFixture {
pub pubkey: String,
#[serde(default)]
pub signer: bool,
#[serde(default)]
pub writable: bool,
#[serde(default)]
pub lamports: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
}
impl BenchPlan {
pub fn from_toml(s: &str) -> Result<Self> {
let plan: BenchPlan = toml::from_str(s).map_err(|e| Error::Config(e.to_string()))?;
plan.validate()?;
Ok(plan)
}
pub fn validate(&self) -> Result<()> {
if self.instructions.is_empty() {
return Err(Error::Config(
"bench plan has no `[[instruction]]` entries".to_string(),
));
}
for ix in &self.instructions {
ix.validate()?;
}
Ok(())
}
}
impl InstructionFixture {
fn validate(&self) -> Result<()> {
let ctx = format!("instruction `{}`", self.scenario);
if self.scenario.is_empty() {
return Err(Error::Config(
"an instruction has an empty `scenario`".to_string(),
));
}
validate_base58(&self.program_id, &format!("{ctx}: program_id"))?;
validate_hex(&self.data, &format!("{ctx}: data"))?;
for acc in &self.accounts {
validate_base58(&acc.pubkey, &format!("{ctx}: account pubkey"))?;
if let Some(owner) = &acc.owner {
validate_base58(owner, &format!("{ctx}: account owner"))?;
}
if let Some(data) = &acc.data {
validate_hex(data, &format!("{ctx}: account data"))?;
}
}
Ok(())
}
}
const BASE58_ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
fn validate_base58(s: &str, what: &str) -> Result<()> {
if !(32..=44).contains(&s.len()) {
return Err(Error::Config(format!(
"{what}: `{s}` is not a 32-byte base58 address (length {})",
s.len()
)));
}
if let Some(bad) = s.bytes().find(|b| !BASE58_ALPHABET.contains(b)) {
return Err(Error::Config(format!(
"{what}: `{s}` contains a non-base58 character `{}`",
bad as char
)));
}
Ok(())
}
fn validate_hex(s: &str, what: &str) -> Result<()> {
if s.len() % 2 != 0 {
return Err(Error::Config(format!(
"{what}: hex string has an odd length ({})",
s.len()
)));
}
if let Some(bad) = s.bytes().find(|b| !b.is_ascii_hexdigit()) {
return Err(Error::Config(format!(
"{what}: `{s}` contains a non-hex character `{}`",
bad as char
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const SYS: &str = "11111111111111111111111111111111";
fn plan_toml() -> String {
format!(
"[[instruction]]\nscenario=\"swap\"\nprogram_id=\"{SYS}\"\ndata=\"01ab\"\n\
[[instruction.account]]\npubkey=\"{SYS}\"\nsigner=true\nwritable=true\nlamports=1000000\n"
)
}
#[test]
fn parses_and_validates_a_plan() {
let plan = BenchPlan::from_toml(&plan_toml()).unwrap();
assert_eq!(plan.instructions.len(), 1);
let ix = &plan.instructions[0];
assert_eq!(ix.scenario, "swap");
assert_eq!(ix.data, "01ab");
assert_eq!(ix.accounts.len(), 1);
assert!(ix.accounts[0].signer && ix.accounts[0].writable);
}
#[test]
fn empty_plan_is_rejected() {
assert!(BenchPlan::from_toml("").is_err());
}
#[test]
fn rejects_unknown_keys() {
let toml = format!("[[instruction]]\nscenario=\"s\"\nprogram_id=\"{SYS}\"\nbogus=1\n");
assert!(BenchPlan::from_toml(&toml).is_err());
}
#[test]
fn rejects_bad_base58_program_id() {
let toml = "[[instruction]]\nscenario=\"s\"\nprogram_id=\"not-base58-0OIl\"\n";
let err = BenchPlan::from_toml(toml).unwrap_err().to_string();
assert!(err.contains("base58"), "{err}");
}
#[test]
fn rejects_odd_and_nonhex_data() {
let odd = format!("[[instruction]]\nscenario=\"s\"\nprogram_id=\"{SYS}\"\ndata=\"abc\"\n");
assert!(
BenchPlan::from_toml(&odd)
.unwrap_err()
.to_string()
.contains("odd")
);
let nonhex =
format!("[[instruction]]\nscenario=\"s\"\nprogram_id=\"{SYS}\"\ndata=\"zz\"\n");
assert!(
BenchPlan::from_toml(&nonhex)
.unwrap_err()
.to_string()
.contains("non-hex")
);
}
}