1use std::error::Error;
2use std::fmt;
3
4use crate::OpaquePayload;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum ValueError {
10 NonFiniteF32,
12 NonFiniteF64,
14 DuplicateObjectKey { key: String },
16}
17
18impl fmt::Display for ValueError {
19 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::NonFiniteF32 => formatter.write_str("f32 contract values must be finite"),
22 Self::NonFiniteF64 => formatter.write_str("f64 contract values must be finite"),
23 Self::DuplicateObjectKey { key } => {
24 write!(formatter, "duplicate object key: {key:?}")
25 }
26 }
27 }
28}
29
30impl Error for ValueError {}
31
32#[derive(Debug, Clone, PartialEq)]
37pub enum SlotValue {
38 Missing,
40 Null,
42 Value(ContractValue),
44}
45
46#[derive(Clone, PartialEq)]
51pub struct ContractValue {
52 repr: Repr,
53}
54
55#[derive(Clone, PartialEq)]
56enum Repr {
57 Null,
58 Bool(bool),
59 I64(i64),
60 U64(u64),
61 F32(f32),
62 F64(f64),
63 String(String),
64 Bytes(Vec<u8>),
65 List(Vec<ContractValue>),
66 Object(Vec<(String, ContractValue)>),
67 Enum {
68 tag: String,
69 payload: Box<SlotValue>,
70 },
71 Opaque(OpaquePayload),
72 Sensitive(Box<ContractValue>),
73}
74
75impl fmt::Debug for ContractValue {
76 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match &self.repr {
78 Repr::Null => formatter.write_str("Null"),
79 Repr::Bool(value) => formatter.debug_tuple("Bool").field(value).finish(),
80 Repr::I64(value) => formatter.debug_tuple("I64").field(value).finish(),
81 Repr::U64(value) => formatter.debug_tuple("U64").field(value).finish(),
82 Repr::F32(value) => formatter.debug_tuple("F32").field(value).finish(),
83 Repr::F64(value) => formatter.debug_tuple("F64").field(value).finish(),
84 Repr::String(value) => formatter.debug_tuple("String").field(value).finish(),
85 Repr::Bytes(value) => formatter.debug_tuple("Bytes").field(value).finish(),
86 Repr::List(items) => formatter.debug_tuple("List").field(items).finish(),
87 Repr::Object(entries) => formatter.debug_tuple("Object").field(entries).finish(),
88 Repr::Enum { tag, payload } => formatter
89 .debug_struct("Enum")
90 .field("tag", tag)
91 .field("payload", payload)
92 .finish(),
93 Repr::Opaque(_) => formatter.write_str("Opaque(<redacted>)"),
94 Repr::Sensitive(_) => formatter.write_str("Sensitive(<redacted>)"),
95 }
96 }
97}
98
99impl ContractValue {
100 pub fn null() -> Self {
102 Self { repr: Repr::Null }
103 }
104
105 pub fn bool(value: bool) -> Self {
107 Self {
108 repr: Repr::Bool(value),
109 }
110 }
111
112 pub fn i64(value: i64) -> Self {
114 Self {
115 repr: Repr::I64(value),
116 }
117 }
118
119 pub fn u64(value: u64) -> Self {
121 Self {
122 repr: Repr::U64(value),
123 }
124 }
125
126 pub fn f32(value: f32) -> Result<Self, ValueError> {
128 value
129 .is_finite()
130 .then_some(Self {
131 repr: Repr::F32(value),
132 })
133 .ok_or(ValueError::NonFiniteF32)
134 }
135
136 pub fn f64(value: f64) -> Result<Self, ValueError> {
138 value
139 .is_finite()
140 .then_some(Self {
141 repr: Repr::F64(value),
142 })
143 .ok_or(ValueError::NonFiniteF64)
144 }
145
146 pub fn string(value: impl Into<String>) -> Self {
148 Self {
149 repr: Repr::String(value.into()),
150 }
151 }
152
153 pub fn bytes(value: impl Into<Vec<u8>>) -> Self {
155 Self {
156 repr: Repr::Bytes(value.into()),
157 }
158 }
159
160 pub fn list(items: impl IntoIterator<Item = ContractValue>) -> Self {
168 Self {
169 repr: Repr::List(items.into_iter().collect()),
170 }
171 }
172
173 pub fn object(
181 entries: impl IntoIterator<Item = (String, ContractValue)>,
182 ) -> Result<Self, ValueError> {
183 let mut collected = Vec::new();
184 for (key, value) in entries {
185 if collected
186 .iter()
187 .any(|(existing, _): &(String, ContractValue)| existing == &key)
188 {
189 return Err(ValueError::DuplicateObjectKey { key });
190 }
191 collected.push((key, value));
192 }
193 Ok(Self {
194 repr: Repr::Object(collected),
195 })
196 }
197
198 pub fn enum_value(tag: impl Into<String>, payload: SlotValue) -> Self {
200 Self {
201 repr: Repr::Enum {
202 tag: tag.into(),
203 payload: Box::new(payload),
204 },
205 }
206 }
207
208 pub fn opaque(payload: OpaquePayload) -> Self {
210 Self {
211 repr: Repr::Opaque(payload),
212 }
213 }
214
215 pub fn sensitive(inner: ContractValue) -> Self {
217 Self {
218 repr: Repr::Sensitive(Box::new(inner)),
219 }
220 }
221
222 pub fn view(&self) -> ValueRef<'_> {
224 match &self.repr {
225 Repr::Null => ValueRef::Null,
226 Repr::Bool(value) => ValueRef::Bool(*value),
227 Repr::I64(value) => ValueRef::I64(*value),
228 Repr::U64(value) => ValueRef::U64(*value),
229 Repr::F32(value) => ValueRef::F32(*value),
230 Repr::F64(value) => ValueRef::F64(*value),
231 Repr::String(value) => ValueRef::String(value),
232 Repr::Bytes(value) => ValueRef::Bytes(value),
233 Repr::List(items) => ValueRef::List(items),
234 Repr::Object(entries) => ValueRef::Object(ObjectRef { entries }),
235 Repr::Enum { tag, payload } => ValueRef::Enum { tag, payload },
236 Repr::Opaque(payload) => ValueRef::Opaque(payload),
237 Repr::Sensitive(inner) => ValueRef::Sensitive(inner),
238 }
239 }
240}
241
242#[derive(Clone, Copy)]
244pub enum ValueRef<'a> {
245 Null,
246 Bool(bool),
247 I64(i64),
248 U64(u64),
249 F32(f32),
250 F64(f64),
251 String(&'a str),
252 Bytes(&'a [u8]),
253 List(&'a [ContractValue]),
254 Object(ObjectRef<'a>),
255 Enum {
256 tag: &'a str,
257 payload: &'a SlotValue,
258 },
259 Opaque(&'a OpaquePayload),
260 Sensitive(&'a ContractValue),
261}
262
263#[derive(Clone, Copy)]
265pub struct ObjectRef<'a> {
266 entries: &'a [(String, ContractValue)],
267}
268
269impl<'a> ObjectRef<'a> {
270 pub fn len(&self) -> usize {
271 self.entries.len()
272 }
273
274 pub fn is_empty(&self) -> bool {
275 self.entries.is_empty()
276 }
277
278 pub fn get(&self, key: &str) -> Option<&'a ContractValue> {
279 self.entries
280 .iter()
281 .find_map(|(entry_key, value)| (entry_key == key).then_some(value))
282 }
283
284 pub fn entries(self) -> impl Iterator<Item = (&'a str, &'a ContractValue)> + 'a {
285 self.entries
286 .iter()
287 .map(|(key, value)| (key.as_str(), value))
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::OpaqueTree;
295
296 #[test]
297 fn rejects_every_non_finite_float() {
298 for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
299 assert_eq!(ContractValue::f32(value), Err(ValueError::NonFiniteF32));
300 }
301 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
302 assert_eq!(ContractValue::f64(value), Err(ValueError::NonFiniteF64));
303 }
304 }
305
306 #[test]
307 fn objects_reject_duplicates_and_preserve_order() {
308 let duplicate = |entries| ContractValue::object(entries);
309 assert_eq!(
310 duplicate(vec![
311 ("x".into(), ContractValue::null()),
312 ("x".into(), ContractValue::bool(true))
313 ]),
314 Err(ValueError::DuplicateObjectKey { key: "x".into() })
315 );
316 assert_eq!(
317 duplicate(vec![
318 ("a".into(), ContractValue::null()),
319 ("b".into(), ContractValue::null()),
320 ("c".into(), ContractValue::null()),
321 ("b".into(), ContractValue::null())
322 ]),
323 Err(ValueError::DuplicateObjectKey { key: "b".into() })
324 );
325 let value = ContractValue::object([
326 ("".into(), ContractValue::u64(1)),
327 ("second".into(), ContractValue::u64(2)),
328 ])
329 .unwrap();
330 let ValueRef::Object(object) = value.view() else {
331 panic!()
332 };
333 assert_eq!(
334 escaping_entries(object)
335 .map(|(key, _)| key)
336 .collect::<Vec<_>>(),
337 ["", "second"]
338 );
339 assert_eq!(object.get("second"), Some(&ContractValue::u64(2)));
340 assert_eq!(object.len(), 2);
341 let empty = ContractValue::object(Vec::<(String, ContractValue)>::new()).unwrap();
342 let ValueRef::Object(empty) = empty.view() else {
343 panic!()
344 };
345 assert!(empty.is_empty());
346 }
347
348 fn escaping_entries<'a>(
349 object: ObjectRef<'a>,
350 ) -> impl Iterator<Item = (&'a str, &'a ContractValue)> + 'a {
351 object.entries()
352 }
353
354 #[test]
355 fn public_visitors_rebuild_every_value_kind_and_nesting() {
356 let values = ContractValue::list([
357 ContractValue::null(),
358 ContractValue::bool(true),
359 ContractValue::i64(-1),
360 ContractValue::u64(2),
361 ContractValue::f32(3.5).unwrap(),
362 ContractValue::f64(4.5).unwrap(),
363 ContractValue::string("text"),
364 ContractValue::bytes([5, 6]),
365 ContractValue::list([ContractValue::bool(false)]),
366 ContractValue::object([("nested".into(), ContractValue::string("value"))]).unwrap(),
367 ContractValue::enum_value("missing", SlotValue::Missing),
368 ContractValue::enum_value("null", SlotValue::Null),
369 ContractValue::enum_value(
370 "value",
371 SlotValue::Value(ContractValue::list([ContractValue::i64(7)])),
372 ),
373 ContractValue::opaque(OpaquePayload::new(OpaqueTree::List(vec![
374 OpaqueTree::Bool(true),
375 OpaqueTree::String("opaque".into()),
376 ]))),
377 ContractValue::sensitive(ContractValue::sensitive(
378 ContractValue::object([("secret".into(), ContractValue::string("hidden"))])
379 .unwrap(),
380 )),
381 ]);
382 assert_eq!(rebuild(&values), values);
383 }
384
385 fn rebuild(value: &ContractValue) -> ContractValue {
386 match value.view() {
387 ValueRef::Null => ContractValue::null(),
388 ValueRef::Bool(value) => ContractValue::bool(value),
389 ValueRef::I64(value) => ContractValue::i64(value),
390 ValueRef::U64(value) => ContractValue::u64(value),
391 ValueRef::F32(value) => ContractValue::f32(value).unwrap(),
392 ValueRef::F64(value) => ContractValue::f64(value).unwrap(),
393 ValueRef::String(value) => ContractValue::string(value),
394 ValueRef::Bytes(value) => ContractValue::bytes(value),
395 ValueRef::List(items) => ContractValue::list(items.iter().map(rebuild)),
396 ValueRef::Object(object) => ContractValue::object(
397 object
398 .entries()
399 .map(|(key, value)| (key.into(), rebuild(value))),
400 )
401 .unwrap(),
402 ValueRef::Enum { tag, payload } => {
403 ContractValue::enum_value(tag, rebuild_slot(payload))
404 }
405 ValueRef::Opaque(payload) => ContractValue::opaque(payload.forward()),
406 ValueRef::Sensitive(inner) => ContractValue::sensitive(rebuild(inner)),
407 }
408 }
409
410 fn rebuild_slot(slot: &SlotValue) -> SlotValue {
411 match slot {
412 SlotValue::Missing => SlotValue::Missing,
413 SlotValue::Null => SlotValue::Null,
414 SlotValue::Value(value) => SlotValue::Value(rebuild(value)),
415 }
416 }
417
418 #[test]
419 fn debug_redacts_sensitive_and_opaque_subtrees() {
420 const SENTINEL: &str = "never-print-this-value";
421 let secret = || ContractValue::sensitive(ContractValue::string(SENTINEL));
422 let values = [
423 ContractValue::object([("secret".into(), secret())]).unwrap(),
424 ContractValue::list([secret()]),
425 ContractValue::enum_value("secret", SlotValue::Value(secret())),
426 ];
427 for value in values {
428 let output = format!("{value:?}");
429 assert!(!output.contains(SENTINEL));
430 assert!(output.contains("<redacted>"));
431 }
432 assert_eq!(format!("{:?}", secret()), "Sensitive(<redacted>)");
433 let visible = format!("{:?}", ContractValue::string("ordinary"));
434 assert!(visible.contains("ordinary"));
435
436 let payload = OpaquePayload::new(OpaqueTree::Object(vec![(
437 "raw".into(),
438 OpaqueTree::String(SENTINEL.into()),
439 )]));
440 for payload in [payload.clone(), payload.forward()] {
441 let values = [
442 ContractValue::list([ContractValue::opaque(payload.forward())]),
443 ContractValue::object([(
444 "opaque".into(),
445 ContractValue::opaque(payload.forward()),
446 )])
447 .unwrap(),
448 ContractValue::enum_value(
449 "opaque",
450 SlotValue::Value(ContractValue::opaque(payload.forward())),
451 ),
452 ContractValue::sensitive(ContractValue::opaque(payload.forward())),
453 ];
454 for value in values {
455 let contract_debug = format!("{value:?}");
456 let slot_debug = format!("{:?}", SlotValue::Value(value));
457 assert!(!contract_debug.contains(SENTINEL));
458 assert!(!slot_debug.contains(SENTINEL));
459 assert!(contract_debug.contains("<redacted>"));
460 assert!(slot_debug.contains("<redacted>"));
461 }
462 }
463 assert_eq!(
464 format!("{:?}", ContractValue::opaque(payload)),
465 "Opaque(<redacted>)"
466 );
467 }
468
469 #[test]
470 fn generated_values_round_trip_through_public_views() {
471 let mut rng = SplitMix64(0x57a1_1eed_cafe_f00d);
472 let mut kinds = 0_u16;
473 for index in 0..256 {
474 let kind = index % KIND_COUNT;
475 kinds |= 1 << kind;
476 let value = generated_kind(&mut rng, 4, kind);
477 assert_eq!(rebuild(&value), value, "case {index}");
478 }
479 assert_eq!(kinds, (1 << KIND_COUNT) - 1);
480 }
481
482 #[test]
483 fn generated_sensitive_positions_never_leak() {
484 let mut rng = SplitMix64(0xd15c_a11e_5afe_f00d);
485 let mut positions = 0_u8;
486 for _ in 0..256 {
487 let (value, position) = generated_hidden(&mut rng);
488 positions |= 1 << position;
489 let output = format!("{value:?}");
490 assert!(!output.contains(SENTINEL));
491 assert!(output.contains("<redacted>"));
492 }
493 assert_eq!(positions, 0b1111);
494 }
495
496 #[test]
497 fn generated_construction_does_not_panic_and_duplicates_are_exact() {
498 let mut rng = SplitMix64(0xc0de_cafe_1234_5678);
499 for index in 0..256 {
500 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
501 generated_kind(&mut rng, 4, index % KIND_COUNT)
502 }));
503 assert!(result.is_ok(), "constructor panic in case {index}");
504
505 let mut entries = generated_entries(&mut rng, 3, false);
506 let key = entries[rng.range(entries.len())].0.clone();
507 entries.push((key.clone(), generated(&mut rng, 2)));
508 assert_eq!(
509 ContractValue::object(entries),
510 Err(ValueError::DuplicateObjectKey { key })
511 );
512 }
513 }
514
515 const KIND_COUNT: usize = 13;
516 const SENTINEL: &str = "property-secret-sentinel";
517
518 struct SplitMix64(u64);
519
520 impl SplitMix64 {
521 fn next(&mut self) -> u64 {
522 self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
523 let mut value = self.0;
524 value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
525 value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
526 value ^ (value >> 31)
527 }
528
529 fn range(&mut self, upper: usize) -> usize {
530 (self.next() % upper as u64) as usize
531 }
532 }
533
534 fn generated(rng: &mut SplitMix64, depth: usize) -> ContractValue {
535 let kinds = if depth == 0 { 8 } else { KIND_COUNT };
536 let kind = rng.range(kinds);
537 generated_kind(rng, depth, kind)
538 }
539
540 fn generated_kind(rng: &mut SplitMix64, depth: usize, kind: usize) -> ContractValue {
541 match kind {
542 0 => ContractValue::null(),
543 1 => ContractValue::bool(rng.next() & 1 == 1),
544 2 => ContractValue::i64(rng.next() as i64),
545 3 => ContractValue::u64(rng.next()),
546 4 => ContractValue::f32((rng.next() as i32) as f32).unwrap(),
547 5 => ContractValue::f64((rng.next() as i64) as f64).unwrap(),
548 6 => ContractValue::string(format!("s{:x}", rng.next())),
549 7 => ContractValue::bytes(rng.next().to_le_bytes()),
550 8 => ContractValue::list(
551 (0..rng.range(5)).map(|_| generated(rng, depth.saturating_sub(1))),
552 ),
553 9 => ContractValue::object(generated_entries(rng, depth, true)).unwrap(),
554 10 => {
555 let payload = match rng.range(3) {
556 0 => SlotValue::Missing,
557 1 => SlotValue::Null,
558 _ => SlotValue::Value(generated(rng, depth.saturating_sub(1))),
559 };
560 ContractValue::enum_value(format!("e{:x}", rng.next()), payload)
561 }
562 11 => ContractValue::opaque(OpaquePayload::new(OpaqueTree::List(vec![
563 OpaqueTree::Bool(rng.next() & 1 == 1),
564 OpaqueTree::String(format!("o{:x}", rng.next())),
565 ]))),
566 12 => ContractValue::sensitive(generated(rng, depth.saturating_sub(1))),
567 _ => unreachable!(),
568 }
569 }
570
571 fn generated_entries(
572 rng: &mut SplitMix64,
573 depth: usize,
574 may_be_empty: bool,
575 ) -> Vec<(String, ContractValue)> {
576 let count = rng.range(4) + usize::from(!may_be_empty);
577 (0..count)
578 .map(|index| {
579 (
580 format!("k{index}-{:x}", rng.next()),
581 generated(rng, depth.saturating_sub(1)),
582 )
583 })
584 .collect()
585 }
586
587 fn generated_hidden(rng: &mut SplitMix64) -> (ContractValue, usize) {
588 let position = rng.range(4);
589 let secret = ContractValue::sensitive(ContractValue::string(SENTINEL));
590 let value = match position {
591 0 => secret,
592 1 => ContractValue::list([generated(rng, 0), secret]),
593 2 => ContractValue::object([
594 ("noise".into(), generated(rng, 0)),
595 ("secret".into(), secret),
596 ])
597 .unwrap(),
598 3 => ContractValue::enum_value("secret", SlotValue::Value(secret)),
599 _ => unreachable!(),
600 };
601 (value, position)
602 }
603
604 #[test]
605 fn slot_null_forms_remain_distinct() {
606 assert_ne!(SlotValue::Null, SlotValue::Value(ContractValue::null()));
607 }
608
609 #[test]
610 fn public_values_are_send_sync_and_static() {
611 fn assert_bounds<T: Send + Sync + 'static>() {}
612 assert_bounds::<ContractValue>();
613 assert_bounds::<SlotValue>();
614 }
615}