1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::identifier::DecoderBindingId;
5use crate::{
6 hash_framed_tuple, hash_jcs, parse_json_bytes_strict, Compiler, HashError, HashId,
7 ProgramRelease, ProgramSpec, SdkDefinition, SdkOutputTree, TupleField,
8};
9
10pub const COMPILER_SCHEMA_V1: &str = "arete.compiler/v1";
11pub const SDK_DEFINITION_SCHEMA_V1: &str = "arete.sdk-definition/v1";
12pub const SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND: &str = "program-spec";
13pub const OSS_DECODER_ENGINE_ID: &str = "arete-oss-generated-decoder/v1";
14pub const PROGRAM_RELEASE_SCHEMA_V1: &str = "arete.program-release/v1";
15pub const PROGRAM_RELEASE_SCHEMA_V2: &str = "arete.program-release/v2";
16pub const PROGRAM_RELEASE_SCHEMA_V3: &str = "arete.program-release/v3";
17pub const HOSTED_MANAGED_RELEASE_PROFILE: &str = "hosted-managed";
18pub const HOSTED_PRIVATE_RELEASE_PROFILE: &str = "hosted-private";
19pub const HOSTED_PRIVATE_EXECUTABLE_POLICY: &str = "observed";
20pub const OSS_GENERATED_RELEASE_PROFILE: &str = "oss-generated";
21pub const SOLANA_EXECUTABLE_IDENTITY_SCHEMA_V1: &str = "arete.solana-executable-identity/v1";
22pub const SOLANA_BPF_LOADER_V2_PROGRAM_ID: &str = "BPFLoader2111111111111111111111111111111111";
23pub const SOLANA_BPF_UPGRADEABLE_LOADER_PROGRAM_ID: &str =
24 "BPFLoaderUpgradeab1e11111111111111111111111";
25pub const SOLANA_NATIVE_LOADER_PROGRAM_ID: &str = "NativeLoader1111111111111111111111111111111";
26pub const SOLANA_EXECUTABLE_PAYLOAD_SHA256_PREFIX: &str = "sha256:";
27pub const SOLANA_NATIVE_BUILTIN_NAME_MAX_BYTES: usize = 64;
29
30pub fn project_without_artifact_hash(value: &Value) -> Result<Value, HashError> {
34 let mut projection = value
35 .as_object()
36 .cloned()
37 .ok_or(HashError::InvalidSelfHashProjection)?;
38 projection.remove("artifactHash");
39 Ok(Value::Object(projection))
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct CompilerSourceV1 {
44 pub path: String,
45 pub bytes: Vec<u8>,
46}
47
48impl CompilerSourceV1 {
49 pub fn new(path: impl Into<String>, bytes: impl Into<Vec<u8>>) -> Self {
50 Self {
51 path: path.into(),
52 bytes: bytes.into(),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct CompilerV1 {
60 pub schema: String,
61 pub sources: Vec<CompilerSourceV1>,
62}
63
64impl CompilerV1 {
65 pub fn new(sources: impl IntoIterator<Item = CompilerSourceV1>) -> Result<Self, HashError> {
66 let mut projection = Self {
67 schema: COMPILER_SCHEMA_V1.to_string(),
68 sources: sources.into_iter().collect(),
69 };
70 projection
71 .sources
72 .sort_by(|left, right| left.path.as_bytes().cmp(right.path.as_bytes()));
73 projection.validate()?;
74 Ok(projection)
75 }
76
77 pub fn hash(&self) -> Result<HashId<Compiler>, HashError> {
78 self.validate()?;
79 let mut fields = Vec::with_capacity(self.sources.len() + 1);
80 fields.push(TupleField::new("schema", self.schema.as_bytes()));
81 fields.extend(
82 self.sources
83 .iter()
84 .map(|source| TupleField::new(&source.path, &source.bytes)),
85 );
86 hash_framed_tuple(&fields)
87 }
88
89 fn validate(&self) -> Result<(), HashError> {
90 if self.schema != COMPILER_SCHEMA_V1 {
91 return Err(HashError::UnknownVersion(self.schema.clone()));
92 }
93 if self.sources.is_empty() {
94 return Err(HashError::InvalidProjection {
95 projection: "compiler",
96 reason: "sources must not be empty".to_string(),
97 });
98 }
99 let mut previous: Option<&[u8]> = None;
100 for source in &self.sources {
101 if source.path.is_empty() || source.path == "schema" {
102 return Err(HashError::InvalidProjection {
103 projection: "compiler",
104 reason: format!("invalid source path '{}'", source.path),
105 });
106 }
107 if let Some(previous) = previous {
108 match previous.cmp(source.path.as_bytes()) {
109 std::cmp::Ordering::Greater => {
110 return Err(HashError::InvalidProjection {
111 projection: "compiler",
112 reason: "sources must be sorted by raw UTF-8 path bytes".to_string(),
113 })
114 }
115 std::cmp::Ordering::Equal => {
116 return Err(HashError::InvalidProjection {
117 projection: "compiler",
118 reason: format!("duplicate source path '{}'", source.path),
119 })
120 }
121 std::cmp::Ordering::Less => {}
122 }
123 }
124 previous = Some(source.path.as_bytes());
125 }
126 Ok(())
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct SdkDefinitionV1 {
134 pub schema: String,
135 pub input_kind: String,
136 pub input_hash: HashId<ProgramSpec>,
137 pub compiler_hash: HashId<Compiler>,
138}
139
140impl SdkDefinitionV1 {
141 pub fn new(input_hash: HashId<ProgramSpec>, compiler_hash: HashId<Compiler>) -> Self {
142 Self {
143 schema: SDK_DEFINITION_SCHEMA_V1.to_string(),
144 input_kind: SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND.to_string(),
145 input_hash,
146 compiler_hash,
147 }
148 }
149
150 pub fn hash(&self) -> Result<HashId<SdkDefinition>, HashError> {
151 if self.schema != SDK_DEFINITION_SCHEMA_V1 {
152 return Err(HashError::UnknownVersion(self.schema.clone()));
153 }
154 if self.input_kind != SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND {
155 return Err(HashError::InvalidProjection {
156 projection: "SDK definition",
157 reason: format!(
158 "inputKind must be '{}', not '{}'",
159 SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND, self.input_kind
160 ),
161 });
162 }
163 hash_jcs(self)
164 }
165}
166
167pub const SDK_DEFINITION_SCHEMA_V2: &str = "arete.sdk-definition/v2";
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase", deny_unknown_fields)]
174pub struct SdkDefinitionV2 {
175 pub schema: String,
176 pub input_kind: String,
177 pub input_hash: HashId<ProgramSpec>,
178 pub target: String,
179 pub runtime_contract: String,
180 pub output_tree_hash: HashId<SdkOutputTree>,
181}
182
183impl SdkDefinitionV2 {
184 pub fn new(
185 input_hash: HashId<ProgramSpec>,
186 target: impl Into<String>,
187 runtime_contract: impl Into<String>,
188 output_tree_hash: HashId<SdkOutputTree>,
189 ) -> Self {
190 Self {
191 schema: SDK_DEFINITION_SCHEMA_V2.to_string(),
192 input_kind: SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND.to_string(),
193 input_hash,
194 target: target.into(),
195 runtime_contract: runtime_contract.into(),
196 output_tree_hash,
197 }
198 }
199
200 pub fn hash(&self) -> Result<HashId<SdkDefinition>, HashError> {
201 if self.schema != SDK_DEFINITION_SCHEMA_V2 {
202 return Err(HashError::UnknownVersion(self.schema.clone()));
203 }
204 if self.input_kind != SDK_DEFINITION_PROGRAM_SPEC_INPUT_KIND
205 || !matches!(self.target.as_str(), "typescript" | "rust" | "python")
206 || self.runtime_contract.is_empty()
207 || self.runtime_contract.len() > 128
208 || !self
209 .runtime_contract
210 .bytes()
211 .all(|byte| byte.is_ascii_graphic())
212 {
213 return Err(HashError::InvalidProjection {
214 projection: "SDK definition",
215 reason: "expected program-spec input, a supported target and a printable ASCII runtime contract (1..128 bytes)".to_string(),
216 });
217 }
218 hash_jcs(self)
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase", deny_unknown_fields)]
224pub struct SolanaExecutableIdentityV1 {
225 pub schema: String,
226 pub genesis_hash: String,
227 pub loader: SolanaExecutableLoaderV1,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(
232 tag = "kind",
233 rename_all = "kebab-case",
234 rename_all_fields = "camelCase",
235 deny_unknown_fields
236)]
237pub enum SolanaExecutableLoaderV1 {
238 BpfLoaderV2(SolanaBpfLoaderV2IdentityV1),
239 BpfUpgradeableLoader(SolanaBpfUpgradeableLoaderIdentityV1),
240 NativeBuiltin(SolanaNativeBuiltinIdentityV1),
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase", deny_unknown_fields)]
245pub struct SolanaBpfLoaderV2IdentityV1 {
246 pub loader_program_id: String,
247 pub executable_payload_sha256: String,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "camelCase", deny_unknown_fields)]
252pub struct SolanaBpfUpgradeableLoaderIdentityV1 {
253 pub loader_program_id: String,
254 pub program_data_address: String,
255 pub deployment_slot: String,
256 pub upgrade_authority: SolanaUpgradeAuthorityV1,
257 pub executable_payload_sha256: String,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase", deny_unknown_fields)]
270pub struct SolanaNativeBuiltinIdentityV1 {
271 pub loader_program_id: String,
272 pub builtin_name: String,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(
277 tag = "kind",
278 rename_all = "kebab-case",
279 rename_all_fields = "camelCase",
280 deny_unknown_fields
281)]
282pub enum SolanaUpgradeAuthorityV1 {
283 None(SolanaNoUpgradeAuthorityV1),
284 Address(SolanaUpgradeAuthorityAddressV1),
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(deny_unknown_fields)]
289pub struct SolanaNoUpgradeAuthorityV1 {}
290
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(rename_all = "camelCase", deny_unknown_fields)]
293pub struct SolanaUpgradeAuthorityAddressV1 {
294 pub address: String,
295}
296
297impl SolanaExecutableIdentityV1 {
298 pub fn new(
299 genesis_hash: impl Into<String>,
300 loader: SolanaExecutableLoaderV1,
301 ) -> Result<Self, HashError> {
302 let identity = Self {
303 schema: SOLANA_EXECUTABLE_IDENTITY_SCHEMA_V1.to_string(),
304 genesis_hash: genesis_hash.into(),
305 loader,
306 };
307 validate_solana_executable_identity_v1(&identity)?;
308 Ok(identity)
309 }
310}
311
312impl SolanaExecutableLoaderV1 {
313 pub fn bpf_loader_v2(executable_payload_sha256: impl Into<String>) -> Result<Self, HashError> {
314 let loader = Self::BpfLoaderV2(SolanaBpfLoaderV2IdentityV1 {
315 loader_program_id: SOLANA_BPF_LOADER_V2_PROGRAM_ID.to_string(),
316 executable_payload_sha256: executable_payload_sha256.into(),
317 });
318 validate_solana_executable_loader_v1(&loader)?;
319 Ok(loader)
320 }
321
322 pub fn bpf_upgradeable_loader(
323 program_data_address: impl Into<String>,
324 deployment_slot: u64,
325 upgrade_authority: SolanaUpgradeAuthorityV1,
326 executable_payload_sha256: impl Into<String>,
327 ) -> Result<Self, HashError> {
328 let loader = Self::BpfUpgradeableLoader(SolanaBpfUpgradeableLoaderIdentityV1 {
329 loader_program_id: SOLANA_BPF_UPGRADEABLE_LOADER_PROGRAM_ID.to_string(),
330 program_data_address: program_data_address.into(),
331 deployment_slot: deployment_slot.to_string(),
332 upgrade_authority,
333 executable_payload_sha256: executable_payload_sha256.into(),
334 });
335 validate_solana_executable_loader_v1(&loader)?;
336 Ok(loader)
337 }
338
339 pub fn native_builtin(builtin_name: impl Into<String>) -> Result<Self, HashError> {
342 let loader = Self::NativeBuiltin(SolanaNativeBuiltinIdentityV1 {
343 loader_program_id: SOLANA_NATIVE_LOADER_PROGRAM_ID.to_string(),
344 builtin_name: builtin_name.into(),
345 });
346 validate_solana_executable_loader_v1(&loader)?;
347 Ok(loader)
348 }
349}
350
351impl SolanaUpgradeAuthorityV1 {
352 pub const fn none() -> Self {
353 Self::None(SolanaNoUpgradeAuthorityV1 {})
354 }
355
356 pub fn address(address: impl Into<String>) -> Result<Self, HashError> {
357 let address = address.into();
358 validate_base58_32(&address, "upgradeAuthority.address")?;
359 Ok(Self::Address(SolanaUpgradeAuthorityAddressV1 { address }))
360 }
361}
362
363pub fn parse_solana_executable_identity_v1(
364 bytes: &[u8],
365) -> Result<SolanaExecutableIdentityV1, HashError> {
366 let value = parse_json_bytes_strict(bytes)?;
367 let identity: SolanaExecutableIdentityV1 = serde_json::from_value(value)
368 .map_err(|error| release_projection_error(error.to_string()))?;
369 validate_solana_executable_identity_v1(&identity)?;
370 Ok(identity)
371}
372
373pub fn validate_solana_executable_identity_v1(
374 identity: &SolanaExecutableIdentityV1,
375) -> Result<(), HashError> {
376 if identity.schema != SOLANA_EXECUTABLE_IDENTITY_SCHEMA_V1 {
377 return Err(HashError::UnknownVersion(identity.schema.clone()));
378 }
379 validate_base58_32(&identity.genesis_hash, "genesisHash")?;
380 validate_solana_executable_loader_v1(&identity.loader)
381}
382
383fn validate_solana_executable_loader_v1(
384 loader: &SolanaExecutableLoaderV1,
385) -> Result<(), HashError> {
386 match loader {
387 SolanaExecutableLoaderV1::BpfLoaderV2(loader) => {
388 validate_loader_program_id(
389 &loader.loader_program_id,
390 SOLANA_BPF_LOADER_V2_PROGRAM_ID,
391 "bpf-loader-v2",
392 )?;
393 validate_sha256_digest(&loader.executable_payload_sha256, "executablePayloadSha256")
394 }
395 SolanaExecutableLoaderV1::BpfUpgradeableLoader(loader) => {
396 validate_loader_program_id(
397 &loader.loader_program_id,
398 SOLANA_BPF_UPGRADEABLE_LOADER_PROGRAM_ID,
399 "bpf-upgradeable-loader",
400 )?;
401 validate_base58_32(&loader.program_data_address, "programDataAddress")?;
402 validate_deployment_slot(&loader.deployment_slot)?;
403 if let SolanaUpgradeAuthorityV1::Address(authority) = &loader.upgrade_authority {
404 validate_base58_32(&authority.address, "upgradeAuthority.address")?;
405 }
406 validate_sha256_digest(&loader.executable_payload_sha256, "executablePayloadSha256")
407 }
408 SolanaExecutableLoaderV1::NativeBuiltin(loader) => {
409 validate_loader_program_id(
410 &loader.loader_program_id,
411 SOLANA_NATIVE_LOADER_PROGRAM_ID,
412 "native-builtin",
413 )?;
414 validate_native_builtin_name(&loader.builtin_name)
415 }
416 }
417}
418
419pub fn is_valid_native_builtin_name(bytes: &[u8]) -> bool {
423 !bytes.is_empty()
424 && bytes.len() <= SOLANA_NATIVE_BUILTIN_NAME_MAX_BYTES
425 && bytes.iter().all(u8::is_ascii_graphic)
426}
427
428fn validate_native_builtin_name(value: &str) -> Result<(), HashError> {
429 if !is_valid_native_builtin_name(value.as_bytes()) {
430 return Err(release_projection_error(format!(
431 "builtinName must be 1..{SOLANA_NATIVE_BUILTIN_NAME_MAX_BYTES} bytes of printable ASCII"
432 )));
433 }
434 Ok(())
435}
436
437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase", deny_unknown_fields)]
439pub struct HostedManagedProgramReleaseV2 {
440 pub schema: String,
441 pub release_profile: String,
442 pub program_id: String,
443 pub program_spec_hash: HashId<ProgramSpec>,
444 pub idl_content_hash: HashId<crate::IdlContent>,
445 pub normalized_idl_hash: HashId<crate::IdlNormalized>,
446 pub decoder_abi_version: String,
447 pub decoder_engine_id: String,
448 pub decoder_binding_id: String,
449 pub executable_identity: SolanaExecutableIdentityV1,
450}
451
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct HostedManagedProgramReleaseV2Fields {
454 pub program_id: String,
455 pub program_spec_hash: HashId<ProgramSpec>,
456 pub idl_content_hash: HashId<crate::IdlContent>,
457 pub normalized_idl_hash: HashId<crate::IdlNormalized>,
458 pub decoder_abi_version: String,
459 pub decoder_engine_id: String,
460 pub decoder_binding_id: String,
461 pub executable_identity: SolanaExecutableIdentityV1,
462}
463
464impl HostedManagedProgramReleaseV2 {
465 pub fn new(fields: HostedManagedProgramReleaseV2Fields) -> Result<Self, HashError> {
466 let release = Self {
467 schema: PROGRAM_RELEASE_SCHEMA_V2.to_string(),
468 release_profile: HOSTED_MANAGED_RELEASE_PROFILE.to_string(),
469 program_id: fields.program_id,
470 program_spec_hash: fields.program_spec_hash,
471 idl_content_hash: fields.idl_content_hash,
472 normalized_idl_hash: fields.normalized_idl_hash,
473 decoder_abi_version: fields.decoder_abi_version,
474 decoder_engine_id: fields.decoder_engine_id,
475 decoder_binding_id: fields.decoder_binding_id,
476 executable_identity: fields.executable_identity,
477 };
478 validate_hosted_managed_program_release_v2(&release)?;
479 Ok(release)
480 }
481
482 pub fn hash(&self) -> Result<HashId<ProgramRelease>, HashError> {
483 validate_hosted_managed_program_release_v2(self)?;
484 hash_jcs(self)
485 }
486}
487
488pub fn parse_hosted_managed_program_release_v2(
489 bytes: &[u8],
490) -> Result<HostedManagedProgramReleaseV2, HashError> {
491 let value = parse_json_bytes_strict(bytes)?;
492 let release: HostedManagedProgramReleaseV2 = serde_json::from_value(value)
493 .map_err(|error| release_projection_error(error.to_string()))?;
494 validate_hosted_managed_program_release_v2(&release)?;
495 Ok(release)
496}
497
498pub fn validate_hosted_managed_program_release_v2(
499 release: &HostedManagedProgramReleaseV2,
500) -> Result<(), HashError> {
501 validate_release_projection(
502 (&release.schema, PROGRAM_RELEASE_SCHEMA_V2),
503 (&release.release_profile, HOSTED_MANAGED_RELEASE_PROFILE),
504 &release.program_id,
505 &release.decoder_engine_id,
506 Some(&release.decoder_abi_version),
507 Some(&release.decoder_binding_id),
508 )?;
509 validate_base58_32(&release.program_id, "programId")?;
510 validate_release_identifier(&release.decoder_abi_version, "decoderAbiVersion", 64)?;
511 validate_release_identifier(&release.decoder_engine_id, "decoderEngineId", 128)?;
512 validate_release_identifier(&release.decoder_binding_id, "decoderBindingId", 128)?;
513 validate_solana_executable_identity_v1(&release.executable_identity)
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
522#[serde(rename_all = "camelCase", deny_unknown_fields)]
523pub struct HostedPrivateProgramReleaseV3 {
524 pub schema: String,
525 pub release_profile: String,
526 pub program_id: String,
527 pub program_spec_hash: HashId<ProgramSpec>,
528 pub idl_content_hash: HashId<crate::IdlContent>,
529 pub normalized_idl_hash: HashId<crate::IdlNormalized>,
530 pub decoder_abi_version: String,
531 pub decoder_engine_id: String,
532 pub decoder_binding_id: String,
533 pub executable_policy: String,
534}
535
536#[derive(Debug, Clone, PartialEq, Eq)]
537pub struct HostedPrivateProgramReleaseV3Fields {
538 pub program_id: String,
539 pub program_spec_hash: HashId<ProgramSpec>,
540 pub idl_content_hash: HashId<crate::IdlContent>,
541 pub normalized_idl_hash: HashId<crate::IdlNormalized>,
542 pub decoder_abi_version: String,
543 pub decoder_engine_id: String,
544 pub decoder_binding_id: String,
545}
546
547impl HostedPrivateProgramReleaseV3 {
548 pub fn new(fields: HostedPrivateProgramReleaseV3Fields) -> Result<Self, HashError> {
549 let release = Self {
550 schema: PROGRAM_RELEASE_SCHEMA_V3.to_string(),
551 release_profile: HOSTED_PRIVATE_RELEASE_PROFILE.to_string(),
552 program_id: fields.program_id,
553 program_spec_hash: fields.program_spec_hash,
554 idl_content_hash: fields.idl_content_hash,
555 normalized_idl_hash: fields.normalized_idl_hash,
556 decoder_abi_version: fields.decoder_abi_version,
557 decoder_engine_id: fields.decoder_engine_id,
558 decoder_binding_id: fields.decoder_binding_id,
559 executable_policy: HOSTED_PRIVATE_EXECUTABLE_POLICY.to_string(),
560 };
561 validate_hosted_private_program_release_v3(&release)?;
562 Ok(release)
563 }
564
565 pub fn hash(&self) -> Result<HashId<ProgramRelease>, HashError> {
566 validate_hosted_private_program_release_v3(self)?;
567 hash_jcs(self)
568 }
569}
570
571pub fn parse_hosted_private_program_release_v3(
572 bytes: &[u8],
573) -> Result<HostedPrivateProgramReleaseV3, HashError> {
574 let value = parse_json_bytes_strict(bytes)?;
575 let release: HostedPrivateProgramReleaseV3 = serde_json::from_value(value)
576 .map_err(|error| release_projection_error(error.to_string()))?;
577 validate_hosted_private_program_release_v3(&release)?;
578 Ok(release)
579}
580
581pub fn validate_hosted_private_program_release_v3(
582 release: &HostedPrivateProgramReleaseV3,
583) -> Result<(), HashError> {
584 validate_release_projection(
585 (&release.schema, PROGRAM_RELEASE_SCHEMA_V3),
586 (&release.release_profile, HOSTED_PRIVATE_RELEASE_PROFILE),
587 &release.program_id,
588 &release.decoder_engine_id,
589 Some(&release.decoder_abi_version),
590 Some(&release.decoder_binding_id),
591 )?;
592 validate_base58_32(&release.program_id, "programId")?;
593 validate_release_identifier(&release.decoder_abi_version, "decoderAbiVersion", 64)?;
594 validate_release_identifier(&release.decoder_engine_id, "decoderEngineId", 128)?;
595 DecoderBindingId::new(&release.decoder_binding_id)?;
596 if release.executable_policy != HOSTED_PRIVATE_EXECUTABLE_POLICY {
597 return Err(release_projection_error(format!(
598 "executablePolicy must be '{}', not '{}'",
599 HOSTED_PRIVATE_EXECUTABLE_POLICY, release.executable_policy
600 )));
601 }
602 Ok(())
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct OssGeneratedProgramReleaseV1 {
608 pub schema: String,
609 pub release_profile: String,
610 pub program_id: String,
611 pub program_spec_hash: HashId<ProgramSpec>,
612 pub idl_content_hash: HashId<crate::IdlContent>,
613 pub normalized_idl_hash: HashId<crate::IdlNormalized>,
614 pub decoder_engine_id: String,
615}
616
617impl OssGeneratedProgramReleaseV1 {
618 pub fn new(
619 program_id: impl Into<String>,
620 program_spec_hash: HashId<ProgramSpec>,
621 idl_content_hash: HashId<crate::IdlContent>,
622 normalized_idl_hash: HashId<crate::IdlNormalized>,
623 ) -> Self {
624 Self::with_decoder_engine(
625 program_id,
626 program_spec_hash,
627 idl_content_hash,
628 normalized_idl_hash,
629 OSS_DECODER_ENGINE_ID,
630 )
631 }
632
633 pub fn with_decoder_engine(
634 program_id: impl Into<String>,
635 program_spec_hash: HashId<ProgramSpec>,
636 idl_content_hash: HashId<crate::IdlContent>,
637 normalized_idl_hash: HashId<crate::IdlNormalized>,
638 decoder_engine_id: impl Into<String>,
639 ) -> Self {
640 Self {
641 schema: PROGRAM_RELEASE_SCHEMA_V1.to_string(),
642 release_profile: OSS_GENERATED_RELEASE_PROFILE.to_string(),
643 program_id: program_id.into(),
644 program_spec_hash,
645 idl_content_hash,
646 normalized_idl_hash,
647 decoder_engine_id: decoder_engine_id.into(),
648 }
649 }
650
651 pub fn hash(&self) -> Result<HashId<ProgramRelease>, HashError> {
652 validate_release_projection(
653 (&self.schema, PROGRAM_RELEASE_SCHEMA_V1),
654 (&self.release_profile, OSS_GENERATED_RELEASE_PROFILE),
655 &self.program_id,
656 &self.decoder_engine_id,
657 None,
658 None,
659 )?;
660 hash_jcs(self)
661 }
662}
663
664fn validate_release_projection(
665 schema: (&str, &'static str),
666 release_profile: (&str, &'static str),
667 program_id: &str,
668 decoder_engine_id: &str,
669 decoder_abi_version: Option<&str>,
670 decoder_binding_id: Option<&str>,
671) -> Result<(), HashError> {
672 let (schema, expected_schema) = schema;
673 if schema != expected_schema {
674 return Err(HashError::UnknownVersion(schema.to_string()));
675 }
676 let (release_profile, expected_profile) = release_profile;
677 if release_profile != expected_profile {
678 return Err(HashError::InvalidProjection {
679 projection: "program release",
680 reason: format!("releaseProfile must be '{expected_profile}', not '{release_profile}'"),
681 });
682 }
683 if program_id.is_empty() {
684 return Err(HashError::InvalidProjection {
685 projection: "program release",
686 reason: "programId must not be empty".to_string(),
687 });
688 }
689 if decoder_engine_id.is_empty() {
690 return Err(HashError::InvalidProjection {
691 projection: "program release",
692 reason: "decoderEngineId must not be empty".to_string(),
693 });
694 }
695 if decoder_abi_version.is_some_and(str::is_empty) {
696 return Err(HashError::InvalidProjection {
697 projection: "program release",
698 reason: "decoderAbiVersion must not be empty".to_string(),
699 });
700 }
701 if decoder_binding_id.is_some_and(str::is_empty) {
702 return Err(HashError::InvalidProjection {
703 projection: "program release",
704 reason: "decoderBindingId must not be empty".to_string(),
705 });
706 }
707 Ok(())
708}
709
710fn validate_loader_program_id(
711 actual: &str,
712 expected: &'static str,
713 variant: &'static str,
714) -> Result<(), HashError> {
715 if actual != expected {
716 return Err(release_projection_error(format!(
717 "loaderProgramId for '{variant}' must be '{expected}', not '{actual}'"
718 )));
719 }
720 Ok(())
721}
722
723fn validate_release_identifier(
724 value: &str,
725 field: &str,
726 max_length: usize,
727) -> Result<(), HashError> {
728 if value.is_empty() || value.trim() != value || value.len() > max_length {
729 return Err(release_projection_error(format!(
730 "{field} must be a nonempty, trimmed string of at most {max_length} bytes"
731 )));
732 }
733 Ok(())
734}
735
736fn validate_deployment_slot(value: &str) -> Result<(), HashError> {
737 let canonical = value == "0"
738 || value
739 .strip_prefix(|character: char| ('1'..='9').contains(&character))
740 .is_some_and(|rest| rest.bytes().all(|byte| byte.is_ascii_digit()));
741 if !canonical || value.parse::<u64>().is_err() {
742 return Err(release_projection_error(
743 "deploymentSlot must be a canonical unsigned decimal u64 string".to_string(),
744 ));
745 }
746 Ok(())
747}
748
749fn validate_sha256_digest(value: &str, field: &str) -> Result<(), HashError> {
750 let Some(digest) = value.strip_prefix(SOLANA_EXECUTABLE_PAYLOAD_SHA256_PREFIX) else {
751 return Err(release_projection_error(format!(
752 "{field} must use the sha256:<lowercase-hex> format"
753 )));
754 };
755 if digest.len() != 64
756 || !digest
757 .bytes()
758 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
759 {
760 return Err(release_projection_error(format!(
761 "{field} must use the sha256:<lowercase-hex> format"
762 )));
763 }
764 Ok(())
765}
766
767fn validate_base58_32(value: &str, field: &str) -> Result<(), HashError> {
768 let decoded = bs58::decode(value).into_vec().map_err(|_| {
769 release_projection_error(format!("{field} must be a canonical 32-byte base58 value"))
770 })?;
771 if decoded.len() != 32 || bs58::encode(decoded).into_string() != value {
772 return Err(release_projection_error(format!(
773 "{field} must be a canonical 32-byte base58 value"
774 )));
775 }
776 Ok(())
777}
778
779fn release_projection_error(reason: String) -> HashError {
780 HashError::InvalidProjection {
781 projection: "program release",
782 reason,
783 }
784}