runmat_execution_artifact/object/
descriptor.rs1use 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}