runmat_execution_artifact/program/
target.rs1use serde::{Deserialize, Serialize};
2
3use super::ExecutableForm;
4use crate::{ArtifactError, ArtifactResult};
5
6pub const PROGRAM_TARGET_SCHEMA_VERSION: u16 = 1;
7
8#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ProgramTargetCohort {
11 Portable,
12 Native,
13}
14
15#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct NativeTargetIdentity {
18 pub architecture: String,
19 pub operating_system: String,
20 pub pointer_width: u16,
21 pub abi: String,
22 pub object_format: String,
23}
24
25impl NativeTargetIdentity {
26 pub fn validate(&self) -> ArtifactResult<()> {
27 if !valid_token(&self.architecture, 64)
28 || !valid_token(&self.operating_system, 64)
29 || !valid_token(&self.abi, 256)
30 || !valid_token(&self.object_format, 32)
31 || !matches!(self.pointer_width, 32 | 64)
32 {
33 return Err(ArtifactError::Invalid(
34 "native artifact target is not canonical".into(),
35 ));
36 }
37 Ok(())
38 }
39}
40
41#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct ProgramTarget {
44 pub schema_version: u16,
45 pub profile: String,
46 pub cohort: ProgramTargetCohort,
47 pub native: Option<NativeTargetIdentity>,
48}
49
50impl ProgramTarget {
51 pub fn portable(profile: impl Into<String>) -> Self {
52 Self {
53 schema_version: PROGRAM_TARGET_SCHEMA_VERSION,
54 profile: profile.into(),
55 cohort: ProgramTargetCohort::Portable,
56 native: None,
57 }
58 }
59
60 pub fn native(profile: impl Into<String>, target: NativeTargetIdentity) -> Self {
61 Self {
62 schema_version: PROGRAM_TARGET_SCHEMA_VERSION,
63 profile: profile.into(),
64 cohort: ProgramTargetCohort::Native,
65 native: Some(target),
66 }
67 }
68
69 pub fn validate(&self) -> ArtifactResult<()> {
70 if self.schema_version != PROGRAM_TARGET_SCHEMA_VERSION || !valid_token(&self.profile, 256)
71 {
72 return Err(ArtifactError::Invalid(
73 "program artifact target is not canonical".into(),
74 ));
75 }
76 match (self.cohort, self.native.as_ref()) {
77 (ProgramTargetCohort::Portable, None) => Ok(()),
78 (ProgramTargetCohort::Native, Some(native)) => native.validate(),
79 _ => Err(ArtifactError::Invalid(
80 "program target cohort has inconsistent native identity".into(),
81 )),
82 }
83 }
84
85 pub fn validate_form(&self, form: ExecutableForm) -> ArtifactResult<()> {
86 self.validate()?;
87 let compatible = match form {
88 ExecutableForm::NativeObjectV1 => self.cohort == ProgramTargetCohort::Native,
89 ExecutableForm::InterpreterBytecodeV1
90 | ExecutableForm::InterpreterScriptV1
91 | ExecutableForm::TestAttemptV1
92 | ExecutableForm::MeshingWorkload
93 | ExecutableForm::ExecutableUnitV3 => self.cohort == ProgramTargetCohort::Portable,
94 };
95 if compatible {
96 Ok(())
97 } else {
98 Err(ArtifactError::Invalid(
99 "program executable form is incompatible with its target cohort".into(),
100 ))
101 }
102 }
103
104 pub fn validate_for_portable_host(&self) -> ArtifactResult<()> {
105 self.validate()?;
106 if self.cohort == ProgramTargetCohort::Portable {
107 Ok(())
108 } else {
109 Err(ArtifactError::Invalid(
110 "native program artifact is incompatible with a portable execution host".into(),
111 ))
112 }
113 }
114
115 pub fn validate_for_native_host(&self, host: &NativeTargetIdentity) -> ArtifactResult<()> {
116 self.validate()?;
117 host.validate()?;
118 match self.cohort {
119 ProgramTargetCohort::Portable => Ok(()),
120 ProgramTargetCohort::Native if self.native.as_ref() == Some(host) => Ok(()),
121 ProgramTargetCohort::Native => Err(ArtifactError::Invalid(
122 "native program artifact does not match this execution host".into(),
123 )),
124 }
125 }
126
127 pub fn canonical_bytes(&self) -> ArtifactResult<Vec<u8>> {
128 self.validate()?;
129 serde_json::to_vec(self).map_err(|error| ArtifactError::Encoding(error.to_string()))
130 }
131
132 pub fn from_canonical_bytes(bytes: &[u8]) -> ArtifactResult<Self> {
133 let target: Self = serde_json::from_slice(bytes)
134 .map_err(|error| ArtifactError::Encoding(error.to_string()))?;
135 target.validate()?;
136 if target.canonical_bytes()? != bytes {
137 return Err(ArtifactError::Invalid(
138 "program target encoding is not canonical".into(),
139 ));
140 }
141 Ok(target)
142 }
143}
144
145fn valid_token(value: &str, maximum: usize) -> bool {
146 !value.is_empty()
147 && value.len() <= maximum
148 && value.is_ascii()
149 && !value.chars().any(char::is_control)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn native() -> NativeTargetIdentity {
157 NativeTargetIdentity {
158 architecture: "aarch64".into(),
159 operating_system: "macos".into(),
160 pointer_width: 64,
161 abi: "runmat-native-abi-v1".into(),
162 object_format: "mach-o".into(),
163 }
164 }
165
166 #[test]
167 fn target_cohorts_reject_incompatible_forms_and_hosts() {
168 let portable = ProgramTarget::portable("portable-executable-unit-v3");
169 portable
170 .validate_form(ExecutableForm::ExecutableUnitV3)
171 .unwrap();
172 assert!(portable
173 .validate_form(ExecutableForm::NativeObjectV1)
174 .is_err());
175
176 let target = native();
177 let native_program = ProgramTarget::native("native-object-v1", target.clone());
178 native_program
179 .validate_form(ExecutableForm::NativeObjectV1)
180 .unwrap();
181 assert!(native_program.validate_for_portable_host().is_err());
182 native_program.validate_for_native_host(&target).unwrap();
183
184 let mut different_host = target.clone();
185 different_host.architecture = "x86_64".into();
186 assert!(native_program
187 .validate_for_native_host(&different_host)
188 .is_err());
189 }
190
191 #[test]
192 fn target_cohorts_require_exactly_one_consistent_native_identity() {
193 let mut portable_with_native = ProgramTarget::portable("portable-test");
194 portable_with_native.native = Some(native());
195 assert!(portable_with_native.validate().is_err());
196
197 let mut native_without_identity = ProgramTarget::native("native-test", native());
198 native_without_identity.native = None;
199 assert!(native_without_identity.validate().is_err());
200 }
201}