1use std::error::Error;
2use std::fmt;
3
4use riffdb_types::{
5 AdapterConformanceManifestHash, ApplicationLockHash, ApplicationManifestHash,
6 ApplicationRoleHash, ContractBundleHash, ContractLineage, ContractVersion,
7 GeneratedArtifactHash, MAX_ADAPTER_PROBE_ITEMS, MigrationBundleHash,
8 hash_adapter_conformance_manifest,
9};
10use serde::{Deserialize, Serialize};
11
12use crate::{
13 ApplicationInstallationPlan, InstallationArtifact, InstallationArtifactKind,
14 InstallationContract, InstallationDriver, InstallationFeature, InstallationSymbol,
15 RoleOperation, RoleOperationKind,
16};
17
18pub const ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V1: &str = "riffdb.adapter-conformance-manifest/v1";
20pub const ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2: &str = "riffdb.adapter-conformance-manifest/v2";
22pub const MAX_ADAPTER_CONFORMANCE_MANIFEST_BYTES: usize = 1_048_576;
24pub const MAX_ADAPTER_FEATURE_CLAIMS: usize = 32;
26pub const MAX_ADAPTER_ARTIFACTS: usize = 256;
28pub const MAX_ADAPTER_ROLES: usize = 128;
30pub const MAX_ADAPTER_DRIVERS: usize = 16;
32pub const MAX_ADAPTER_PLATFORMS: usize = 16;
34pub const MAX_ADAPTER_CONFORMANCE_PROBES: usize = 256;
36pub const MAX_ADAPTER_EVOLUTIONS: usize = 32;
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum AdapterConformanceErrorKind {
42 LimitExceeded,
44 InvalidEncoding,
46 UnsupportedVersion,
48 NonCanonical,
50 InvalidShape,
52 Duplicate,
54 IdentityMismatch,
56 UnsupportedFeature,
58 UnsupportedDriver,
60}
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub struct AdapterConformanceError {
65 kind: AdapterConformanceErrorKind,
66}
67
68impl AdapterConformanceError {
69 const fn new(kind: AdapterConformanceErrorKind) -> Self {
70 Self { kind }
71 }
72
73 #[must_use]
75 pub const fn kind(self) -> AdapterConformanceErrorKind {
76 self.kind
77 }
78}
79
80impl fmt::Display for AdapterConformanceError {
81 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 formatter.write_str(match self.kind {
83 AdapterConformanceErrorKind::LimitExceeded => {
84 "adapter conformance manifest exceeds a fixed limit"
85 }
86 AdapterConformanceErrorKind::InvalidEncoding => {
87 "adapter conformance manifest encoding is invalid"
88 }
89 AdapterConformanceErrorKind::UnsupportedVersion => {
90 "adapter conformance manifest version is unsupported"
91 }
92 AdapterConformanceErrorKind::NonCanonical => {
93 "adapter conformance manifest encoding is not canonical"
94 }
95 AdapterConformanceErrorKind::InvalidShape => {
96 "adapter conformance manifest shape is invalid"
97 }
98 AdapterConformanceErrorKind::Duplicate => {
99 "adapter conformance manifest contains a duplicate identity"
100 }
101 AdapterConformanceErrorKind::IdentityMismatch => {
102 "adapter conformance manifest identity does not match the application plan"
103 }
104 AdapterConformanceErrorKind::UnsupportedFeature => {
105 "adapter requires an unavailable RiffDB feature"
106 }
107 AdapterConformanceErrorKind::UnsupportedDriver => {
108 "adapter requires an unavailable driver or platform"
109 }
110 })
111 }
112}
113
114impl Error for AdapterConformanceError {}
115
116#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
118pub enum AdapterFeatureDisposition {
119 Required,
121 Optional,
123 Degraded,
125 Unavailable,
127}
128
129impl AdapterFeatureDisposition {
130 const fn tag(self) -> &'static str {
131 match self {
132 Self::Required => "required",
133 Self::Optional => "optional",
134 Self::Degraded => "degraded",
135 Self::Unavailable => "unavailable",
136 }
137 }
138
139 fn parse(value: &str) -> Option<Self> {
140 Some(match value {
141 "required" => Self::Required,
142 "optional" => Self::Optional,
143 "degraded" => Self::Degraded,
144 "unavailable" => Self::Unavailable,
145 _ => return None,
146 })
147 }
148}
149
150#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
152pub enum AdapterLimitation {
153 ProductUnavailable,
155 PlatformLimited,
157 PerformanceLimited,
159}
160
161impl AdapterLimitation {
162 const fn tag(self) -> &'static str {
163 match self {
164 Self::ProductUnavailable => "product_unavailable",
165 Self::PlatformLimited => "platform_limited",
166 Self::PerformanceLimited => "performance_limited",
167 }
168 }
169
170 fn parse(value: &str) -> Option<Self> {
171 Some(match value {
172 "product_unavailable" => Self::ProductUnavailable,
173 "platform_limited" => Self::PlatformLimited,
174 "performance_limited" => Self::PerformanceLimited,
175 _ => return None,
176 })
177 }
178}
179
180#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
182pub struct AdapterFeatureClaim {
183 feature: InstallationFeature,
184 disposition: AdapterFeatureDisposition,
185 limitation: Option<AdapterLimitation>,
186}
187
188impl AdapterFeatureClaim {
189 pub fn new(
191 feature: InstallationFeature,
192 disposition: AdapterFeatureDisposition,
193 limitation: Option<AdapterLimitation>,
194 ) -> Result<Self, AdapterConformanceError> {
195 if matches!(
196 disposition,
197 AdapterFeatureDisposition::Degraded | AdapterFeatureDisposition::Unavailable
198 ) != limitation.is_some()
199 {
200 return Err(AdapterConformanceError::new(
201 AdapterConformanceErrorKind::InvalidShape,
202 ));
203 }
204 Ok(Self {
205 feature,
206 disposition,
207 limitation,
208 })
209 }
210
211 #[must_use]
213 pub const fn required(feature: InstallationFeature) -> Self {
214 Self {
215 feature,
216 disposition: AdapterFeatureDisposition::Required,
217 limitation: None,
218 }
219 }
220
221 #[must_use]
223 pub const fn unavailable(feature: InstallationFeature, limitation: AdapterLimitation) -> Self {
224 Self {
225 feature,
226 disposition: AdapterFeatureDisposition::Unavailable,
227 limitation: Some(limitation),
228 }
229 }
230
231 #[must_use]
233 pub const fn feature(self) -> InstallationFeature {
234 self.feature
235 }
236
237 #[must_use]
239 pub const fn disposition(self) -> AdapterFeatureDisposition {
240 self.disposition
241 }
242
243 #[must_use]
245 pub const fn limitation(self) -> Option<AdapterLimitation> {
246 self.limitation
247 }
248}
249
250#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
252pub enum AdapterPlatform {
253 LinuxX86_64Gnu,
255 LinuxX86_64Musl,
257 LinuxAarch64Gnu,
259 MacosX86_64,
261 MacosAarch64,
263 WindowsX86_64,
265}
266
267impl AdapterPlatform {
268 const fn tag(self) -> &'static str {
269 match self {
270 Self::LinuxX86_64Gnu => "linux-x86_64-gnu",
271 Self::LinuxX86_64Musl => "linux-x86_64-musl",
272 Self::LinuxAarch64Gnu => "linux-aarch64-gnu",
273 Self::MacosX86_64 => "macos-x86_64",
274 Self::MacosAarch64 => "macos-aarch64",
275 Self::WindowsX86_64 => "windows-x86_64",
276 }
277 }
278
279 fn parse(value: &str) -> Option<Self> {
280 Some(match value {
281 "linux-x86_64-gnu" => Self::LinuxX86_64Gnu,
282 "linux-x86_64-musl" => Self::LinuxX86_64Musl,
283 "linux-aarch64-gnu" => Self::LinuxAarch64Gnu,
284 "macos-x86_64" => Self::MacosX86_64,
285 "macos-aarch64" => Self::MacosAarch64,
286 "windows-x86_64" => Self::WindowsX86_64,
287 _ => return None,
288 })
289 }
290}
291
292#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
294pub struct AdapterDriverRequirement {
295 driver: InstallationDriver,
296 runtime_version: InstallationSymbol,
297 platforms: Vec<AdapterPlatform>,
298 conformance_hash: GeneratedArtifactHash,
299}
300
301impl AdapterDriverRequirement {
302 pub fn new(
304 driver: InstallationDriver,
305 runtime_version: InstallationSymbol,
306 mut platforms: Vec<AdapterPlatform>,
307 conformance_hash: GeneratedArtifactHash,
308 ) -> Result<Self, AdapterConformanceError> {
309 let runtime = runtime_version.as_str();
310 if !runtime.bytes().any(|byte| byte.is_ascii_digit())
311 || runtime.contains("..")
312 || matches!(runtime, "latest" | "current" | "stable" | "any")
313 {
314 return Err(AdapterConformanceError::new(
315 AdapterConformanceErrorKind::InvalidShape,
316 ));
317 }
318 platforms.sort();
319 if platforms.is_empty()
320 || platforms.len() > MAX_ADAPTER_PLATFORMS
321 || platforms.windows(2).any(|pair| pair[0] == pair[1])
322 {
323 return Err(AdapterConformanceError::new(if platforms.is_empty() {
324 AdapterConformanceErrorKind::InvalidShape
325 } else if platforms.len() > MAX_ADAPTER_PLATFORMS {
326 AdapterConformanceErrorKind::LimitExceeded
327 } else {
328 AdapterConformanceErrorKind::Duplicate
329 }));
330 }
331 Ok(Self {
332 driver,
333 runtime_version,
334 platforms,
335 conformance_hash,
336 })
337 }
338
339 #[must_use]
341 pub const fn driver(&self) -> InstallationDriver {
342 self.driver
343 }
344
345 #[must_use]
347 pub const fn runtime_version(&self) -> &InstallationSymbol {
348 &self.runtime_version
349 }
350
351 #[must_use]
353 pub fn platforms(&self) -> &[AdapterPlatform] {
354 &self.platforms
355 }
356
357 #[must_use]
359 pub const fn conformance_hash(&self) -> GeneratedArtifactHash {
360 self.conformance_hash
361 }
362}
363
364#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
366pub struct AdapterRoleRequirement {
367 name: InstallationSymbol,
368 role_hash: ApplicationRoleHash,
369 operations: Vec<RoleOperation>,
370}
371
372impl AdapterRoleRequirement {
373 pub fn new(
375 name: InstallationSymbol,
376 role_hash: ApplicationRoleHash,
377 mut operations: Vec<RoleOperation>,
378 ) -> Result<Self, AdapterConformanceError> {
379 operations.sort();
380 if operations.is_empty()
381 || operations.len() > crate::MAX_ROLE_OPERATIONS
382 || operations.windows(2).any(|pair| pair[0] == pair[1])
383 {
384 return Err(AdapterConformanceError::new(if operations.is_empty() {
385 AdapterConformanceErrorKind::InvalidShape
386 } else if operations.len() > crate::MAX_ROLE_OPERATIONS {
387 AdapterConformanceErrorKind::LimitExceeded
388 } else {
389 AdapterConformanceErrorKind::Duplicate
390 }));
391 }
392 Ok(Self {
393 name,
394 role_hash,
395 operations,
396 })
397 }
398
399 #[must_use]
401 pub const fn name(&self) -> &InstallationSymbol {
402 &self.name
403 }
404
405 #[must_use]
407 pub const fn role_hash(&self) -> ApplicationRoleHash {
408 self.role_hash
409 }
410
411 #[must_use]
413 pub fn operations(&self) -> &[RoleOperation] {
414 &self.operations
415 }
416}
417
418#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
420pub struct AdapterConformanceProbe {
421 name: InstallationSymbol,
422 role: InstallationSymbol,
423 operation: RoleOperation,
424 expected_observation_hash: GeneratedArtifactHash,
425 maximum_items: u32,
426}
427
428impl AdapterConformanceProbe {
429 pub fn new(
431 name: InstallationSymbol,
432 role: InstallationSymbol,
433 operation: RoleOperation,
434 expected_observation_hash: GeneratedArtifactHash,
435 maximum_items: u32,
436 ) -> Result<Self, AdapterConformanceError> {
437 if maximum_items == 0 || maximum_items > MAX_ADAPTER_PROBE_ITEMS {
438 return Err(AdapterConformanceError::new(
439 AdapterConformanceErrorKind::LimitExceeded,
440 ));
441 }
442 Ok(Self {
443 name,
444 role,
445 operation,
446 expected_observation_hash,
447 maximum_items,
448 })
449 }
450
451 #[must_use]
453 pub const fn name(&self) -> &InstallationSymbol {
454 &self.name
455 }
456
457 #[must_use]
459 pub const fn role(&self) -> &InstallationSymbol {
460 &self.role
461 }
462
463 #[must_use]
465 pub const fn operation(&self) -> &RoleOperation {
466 &self.operation
467 }
468
469 #[must_use]
471 pub const fn expected_observation_hash(&self) -> GeneratedArtifactHash {
472 self.expected_observation_hash
473 }
474
475 #[must_use]
477 pub const fn maximum_items(&self) -> u32 {
478 self.maximum_items
479 }
480}
481
482#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
484pub enum AdapterEvolutionClass {
485 EmptyInstall,
487 CompatibleUpgrade,
489 ExplicitMigration,
491}
492
493impl AdapterEvolutionClass {
494 const fn tag(self) -> &'static str {
495 match self {
496 Self::EmptyInstall => "empty_install",
497 Self::CompatibleUpgrade => "compatible_upgrade",
498 Self::ExplicitMigration => "explicit_migration",
499 }
500 }
501
502 fn parse(value: &str) -> Option<Self> {
503 Some(match value {
504 "empty_install" => Self::EmptyInstall,
505 "compatible_upgrade" => Self::CompatibleUpgrade,
506 "explicit_migration" => Self::ExplicitMigration,
507 _ => return None,
508 })
509 }
510}
511
512#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
514pub struct AdapterEvolutionRequirement {
515 class: AdapterEvolutionClass,
516 predecessor: Option<InstallationContract>,
517 successor: InstallationContract,
518 migration_hash: Option<MigrationBundleHash>,
519 expected_observation_hash: GeneratedArtifactHash,
520}
521
522impl AdapterEvolutionRequirement {
523 pub fn new(
525 class: AdapterEvolutionClass,
526 predecessor: Option<InstallationContract>,
527 successor: InstallationContract,
528 migration_hash: Option<MigrationBundleHash>,
529 expected_observation_hash: GeneratedArtifactHash,
530 ) -> Result<Self, AdapterConformanceError> {
531 let shape_valid = match class {
532 AdapterEvolutionClass::EmptyInstall => {
533 predecessor.is_none() && migration_hash.is_none()
534 }
535 AdapterEvolutionClass::CompatibleUpgrade => {
536 predecessor.is_some_and(|value| {
537 value.version() < successor.version()
538 || (value.version() == successor.version()
539 && value.bundle_hash() == successor.bundle_hash())
540 }) && migration_hash.is_none()
541 }
542 AdapterEvolutionClass::ExplicitMigration => {
543 predecessor.is_some_and(|value| value.version() < successor.version())
544 && migration_hash.is_some()
545 }
546 };
547 if !shape_valid {
548 return Err(AdapterConformanceError::new(
549 AdapterConformanceErrorKind::InvalidShape,
550 ));
551 }
552 Ok(Self {
553 class,
554 predecessor,
555 successor,
556 migration_hash,
557 expected_observation_hash,
558 })
559 }
560
561 #[must_use]
563 pub const fn successor(&self) -> InstallationContract {
564 self.successor
565 }
566
567 #[must_use]
569 pub const fn class(&self) -> AdapterEvolutionClass {
570 self.class
571 }
572
573 #[must_use]
575 pub const fn predecessor(&self) -> Option<InstallationContract> {
576 self.predecessor
577 }
578
579 #[must_use]
581 pub const fn migration_hash(&self) -> Option<MigrationBundleHash> {
582 self.migration_hash
583 }
584
585 #[must_use]
587 pub const fn expected_observation_hash(&self) -> GeneratedArtifactHash {
588 self.expected_observation_hash
589 }
590}
591
592#[derive(Clone, Debug, Eq, PartialEq)]
594pub struct AdapterConformanceManifestInput {
595 pub adapter: InstallationSymbol,
597 pub adapter_version: InstallationSymbol,
599 pub application_manifest_hash: ApplicationManifestHash,
601 pub application_lock_hash: ApplicationLockHash,
603 pub contract_lineage: ContractLineage,
605 pub feature_claims: Vec<AdapterFeatureClaim>,
607 pub artifacts: Vec<InstallationArtifact>,
609 pub roles: Vec<AdapterRoleRequirement>,
611 pub drivers: Vec<AdapterDriverRequirement>,
613 pub conformance: Vec<AdapterConformanceProbe>,
615 pub evolution: Vec<AdapterEvolutionRequirement>,
617}
618
619#[derive(Clone, Eq, PartialEq)]
621pub struct AdapterConformanceManifest {
622 schema: &'static str,
623 input: AdapterConformanceManifestInput,
624 identity: AdapterConformanceManifestHash,
625 canonical_bytes: Vec<u8>,
626}
627
628impl AdapterConformanceManifest {
629 pub fn compile(
631 input: AdapterConformanceManifestInput,
632 ) -> Result<Self, AdapterConformanceError> {
633 Self::compile_version(ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V1, input, false)
634 }
635
636 pub fn compile_v2(
638 input: AdapterConformanceManifestInput,
639 ) -> Result<Self, AdapterConformanceError> {
640 Self::compile_version(ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2, input, true)
641 }
642
643 fn compile_version(
644 schema: &'static str,
645 mut input: AdapterConformanceManifestInput,
646 exhaustive_features: bool,
647 ) -> Result<Self, AdapterConformanceError> {
648 validate_and_sort(&mut input, exhaustive_features)?;
649 let dto = ManifestDto::from_input(schema, &input);
650 let mut canonical_bytes = serde_json::to_vec(&dto).map_err(|_| {
651 AdapterConformanceError::new(AdapterConformanceErrorKind::InvalidEncoding)
652 })?;
653 canonical_bytes.push(b'\n');
654 if canonical_bytes.len() > MAX_ADAPTER_CONFORMANCE_MANIFEST_BYTES {
655 return Err(AdapterConformanceError::new(
656 AdapterConformanceErrorKind::LimitExceeded,
657 ));
658 }
659 let identity = hash_adapter_conformance_manifest(&canonical_bytes);
660 Ok(Self {
661 schema,
662 input,
663 identity,
664 canonical_bytes,
665 })
666 }
667
668 pub fn decode_canonical(bytes: &[u8]) -> Result<Self, AdapterConformanceError> {
670 if bytes.is_empty() || bytes.len() > MAX_ADAPTER_CONFORMANCE_MANIFEST_BYTES {
671 return Err(AdapterConformanceError::new(
672 AdapterConformanceErrorKind::LimitExceeded,
673 ));
674 }
675 let dto: ManifestDto = serde_json::from_slice(bytes).map_err(|_| {
676 AdapterConformanceError::new(AdapterConformanceErrorKind::InvalidEncoding)
677 })?;
678 let schema = match dto.schema.as_str() {
679 ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V1 => ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V1,
680 ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2 => ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2,
681 _ => {
682 return Err(AdapterConformanceError::new(
683 AdapterConformanceErrorKind::UnsupportedVersion,
684 ));
685 }
686 };
687 let input = dto.into_input()?;
688 let manifest = Self::compile_version(
689 schema,
690 input,
691 schema == ADAPTER_CONFORMANCE_MANIFEST_SCHEMA_V2,
692 )?;
693 if manifest.canonical_bytes != bytes {
694 return Err(AdapterConformanceError::new(
695 AdapterConformanceErrorKind::NonCanonical,
696 ));
697 }
698 Ok(manifest)
699 }
700
701 #[must_use]
703 pub const fn identity(&self) -> AdapterConformanceManifestHash {
704 self.identity
705 }
706
707 #[must_use]
709 pub const fn schema(&self) -> &'static str {
710 self.schema
711 }
712
713 #[must_use]
715 pub const fn input(&self) -> &AdapterConformanceManifestInput {
716 &self.input
717 }
718
719 #[must_use]
721 pub fn canonical_bytes(&self) -> &[u8] {
722 &self.canonical_bytes
723 }
724
725 pub fn validate_installation_plan(
727 &self,
728 plan: &ApplicationInstallationPlan,
729 ) -> Result<(), AdapterConformanceError> {
730 let planned = plan.input();
731 if planned.adapter_manifest_hash != Some(self.identity)
732 || planned.manifest_hash != self.input.application_manifest_hash
733 || planned.lock_hash != self.input.application_lock_hash
734 || planned.target.lineage() != &self.input.contract_lineage
735 || self
736 .input
737 .evolution
738 .iter()
739 .any(|evolution| evolution.successor != planned.contract)
740 {
741 return Err(AdapterConformanceError::new(
742 AdapterConformanceErrorKind::IdentityMismatch,
743 ));
744 }
745 let self_artifact_hash = GeneratedArtifactHash::from_bytes(*self.identity.as_bytes());
746 if !planned.artifacts.iter().any(|artifact| {
747 artifact.kind() == InstallationArtifactKind::AdapterManifest
748 && artifact.name() == &self.input.adapter
749 && artifact.content_hash() == self_artifact_hash
750 }) || self
751 .input
752 .artifacts
753 .iter()
754 .any(|required| planned.artifacts.binary_search(required).is_err())
755 {
756 return Err(AdapterConformanceError::new(
757 AdapterConformanceErrorKind::IdentityMismatch,
758 ));
759 }
760 for required in &self.input.roles {
761 let Some(role) = planned
762 .roles
763 .iter()
764 .find(|role| role.name() == required.name())
765 else {
766 return Err(AdapterConformanceError::new(
767 AdapterConformanceErrorKind::IdentityMismatch,
768 ));
769 };
770 if role.role_hash() != required.role_hash
771 || role.desired_operations() != required.operations
772 {
773 return Err(AdapterConformanceError::new(
774 AdapterConformanceErrorKind::IdentityMismatch,
775 ));
776 }
777 }
778 if self
779 .input
780 .drivers
781 .iter()
782 .any(|required| planned.drivers.binary_search(&required.driver).is_err())
783 || self.input.feature_claims.iter().any(|claim| {
784 claim.disposition == AdapterFeatureDisposition::Required
785 && planned
786 .required_features
787 .binary_search(&claim.feature)
788 .is_err()
789 })
790 {
791 return Err(AdapterConformanceError::new(
792 AdapterConformanceErrorKind::IdentityMismatch,
793 ));
794 }
795 Ok(())
796 }
797
798 pub fn validate_catalog(
800 &self,
801 supported_features: &[InstallationFeature],
802 supported_drivers: &[AdapterDriverRequirement],
803 ) -> Result<(), AdapterConformanceError> {
804 if self.input.feature_claims.iter().any(|claim| {
805 claim.disposition == AdapterFeatureDisposition::Required
806 && !supported_features.contains(&claim.feature)
807 }) {
808 return Err(AdapterConformanceError::new(
809 AdapterConformanceErrorKind::UnsupportedFeature,
810 ));
811 }
812 if self
813 .input
814 .drivers
815 .iter()
816 .any(|required| !supported_drivers.contains(required))
817 {
818 return Err(AdapterConformanceError::new(
819 AdapterConformanceErrorKind::UnsupportedDriver,
820 ));
821 }
822 Ok(())
823 }
824}
825
826impl fmt::Debug for AdapterConformanceManifest {
827 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
828 formatter
829 .debug_struct("AdapterConformanceManifest")
830 .field("adapter", &self.input.adapter)
831 .field("adapter_version", &self.input.adapter_version)
832 .field("identity", &self.identity)
833 .finish_non_exhaustive()
834 }
835}
836
837fn validate_and_sort(
838 input: &mut AdapterConformanceManifestInput,
839 exhaustive_features: bool,
840) -> Result<(), AdapterConformanceError> {
841 if input.feature_claims.len() > MAX_ADAPTER_FEATURE_CLAIMS
842 || input.artifacts.is_empty()
843 || input.artifacts.len() > MAX_ADAPTER_ARTIFACTS
844 || input.roles.is_empty()
845 || input.roles.len() > MAX_ADAPTER_ROLES
846 || input.drivers.is_empty()
847 || input.drivers.len() > MAX_ADAPTER_DRIVERS
848 || input.conformance.is_empty()
849 || input.conformance.len() > MAX_ADAPTER_CONFORMANCE_PROBES
850 || input.evolution.is_empty()
851 || input.evolution.len() > MAX_ADAPTER_EVOLUTIONS
852 {
853 return Err(AdapterConformanceError::new(
854 AdapterConformanceErrorKind::LimitExceeded,
855 ));
856 }
857 if exhaustive_features
858 && input.feature_claims.len() != InstallationFeature::ADAPTER_CONFORMANCE.len()
859 {
860 return Err(AdapterConformanceError::new(
861 AdapterConformanceErrorKind::InvalidShape,
862 ));
863 }
864 input.feature_claims.sort();
865 reject_duplicate(
866 input
867 .feature_claims
868 .windows(2)
869 .any(|pair| pair[0].feature == pair[1].feature),
870 )?;
871 if exhaustive_features
872 && input
873 .feature_claims
874 .iter()
875 .map(|claim| claim.feature)
876 .ne(InstallationFeature::ADAPTER_CONFORMANCE)
877 {
878 return Err(AdapterConformanceError::new(
879 AdapterConformanceErrorKind::InvalidShape,
880 ));
881 }
882 if !input.feature_claims.iter().any(|claim| {
883 claim.feature == InstallationFeature::InstallationCampaigns
884 && claim.disposition == AdapterFeatureDisposition::Required
885 }) {
886 return Err(AdapterConformanceError::new(
887 AdapterConformanceErrorKind::InvalidShape,
888 ));
889 }
890 input.artifacts.sort();
891 reject_duplicate(
892 input
893 .artifacts
894 .windows(2)
895 .any(|pair| (pair[0].kind(), pair[0].name()) == (pair[1].kind(), pair[1].name())),
896 )?;
897 input.roles.sort();
898 reject_duplicate(
899 input
900 .roles
901 .windows(2)
902 .any(|pair| pair[0].name == pair[1].name),
903 )?;
904 input.drivers.sort();
905 reject_duplicate(
906 input
907 .drivers
908 .windows(2)
909 .any(|pair| pair[0].driver == pair[1].driver),
910 )?;
911 input.conformance.sort();
912 reject_duplicate(
913 input
914 .conformance
915 .windows(2)
916 .any(|pair| pair[0].name == pair[1].name),
917 )?;
918 for probe in &input.conformance {
919 let Some(role) = input.roles.iter().find(|role| role.name == probe.role) else {
920 return Err(AdapterConformanceError::new(
921 AdapterConformanceErrorKind::IdentityMismatch,
922 ));
923 };
924 if role.operations.binary_search(&probe.operation).is_err() {
925 return Err(AdapterConformanceError::new(
926 AdapterConformanceErrorKind::IdentityMismatch,
927 ));
928 }
929 }
930 input.evolution.sort();
931 reject_duplicate(input.evolution.windows(2).any(|pair| pair[0] == pair[1]))?;
932 let genesis = input
933 .evolution
934 .iter()
935 .filter(|item| item.class == AdapterEvolutionClass::EmptyInstall)
936 .collect::<Vec<_>>();
937 if genesis.len() != 1
938 || input
939 .evolution
940 .iter()
941 .any(|item| item.successor != genesis[0].successor)
942 {
943 return Err(AdapterConformanceError::new(
944 AdapterConformanceErrorKind::InvalidShape,
945 ));
946 }
947 Ok(())
948}
949
950fn reject_duplicate(value: bool) -> Result<(), AdapterConformanceError> {
951 if value {
952 Err(AdapterConformanceError::new(
953 AdapterConformanceErrorKind::Duplicate,
954 ))
955 } else {
956 Ok(())
957 }
958}
959
960#[derive(Serialize, Deserialize)]
961#[serde(deny_unknown_fields)]
962struct ManifestDto {
963 schema: String,
964 adapter: String,
965 adapter_version: String,
966 application_manifest_hash: String,
967 application_lock_hash: String,
968 contract_lineage: String,
969 feature_claims: Vec<FeatureClaimDto>,
970 artifacts: Vec<ArtifactDto>,
971 roles: Vec<RoleDto>,
972 drivers: Vec<DriverDto>,
973 conformance: Vec<ProbeDto>,
974 evolution: Vec<EvolutionDto>,
975}
976
977#[derive(Serialize, Deserialize)]
978#[serde(deny_unknown_fields)]
979struct FeatureClaimDto {
980 feature: String,
981 disposition: String,
982 limitation: Option<String>,
983}
984
985#[derive(Serialize, Deserialize)]
986#[serde(deny_unknown_fields)]
987struct ArtifactDto {
988 kind: String,
989 name: String,
990 content_hash: String,
991}
992
993#[derive(Serialize, Deserialize)]
994#[serde(deny_unknown_fields)]
995struct RoleDto {
996 name: String,
997 role_hash: String,
998 operations: Vec<OperationDto>,
999}
1000
1001#[derive(Serialize, Deserialize)]
1002#[serde(deny_unknown_fields)]
1003struct OperationDto {
1004 kind: String,
1005 name: String,
1006}
1007
1008#[derive(Serialize, Deserialize)]
1009#[serde(deny_unknown_fields)]
1010struct DriverDto {
1011 driver: String,
1012 runtime_version: String,
1013 platforms: Vec<String>,
1014 conformance_hash: String,
1015}
1016
1017#[derive(Serialize, Deserialize)]
1018#[serde(deny_unknown_fields)]
1019struct ProbeDto {
1020 name: String,
1021 role: String,
1022 operation: OperationDto,
1023 expected_observation_hash: String,
1024 maximum_items: u32,
1025}
1026
1027#[derive(Serialize, Deserialize)]
1028#[serde(deny_unknown_fields)]
1029struct EvolutionDto {
1030 class: String,
1031 predecessor: Option<ContractDto>,
1032 successor: ContractDto,
1033 migration_hash: Option<String>,
1034 expected_observation_hash: String,
1035}
1036
1037#[derive(Serialize, Deserialize)]
1038#[serde(deny_unknown_fields)]
1039struct ContractDto {
1040 version: u64,
1041 bundle_hash: String,
1042}
1043
1044impl ManifestDto {
1045 fn from_input(schema: &str, input: &AdapterConformanceManifestInput) -> Self {
1046 Self {
1047 schema: schema.to_owned(),
1048 adapter: input.adapter.as_str().to_owned(),
1049 adapter_version: input.adapter_version.as_str().to_owned(),
1050 application_manifest_hash: hex(input.application_manifest_hash.as_bytes()),
1051 application_lock_hash: hex(input.application_lock_hash.as_bytes()),
1052 contract_lineage: input.contract_lineage.as_str().to_owned(),
1053 feature_claims: input
1054 .feature_claims
1055 .iter()
1056 .map(FeatureClaimDto::from_claim)
1057 .collect(),
1058 artifacts: input
1059 .artifacts
1060 .iter()
1061 .map(ArtifactDto::from_artifact)
1062 .collect(),
1063 roles: input.roles.iter().map(RoleDto::from_role).collect(),
1064 drivers: input.drivers.iter().map(DriverDto::from_driver).collect(),
1065 conformance: input.conformance.iter().map(ProbeDto::from_probe).collect(),
1066 evolution: input
1067 .evolution
1068 .iter()
1069 .map(EvolutionDto::from_evolution)
1070 .collect(),
1071 }
1072 }
1073
1074 fn into_input(self) -> Result<AdapterConformanceManifestInput, AdapterConformanceError> {
1075 Ok(AdapterConformanceManifestInput {
1076 adapter: symbol(self.adapter)?,
1077 adapter_version: symbol(self.adapter_version)?,
1078 application_manifest_hash: ApplicationManifestHash::from_bytes(parse_hash(
1079 &self.application_manifest_hash,
1080 )?),
1081 application_lock_hash: ApplicationLockHash::from_bytes(parse_hash(
1082 &self.application_lock_hash,
1083 )?),
1084 contract_lineage: ContractLineage::new(self.contract_lineage)
1085 .map_err(|_| invalid_encoding())?,
1086 feature_claims: self
1087 .feature_claims
1088 .into_iter()
1089 .map(FeatureClaimDto::into_claim)
1090 .collect::<Result<_, _>>()?,
1091 artifacts: self
1092 .artifacts
1093 .into_iter()
1094 .map(ArtifactDto::into_artifact)
1095 .collect::<Result<_, _>>()?,
1096 roles: self
1097 .roles
1098 .into_iter()
1099 .map(RoleDto::into_role)
1100 .collect::<Result<_, _>>()?,
1101 drivers: self
1102 .drivers
1103 .into_iter()
1104 .map(DriverDto::into_driver)
1105 .collect::<Result<_, _>>()?,
1106 conformance: self
1107 .conformance
1108 .into_iter()
1109 .map(ProbeDto::into_probe)
1110 .collect::<Result<_, _>>()?,
1111 evolution: self
1112 .evolution
1113 .into_iter()
1114 .map(EvolutionDto::into_evolution)
1115 .collect::<Result<_, _>>()?,
1116 })
1117 }
1118}
1119
1120impl FeatureClaimDto {
1121 fn from_claim(claim: &AdapterFeatureClaim) -> Self {
1122 Self {
1123 feature: claim.feature.tag().to_owned(),
1124 disposition: claim.disposition.tag().to_owned(),
1125 limitation: claim.limitation.map(|value| value.tag().to_owned()),
1126 }
1127 }
1128
1129 fn into_claim(self) -> Result<AdapterFeatureClaim, AdapterConformanceError> {
1130 AdapterFeatureClaim::new(
1131 InstallationFeature::parse(&self.feature).ok_or_else(invalid_encoding)?,
1132 AdapterFeatureDisposition::parse(&self.disposition).ok_or_else(invalid_encoding)?,
1133 self.limitation
1134 .map(|value| AdapterLimitation::parse(&value).ok_or_else(invalid_encoding))
1135 .transpose()?,
1136 )
1137 }
1138}
1139
1140impl ArtifactDto {
1141 fn from_artifact(value: &InstallationArtifact) -> Self {
1142 Self {
1143 kind: value.kind().tag().to_owned(),
1144 name: value.name().as_str().to_owned(),
1145 content_hash: hex(value.content_hash().as_bytes()),
1146 }
1147 }
1148
1149 fn into_artifact(self) -> Result<InstallationArtifact, AdapterConformanceError> {
1150 Ok(InstallationArtifact::new(
1151 InstallationArtifactKind::parse(&self.kind).ok_or_else(invalid_encoding)?,
1152 symbol(self.name)?,
1153 GeneratedArtifactHash::from_bytes(parse_hash(&self.content_hash)?),
1154 ))
1155 }
1156}
1157
1158impl RoleDto {
1159 fn from_role(value: &AdapterRoleRequirement) -> Self {
1160 Self {
1161 name: value.name.as_str().to_owned(),
1162 role_hash: hex(value.role_hash.as_bytes()),
1163 operations: value
1164 .operations
1165 .iter()
1166 .map(OperationDto::from_operation)
1167 .collect(),
1168 }
1169 }
1170
1171 fn into_role(self) -> Result<AdapterRoleRequirement, AdapterConformanceError> {
1172 AdapterRoleRequirement::new(
1173 symbol(self.name)?,
1174 ApplicationRoleHash::from_bytes(parse_hash(&self.role_hash)?),
1175 self.operations
1176 .into_iter()
1177 .map(OperationDto::into_operation)
1178 .collect::<Result<_, _>>()?,
1179 )
1180 }
1181}
1182
1183impl OperationDto {
1184 fn from_operation(value: &RoleOperation) -> Self {
1185 Self {
1186 kind: value.kind().tag().to_owned(),
1187 name: value.name().as_str().to_owned(),
1188 }
1189 }
1190
1191 fn into_operation(self) -> Result<RoleOperation, AdapterConformanceError> {
1192 Ok(RoleOperation::new(
1193 RoleOperationKind::parse(&self.kind).ok_or_else(invalid_encoding)?,
1194 symbol(self.name)?,
1195 ))
1196 }
1197}
1198
1199impl DriverDto {
1200 fn from_driver(value: &AdapterDriverRequirement) -> Self {
1201 Self {
1202 driver: value.driver.tag().to_owned(),
1203 runtime_version: value.runtime_version.as_str().to_owned(),
1204 platforms: value
1205 .platforms
1206 .iter()
1207 .map(|value| value.tag().to_owned())
1208 .collect(),
1209 conformance_hash: hex(value.conformance_hash.as_bytes()),
1210 }
1211 }
1212
1213 fn into_driver(self) -> Result<AdapterDriverRequirement, AdapterConformanceError> {
1214 AdapterDriverRequirement::new(
1215 InstallationDriver::parse(&self.driver).ok_or_else(invalid_encoding)?,
1216 symbol(self.runtime_version)?,
1217 self.platforms
1218 .into_iter()
1219 .map(|value| AdapterPlatform::parse(&value).ok_or_else(invalid_encoding))
1220 .collect::<Result<_, _>>()?,
1221 GeneratedArtifactHash::from_bytes(parse_hash(&self.conformance_hash)?),
1222 )
1223 }
1224}
1225
1226impl ProbeDto {
1227 fn from_probe(value: &AdapterConformanceProbe) -> Self {
1228 Self {
1229 name: value.name.as_str().to_owned(),
1230 role: value.role.as_str().to_owned(),
1231 operation: OperationDto::from_operation(&value.operation),
1232 expected_observation_hash: hex(value.expected_observation_hash.as_bytes()),
1233 maximum_items: value.maximum_items,
1234 }
1235 }
1236
1237 fn into_probe(self) -> Result<AdapterConformanceProbe, AdapterConformanceError> {
1238 AdapterConformanceProbe::new(
1239 symbol(self.name)?,
1240 symbol(self.role)?,
1241 self.operation.into_operation()?,
1242 GeneratedArtifactHash::from_bytes(parse_hash(&self.expected_observation_hash)?),
1243 self.maximum_items,
1244 )
1245 }
1246}
1247
1248impl EvolutionDto {
1249 fn from_evolution(value: &AdapterEvolutionRequirement) -> Self {
1250 Self {
1251 class: value.class.tag().to_owned(),
1252 predecessor: value.predecessor.map(ContractDto::from_contract),
1253 successor: ContractDto::from_contract(value.successor),
1254 migration_hash: value.migration_hash.map(|value| hex(value.as_bytes())),
1255 expected_observation_hash: hex(value.expected_observation_hash.as_bytes()),
1256 }
1257 }
1258
1259 fn into_evolution(self) -> Result<AdapterEvolutionRequirement, AdapterConformanceError> {
1260 AdapterEvolutionRequirement::new(
1261 AdapterEvolutionClass::parse(&self.class).ok_or_else(invalid_encoding)?,
1262 self.predecessor
1263 .map(ContractDto::into_contract)
1264 .transpose()?,
1265 self.successor.into_contract()?,
1266 self.migration_hash
1267 .map(|value| parse_hash(&value).map(MigrationBundleHash::from_bytes))
1268 .transpose()?,
1269 GeneratedArtifactHash::from_bytes(parse_hash(&self.expected_observation_hash)?),
1270 )
1271 }
1272}
1273
1274impl ContractDto {
1275 fn from_contract(value: InstallationContract) -> Self {
1276 Self {
1277 version: value.version().get(),
1278 bundle_hash: hex(value.bundle_hash().as_bytes()),
1279 }
1280 }
1281
1282 fn into_contract(self) -> Result<InstallationContract, AdapterConformanceError> {
1283 Ok(InstallationContract::new(
1284 ContractVersion::new(self.version).ok_or_else(invalid_encoding)?,
1285 ContractBundleHash::from_bytes(parse_hash(&self.bundle_hash)?),
1286 ))
1287 }
1288}
1289
1290fn symbol(value: String) -> Result<InstallationSymbol, AdapterConformanceError> {
1291 InstallationSymbol::new(value).map_err(|_| invalid_encoding())
1292}
1293
1294fn invalid_encoding() -> AdapterConformanceError {
1295 AdapterConformanceError::new(AdapterConformanceErrorKind::InvalidEncoding)
1296}
1297
1298fn hex(bytes: &[u8; 32]) -> String {
1299 const DIGITS: &[u8; 16] = b"0123456789abcdef";
1300 let mut output = String::with_capacity(64);
1301 for byte in bytes {
1302 output.push(char::from(DIGITS[usize::from(byte >> 4)]));
1303 output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
1304 }
1305 output
1306}
1307
1308fn parse_hash(value: &str) -> Result<[u8; 32], AdapterConformanceError> {
1309 if value.len() != 64 {
1310 return Err(invalid_encoding());
1311 }
1312 let mut bytes = [0_u8; 32];
1313 for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
1314 bytes[index] = (nibble(pair[0]).ok_or_else(invalid_encoding)? << 4)
1315 | nibble(pair[1]).ok_or_else(invalid_encoding)?;
1316 }
1317 Ok(bytes)
1318}
1319
1320const fn nibble(value: u8) -> Option<u8> {
1321 match value {
1322 b'0'..=b'9' => Some(value - b'0'),
1323 b'a'..=b'f' => Some(value - b'a' + 10),
1324 _ => None,
1325 }
1326}