1use core::{fmt, str::FromStr};
17
18use crate::{Error, Result};
19
20const MAX_VERSION_BYTES: usize = 64;
21const MAX_REQUIREMENT_BYTES: usize = (MAX_VERSION_BYTES * 2) + 3;
22
23#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
25pub struct OpenBaoCompatibilityPolicy {
26 value: OpenBaoCompatibilityPolicyValue,
27}
28
29#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
31#[non_exhaustive]
32pub enum OpenBaoCompatibilityPolicyKind {
33 Exact,
35 Range,
37 AutomaticStrict,
39 Assumed,
41 AcknowledgedUnknownNewer,
43}
44
45#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
46enum OpenBaoCompatibilityPolicyValue {
47 Exact(OpenBaoVersion),
48 Range(OpenBaoVersionRequirement),
49 AutomaticStrict,
50 Assumed(OpenBaoVersion),
51 AcknowledgedUnknownNewer,
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub(crate) enum OpenBaoCompatibilityFailure {
56 VersionMismatch {
57 detected: OpenBaoVersion,
58 requirement: OpenBaoVersionRequirement,
59 },
60 UnknownVersion(OpenBaoVersion),
61}
62
63#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
70pub struct UnknownNewerOpenBaoAcknowledgement(());
71
72impl UnknownNewerOpenBaoAcknowledgement {
73 #[must_use]
75 pub const fn acknowledge() -> Self {
76 Self(())
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
82#[non_exhaustive]
83pub enum OpenBaoCompatibilityStatus {
84 Unverified,
86 Verified,
88 Assumed,
90 AcknowledgedUnknownNewer,
92}
93
94#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
96pub struct OpenBaoCompatibilityReport {
97 status: OpenBaoCompatibilityStatus,
98 policy: Option<OpenBaoCompatibilityPolicyKind>,
99 requirement: Option<OpenBaoVersionRequirement>,
100 detected_version: Option<OpenBaoVersion>,
101 profile_version: Option<OpenBaoVersion>,
102}
103
104impl OpenBaoCompatibilityReport {
105 pub const fn status(self) -> OpenBaoCompatibilityStatus {
107 self.status
108 }
109
110 pub const fn policy(self) -> Option<OpenBaoCompatibilityPolicyKind> {
112 self.policy
113 }
114
115 pub const fn requirement(self) -> Option<OpenBaoVersionRequirement> {
117 self.requirement
118 }
119
120 pub const fn detected_version(self) -> Option<OpenBaoVersion> {
122 self.detected_version
123 }
124
125 pub const fn profile_version(self) -> Option<OpenBaoVersion> {
127 self.profile_version
128 }
129
130 pub(crate) const fn unverified() -> Self {
131 Self {
132 status: OpenBaoCompatibilityStatus::Unverified,
133 policy: None,
134 requirement: None,
135 detected_version: None,
136 profile_version: None,
137 }
138 }
139
140 pub(crate) const fn verified(
141 policy: OpenBaoCompatibilityPolicyKind,
142 detected_version: OpenBaoVersion,
143 requirement: Option<OpenBaoVersionRequirement>,
144 ) -> Self {
145 Self {
146 status: OpenBaoCompatibilityStatus::Verified,
147 policy: Some(policy),
148 requirement,
149 detected_version: Some(detected_version),
150 profile_version: Some(detected_version),
151 }
152 }
153
154 pub(crate) const fn assumed(version: OpenBaoVersion) -> Self {
155 Self {
156 status: OpenBaoCompatibilityStatus::Assumed,
157 policy: Some(OpenBaoCompatibilityPolicyKind::Assumed),
158 requirement: Some(OpenBaoVersionRequirement::exact(version)),
159 detected_version: None,
160 profile_version: Some(version),
161 }
162 }
163
164 pub(crate) const fn acknowledged_unknown_newer(
165 detected_version: OpenBaoVersion,
166 profile_version: OpenBaoVersion,
167 ) -> Self {
168 Self {
169 status: OpenBaoCompatibilityStatus::AcknowledgedUnknownNewer,
170 policy: Some(OpenBaoCompatibilityPolicyKind::AcknowledgedUnknownNewer),
171 requirement: None,
172 detected_version: Some(detected_version),
173 profile_version: Some(profile_version),
174 }
175 }
176}
177
178#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
184pub struct OpenBaoVersion {
185 major: u32,
186 minor: u32,
187 patch: u32,
188}
189
190impl OpenBaoVersion {
191 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
193 Self {
194 major,
195 minor,
196 patch,
197 }
198 }
199
200 pub const fn major(self) -> u32 {
202 self.major
203 }
204
205 pub const fn minor(self) -> u32 {
207 self.minor
208 }
209
210 pub const fn patch(self) -> u32 {
212 self.patch
213 }
214}
215
216impl fmt::Display for OpenBaoVersion {
217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218 write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
219 }
220}
221
222impl FromStr for OpenBaoVersion {
223 type Err = Error;
224
225 fn from_str(input: &str) -> Result<Self> {
226 if input.is_empty() {
227 return Err(invalid_version("version must not be empty"));
228 }
229 if input.len() > MAX_VERSION_BYTES {
230 return Err(invalid_version("version exceeds the input limit"));
231 }
232 if !input.is_ascii() {
233 return Err(invalid_version(
234 "version must contain only ASCII digits and dots",
235 ));
236 }
237
238 let mut components = input.split('.');
239 let major = components
240 .next()
241 .ok_or_else(|| invalid_version("version must contain three components"))?;
242 let minor = components
243 .next()
244 .ok_or_else(|| invalid_version("version must contain three components"))?;
245 let patch = components
246 .next()
247 .ok_or_else(|| invalid_version("version must contain three components"))?;
248 if components.next().is_some() {
249 return Err(invalid_version("version must contain three components"));
250 }
251
252 Ok(Self::new(
253 parse_component(major)?,
254 parse_component(minor)?,
255 parse_component(patch)?,
256 ))
257 }
258}
259
260#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
266pub enum OpenBaoVersionRequirement {
267 Exact(OpenBaoVersion),
269 Inclusive {
271 minimum: OpenBaoVersion,
273 maximum: OpenBaoVersion,
275 },
276}
277
278impl OpenBaoVersionRequirement {
279 pub const fn exact(version: OpenBaoVersion) -> Self {
281 Self::Exact(version)
282 }
283
284 pub fn inclusive(minimum: OpenBaoVersion, maximum: OpenBaoVersion) -> Result<Self> {
286 if minimum > maximum {
287 return Err(invalid_requirement(
288 "minimum version must not exceed maximum version",
289 ));
290 }
291 Ok(Self::Inclusive { minimum, maximum })
292 }
293
294 pub const fn minimum(self) -> OpenBaoVersion {
296 match self {
297 Self::Exact(version) => version,
298 Self::Inclusive { minimum, .. } => minimum,
299 }
300 }
301
302 pub const fn maximum(self) -> OpenBaoVersion {
304 match self {
305 Self::Exact(version) => version,
306 Self::Inclusive { maximum, .. } => maximum,
307 }
308 }
309
310 pub fn contains(self, version: OpenBaoVersion) -> bool {
312 version_in_closed_interval(version, self.minimum(), self.maximum())
313 }
314}
315
316pub(crate) fn version_in_closed_interval(
317 version: OpenBaoVersion,
318 minimum: OpenBaoVersion,
319 maximum: OpenBaoVersion,
320) -> bool {
321 version >= minimum && version <= maximum
322}
323
324impl fmt::Display for OpenBaoVersionRequirement {
325 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
326 match self {
327 Self::Exact(version) => version.fmt(formatter),
328 Self::Inclusive { minimum, maximum } => {
329 write!(formatter, "{minimum}..={maximum}")
330 }
331 }
332 }
333}
334
335impl FromStr for OpenBaoVersionRequirement {
336 type Err = Error;
337
338 fn from_str(input: &str) -> Result<Self> {
339 if input.is_empty() {
340 return Err(invalid_requirement("requirement must not be empty"));
341 }
342 if input.len() > MAX_REQUIREMENT_BYTES {
343 return Err(invalid_requirement("requirement exceeds the input limit"));
344 }
345
346 if let Some((minimum, maximum)) = input.split_once("..=") {
347 if minimum.is_empty() || maximum.is_empty() || maximum.contains("..=") {
348 return Err(invalid_requirement(
349 "requirement must be an exact version or one inclusive range",
350 ));
351 }
352 return Self::inclusive(
353 parse_requirement_version(minimum)?,
354 parse_requirement_version(maximum)?,
355 );
356 }
357
358 Ok(Self::exact(parse_requirement_version(input)?))
359 }
360}
361
362impl OpenBaoCompatibilityPolicy {
363 pub fn exact(version: OpenBaoVersion) -> Result<Self> {
365 require_routable_profile(version)?;
366 Ok(Self {
367 value: OpenBaoCompatibilityPolicyValue::Exact(version),
368 })
369 }
370
371 pub fn range(requirement: OpenBaoVersionRequirement) -> Result<Self> {
375 require_routable_profile(requirement.minimum())?;
376 require_routable_profile(requirement.maximum())?;
377 Ok(Self {
378 value: OpenBaoCompatibilityPolicyValue::Range(requirement),
379 })
380 }
381
382 #[must_use]
384 pub const fn automatic_strict() -> Self {
385 Self {
386 value: OpenBaoCompatibilityPolicyValue::AutomaticStrict,
387 }
388 }
389
390 pub fn assume(version: OpenBaoVersion) -> Result<Self> {
396 require_routable_profile(version)?;
397 Ok(Self {
398 value: OpenBaoCompatibilityPolicyValue::Assumed(version),
399 })
400 }
401
402 #[must_use]
408 pub const fn automatic_allow_unknown_newer(
409 _acknowledgement: UnknownNewerOpenBaoAcknowledgement,
410 ) -> Self {
411 Self {
412 value: OpenBaoCompatibilityPolicyValue::AcknowledgedUnknownNewer,
413 }
414 }
415
416 pub const fn kind(self) -> OpenBaoCompatibilityPolicyKind {
418 match self.value {
419 OpenBaoCompatibilityPolicyValue::Exact(_) => OpenBaoCompatibilityPolicyKind::Exact,
420 OpenBaoCompatibilityPolicyValue::Range(_) => OpenBaoCompatibilityPolicyKind::Range,
421 OpenBaoCompatibilityPolicyValue::AutomaticStrict => {
422 OpenBaoCompatibilityPolicyKind::AutomaticStrict
423 }
424 OpenBaoCompatibilityPolicyValue::Assumed(_) => OpenBaoCompatibilityPolicyKind::Assumed,
425 OpenBaoCompatibilityPolicyValue::AcknowledgedUnknownNewer => {
426 OpenBaoCompatibilityPolicyKind::AcknowledgedUnknownNewer
427 }
428 }
429 }
430
431 pub const fn requirement(self) -> Option<OpenBaoVersionRequirement> {
433 match self.value {
434 OpenBaoCompatibilityPolicyValue::Exact(version)
435 | OpenBaoCompatibilityPolicyValue::Assumed(version) => {
436 Some(OpenBaoVersionRequirement::exact(version))
437 }
438 OpenBaoCompatibilityPolicyValue::Range(requirement) => Some(requirement),
439 OpenBaoCompatibilityPolicyValue::AutomaticStrict
440 | OpenBaoCompatibilityPolicyValue::AcknowledgedUnknownNewer => None,
441 }
442 }
443
444 pub(crate) fn immediate_report(self) -> Option<OpenBaoCompatibilityReport> {
445 match self.value {
446 OpenBaoCompatibilityPolicyValue::Assumed(version) => {
447 Some(OpenBaoCompatibilityReport::assumed(version))
448 }
449 _ => None,
450 }
451 }
452
453 pub(crate) fn evaluate_detected(
454 self,
455 detected: OpenBaoVersion,
456 ) -> core::result::Result<OpenBaoCompatibilityReport, OpenBaoCompatibilityFailure> {
457 match self.value {
458 OpenBaoCompatibilityPolicyValue::Exact(expected) => {
459 if detected != expected {
460 return Err(OpenBaoCompatibilityFailure::VersionMismatch {
461 detected,
462 requirement: OpenBaoVersionRequirement::exact(expected),
463 });
464 }
465 Ok(OpenBaoCompatibilityReport::verified(
466 self.kind(),
467 detected,
468 self.requirement(),
469 ))
470 }
471 OpenBaoCompatibilityPolicyValue::Range(requirement) => {
472 if !requirement.contains(detected) {
473 return Err(OpenBaoCompatibilityFailure::VersionMismatch {
474 detected,
475 requirement,
476 });
477 }
478 if !is_routable_profile(detected) {
479 return Err(OpenBaoCompatibilityFailure::UnknownVersion(detected));
480 }
481 Ok(OpenBaoCompatibilityReport::verified(
482 self.kind(),
483 detected,
484 self.requirement(),
485 ))
486 }
487 OpenBaoCompatibilityPolicyValue::AutomaticStrict => {
488 if !is_routable_profile(detected) {
489 return Err(OpenBaoCompatibilityFailure::UnknownVersion(detected));
490 }
491 Ok(OpenBaoCompatibilityReport::verified(
492 self.kind(),
493 detected,
494 self.requirement(),
495 ))
496 }
497 OpenBaoCompatibilityPolicyValue::Assumed(version) => {
498 Ok(OpenBaoCompatibilityReport::assumed(version))
499 }
500 OpenBaoCompatibilityPolicyValue::AcknowledgedUnknownNewer => {
501 if is_routable_profile(detected) {
502 return Ok(OpenBaoCompatibilityReport::verified(
503 self.kind(),
504 detected,
505 self.requirement(),
506 ));
507 }
508 let Some(latest) = latest_routable_profile() else {
509 return Err(OpenBaoCompatibilityFailure::UnknownVersion(detected));
510 };
511 if detected > latest {
512 return Ok(OpenBaoCompatibilityReport::acknowledged_unknown_newer(
513 detected, latest,
514 ));
515 }
516 Err(OpenBaoCompatibilityFailure::UnknownVersion(detected))
517 }
518 }
519 }
520}
521
522#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
528pub enum OpenBaoHttpMethod {
529 Acme,
531 Delete,
533 Get,
535 Head,
537 List,
539 Patch,
541 Post,
543 Put,
545 Scan,
547}
548
549impl OpenBaoHttpMethod {
550 pub const fn as_str(self) -> &'static str {
552 match self {
553 Self::Acme => "ACME",
554 Self::Delete => "DELETE",
555 Self::Get => "GET",
556 Self::Head => "HEAD",
557 Self::List => "LIST",
558 Self::Patch => "PATCH",
559 Self::Post => "POST",
560 Self::Put => "PUT",
561 Self::Scan => "SCAN",
562 }
563 }
564}
565
566#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
568pub enum OpenBaoOperationDisposition {
569 Typed,
571 TypedGated,
573 SecurityBlocked,
575}
576
577#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
579pub enum OpenBaoCapabilityEvidence {
580 None,
582 TaggedDocumentation,
584 LockedOpenApi,
586 CorrectedCurrentContract,
588}
589
590#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
592pub enum OpenBaoCapabilityAvailability {
593 DocumentedRoute,
595 NotDocumented,
597 SecurityBlocked,
599}
600
601#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
603pub struct OpenBaoCapabilityRange {
604 minimum: OpenBaoVersion,
605 maximum: OpenBaoVersion,
606 evidence: OpenBaoCapabilityEvidence,
607}
608
609impl OpenBaoCapabilityRange {
610 pub(crate) const fn generated(
611 minimum: OpenBaoVersion,
612 maximum: OpenBaoVersion,
613 evidence: OpenBaoCapabilityEvidence,
614 ) -> Self {
615 Self {
616 minimum,
617 maximum,
618 evidence,
619 }
620 }
621
622 pub const fn minimum(self) -> OpenBaoVersion {
624 self.minimum
625 }
626
627 pub const fn maximum(self) -> OpenBaoVersion {
629 self.maximum
630 }
631
632 pub const fn evidence(self) -> OpenBaoCapabilityEvidence {
634 self.evidence
635 }
636
637 fn contains(self, version: OpenBaoVersion) -> bool {
638 version_in_closed_interval(version, self.minimum, self.maximum)
639 }
640}
641
642#[derive(Clone, Copy, Debug)]
648pub struct OpenBaoOperation {
649 id: &'static str,
650 method: OpenBaoHttpMethod,
651 path_template: &'static str,
652 disposition: OpenBaoOperationDisposition,
653 ranges: &'static [OpenBaoCapabilityRange],
654}
655
656impl OpenBaoOperation {
657 const fn generated(
658 id: &'static str,
659 method: OpenBaoHttpMethod,
660 path_template: &'static str,
661 disposition: OpenBaoOperationDisposition,
662 ranges: &'static [OpenBaoCapabilityRange],
663 ) -> Self {
664 Self {
665 id,
666 method,
667 path_template,
668 disposition,
669 ranges,
670 }
671 }
672
673 pub const fn id(self) -> &'static str {
675 self.id
676 }
677
678 pub const fn method(self) -> OpenBaoHttpMethod {
680 self.method
681 }
682
683 pub const fn path_template(self) -> &'static str {
685 self.path_template
686 }
687
688 pub const fn disposition(self) -> OpenBaoOperationDisposition {
690 self.disposition
691 }
692
693 pub const fn ranges(self) -> &'static [OpenBaoCapabilityRange] {
695 self.ranges
696 }
697
698 pub fn availability(self, version: OpenBaoVersion) -> Option<OpenBaoCapabilityAvailability> {
704 if !is_generated_profile(version) {
705 return None;
706 }
707 let range = select_capability_range(self.ranges, version)?;
708 if range.evidence == OpenBaoCapabilityEvidence::None {
709 return Some(OpenBaoCapabilityAvailability::NotDocumented);
710 }
711 if self.disposition == OpenBaoOperationDisposition::SecurityBlocked {
712 return Some(OpenBaoCapabilityAvailability::SecurityBlocked);
713 }
714 Some(OpenBaoCapabilityAvailability::DocumentedRoute)
715 }
716
717 pub fn evidence(self, version: OpenBaoVersion) -> Option<OpenBaoCapabilityEvidence> {
719 if !is_generated_profile(version) {
720 return None;
721 }
722 select_capability_range(self.ranges, version).map(OpenBaoCapabilityRange::evidence)
723 }
724}
725
726pub(crate) fn select_capability_range(
727 ranges: &[OpenBaoCapabilityRange],
728 version: OpenBaoVersion,
729) -> Option<OpenBaoCapabilityRange> {
730 ranges.iter().copied().find(|range| range.contains(version))
731}
732
733#[derive(Clone, Copy, Debug)]
735pub struct OpenBaoCapabilityStatus {
736 operation: OpenBaoOperation,
737 availability: OpenBaoCapabilityAvailability,
738 evidence: OpenBaoCapabilityEvidence,
739}
740
741impl OpenBaoCapabilityStatus {
742 pub const fn operation(self) -> OpenBaoOperation {
744 self.operation
745 }
746
747 pub const fn availability(self) -> OpenBaoCapabilityAvailability {
749 self.availability
750 }
751
752 pub const fn evidence(self) -> OpenBaoCapabilityEvidence {
754 self.evidence
755 }
756}
757
758#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
760pub struct OpenBaoCapabilityProfile {
761 version: OpenBaoVersion,
762}
763
764#[derive(Clone, Copy, Debug)]
766pub(crate) struct OpenBaoEndpointSpec {
767 id: &'static str,
768 variants: &'static [OpenBaoEndpointVariant],
769}
770
771impl OpenBaoEndpointSpec {
772 #[allow(dead_code)]
773 pub(crate) const fn new(id: &'static str, variants: &'static [OpenBaoEndpointVariant]) -> Self {
774 Self { id, variants }
775 }
776
777 pub(crate) const fn id(self) -> &'static str {
778 self.id
779 }
780
781 pub(crate) const fn variants(self) -> &'static [OpenBaoEndpointVariant] {
782 self.variants
783 }
784}
785
786#[derive(Clone, Copy, Debug)]
788pub(crate) struct OpenBaoEndpointVariant {
789 operation_id: &'static str,
790 minimum: OpenBaoVersion,
791 maximum: OpenBaoVersion,
792}
793
794impl OpenBaoEndpointVariant {
795 #[allow(dead_code)]
796 pub(crate) const fn new(
797 operation_id: &'static str,
798 minimum: OpenBaoVersion,
799 maximum: OpenBaoVersion,
800 ) -> Self {
801 Self {
802 operation_id,
803 minimum,
804 maximum,
805 }
806 }
807
808 pub(crate) const fn operation_id(self) -> &'static str {
809 self.operation_id
810 }
811
812 pub(crate) const fn minimum(self) -> OpenBaoVersion {
813 self.minimum
814 }
815
816 pub(crate) const fn maximum(self) -> OpenBaoVersion {
817 self.maximum
818 }
819
820 pub(crate) fn contains(self, version: OpenBaoVersion) -> bool {
821 version >= self.minimum && version <= self.maximum
822 }
823}
824
825impl OpenBaoCapabilityProfile {
826 pub fn for_version(version: OpenBaoVersion) -> Option<Self> {
828 is_generated_profile(version).then_some(Self { version })
829 }
830
831 pub const fn version(self) -> OpenBaoVersion {
833 self.version
834 }
835
836 pub fn operation(self, id: &str) -> Option<OpenBaoCapabilityStatus> {
838 let operation = openbao_operation(id)?;
839 capability_status(operation, self.version)
840 }
841
842 pub fn operations(self) -> impl Iterator<Item = OpenBaoCapabilityStatus> {
844 generated::GENERATED_OPERATIONS
845 .iter()
846 .copied()
847 .filter_map(move |operation| capability_status(operation, self.version))
848 }
849}
850
851pub fn openbao_operations() -> &'static [OpenBaoOperation] {
853 generated::GENERATED_OPERATIONS
854}
855
856pub fn openbao_operation(id: &str) -> Option<OpenBaoOperation> {
858 generated::GENERATED_OPERATIONS
859 .binary_search_by_key(&id, |operation| operation.id())
860 .ok()
861 .map(|index| generated::GENERATED_OPERATIONS[index])
862}
863
864pub fn openbao_profile_versions() -> &'static [OpenBaoVersion] {
869 generated::GENERATED_PROFILE_VERSIONS
870}
871
872pub(crate) fn is_generated_profile(version: OpenBaoVersion) -> bool {
873 generated::GENERATED_PROFILE_VERSIONS
874 .binary_search(&version)
875 .is_ok()
876}
877
878pub(crate) fn is_routable_profile(version: OpenBaoVersion) -> bool {
879 generated::GENERATED_ROUTABLE_PROFILE_VERSIONS
880 .binary_search(&version)
881 .is_ok()
882}
883
884pub(crate) fn latest_routable_profile() -> Option<OpenBaoVersion> {
885 generated::GENERATED_ROUTABLE_PROFILE_VERSIONS
886 .last()
887 .copied()
888}
889
890fn require_routable_profile(version: OpenBaoVersion) -> Result<()> {
891 if is_routable_profile(version) {
892 Ok(())
893 } else {
894 Err(invalid_requirement(
895 "compatibility policy version is absent from the runtime-approved release inventory",
896 ))
897 }
898}
899
900fn capability_status(
901 operation: OpenBaoOperation,
902 version: OpenBaoVersion,
903) -> Option<OpenBaoCapabilityStatus> {
904 Some(OpenBaoCapabilityStatus {
905 availability: operation.availability(version)?,
906 evidence: operation.evidence(version)?,
907 operation,
908 })
909}
910
911pub(crate) mod generated {
912 use super::{
913 OpenBaoCapabilityEvidence, OpenBaoCapabilityRange, OpenBaoEndpointSpec,
914 OpenBaoEndpointVariant, OpenBaoHttpMethod, OpenBaoOperation, OpenBaoOperationDisposition,
915 OpenBaoVersion,
916 };
917
918 include!("generated/openbao_capabilities.rs");
919}
920
921fn parse_component(component: &str) -> Result<u32> {
922 if component.is_empty() {
923 return Err(invalid_version("version components must not be empty"));
924 }
925 if component.len() > 1 && component.starts_with('0') {
926 return Err(invalid_version(
927 "version components must use canonical decimal form",
928 ));
929 }
930 if !component.bytes().all(|byte| byte.is_ascii_digit()) {
931 return Err(invalid_version(
932 "version components must be decimal integers",
933 ));
934 }
935 component
936 .parse::<u32>()
937 .map_err(|_| invalid_version("version component exceeds u32"))
938}
939
940fn parse_requirement_version(input: &str) -> Result<OpenBaoVersion> {
941 input
942 .parse()
943 .map_err(|_| invalid_requirement("requirement contains an invalid stable OpenBao version"))
944}
945
946const fn invalid_version(reason: &'static str) -> Error {
947 Error::InvalidOpenBaoVersion(reason)
948}
949
950const fn invalid_requirement(reason: &'static str) -> Error {
951 Error::InvalidOpenBaoVersionRequirement(reason)
952}
953
954#[cfg(test)]
955mod tests {
956 #![allow(clippy::panic)]
957
958 use core::str::FromStr;
959
960 use super::{
961 OpenBaoCapabilityAvailability, OpenBaoCapabilityEvidence, OpenBaoCapabilityProfile,
962 OpenBaoCompatibilityFailure, OpenBaoCompatibilityPolicy, OpenBaoCompatibilityPolicyKind,
963 OpenBaoCompatibilityStatus, OpenBaoHttpMethod, OpenBaoOperationDisposition, OpenBaoVersion,
964 OpenBaoVersionRequirement, UnknownNewerOpenBaoAcknowledgement, is_routable_profile,
965 latest_routable_profile, openbao_operation, openbao_operations, openbao_profile_versions,
966 };
967 use crate::{Error, Result};
968
969 #[test]
970 fn exact_version_parsing_is_canonical_and_ordered() -> Result<()> {
971 let version = OpenBaoVersion::from_str("2.5.5")?;
972
973 assert_eq!(version.major(), 2);
974 assert_eq!(version.minor(), 5);
975 assert_eq!(version.patch(), 5);
976 assert_eq!(version.to_string(), "2.5.5");
977 assert!(OpenBaoVersion::new(2, 5, 4) < version);
978 assert!(version < OpenBaoVersion::new(3, 0, 0));
979 Ok(())
980 }
981
982 #[test]
983 fn compatibility_policies_require_runtime_approved_profiles() -> Result<()> {
984 let previous = OpenBaoVersion::new(2, 5, 5);
985 let known = OpenBaoVersion::new(2, 6, 0);
986 let unpublished = OpenBaoVersion::new(2, 4, 2);
987
988 assert_eq!(
989 OpenBaoCompatibilityPolicy::exact(known)?.kind(),
990 OpenBaoCompatibilityPolicyKind::Exact
991 );
992 assert!(OpenBaoCompatibilityPolicy::exact(previous).is_ok());
993 let approved_range = OpenBaoVersionRequirement::inclusive(previous, known)?;
994 assert!(OpenBaoCompatibilityPolicy::range(approved_range).is_ok());
995 assert!(OpenBaoCompatibilityPolicy::exact(unpublished).is_err());
996 assert!(OpenBaoCompatibilityPolicy::assume(unpublished).is_err());
997 assert!(
998 OpenBaoCompatibilityPolicy::range(OpenBaoVersionRequirement::inclusive(
999 OpenBaoVersion::new(2, 4, 1),
1000 unpublished,
1001 )?)
1002 .is_err()
1003 );
1004 Ok(())
1005 }
1006
1007 #[test]
1008 fn strict_and_unknown_newer_policies_fail_closed_or_report_acknowledgement() -> Result<()> {
1009 let strict = OpenBaoCompatibilityPolicy::automatic_strict();
1010 let unknown = OpenBaoVersion::new(2, 6, 1);
1011 assert_eq!(
1012 strict.evaluate_detected(unknown),
1013 Err(OpenBaoCompatibilityFailure::UnknownVersion(unknown))
1014 );
1015
1016 let acknowledged = OpenBaoCompatibilityPolicy::automatic_allow_unknown_newer(
1017 UnknownNewerOpenBaoAcknowledgement::acknowledge(),
1018 )
1019 .evaluate_detected(unknown)
1020 .map_err(|_| Error::Internal("acknowledged newer policy rejected a newer version"))?;
1021 assert_eq!(
1022 acknowledged.status(),
1023 OpenBaoCompatibilityStatus::AcknowledgedUnknownNewer
1024 );
1025 assert_eq!(acknowledged.detected_version(), Some(unknown));
1026 assert_eq!(
1027 acknowledged.profile_version(),
1028 Some(OpenBaoVersion::new(2, 6, 0))
1029 );
1030 Ok(())
1031 }
1032
1033 #[test]
1034 fn assumed_policy_is_never_reported_as_verified() -> Result<()> {
1035 let version = OpenBaoVersion::new(2, 2, 0);
1036 let report = OpenBaoCompatibilityPolicy::assume(version)?
1037 .immediate_report()
1038 .ok_or(Error::Internal("assumed policy did not produce a report"))?;
1039 assert_eq!(report.status(), OpenBaoCompatibilityStatus::Assumed);
1040 assert_eq!(report.detected_version(), None);
1041 assert_eq!(report.profile_version(), Some(version));
1042 assert_eq!(
1043 report.requirement(),
1044 Some(OpenBaoVersionRequirement::exact(version))
1045 );
1046 Ok(())
1047 }
1048
1049 #[test]
1050 fn version_components_accept_the_full_u32_range() -> Result<()> {
1051 let maximum = OpenBaoVersion::from_str("4294967295.4294967295.4294967295")?;
1052
1053 assert_eq!(maximum, OpenBaoVersion::new(u32::MAX, u32::MAX, u32::MAX));
1054 Ok(())
1055 }
1056
1057 #[test]
1058 fn malformed_versions_are_rejected() {
1059 for input in [
1060 "",
1061 "2",
1062 "2.5",
1063 "2.5.5.0",
1064 "v2.5.5",
1065 "2.5.5 ",
1066 " 2.5.5",
1067 "02.5.5",
1068 "2.05.5",
1069 "2.5.05",
1070 "2..5",
1071 "2.5.-1",
1072 "2.5.5-rc1",
1073 "2.5.5+vendor",
1074 "2.5.5",
1075 "4294967296.0.0",
1076 ] {
1077 assert!(
1078 input.parse::<OpenBaoVersion>().is_err(),
1079 "accepted {input:?}"
1080 );
1081 }
1082 }
1083
1084 #[test]
1085 fn version_input_is_bounded_and_errors_do_not_echo_it() {
1086 let hostile = format!("2.5.5\n{}", "9".repeat(128));
1087 let result = hostile.parse::<OpenBaoVersion>();
1088 assert!(result.is_err());
1089 let Err(error) = result else {
1090 return;
1091 };
1092 let display = error.to_string();
1093
1094 assert!(matches!(error, Error::InvalidOpenBaoVersion(_)));
1095 assert!(!display.contains('\n'));
1096 assert!(!display.contains(&hostile));
1097 assert!(display.len() < 128);
1098 }
1099
1100 #[test]
1101 fn exact_requirement_matches_only_one_release() {
1102 let version = OpenBaoVersion::new(2, 5, 5);
1103 let requirement = OpenBaoVersionRequirement::exact(version);
1104
1105 assert!(requirement.contains(version));
1106 assert!(!requirement.contains(OpenBaoVersion::new(2, 5, 4)));
1107 assert_eq!(requirement.minimum(), version);
1108 assert_eq!(requirement.maximum(), version);
1109 assert_eq!(requirement.to_string(), "2.5.5");
1110 }
1111
1112 #[test]
1113 fn inclusive_requirement_matches_both_boundaries() -> Result<()> {
1114 let minimum = OpenBaoVersion::new(2, 4, 0);
1115 let maximum = OpenBaoVersion::new(2, 5, 5);
1116 let requirement = OpenBaoVersionRequirement::inclusive(minimum, maximum)?;
1117
1118 assert!(requirement.contains(minimum));
1119 assert!(requirement.contains(OpenBaoVersion::new(2, 5, 0)));
1120 assert!(requirement.contains(maximum));
1121 assert!(!requirement.contains(OpenBaoVersion::new(2, 3, 2)));
1122 assert!(!requirement.contains(OpenBaoVersion::new(2, 5, 6)));
1123 assert_eq!(requirement.to_string(), "2.4.0..=2.5.5");
1124 Ok(())
1125 }
1126
1127 #[test]
1128 fn requirement_parser_round_trips_exact_and_range_values() -> Result<()> {
1129 for input in ["2.5.5", "2.0.0..=2.5.5", "2.5.5..=2.5.5"] {
1130 let requirement = input.parse::<OpenBaoVersionRequirement>()?;
1131 assert_eq!(requirement.to_string(), input);
1132 }
1133 Ok(())
1134 }
1135
1136 #[test]
1137 fn malformed_requirements_are_rejected_without_echoing_input() {
1138 for input in [
1139 "",
1140 "2.5",
1141 "v2.5.5",
1142 "2.5.5-rc1",
1143 "2.5.5..",
1144 "..=2.5.5",
1145 "2.5.5..=",
1146 "2.5.5..=2.5.5..=2.6.0",
1147 "2.5.5..=2.5.4",
1148 ">=2.5.5",
1149 "2.5.5..2.6.0",
1150 ] {
1151 let result = input.parse::<OpenBaoVersionRequirement>();
1152 assert!(
1153 matches!(result, Err(Error::InvalidOpenBaoVersionRequirement(_))),
1154 "wrong result for {input:?}"
1155 );
1156 if let Err(error) = result {
1157 assert!(input.is_empty() || !error.to_string().contains(input));
1158 }
1159 }
1160 }
1161
1162 #[test]
1163 fn requirement_input_is_bounded() {
1164 let input = "1".repeat(132);
1165 let result = input.parse::<OpenBaoVersionRequirement>();
1166
1167 assert!(matches!(
1168 result,
1169 Err(Error::InvalidOpenBaoVersionRequirement(
1170 "requirement exceeds the input limit"
1171 ))
1172 ));
1173 }
1174
1175 #[test]
1176 fn generated_capability_registry_is_complete_and_lookup_is_stable() {
1177 let operations = openbao_operations();
1178 let versions = openbao_profile_versions();
1179
1180 assert_eq!(operations.len(), 690);
1181 assert_eq!(versions.len(), 22);
1182 assert_eq!(versions[0], OpenBaoVersion::new(2, 0, 0));
1183 assert_eq!(versions[20], OpenBaoVersion::new(2, 5, 5));
1184 assert_eq!(versions[21], OpenBaoVersion::new(2, 6, 0));
1185 assert_eq!(
1186 latest_routable_profile(),
1187 Some(OpenBaoVersion::new(2, 6, 0))
1188 );
1189 assert!(is_routable_profile(OpenBaoVersion::new(2, 5, 5)));
1190 assert!(is_routable_profile(OpenBaoVersion::new(2, 6, 0)));
1191 assert!(OpenBaoCapabilityProfile::for_version(OpenBaoVersion::new(2, 6, 0)).is_some());
1192 assert!(OpenBaoCapabilityProfile::for_version(OpenBaoVersion::new(2, 4, 2)).is_none());
1193
1194 let mut previous = None;
1195 for operation in operations {
1196 assert!(operation.path_template().starts_with('/'));
1197 assert!(!operation.path_template().chars().any(char::is_control));
1198 if let Some(previous) = previous {
1199 assert!(previous < operation.id());
1200 }
1201 previous = Some(operation.id());
1202 assert_eq!(
1203 openbao_operation(operation.id()).map(|value| value.id()),
1204 Some(operation.id())
1205 );
1206 for version in versions {
1207 assert!(operation.availability(*version).is_some());
1208 assert!(operation.evidence(*version).is_some());
1209 }
1210 }
1211
1212 let openapi_only = operations
1213 .iter()
1214 .find(|operation| operation.path_template() == "/identity/oidc/.well-known/keys")
1215 .unwrap_or_else(|| panic!("missing locked OpenAPI-only Identity JWKS route"));
1216 assert_eq!(
1217 openapi_only.evidence(OpenBaoVersion::new(2, 5, 5)),
1218 Some(OpenBaoCapabilityEvidence::LockedOpenApi)
1219 );
1220
1221 for (method, path, disposition) in [
1222 (
1223 OpenBaoHttpMethod::Delete,
1224 "/auth/jwt/cel/role/:name",
1225 OpenBaoOperationDisposition::Typed,
1226 ),
1227 (
1228 OpenBaoHttpMethod::Get,
1229 "/auth/jwt/cel/role/:name",
1230 OpenBaoOperationDisposition::Typed,
1231 ),
1232 (
1233 OpenBaoHttpMethod::List,
1234 "/auth/jwt/cel/role",
1235 OpenBaoOperationDisposition::Typed,
1236 ),
1237 (
1238 OpenBaoHttpMethod::Patch,
1239 "/auth/jwt/cel/role/:name",
1240 OpenBaoOperationDisposition::SecurityBlocked,
1241 ),
1242 (
1243 OpenBaoHttpMethod::Post,
1244 "/auth/jwt/cel/login",
1245 OpenBaoOperationDisposition::Typed,
1246 ),
1247 (
1248 OpenBaoHttpMethod::Post,
1249 "/auth/jwt/cel/role/:name",
1250 OpenBaoOperationDisposition::Typed,
1251 ),
1252 (
1253 OpenBaoHttpMethod::Delete,
1254 "/sys/namespaces/:path/delete-sealed",
1255 OpenBaoOperationDisposition::TypedGated,
1256 ),
1257 (
1258 OpenBaoHttpMethod::Get,
1259 "/sys/namespaces/:path/seal-status",
1260 OpenBaoOperationDisposition::Typed,
1261 ),
1262 (
1263 OpenBaoHttpMethod::Post,
1264 "/sys/namespaces/:path/seal",
1265 OpenBaoOperationDisposition::TypedGated,
1266 ),
1267 (
1268 OpenBaoHttpMethod::Post,
1269 "/sys/namespaces/:path/unseal",
1270 OpenBaoOperationDisposition::TypedGated,
1271 ),
1272 ] {
1273 let operation = operations
1274 .iter()
1275 .copied()
1276 .find(|operation| operation.method() == method && operation.path_template() == path)
1277 .unwrap_or_else(|| panic!("missing OpenBao 2.6 operation {method:?} {path}"));
1278 assert_eq!(operation.disposition(), disposition);
1279 assert_eq!(
1280 operation.availability(OpenBaoVersion::new(2, 5, 5)),
1281 Some(OpenBaoCapabilityAvailability::NotDocumented)
1282 );
1283 let expected = if disposition == OpenBaoOperationDisposition::SecurityBlocked {
1284 OpenBaoCapabilityAvailability::SecurityBlocked
1285 } else {
1286 OpenBaoCapabilityAvailability::DocumentedRoute
1287 };
1288 assert_eq!(
1289 operation.availability(OpenBaoVersion::new(2, 6, 0)),
1290 Some(expected)
1291 );
1292 }
1293 }
1294
1295 #[test]
1296 fn generated_root_token_routes_are_gapless_and_profile_specific() {
1297 let endpoints = [
1298 super::generated::GENERATED_SYS_GENERATE_ROOT_CANCEL,
1299 super::generated::GENERATED_SYS_GENERATE_ROOT_START,
1300 super::generated::GENERATED_SYS_GENERATE_ROOT_STATUS,
1301 super::generated::GENERATED_SYS_GENERATE_ROOT_UPDATE,
1302 ];
1303
1304 for endpoint in endpoints {
1305 let variants = endpoint.variants();
1306 assert_eq!(variants.len(), 2);
1307 assert_eq!(variants[0].minimum(), OpenBaoVersion::new(2, 0, 0));
1308 assert_eq!(variants[0].maximum(), OpenBaoVersion::new(2, 5, 5));
1309 assert_eq!(variants[1].minimum(), OpenBaoVersion::new(2, 6, 0));
1310 assert_eq!(variants[1].maximum(), OpenBaoVersion::new(2, 6, 0));
1311 assert_ne!(variants[0].operation_id(), variants[1].operation_id());
1312 for (variant, version) in [
1313 (variants[0], OpenBaoVersion::new(2, 5, 5)),
1314 (variants[1], OpenBaoVersion::new(2, 6, 0)),
1315 ] {
1316 let operation = openbao_operation(variant.operation_id())
1317 .unwrap_or_else(|| panic!("missing root-generation operation"));
1318 assert_eq!(
1319 operation.availability(version),
1320 Some(OpenBaoCapabilityAvailability::DocumentedRoute)
1321 );
1322 }
1323 }
1324 }
1325
1326 #[test]
1327 fn generated_profiles_preserve_removed_and_feature_gated_routes() {
1328 let historical = openbao_operations()
1329 .iter()
1330 .copied()
1331 .find(|operation| operation.path_template() == "/sys/internal/ui/feature-flags")
1332 .unwrap_or_else(|| panic!("missing historical feature-flags operation"));
1333 assert_eq!(historical.method(), OpenBaoHttpMethod::Get);
1334 assert_eq!(
1335 historical.availability(OpenBaoVersion::new(2, 4, 4)),
1336 Some(OpenBaoCapabilityAvailability::DocumentedRoute)
1337 );
1338 assert_eq!(
1339 historical.evidence(OpenBaoVersion::new(2, 4, 4)),
1340 Some(OpenBaoCapabilityEvidence::TaggedDocumentation)
1341 );
1342 assert_eq!(
1343 historical.availability(OpenBaoVersion::new(2, 5, 5)),
1344 Some(OpenBaoCapabilityAvailability::NotDocumented)
1345 );
1346 assert_eq!(
1347 historical.disposition(),
1348 OpenBaoOperationDisposition::TypedGated
1349 );
1350
1351 let monitor = openbao_operations()
1352 .iter()
1353 .copied()
1354 .find(|operation| {
1355 operation.method() == OpenBaoHttpMethod::Get
1356 && operation.path_template() == "/sys/monitor"
1357 })
1358 .unwrap_or_else(|| panic!("missing monitor operation"));
1359 assert_eq!(
1360 monitor.availability(OpenBaoVersion::new(2, 5, 5)),
1361 Some(OpenBaoCapabilityAvailability::DocumentedRoute)
1362 );
1363 assert_eq!(
1364 monitor.disposition(),
1365 OpenBaoOperationDisposition::TypedGated
1366 );
1367 }
1368
1369 #[test]
1370 fn profile_reporting_contains_every_operation_without_support_inference() {
1371 let profile = OpenBaoCapabilityProfile::for_version(OpenBaoVersion::new(2, 5, 5))
1372 .unwrap_or_else(|| panic!("missing current capability profile"));
1373 let statuses = profile.operations().collect::<Vec<_>>();
1374
1375 assert_eq!(statuses.len(), openbao_operations().len());
1376 assert_eq!(profile.version(), OpenBaoVersion::new(2, 5, 5));
1377 assert!(statuses.iter().any(|status| {
1378 status.operation().disposition() == OpenBaoOperationDisposition::Typed
1379 && status.availability() == OpenBaoCapabilityAvailability::DocumentedRoute
1380 && status.evidence() != OpenBaoCapabilityEvidence::None
1381 }));
1382 }
1383}