stronger parsing for signal trust set
This commit is contained in:
@@ -196,8 +196,8 @@ signal_cli_tcp_addr = "127.0.0.1:7583"
|
|||||||
signal_cli_retry_delay = "10s"
|
signal_cli_retry_delay = "10s"
|
||||||
|
|
||||||
[signal_admins]
|
[signal_admins]
|
||||||
"abc-123-uuid" = ["12345 67890 12345 67890 12345 67890"]
|
"12345678-1234-1234-1234-123456789abc" = ["12345 67890 12345 67890 12345 67890 12345 67890 12345 67890 12345 67890"]
|
||||||
"def-456-uuid" = []
|
"abcdef12-abcd-abcd-abcd-abcdef123456" = []
|
||||||
|
|
||||||
[syslog]
|
[syslog]
|
||||||
listen_addr = "0.0.0.0:1514"
|
listen_addr = "0.0.0.0:1514"
|
||||||
@@ -250,7 +250,7 @@ limits = [
|
|||||||
config
|
config
|
||||||
.gateway
|
.gateway
|
||||||
.signal_admins
|
.signal_admins
|
||||||
.get("abc-123-uuid")
|
.get("12345678-1234-1234-1234-123456789abc")
|
||||||
.is_some()
|
.is_some()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ impl Gateway {
|
|||||||
if let Some(group_id) = &self.config.alert_group_id {
|
if let Some(group_id) = &self.config.alert_group_id {
|
||||||
MessageTarget::Group(group_id.clone())
|
MessageTarget::Group(group_id.clone())
|
||||||
} else {
|
} else {
|
||||||
MessageTarget::Recipients(self.config.signal_admins.uuids().cloned().collect())
|
MessageTarget::Recipients(self.config.signal_admins.uuids().map(str::to_owned).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,10 +7,169 @@
|
|||||||
use crate::signal_jsonrpc::{Envelope, Identity, RpcClient};
|
use crate::signal_jsonrpc::{Envelope, Identity, RpcClient};
|
||||||
use serde::de::{MapAccess, SeqAccess, Visitor};
|
use serde::de::{MapAccess, SeqAccess, Visitor};
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
use std::borrow::Borrow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::str::FromStr;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
/// A validated Signal UUID in the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
|
||||||
|
#[serde(try_from = "String")]
|
||||||
|
pub struct Uuid(String);
|
||||||
|
|
||||||
|
impl FromStr for Uuid {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
// UUID format: 8-4-4-4-12 hex chars (36 chars total with dashes)
|
||||||
|
if s.len() != 36 {
|
||||||
|
return Err(format!("UUID must be 36 characters, got {}", s.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts: Vec<&str> = s.split('-').collect();
|
||||||
|
if parts.len() != 5 {
|
||||||
|
return Err(format!(
|
||||||
|
"UUID must have 5 dash-separated parts, got {}",
|
||||||
|
parts.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected_lens = [8, 4, 4, 4, 12];
|
||||||
|
for (i, (part, &expected)) in parts.iter().zip(&expected_lens).enumerate() {
|
||||||
|
if part.len() != expected {
|
||||||
|
return Err(format!(
|
||||||
|
"UUID part {} has wrong length: expected {}, got {}",
|
||||||
|
i + 1,
|
||||||
|
expected,
|
||||||
|
part.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !part.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
return Err(format!("UUID part {} contains non-hex characters", i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Uuid(s.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for Uuid {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
s.parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Uuid {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
|
s.parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Uuid {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for Uuid {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Borrow<str> for Uuid {
|
||||||
|
fn borrow(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Uuid {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A validated Signal safety number (60 digits, optionally separated by whitespace).
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
|
||||||
|
#[serde(try_from = "String")]
|
||||||
|
pub struct SafetyNumber(String);
|
||||||
|
|
||||||
|
impl FromStr for SafetyNumber {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
// Safety number format: 60 digits, optionally grouped with whitespace
|
||||||
|
let mut digit_count = 0;
|
||||||
|
for c in s.chars() {
|
||||||
|
if c.is_ascii_digit() {
|
||||||
|
digit_count += 1;
|
||||||
|
} else if !c.is_whitespace() {
|
||||||
|
return Err(format!(
|
||||||
|
"Safety number must contain only digits and whitespace, found '{c}'"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if digit_count != 60 {
|
||||||
|
return Err(format!(
|
||||||
|
"Safety number must contain exactly 60 digits, got {digit_count}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SafetyNumber(s.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for SafetyNumber {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
s.parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for SafetyNumber {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
|
s.parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SafetyNumber {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for SafetyNumber {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Borrow<str> for SafetyNumber {
|
||||||
|
fn borrow(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SafetyNumber {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A set of Signal UUIDs with optional safety numbers for trust verification.
|
/// A set of Signal UUIDs with optional safety numbers for trust verification.
|
||||||
///
|
///
|
||||||
/// Can be deserialized from either:
|
/// Can be deserialized from either:
|
||||||
@@ -18,7 +177,7 @@ use tracing::{debug, info, warn};
|
|||||||
/// - A sequence of UUIDs (no safety numbers): `["uuid1", "uuid2"]`
|
/// - A sequence of UUIDs (no safety numbers): `["uuid1", "uuid2"]`
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct SignalTrustSet {
|
pub struct SignalTrustSet {
|
||||||
map: HashMap<String, Vec<String>>,
|
map: HashMap<Uuid, Vec<SafetyNumber>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignalTrustSet {
|
impl SignalTrustSet {
|
||||||
@@ -32,12 +191,12 @@ impl SignalTrustSet {
|
|||||||
/// Currently checks if the source UUID is in the trust set.
|
/// Currently checks if the source UUID is in the trust set.
|
||||||
/// Will eventually also verify safety numbers.
|
/// Will eventually also verify safety numbers.
|
||||||
pub fn is_trusted(&self, envelope: &Envelope) -> bool {
|
pub fn is_trusted(&self, envelope: &Envelope) -> bool {
|
||||||
self.map.contains_key(&envelope.source_uuid)
|
self.map.contains_key(envelope.source_uuid.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all UUIDs as an iterator.
|
/// Get all UUIDs as an iterator of string slices.
|
||||||
pub fn uuids(&self) -> impl Iterator<Item = &String> {
|
pub fn uuids(&self) -> impl Iterator<Item = &str> {
|
||||||
self.map.keys()
|
self.map.keys().map(|u| u.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of admin UUIDs.
|
/// Get the number of admin UUIDs.
|
||||||
@@ -51,12 +210,12 @@ impl SignalTrustSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over UUID and safety number pairs.
|
/// Iterate over UUID and safety number pairs.
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
|
pub fn iter(&self) -> impl Iterator<Item = (&Uuid, &Vec<SafetyNumber>)> {
|
||||||
self.map.iter()
|
self.map.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get safety numbers for a specific UUID.
|
/// Get safety numbers for a specific UUID.
|
||||||
pub fn get(&self, uuid: &str) -> Option<&Vec<String>> {
|
pub fn get(&self, uuid: &str) -> Option<&Vec<SafetyNumber>> {
|
||||||
self.map.get(uuid)
|
self.map.get(uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +239,12 @@ impl SignalTrustSet {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to check if a safety number string is in our configured list
|
||||||
|
let is_configured = |sn: &str| safety_numbers.iter().any(|s| s.as_ref() == sn);
|
||||||
|
|
||||||
// Get current identities from signal-cli
|
// Get current identities from signal-cli
|
||||||
let current_identities: Vec<Identity> = match signal_cli
|
let current_identities: Vec<Identity> = match signal_cli
|
||||||
.list_identities(Some(signal_account.to_owned()), Some(uuid.clone()))
|
.list_identities(Some(signal_account.to_owned()), Some(uuid.to_string()))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(value) => serde_json::from_value(value).unwrap_or_default(),
|
Ok(value) => serde_json::from_value(value).unwrap_or_default(),
|
||||||
@@ -93,14 +255,14 @@ impl SignalTrustSet {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if any trusted identity in signal-cli is NOT in our config
|
// Check if any trusted identity in signal-cli is NOT in our config
|
||||||
let has_revoked_identity = current_identities.iter().any(|id| {
|
let has_revoked_identity = current_identities
|
||||||
id.trust_level.is_trusted() && !safety_numbers.contains(&id.safety_number)
|
.iter()
|
||||||
});
|
.any(|id| id.trust_level.is_trusted() && !is_configured(&id.safety_number));
|
||||||
|
|
||||||
if has_revoked_identity {
|
if has_revoked_identity {
|
||||||
// Log which identities are being revoked
|
// Log which identities are being revoked
|
||||||
for id in ¤t_identities {
|
for id in ¤t_identities {
|
||||||
if id.trust_level.is_trusted() && !safety_numbers.contains(&id.safety_number) {
|
if id.trust_level.is_trusted() && !is_configured(&id.safety_number) {
|
||||||
warn!(
|
warn!(
|
||||||
"Revoking trust for {uuid}: safety number {} is trusted in signal-cli but not in config",
|
"Revoking trust for {uuid}: safety number {} is trusted in signal-cli but not in config",
|
||||||
id.safety_number
|
id.safety_number
|
||||||
@@ -113,7 +275,7 @@ impl SignalTrustSet {
|
|||||||
signal_cli
|
signal_cli
|
||||||
.remove_contact(
|
.remove_contact(
|
||||||
Some(signal_account.to_owned()),
|
Some(signal_account.to_owned()),
|
||||||
uuid.clone(),
|
uuid.to_string(),
|
||||||
true, // forget - delete identity keys and sessions
|
true, // forget - delete identity keys and sessions
|
||||||
false, // hide
|
false, // hide
|
||||||
)
|
)
|
||||||
@@ -126,9 +288,9 @@ impl SignalTrustSet {
|
|||||||
signal_cli
|
signal_cli
|
||||||
.trust(
|
.trust(
|
||||||
Some(signal_account.to_owned()),
|
Some(signal_account.to_owned()),
|
||||||
uuid.clone(),
|
uuid.to_string(),
|
||||||
false,
|
false,
|
||||||
Some(safety_number.clone()),
|
Some(safety_number.to_string()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Failed to trust {uuid}: {err}"))?;
|
.map_err(|err| format!("Failed to trust {uuid}: {err}"))?;
|
||||||
@@ -136,7 +298,7 @@ impl SignalTrustSet {
|
|||||||
|
|
||||||
// Verify the reset worked correctly
|
// Verify the reset worked correctly
|
||||||
let new_identities: Vec<Identity> = signal_cli
|
let new_identities: Vec<Identity> = signal_cli
|
||||||
.list_identities(Some(signal_account.to_owned()), Some(uuid.clone()))
|
.list_identities(Some(signal_account.to_owned()), Some(uuid.to_string()))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Failed to verify trust reset for {uuid}: {err}"))
|
.map_err(|err| format!("Failed to verify trust reset for {uuid}: {err}"))
|
||||||
.and_then(|value| {
|
.and_then(|value| {
|
||||||
@@ -144,15 +306,15 @@ impl SignalTrustSet {
|
|||||||
.map_err(|err| format!("Failed to parse identities for {uuid}: {err}"))
|
.map_err(|err| format!("Failed to parse identities for {uuid}: {err}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let trusted_now: Vec<_> = new_identities
|
let trusted_now: Vec<&str> = new_identities
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|id| id.trust_level.is_trusted())
|
.filter(|id| id.trust_level.is_trusted())
|
||||||
.map(|id| &id.safety_number)
|
.map(|id| id.safety_number.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Check all configured safety numbers are now trusted
|
// Check all configured safety numbers are now trusted
|
||||||
for safety_number in safety_numbers {
|
for safety_number in safety_numbers {
|
||||||
if !trusted_now.contains(&safety_number) {
|
if !trusted_now.contains(&safety_number.as_ref()) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Verification failed for {uuid}: safety number {} should be trusted but isn't",
|
"Verification failed for {uuid}: safety number {} should be trusted but isn't",
|
||||||
safety_number
|
safety_number
|
||||||
@@ -162,7 +324,7 @@ impl SignalTrustSet {
|
|||||||
|
|
||||||
// Check no unexpected safety numbers are trusted
|
// Check no unexpected safety numbers are trusted
|
||||||
for sn in &trusted_now {
|
for sn in &trusted_now {
|
||||||
if !safety_numbers.contains(sn) {
|
if !is_configured(sn) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Verification failed for {uuid}: safety number {} is trusted but not in config",
|
"Verification failed for {uuid}: safety number {} is trusted but not in config",
|
||||||
sn
|
sn
|
||||||
@@ -176,21 +338,21 @@ impl SignalTrustSet {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Just add any new safety numbers that aren't already trusted
|
// Just add any new safety numbers that aren't already trusted
|
||||||
let already_trusted: Vec<_> = current_identities
|
let already_trusted: Vec<&str> = current_identities
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|id| id.trust_level.is_trusted())
|
.filter(|id| id.trust_level.is_trusted())
|
||||||
.map(|id| &id.safety_number)
|
.map(|id| id.safety_number.as_str())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for safety_number in safety_numbers {
|
for safety_number in safety_numbers {
|
||||||
if !already_trusted.contains(&safety_number) {
|
if !already_trusted.contains(&safety_number.as_ref()) {
|
||||||
info!("Trusting new safety number for {uuid}");
|
info!("Trusting new safety number for {uuid}");
|
||||||
signal_cli
|
signal_cli
|
||||||
.trust(
|
.trust(
|
||||||
Some(signal_account.to_owned()),
|
Some(signal_account.to_owned()),
|
||||||
uuid.clone(),
|
uuid.to_string(),
|
||||||
false,
|
false,
|
||||||
Some(safety_number.clone()),
|
Some(safety_number.to_string()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Failed to trust {uuid}: {err}"))?;
|
.map_err(|err| format!("Failed to trust {uuid}: {err}"))?;
|
||||||
@@ -226,7 +388,7 @@ impl<'de> Visitor<'de> for SignalTrustSetVisitor {
|
|||||||
M: MapAccess<'de>,
|
M: MapAccess<'de>,
|
||||||
{
|
{
|
||||||
let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0));
|
let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0));
|
||||||
while let Some((key, value)) = access.next_entry::<String, Vec<String>>()? {
|
while let Some((key, value)) = access.next_entry::<Uuid, Vec<SafetyNumber>>()? {
|
||||||
map.insert(key, value);
|
map.insert(key, value);
|
||||||
}
|
}
|
||||||
Ok(SignalTrustSet { map })
|
Ok(SignalTrustSet { map })
|
||||||
@@ -237,23 +399,23 @@ impl<'de> Visitor<'de> for SignalTrustSetVisitor {
|
|||||||
S: SeqAccess<'de>,
|
S: SeqAccess<'de>,
|
||||||
{
|
{
|
||||||
let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0));
|
let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0));
|
||||||
while let Some(uuid) = access.next_element::<String>()? {
|
while let Some(uuid) = access.next_element::<Uuid>()? {
|
||||||
map.insert(uuid, Vec::new());
|
map.insert(uuid, Vec::new());
|
||||||
}
|
}
|
||||||
Ok(SignalTrustSet { map })
|
Ok(SignalTrustSet { map })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromIterator<String> for SignalTrustSet {
|
impl FromIterator<Uuid> for SignalTrustSet {
|
||||||
fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
|
fn from_iter<I: IntoIterator<Item = Uuid>>(iter: I) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map: iter.into_iter().map(|uuid| (uuid, Vec::new())).collect(),
|
map: iter.into_iter().map(|uuid| (uuid, Vec::new())).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromIterator<(String, Vec<String>)> for SignalTrustSet {
|
impl FromIterator<(Uuid, Vec<SafetyNumber>)> for SignalTrustSet {
|
||||||
fn from_iter<I: IntoIterator<Item = (String, Vec<String>)>>(iter: I) -> Self {
|
fn from_iter<I: IntoIterator<Item = (Uuid, Vec<SafetyNumber>)>>(iter: I) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map: iter.into_iter().collect(),
|
map: iter.into_iter().collect(),
|
||||||
}
|
}
|
||||||
@@ -261,8 +423,8 @@ impl FromIterator<(String, Vec<String>)> for SignalTrustSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> IntoIterator for &'a SignalTrustSet {
|
impl<'a> IntoIterator for &'a SignalTrustSet {
|
||||||
type Item = (&'a String, &'a Vec<String>);
|
type Item = (&'a Uuid, &'a Vec<SafetyNumber>);
|
||||||
type IntoIter = std::collections::hash_map::Iter<'a, String, Vec<String>>;
|
type IntoIter = std::collections::hash_map::Iter<'a, Uuid, Vec<SafetyNumber>>;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
self.map.iter()
|
self.map.iter()
|
||||||
@@ -273,31 +435,61 @@ impl<'a> IntoIterator for &'a SignalTrustSet {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
const UUID1: &str = "12345678-1234-1234-1234-123456789abc";
|
||||||
|
const UUID2: &str = "abcdef12-abcd-abcd-abcd-abcdef123456";
|
||||||
|
const UUID3: &str = "00000000-0000-0000-0000-000000000000";
|
||||||
|
const SAFETY1: &str = "123456789012345678901234567890123456789012345678901234567890";
|
||||||
|
const SAFETY2: &str = "098765432109876543210987654321098765432109876543210987654321";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uuid_validation() {
|
||||||
|
assert!(UUID1.parse::<Uuid>().is_ok());
|
||||||
|
assert!("not-a-uuid".parse::<Uuid>().is_err());
|
||||||
|
assert!("12345678-1234-1234-1234-12345678".parse::<Uuid>().is_err()); // too short
|
||||||
|
assert!("12345678-1234-1234-1234-123456789abcdef".parse::<Uuid>().is_err()); // too long
|
||||||
|
assert!("12345678-1234-1234-1234-123456789xyz".parse::<Uuid>().is_err()); // non-hex
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_safety_number_validation() {
|
||||||
|
assert!(SAFETY1.parse::<SafetyNumber>().is_ok());
|
||||||
|
// With whitespace (common format)
|
||||||
|
assert!("12345 67890 12345 67890 12345 67890 12345 67890 12345 67890 12345 67890"
|
||||||
|
.parse::<SafetyNumber>()
|
||||||
|
.is_ok());
|
||||||
|
assert!("12345".parse::<SafetyNumber>().is_err()); // too short
|
||||||
|
assert!("12345678901234567890123456789012345678901234567890123456789x"
|
||||||
|
.parse::<SafetyNumber>()
|
||||||
|
.is_err()); // non-digit
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize_map() {
|
fn test_deserialize_map() {
|
||||||
let json = r#"{"uuid1": ["safety1", "safety2"], "uuid2": []}"#;
|
let json = format!(
|
||||||
let trust_set: SignalTrustSet = serde_json::from_str(json).unwrap();
|
r#"{{"{UUID1}": ["{SAFETY1}", "{SAFETY2}"], "{UUID2}": []}}"#
|
||||||
|
);
|
||||||
|
let trust_set: SignalTrustSet = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
assert_eq!(trust_set.len(), 2);
|
assert_eq!(trust_set.len(), 2);
|
||||||
assert!(trust_set.get("uuid1").is_some());
|
assert!(trust_set.get(UUID1).is_some());
|
||||||
assert!(trust_set.get("uuid2").is_some());
|
assert!(trust_set.get(UUID2).is_some());
|
||||||
assert_eq!(trust_set.get("uuid1").unwrap(), &vec!["safety1", "safety2"]);
|
assert_eq!(trust_set.get(UUID1).unwrap().len(), 2);
|
||||||
assert_eq!(trust_set.get("uuid2").unwrap(), &Vec::<String>::new());
|
assert!(trust_set.get(UUID2).unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize_seq() {
|
fn test_deserialize_seq() {
|
||||||
let json = r#"["uuid1", "uuid2", "uuid3"]"#;
|
let json = format!(r#"["{UUID1}", "{UUID2}", "{UUID3}"]"#);
|
||||||
let trust_set: SignalTrustSet = serde_json::from_str(json).unwrap();
|
let trust_set: SignalTrustSet = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
assert_eq!(trust_set.len(), 3);
|
assert_eq!(trust_set.len(), 3);
|
||||||
assert!(trust_set.get("uuid1").is_some());
|
assert!(trust_set.get(UUID1).is_some());
|
||||||
assert!(trust_set.get("uuid2").is_some());
|
assert!(trust_set.get(UUID2).is_some());
|
||||||
assert!(trust_set.get("uuid3").is_some());
|
assert!(trust_set.get(UUID3).is_some());
|
||||||
// All should have empty safety numbers
|
// All should have empty safety numbers
|
||||||
assert!(trust_set.get("uuid1").unwrap().is_empty());
|
assert!(trust_set.get(UUID1).unwrap().is_empty());
|
||||||
assert!(trust_set.get("uuid2").unwrap().is_empty());
|
assert!(trust_set.get(UUID2).unwrap().is_empty());
|
||||||
assert!(trust_set.get("uuid3").unwrap().is_empty());
|
assert!(trust_set.get(UUID3).unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -313,4 +505,16 @@ mod tests {
|
|||||||
let uuids: SignalTrustSet = serde_json::from_str(json).unwrap();
|
let uuids: SignalTrustSet = serde_json::from_str(json).unwrap();
|
||||||
assert!(uuids.is_empty());
|
assert!(uuids.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_uuid_rejected() {
|
||||||
|
let json = r#"["not-a-valid-uuid"]"#;
|
||||||
|
assert!(serde_json::from_str::<SignalTrustSet>(json).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_safety_number_rejected() {
|
||||||
|
let json = format!(r#"{{"{UUID1}": ["invalid"]}}"#);
|
||||||
|
assert!(serde_json::from_str::<SignalTrustSet>(&json).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user