use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FormatResult;
use bevy::ecs::reflect::ReflectResource;
use bevy::prelude::Reflect;
use bevy::prelude::Resource;
use bevy::reflect::ReflectDeserialize;
use bevy::reflect::ReflectSerialize;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error as DeserializeError;
use thiserror::Error;
use super::identity::DeviceIdSource;
use super::identity::DeviceKey;
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Reflect)]
#[reflect(opaque)]
#[reflect(Serialize, Deserialize)]
pub struct SchemeName(String);
impl SchemeName {
pub fn new(value: impl Into<String>) -> Result<Self, SchemeNameError> {
let value = value.into();
if value.is_empty() {
return Err(SchemeNameError::Empty);
}
if !has_valid_scheme_syntax(&value) {
return Err(SchemeNameError::InvalidSyntax);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str { &self.0 }
}
impl Display for SchemeName {
fn fmt(&self, formatter: &mut Formatter<'_>) -> FormatResult { formatter.write_str(&self.0) }
}
impl<'de> Deserialize<'de> for SchemeName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(<D::Error as DeserializeError>::custom)
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum SchemeNameError {
#[error("scheme names must not be empty")]
Empty,
#[error("scheme names must use lowercase ASCII letters, digits, and single hyphens")]
InvalidSyntax,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Reflect)]
#[reflect(opaque)]
#[reflect(Serialize, Deserialize)]
pub struct ReportedId(String);
impl ReportedId {
pub fn new(value: impl Into<String>) -> Result<Self, ReportedIdError> {
let value = value.into();
if value.is_empty() {
return Err(ReportedIdError::Empty);
}
if value.chars().any(char::is_control) {
return Err(ReportedIdError::ContainsControlCharacter);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str { &self.0 }
}
impl<'de> Deserialize<'de> for ReportedId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(<D::Error as DeserializeError>::custom)
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum ReportedIdError {
#[error("reported identifiers must not be empty")]
Empty,
#[error("reported identifiers must not contain control characters")]
ContainsControlCharacter,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Reflect)]
#[reflect(opaque)]
#[reflect(Serialize, Deserialize)]
pub struct AuthoredId(String);
impl AuthoredId {
pub fn new(value: impl Into<String>) -> Result<Self, AuthoredIdError> {
let value = value.into();
if value.is_empty() {
return Err(AuthoredIdError::Empty);
}
if value.chars().any(char::is_control) {
return Err(AuthoredIdError::ContainsControlCharacter);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str { &self.0 }
}
impl<'de> Deserialize<'de> for AuthoredId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(<D::Error as DeserializeError>::custom)
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum AuthoredIdError {
#[error("authored identifiers must not be empty")]
Empty,
#[error("authored identifiers must not contain control characters")]
ContainsControlCharacter,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, Reflect)]
#[reflect(opaque)]
#[reflect(Serialize, Deserialize)]
pub struct Digest(u64);
impl Digest {
#[must_use]
pub const fn new(value: u64) -> Self { Self(value) }
}
#[derive(Debug, Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct RegisteredSchemes {
names: Vec<SchemeName>,
}
impl RegisteredSchemes {
pub fn register(&mut self, name: SchemeName) {
if !self.names.contains(&name) {
self.names.push(name);
}
}
#[must_use]
pub fn contains(&self, name: &SchemeName) -> bool { self.names.contains(name) }
#[cfg(test)]
pub(crate) const fn count(&self) -> usize { self.names.len() }
pub fn validate(&self, key: &DeviceKey) -> Result<(), UnregisteredSchemeError> {
let DeviceIdSource::Reported { scheme, .. } = &key.id else {
return Ok(());
};
if self.contains(scheme) {
Ok(())
} else {
Err(UnregisteredSchemeError {
scheme: scheme.clone(),
})
}
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("device key uses unregistered scheme `{scheme}`")]
pub struct UnregisteredSchemeError {
scheme: SchemeName,
}
impl UnregisteredSchemeError {
#[must_use]
pub(crate) const fn scheme(&self) -> &SchemeName { &self.scheme }
}
fn has_valid_scheme_syntax(value: &str) -> bool {
!value.starts_with('-')
&& !value.ends_with('-')
&& !value.contains("--")
&& value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
}
#[cfg(test)]
mod tests {
use std::error::Error;
use bevy::reflect::PartialReflect;
use bevy::reflect::tuple_struct::DynamicTupleStruct;
use super::RegisteredSchemes;
use super::SchemeName;
use crate::DeviceIdSource;
use crate::DeviceKey;
use crate::DeviceKind;
use crate::ReportedId;
#[test]
fn malformed_scheme_name_fails_ron_deserialization() {
assert!(ron::from_str::<SchemeName>("\"EDID-SERIAL\"").is_err());
}
#[test]
fn empty_reported_id_fails_ron_deserialization() {
assert!(ron::from_str::<ReportedId>("\"\"").is_err());
}
#[test]
fn reported_id_with_control_character_fails_ron_deserialization() {
assert!(ron::from_str::<ReportedId>(r#""device\nserial""#).is_err());
}
#[test]
fn reflection_rejects_malformed_identity_text() -> Result<(), Box<dyn Error>> {
let mut malformed_scheme = DynamicTupleStruct::default();
malformed_scheme.insert(String::from("EDID-SERIAL"));
let mut scheme = SchemeName::new("edid-serial")?;
assert!(scheme.try_apply(&malformed_scheme).is_err());
assert_eq!(scheme.as_str(), "edid-serial");
let mut malformed_reported_id = DynamicTupleStruct::default();
malformed_reported_id.insert(String::from("device\nserial"));
let mut reported_id = ReportedId::new("DELL-U2723QE-9J4K2H3")?;
assert!(reported_id.try_apply(&malformed_reported_id).is_err());
assert_eq!(reported_id.as_str(), "DELL-U2723QE-9J4K2H3");
Ok(())
}
#[test]
fn unregistered_scheme_fails_startup_validation() -> Result<(), Box<dyn Error>> {
let key = reported_display_key("edid-serial")?;
assert!(RegisteredSchemes::default().validate(&key).is_err());
Ok(())
}
#[test]
fn duplicate_scheme_registration_accepts_co_reporting_providers() -> Result<(), Box<dyn Error>>
{
let scheme = SchemeName::new("edid-serial")?;
let mut schemes = RegisteredSchemes::default();
schemes.register(scheme.clone());
schemes.register(scheme);
assert!(
schemes
.validate(&reported_display_key("edid-serial")?)
.is_ok()
);
Ok(())
}
fn reported_display_key(scheme: &str) -> Result<DeviceKey, Box<dyn Error>> {
Ok(DeviceKey {
kind: DeviceKind::Display,
id: DeviceIdSource::Reported {
scheme: SchemeName::new(scheme)?,
value: ReportedId::new("DELL-U2723QE-9J4K2H3")?,
},
})
}
}