Skip to main content

rustev_contract/
profile.rs

1//! Profile documents and definition-independent validation (spec 034, 3.5).
2use crate::{
3    Document, DocumentError, Identified,
4    bounded::{ParseError, parse_bounded},
5    ids::ProfileId,
6    limits::DESCRIPTOR_V1,
7    parameter::MAX_PARAMETERS,
8    schema,
9    version::{Version, VersionRange},
10};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::{collections::BTreeMap, fmt};
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
16#[serde(deny_unknown_fields)]
17pub struct Profile {
18    pub schema: String,
19    pub profile: String,
20    pub version: String,
21    pub definition: ProfileDefinition,
22    pub extends: String,
23    pub values: BTreeMap<String, Value>,
24}
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct ProfileDefinition {
28    pub name: String,
29    pub version: String,
30}
31
32/// Names the invalid document member, for core's `invalid-profile` refusal.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ProfileError {
35    pub member: &'static str,
36}
37impl fmt::Display for ProfileError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "invalid profile member {}", self.member)
40    }
41}
42impl std::error::Error for ProfileError {}
43/// Profile labels are opaque ASCII names (spec 034, 3.5.2).
44pub fn is_profile_name(s: &str) -> bool {
45    (1..=64).contains(&s.len())
46        && s.as_bytes()[0].is_ascii_alphanumeric()
47        && s.bytes()
48            .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
49}
50/// An absent base or an exact profile name and version (spec 034, 3.5.1).
51pub fn is_profile_extends(s: &str) -> bool {
52    s == "none"
53        || s.split_once('@')
54            .is_some_and(|(n, v)| is_profile_name(n) && v.parse::<Version>().is_ok())
55}
56impl Profile {
57    pub fn validate(&self) -> Result<(), ProfileError> {
58        for (member, valid) in [
59            ("schema", self.schema == schema::PROFILE),
60            ("profile", is_profile_name(&self.profile)),
61            ("version", self.version.parse::<Version>().is_ok()),
62            (
63                "definition.version",
64                self.definition.version.parse::<VersionRange>().is_ok(),
65            ),
66            ("extends", is_profile_extends(&self.extends)),
67            ("values", self.values.len() <= MAX_PARAMETERS),
68        ] {
69            if !valid {
70                return Err(ProfileError { member });
71            }
72        }
73        Ok(())
74    }
75}
76impl Document for Profile {
77    const SCHEMA: &'static str = schema::PROFILE;
78    const LIMITS: crate::bounded::ParseLimits = DESCRIPTOR_V1;
79    fn schema(&self) -> &str {
80        &self.schema
81    }
82    fn parse(bytes: &[u8]) -> Result<Self, DocumentError> {
83        let doc: Self = parse_bounded(bytes, &Self::LIMITS).map_err(DocumentError::Parse)?;
84        if doc.schema != Self::SCHEMA {
85            return Err(DocumentError::Schema {
86                expected: Self::SCHEMA,
87                found: doc.schema.chars().take(80).collect(),
88            });
89        }
90        doc.validate().map_err(|e| {
91            DocumentError::Parse(ParseError::Schema {
92                message: e.to_string(),
93            })
94        })?;
95        Ok(doc)
96    }
97}
98impl Identified for Profile {
99    type Id = ProfileId;
100    fn wrap(digest: String) -> ProfileId {
101        ProfileId::from_digest(digest)
102    }
103}