Skip to main content

runmat_execution_artifact/object/
descriptor.rs

1use runmat_execution::Digest;
2use serde::{Deserialize, Serialize};
3
4use crate::{ArtifactError, ArtifactResult};
5
6#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8#[repr(u8)]
9pub enum ObjectNamespace {
10    ProgramSource,
11    PackageRelease,
12    ProgramArtifact,
13    InputValue,
14    ResultValue,
15    DetailedEvent,
16    Log,
17    Checkpoint,
18}
19
20#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct ObjectDescriptor {
23    pub namespace: ObjectNamespace,
24    pub logical_name: String,
25    pub digest: Digest,
26    pub encoded_length: u64,
27    pub media_type: String,
28}
29
30impl ObjectDescriptor {
31    pub fn new(
32        namespace: ObjectNamespace,
33        logical_name: impl Into<String>,
34        media_type: impl Into<String>,
35        bytes: &[u8],
36    ) -> ArtifactResult<Self> {
37        let descriptor = Self {
38            namespace,
39            logical_name: logical_name.into(),
40            digest: Digest::sha256(bytes),
41            encoded_length: bytes.len() as u64,
42            media_type: media_type.into(),
43        };
44        descriptor.validate()?;
45        Ok(descriptor)
46    }
47
48    pub fn validate(&self) -> ArtifactResult<()> {
49        validate_logical_name(&self.logical_name)?;
50        if self.media_type.is_empty()
51            || self.media_type.len() > 128
52            || !self.media_type.is_ascii()
53            || self.media_type.chars().any(char::is_control)
54            || self.media_type.chars().any(char::is_whitespace)
55        {
56            return Err(ArtifactError::Invalid(
57                "object media type is invalid".into(),
58            ));
59        }
60        Ok(())
61    }
62}
63
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct LogicalObject {
66    pub descriptor: ObjectDescriptor,
67    pub bytes: Vec<u8>,
68}
69
70impl LogicalObject {
71    pub fn new(
72        namespace: ObjectNamespace,
73        logical_name: impl Into<String>,
74        media_type: impl Into<String>,
75        bytes: Vec<u8>,
76    ) -> ArtifactResult<Self> {
77        let descriptor = ObjectDescriptor::new(namespace, logical_name, media_type, &bytes)?;
78        Ok(Self { descriptor, bytes })
79    }
80
81    pub fn validate(&self) -> ArtifactResult<()> {
82        self.descriptor.validate()?;
83        if self.descriptor.encoded_length != self.bytes.len() as u64
84            || self.descriptor.digest != Digest::sha256(&self.bytes)
85        {
86            return Err(ArtifactError::Identity(
87                "logical object bytes do not match their descriptor".into(),
88            ));
89        }
90        Ok(())
91    }
92}
93
94fn validate_logical_name(name: &str) -> ArtifactResult<()> {
95    if name.is_empty()
96        || name.len() > 4096
97        || !name.is_ascii()
98        || name.starts_with('/')
99        || name.starts_with('\\')
100        || name.as_bytes().get(1).is_some_and(|byte| *byte == b':')
101        || name.contains('\\')
102        || name
103            .split('/')
104            .any(|part| part.is_empty() || part == "." || part == "..")
105        || name.chars().any(char::is_control)
106    {
107        return Err(ArtifactError::Invalid(format!(
108            "object logical name is not a normalized relative path: {name:?}"
109        )));
110    }
111    Ok(())
112}