1use std::error::Error;
4use std::fmt;
5use std::num::{NonZeroU32, NonZeroUsize};
6
7use serde_json::{Map, Number, Value};
8use sha2::{Digest, Sha256};
9
10const FORMAT_VERSION: u16 = 1;
11const MAX_SCHEMA_ID_BYTES: usize = 128;
12const DEFAULT_MAXIMUM_BYTES: usize = 64 * 1024;
13const DEFAULT_MAXIMUM_DEPTH: usize = 16;
14const MAXIMUM_BYTES: usize = 1024 * 1024;
15const MAXIMUM_DEPTH: usize = 64;
16const MAX_UPGRADE_CHAIN: usize = 64;
22
23#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
25#[non_exhaustive]
26pub enum DurableStateKind {
27 Checkpoint,
29 ExecutionContext,
31 ComponentState,
33}
34
35impl DurableStateKind {
36 const fn format(self) -> &'static str {
37 match self {
38 Self::Checkpoint => "oxide-batch.checkpoint",
39 Self::ExecutionContext => "oxide-batch.execution-context",
40 Self::ComponentState => "oxide-batch.component-state",
41 }
42 }
43}
44
45impl fmt::Display for DurableStateKind {
46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47 formatter.write_str(match self {
48 Self::Checkpoint => "checkpoint",
49 Self::ExecutionContext => "execution context",
50 Self::ComponentState => "component state",
51 })
52 }
53}
54
55#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub struct StateSchemaId(String);
58
59impl StateSchemaId {
60 pub fn new(value: impl Into<String>) -> Result<Self, StateError> {
68 let value = value.into();
69 if value.is_empty() {
70 return Err(StateError::EmptySchemaId);
71 }
72 if value.len() > MAX_SCHEMA_ID_BYTES {
73 return Err(StateError::SchemaIdTooLong {
74 max_bytes: MAX_SCHEMA_ID_BYTES,
75 });
76 }
77 if value.trim() != value {
78 return Err(StateError::SchemaIdHasSurroundingWhitespace);
79 }
80 if value.chars().any(char::is_control) {
81 return Err(StateError::SchemaIdContainsControl);
82 }
83 Ok(Self(value))
84 }
85
86 #[must_use]
88 pub fn as_str(&self) -> &str {
89 &self.0
90 }
91}
92
93impl fmt::Debug for StateSchemaId {
94 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95 formatter.write_str("StateSchemaId(<redacted>)")
96 }
97}
98
99impl fmt::Display for StateSchemaId {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter.write_str(self.as_str())
102 }
103}
104
105#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
107pub struct StateSchemaVersion(NonZeroU32);
108
109impl StateSchemaVersion {
110 pub fn new(value: u32) -> Result<Self, StateError> {
116 NonZeroU32::new(value)
117 .map(Self)
118 .ok_or(StateError::ZeroSchemaVersion)
119 }
120
121 #[must_use]
123 pub const fn get(self) -> u32 {
124 self.0.get()
125 }
126}
127
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130pub struct StateLimits {
131 maximum_bytes: NonZeroUsize,
132 maximum_depth: NonZeroUsize,
133}
134
135impl StateLimits {
136 pub fn new(maximum_bytes: usize, maximum_depth: usize) -> Result<Self, StateError> {
147 if maximum_bytes == 0 || maximum_bytes > MAXIMUM_BYTES {
148 return Err(StateError::InvalidByteLimit {
149 maximum: MAXIMUM_BYTES,
150 });
151 }
152 if maximum_depth == 0 || maximum_depth > MAXIMUM_DEPTH {
153 return Err(StateError::InvalidDepthLimit {
154 maximum: MAXIMUM_DEPTH,
155 });
156 }
157 let Some(maximum_bytes) = NonZeroUsize::new(maximum_bytes) else {
158 return Err(StateError::InvalidByteLimit {
159 maximum: MAXIMUM_BYTES,
160 });
161 };
162 let Some(maximum_depth) = NonZeroUsize::new(maximum_depth) else {
163 return Err(StateError::InvalidDepthLimit {
164 maximum: MAXIMUM_DEPTH,
165 });
166 };
167 Ok(Self {
168 maximum_bytes,
169 maximum_depth,
170 })
171 }
172
173 #[must_use]
175 pub const fn maximum_bytes(self) -> usize {
176 self.maximum_bytes.get()
177 }
178
179 #[must_use]
181 pub const fn maximum_depth(self) -> usize {
182 self.maximum_depth.get()
183 }
184}
185
186impl Default for StateLimits {
187 fn default() -> Self {
188 Self {
189 maximum_bytes: NonZeroUsize::new(DEFAULT_MAXIMUM_BYTES).unwrap_or(NonZeroUsize::MIN),
190 maximum_depth: NonZeroUsize::new(DEFAULT_MAXIMUM_DEPTH).unwrap_or(NonZeroUsize::MIN),
191 }
192 }
193}
194
195#[derive(Clone, Copy)]
205pub struct StateSchemaUpgrade {
206 from: StateSchemaVersion,
207 to: StateSchemaVersion,
208 apply: fn(&[u8]) -> Result<Vec<u8>, StateCodecError>,
209}
210
211impl StateSchemaUpgrade {
212 pub fn new(
223 from: StateSchemaVersion,
224 to: StateSchemaVersion,
225 apply: fn(&[u8]) -> Result<Vec<u8>, StateCodecError>,
226 ) -> Result<Self, StateError> {
227 if to <= from {
228 return Err(StateError::NonIncreasingUpgrade { from, to });
229 }
230 Ok(Self { from, to, apply })
231 }
232
233 #[must_use]
235 pub const fn from(&self) -> StateSchemaVersion {
236 self.from
237 }
238
239 #[must_use]
241 pub const fn to(&self) -> StateSchemaVersion {
242 self.to
243 }
244}
245
246impl fmt::Debug for StateSchemaUpgrade {
247 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
248 formatter
249 .debug_struct("StateSchemaUpgrade")
250 .field("from", &self.from)
251 .field("to", &self.to)
252 .finish_non_exhaustive()
253 }
254}
255
256pub trait VersionedStateCodec<T>: Send + Sync {
269 fn schema_id(&self) -> &StateSchemaId;
271
272 fn current_version(&self) -> StateSchemaVersion;
274
275 fn upgrades(&self) -> &[StateSchemaUpgrade] {
282 &[]
283 }
284
285 fn encode(&self, value: &T) -> Result<Vec<u8>, StateCodecError>;
291
292 fn decode(&self, payload: &[u8]) -> Result<T, StateCodecError>;
300}
301
302#[derive(Clone, Copy, Debug, Eq, PartialEq)]
304#[non_exhaustive]
305pub enum StateCodecError {
306 InvalidPayload,
308 UnsupportedSchemaVersion,
310}
311
312impl fmt::Display for StateCodecError {
313 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
314 formatter.write_str(match self {
315 Self::InvalidPayload => "durable state payload is invalid",
316 Self::UnsupportedSchemaVersion => "durable state schema version is unsupported",
317 })
318 }
319}
320
321impl Error for StateCodecError {}
322
323#[derive(Clone, Eq, PartialEq)]
324struct VersionedState {
325 schema_id: StateSchemaId,
326 schema_version: StateSchemaVersion,
327 payload: Value,
328 encoded_bytes: usize,
329}
330
331impl VersionedState {
332 fn encode<T>(
333 kind: DurableStateKind,
334 value: &T,
335 codec: &(impl VersionedStateCodec<T> + ?Sized),
336 limits: StateLimits,
337 ) -> Result<Self, StateError> {
338 let payload_bytes = codec.encode(value).map_err(StateError::Codec)?;
339 let payload: Value =
340 serde_json::from_slice(&payload_bytes).map_err(|_| StateError::InvalidPayload)?;
341 if !payload.is_object() {
342 return Err(StateError::PayloadNotObject);
343 }
344 Self::from_parts(
345 kind,
346 codec.schema_id().clone(),
347 codec.current_version(),
348 payload,
349 limits,
350 )
351 }
352
353 fn from_json(
354 kind: DurableStateKind,
355 bytes: &[u8],
356 limits: StateLimits,
357 ) -> Result<Self, StateError> {
358 if bytes.len() > limits.maximum_bytes() {
359 return Err(StateError::TooLarge {
360 kind,
361 max_bytes: limits.maximum_bytes(),
362 });
363 }
364 let value: Value =
365 serde_json::from_slice(bytes).map_err(|_| StateError::Malformed { kind })?;
366 if json_depth(&value) > limits.maximum_depth() {
367 return Err(StateError::TooDeep {
368 kind,
369 max_depth: limits.maximum_depth(),
370 });
371 }
372 let object = value.as_object().ok_or(StateError::Malformed { kind })?;
373 let format = object
374 .get("format")
375 .and_then(Value::as_str)
376 .ok_or(StateError::Malformed { kind })?;
377 if format != kind.format() {
378 return Err(StateError::FormatMismatch { kind });
379 }
380 let format_version = object
381 .get("format_version")
382 .and_then(Value::as_u64)
383 .and_then(|version| u16::try_from(version).ok())
384 .ok_or(StateError::Malformed { kind })?;
385 if format_version != FORMAT_VERSION {
386 return Err(StateError::UnsupportedFormatVersion {
387 kind,
388 version: format_version,
389 });
390 }
391 let schema_id = object
392 .get("schema")
393 .and_then(Value::as_str)
394 .ok_or(StateError::Malformed { kind })?;
395 let schema_id = StateSchemaId::new(schema_id)?;
396 let schema_version = object
397 .get("schema_version")
398 .and_then(Value::as_u64)
399 .and_then(|version| u32::try_from(version).ok())
400 .ok_or(StateError::Malformed { kind })?;
401 let schema_version = StateSchemaVersion::new(schema_version)?;
402 let payload = object
403 .get("payload")
404 .cloned()
405 .ok_or(StateError::Malformed { kind })?;
406 if !payload.is_object() {
407 return Err(StateError::PayloadNotObject);
408 }
409 Ok(Self {
410 schema_id,
411 schema_version,
412 payload,
413 encoded_bytes: bytes.len(),
414 })
415 }
416
417 fn from_parts(
418 kind: DurableStateKind,
419 schema_id: StateSchemaId,
420 schema_version: StateSchemaVersion,
421 payload: Value,
422 limits: StateLimits,
423 ) -> Result<Self, StateError> {
424 let envelope = envelope(kind, &schema_id, schema_version, payload.clone());
425 let bytes = serde_json::to_vec(&envelope).map_err(|_| StateError::Malformed { kind })?;
426 if bytes.len() > limits.maximum_bytes() {
427 return Err(StateError::TooLarge {
428 kind,
429 max_bytes: limits.maximum_bytes(),
430 });
431 }
432 if json_depth(&envelope) > limits.maximum_depth() {
433 return Err(StateError::TooDeep {
434 kind,
435 max_depth: limits.maximum_depth(),
436 });
437 }
438 Ok(Self {
439 schema_id,
440 schema_version,
441 payload,
442 encoded_bytes: bytes.len(),
443 })
444 }
445
446 fn decode<T>(
447 &self,
448 kind: DurableStateKind,
449 codec: &(impl VersionedStateCodec<T> + ?Sized),
450 ) -> Result<T, StateError> {
451 if &self.schema_id != codec.schema_id() {
452 return Err(StateError::SchemaMismatch { kind });
453 }
454 let current = codec.current_version();
455 if self.schema_version > current {
456 return Err(StateError::UnsupportedSchemaVersion {
457 kind,
458 found: self.schema_version,
459 current,
460 });
461 }
462 let payload =
463 serde_json::to_vec(&self.payload).map_err(|_| StateError::Malformed { kind })?;
464 let payload = upgrade_schema_chain(kind, self.schema_version, codec, payload)?;
465 codec.decode(&payload).map_err(StateError::Codec)
466 }
467
468 fn to_json(&self, kind: DurableStateKind) -> Result<Vec<u8>, StateError> {
469 serde_json::to_vec(&envelope(
470 kind,
471 &self.schema_id,
472 self.schema_version,
473 self.payload.clone(),
474 ))
475 .map_err(|_| StateError::Malformed { kind })
476 }
477
478 fn payload_json(&self, kind: DurableStateKind) -> Result<Vec<u8>, StateError> {
479 serde_json::to_vec(&self.payload).map_err(|_| StateError::Malformed { kind })
480 }
481}
482
483pub(crate) fn upgrade_schema_chain<T>(
496 kind: DurableStateKind,
497 recorded: StateSchemaVersion,
498 codec: &(impl VersionedStateCodec<T> + ?Sized),
499 mut payload: Vec<u8>,
500) -> Result<Vec<u8>, StateError> {
501 let current = codec.current_version();
502 let upgrades = codec.upgrades();
503 let mut version = recorded;
504 let mut applied = 0_usize;
505 while version < current {
506 let mut edges = upgrades.iter().filter(|upgrade| upgrade.from == version);
507 let edge = edges.next().ok_or(StateError::NoUpgradePath {
508 kind,
509 found: version,
510 current,
511 })?;
512 if edges.next().is_some() {
513 return Err(StateError::AmbiguousUpgrade {
514 kind,
515 from: version,
516 });
517 }
518 if edge.to > current {
519 return Err(StateError::UpgradeOvershootsCurrent {
520 kind,
521 to: edge.to,
522 current,
523 });
524 }
525 applied += 1;
526 if applied > MAX_UPGRADE_CHAIN {
527 return Err(StateError::UpgradeChainTooLong {
528 kind,
529 max_upgrades: MAX_UPGRADE_CHAIN,
530 });
531 }
532 payload = (edge.apply)(&payload).map_err(StateError::Codec)?;
533 check_upgraded(kind, &payload)?;
534 version = edge.to;
535 }
536 Ok(payload)
537}
538
539fn envelope(
540 kind: DurableStateKind,
541 schema_id: &StateSchemaId,
542 schema_version: StateSchemaVersion,
543 payload: Value,
544) -> Value {
545 let mut object = Map::new();
546 object.insert(
547 String::from("format"),
548 Value::String(String::from(kind.format())),
549 );
550 object.insert(
551 String::from("format_version"),
552 Value::Number(Number::from(FORMAT_VERSION)),
553 );
554 object.insert(
555 String::from("schema"),
556 Value::String(String::from(schema_id.as_str())),
557 );
558 object.insert(
559 String::from("schema_version"),
560 Value::Number(Number::from(schema_version.get())),
561 );
562 object.insert(String::from("payload"), payload);
563 Value::Object(object)
564}
565
566fn check_upgraded(kind: DurableStateKind, payload: &[u8]) -> Result<(), StateError> {
576 if payload.len() > MAXIMUM_BYTES {
577 return Err(StateError::TooLarge {
578 kind,
579 max_bytes: MAXIMUM_BYTES,
580 });
581 }
582 let value: Value = serde_json::from_slice(payload)
583 .map_err(|_| StateError::UpgradeProducedInvalidJson { kind })?;
584 if !value.is_object() {
585 return Err(StateError::PayloadNotObject);
586 }
587 if json_depth(&value) > MAXIMUM_DEPTH {
588 return Err(StateError::TooDeep {
589 kind,
590 max_depth: MAXIMUM_DEPTH,
591 });
592 }
593 Ok(())
594}
595
596pub(crate) fn json_depth(value: &Value) -> usize {
602 match value {
603 Value::Array(values) => 1 + values.iter().map(json_depth).max().unwrap_or_default(),
604 Value::Object(values) => 1 + values.values().map(json_depth).max().unwrap_or_default(),
605 _ => 1,
606 }
607}
608
609macro_rules! durable_state {
610 ($name:ident, $kind:expr, $docs:literal) => {
611 #[doc = $docs]
612 #[derive(Clone, Eq, PartialEq)]
613 pub struct $name(VersionedState);
614
615 impl $name {
616 pub fn encode<T>(
623 value: &T,
624 codec: &(impl VersionedStateCodec<T> + ?Sized),
625 limits: StateLimits,
626 ) -> Result<Self, StateError> {
627 VersionedState::encode($kind, value, codec, limits).map(Self)
628 }
629
630 pub fn from_json(bytes: &[u8], limits: StateLimits) -> Result<Self, StateError> {
637 VersionedState::from_json($kind, bytes, limits).map(Self)
638 }
639
640 pub fn decode<T>(
646 &self,
647 codec: &(impl VersionedStateCodec<T> + ?Sized),
648 ) -> Result<T, StateError> {
649 self.0.decode($kind, codec)
650 }
651
652 #[must_use]
654 pub const fn format_version(&self) -> u16 {
655 FORMAT_VERSION
656 }
657
658 #[must_use]
660 pub const fn schema_id(&self) -> &StateSchemaId {
661 &self.0.schema_id
662 }
663
664 #[must_use]
666 pub const fn schema_version(&self) -> StateSchemaVersion {
667 self.0.schema_version
668 }
669
670 #[must_use]
672 pub const fn encoded_len(&self) -> usize {
673 self.0.encoded_bytes
674 }
675
676 pub fn to_json(&self) -> Result<Vec<u8>, StateError> {
683 self.0.to_json($kind)
684 }
685
686 pub fn payload_json(&self) -> Result<Vec<u8>, StateError> {
697 self.0.payload_json($kind)
698 }
699 }
700
701 impl fmt::Debug for $name {
702 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
703 formatter
704 .debug_struct(stringify!($name))
705 .field("format_version", &FORMAT_VERSION)
706 .field("schema_version", &self.schema_version())
707 .field("encoded_bytes", &self.encoded_len())
708 .field("payload", &"<redacted>")
709 .finish()
710 }
711 }
712 };
713}
714
715durable_state!(
716 Checkpoint,
717 DurableStateKind::Checkpoint,
718 "A bounded, versioned reader position committed with a chunk."
719);
720durable_state!(
721 ExecutionContext,
722 DurableStateKind::ExecutionContext,
723 "Bounded, versioned application restart state committed with a chunk."
724);
725
726impl Checkpoint {
727 #[must_use]
732 pub fn generation_digest(&self) -> [u8; 32] {
733 self.to_json()
734 .map_or([0; 32], |bytes| Sha256::digest(&bytes).into())
735 }
736}
737
738#[derive(Clone, Debug, Eq, PartialEq)]
740#[non_exhaustive]
741pub enum StateError {
742 EmptySchemaId,
744 SchemaIdTooLong {
746 max_bytes: usize,
748 },
749 SchemaIdHasSurroundingWhitespace,
751 SchemaIdContainsControl,
753 ZeroSchemaVersion,
755 InvalidByteLimit {
757 maximum: usize,
759 },
760 InvalidDepthLimit {
762 maximum: usize,
764 },
765 TooLarge {
767 kind: DurableStateKind,
769 max_bytes: usize,
771 },
772 TooDeep {
774 kind: DurableStateKind,
776 max_depth: usize,
778 },
779 Malformed {
781 kind: DurableStateKind,
783 },
784 FormatMismatch {
786 kind: DurableStateKind,
788 },
789 UnsupportedFormatVersion {
791 kind: DurableStateKind,
793 version: u16,
795 },
796 SchemaMismatch {
798 kind: DurableStateKind,
800 },
801 NonIncreasingUpgrade {
803 from: StateSchemaVersion,
805 to: StateSchemaVersion,
807 },
808 NoUpgradePath {
810 kind: DurableStateKind,
812 found: StateSchemaVersion,
814 current: StateSchemaVersion,
816 },
817 AmbiguousUpgrade {
820 kind: DurableStateKind,
822 from: StateSchemaVersion,
824 },
825 UpgradeOvershootsCurrent {
827 kind: DurableStateKind,
829 to: StateSchemaVersion,
831 current: StateSchemaVersion,
833 },
834 UpgradeChainTooLong {
836 kind: DurableStateKind,
838 max_upgrades: usize,
840 },
841 UpgradeProducedInvalidJson {
843 kind: DurableStateKind,
845 },
846 UnsupportedSchemaVersion {
848 kind: DurableStateKind,
850 found: StateSchemaVersion,
852 current: StateSchemaVersion,
854 },
855 InvalidPayload,
857 PayloadNotObject,
859 Codec(StateCodecError),
861}
862
863impl fmt::Display for StateError {
864 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
865 match self {
866 Self::EmptySchemaId => formatter.write_str("state schema identifier must not be empty"),
867 Self::SchemaIdTooLong { max_bytes } => {
868 write!(
869 formatter,
870 "state schema identifier exceeds {max_bytes} UTF-8 bytes"
871 )
872 }
873 Self::SchemaIdHasSurroundingWhitespace => {
874 formatter.write_str("state schema identifier has surrounding whitespace")
875 }
876 Self::SchemaIdContainsControl => {
877 formatter.write_str("state schema identifier contains a control character")
878 }
879 Self::ZeroSchemaVersion => formatter.write_str("state schema version must be nonzero"),
880 Self::InvalidByteLimit { maximum } => {
881 write!(
882 formatter,
883 "state byte limit must be between 1 and {maximum}"
884 )
885 }
886 Self::InvalidDepthLimit { maximum } => {
887 write!(
888 formatter,
889 "state depth limit must be between 1 and {maximum}"
890 )
891 }
892 Self::TooLarge { kind, max_bytes } => {
893 write!(formatter, "{kind} exceeds {max_bytes} bytes")
894 }
895 Self::TooDeep { kind, max_depth } => {
896 write!(formatter, "{kind} exceeds JSON depth {max_depth}")
897 }
898 Self::Malformed { kind } => write!(formatter, "{kind} is malformed"),
899 Self::FormatMismatch { kind } => {
900 write!(formatter, "durable state is not a {kind}")
901 }
902 Self::UnsupportedFormatVersion { kind, .. } => {
903 write!(formatter, "{kind} format version is unsupported")
904 }
905 Self::SchemaMismatch { kind } => {
906 write!(formatter, "{kind} schema does not match the component")
907 }
908 Self::NonIncreasingUpgrade { .. } => {
909 formatter.write_str("state schema upgrade must increase the version")
910 }
911 Self::NoUpgradePath { kind, .. } => {
912 write!(formatter, "{kind} schema version has no upgrade path")
913 }
914 Self::AmbiguousUpgrade { kind, .. } => {
915 write!(formatter, "{kind} schema upgrade is ambiguous")
916 }
917 Self::UpgradeOvershootsCurrent { kind, .. } => {
918 write!(
919 formatter,
920 "{kind} schema upgrade passes the current version"
921 )
922 }
923 Self::UpgradeChainTooLong { kind, max_upgrades } => {
924 write!(
925 formatter,
926 "{kind} schema upgrade chain exceeds {max_upgrades} upgrades"
927 )
928 }
929 Self::UpgradeProducedInvalidJson { kind } => {
930 write!(formatter, "{kind} schema upgrade produced invalid JSON")
931 }
932 Self::UnsupportedSchemaVersion { kind, .. } => {
933 write!(formatter, "{kind} schema version is unsupported")
934 }
935 Self::InvalidPayload => formatter.write_str("durable state payload is not valid JSON"),
936 Self::PayloadNotObject => {
937 formatter.write_str("durable state payload must be a JSON object")
938 }
939 Self::Codec(error) => error.fmt(formatter),
940 }
941 }
942}
943
944impl Error for StateError {
945 fn source(&self) -> Option<&(dyn Error + 'static)> {
946 match self {
947 Self::Codec(error) => Some(error),
948 _ => None,
949 }
950 }
951}