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