use snafu::ensure;
use crate::Result;
use crate::error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Share {
scheme: String,
bytes: Vec<u8>,
}
impl Drop for Share {
fn drop(&mut self) {
use zeroize::Zeroize;
self.bytes.zeroize();
}
}
impl Share {
pub fn new(scheme: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
Share {
scheme: scheme.into(),
bytes: bytes.into(),
}
}
pub fn scheme(&self) -> &str {
&self.scheme
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn into_bytes(self) -> Vec<u8> {
self.bytes.clone()
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub fn assert_scheme(&self, expected: &str) -> Result<()> {
ensure!(
self.scheme == expected,
error::ShareSchemeMismatchSnafu {
expected,
actual: self.scheme.as_str(),
}
);
Ok(())
}
pub fn to_bytes(&self) -> Vec<u8> {
let scheme_bytes = self.scheme.as_bytes();
let mut out = Vec::with_capacity(4 + scheme_bytes.len() + self.bytes.len());
out.extend_from_slice(&(scheme_bytes.len() as u32).to_be_bytes());
out.extend_from_slice(scheme_bytes);
out.extend_from_slice(&self.bytes);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(error::ShareTruncatedSnafu {
needed: 4usize,
have: data.len(),
}
.build());
}
let scheme_len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
let scheme_end = 4usize.checked_add(scheme_len).ok_or_else(|| {
error::ShareTruncatedSnafu {
needed: 4 + scheme_len,
have: data.len(),
}
.build()
})?;
if data.len() < scheme_end {
return Err(error::ShareTruncatedSnafu {
needed: scheme_end,
have: data.len(),
}
.build());
}
let scheme = std::str::from_utf8(&data[4..scheme_end])
.map_err(|_| error::ShareInvalidSchemeSnafu {}.build())?
.to_string();
let bytes = data[scheme_end..].to_vec();
Ok(Share { scheme, bytes })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn share_round_trip() {
let original = Share::new("FROST-ed25519", vec![0xDE, 0xAD, 0xBE, 0xEF]);
let encoded = original.to_bytes();
let decoded = Share::from_bytes(&encoded).expect("decode succeeds");
assert_eq!(original, decoded);
}
#[test]
fn share_round_trip_preserves_scheme_name() {
let original = Share::new("GG18-ECDSA-P256", vec![1, 2, 3, 4, 5]);
let encoded = original.to_bytes();
let decoded = Share::from_bytes(&encoded).expect("decode succeeds");
assert_eq!(decoded.scheme(), "GG18-ECDSA-P256");
assert_eq!(decoded.bytes(), &[1, 2, 3, 4, 5]);
}
#[test]
fn share_round_trip_empty_bytes() {
let original = Share::new("test-scheme", Vec::new());
let encoded = original.to_bytes();
let decoded = Share::from_bytes(&encoded).expect("decode succeeds");
assert_eq!(original, decoded);
assert!(decoded.is_empty());
}
#[test]
fn share_from_bytes_rejects_truncated_header() {
let err = Share::from_bytes(&[1, 2]).unwrap_err();
assert!(matches!(err, error::Error::ShareTruncated { .. }));
}
#[test]
fn share_from_bytes_rejects_truncated_scheme() {
let data = [0, 0, 0, 10, b'a', b'b'];
let err = Share::from_bytes(&data).unwrap_err();
assert!(matches!(err, error::Error::ShareTruncated { .. }));
}
#[test]
fn share_assert_scheme_accepts_match() {
let share = Share::new("FROST-ed25519", vec![1]);
share.assert_scheme("FROST-ed25519").expect("match is ok");
}
#[test]
fn share_assert_scheme_rejects_mismatch() {
let share = Share::new("FROST-ed25519", vec![1]);
let err = share.assert_scheme("GG18-ECDSA-P256").unwrap_err();
assert!(matches!(err, error::Error::ShareSchemeMismatch { .. }));
}
#[test]
fn share_into_bytes_consumes_payload() {
let share = Share::new("s", vec![9, 9, 9]);
let bytes = share.into_bytes();
assert_eq!(bytes, vec![9, 9, 9]);
}
}