keyfork/keyfork-derive-util/src/index.rs

133 lines
3.7 KiB
Rust

use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Errors associated with creating a [`DerivationIndex`].
#[derive(Error, Debug)]
pub enum Error {
/// The index was too large and should be less than 2^31.
#[error("Index is too large, must be less than 0x80000000: {0}")]
IndexTooLarge(u32),
/// An integer could not be parsed from the string.
#[error("Unable to parse integer for index")]
IntParseError(#[from] std::num::ParseIntError),
}
type Result<T, E = Error> = std::result::Result<T, E>;
/// Index for a given extended private key.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct DerivationIndex(pub(crate) u32);
impl DerivationIndex {
/// Creates a new [`DerivationIndex`].
///
/// # Errors
///
/// Returns an error if the index is larger than the hardened flag.
pub fn new(index: u32, hardened: bool) -> Result<Self> {
if index & (0b1 << 31) > 0 {
return Err(Error::IndexTooLarge(index));
}
Ok(Self(index | (u32::from(hardened) << 31)))
}
/*
* Probably never used.
pub(crate) fn from_bytes(bytes: [u8; 4]) -> Self {
Self(u32::from_be_bytes(bytes))
}
*/
pub(crate) fn to_bytes(&self) -> [u8; 4] {
self.0.to_be_bytes()
}
/// Whether or not the index is hardened, allowing deriving the key from a known parent key.
pub fn is_hardened(&self) -> bool {
self.0 & (0b1 << 31) != 0
}
}
impl std::str::FromStr for DerivationIndex {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
// Returns &str without suffix if suffix is found
let (s, is_hardened) = match s.strip_suffix('\'') {
Some(subslice) => (subslice, true),
None => (s, false),
};
let index: u32 = s.parse()?;
Self::new(index, is_hardened)
}
}
impl std::fmt::Display for DerivationIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0 & (u32::MAX >> 1))?;
if self.0 & (0b1 << 31) != 0 {
write!(f, "'")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
#[should_panic]
fn fails_on_high_index() {
DerivationIndex::new(0x80000001, false).unwrap();
}
#[test]
fn has_hardened_bit() {
assert_eq!(DerivationIndex::new(0x0, true).unwrap().0, 0b1 << 31);
}
#[test]
fn misc_values() -> Result<()> {
assert_eq!(DerivationIndex::new(0x80000000 - 1, true)?.0, u32::MAX);
assert_eq!(DerivationIndex::new(0x2, false)?.0, 2);
assert_eq!(DerivationIndex::new(0x00ABCDEF, true)?.0, 0x80ABCDEF);
assert_eq!(DerivationIndex::new(0x00ABCDEF, false)?.0, 0x00ABCDEF);
Ok(())
}
#[test]
fn from_str() -> Result<()> {
assert_eq!(DerivationIndex::from_str("100000")?.0, 100000);
assert_eq!(
DerivationIndex::from_str("100000'")?.0,
(0b1 << 31) + 100000
);
Ok(())
}
#[test]
fn display() -> Result<()> {
assert_eq!(&DerivationIndex::new(3232, false)?.to_string(), "3232");
assert_eq!(&DerivationIndex::new(3232, true)?.to_string(), "3232'");
Ok(())
}
#[test]
fn equivalency() -> Result<()> {
let values = ["123456'", "123456", "1726562", "0'", "0"];
for value in values {
assert_eq!(value, DerivationIndex::from_str(value)?.to_string());
}
Ok(())
}
#[test]
#[should_panic]
fn from_str_fails_on_high_index() {
DerivationIndex::from_str(&0x80000001u32.to_string()).unwrap();
}
}