1use std::fmt;
4
5use hmac::{Hmac, KeyInit, Mac};
6use sha2::{Digest, Sha256};
7
8use crate::{
9 AdapterConformanceManifestHash, ApplicationExportManifestHash, ApplicationExportPageHash,
10 ApplicationExportReceiptHash, ApplicationInstallationPlanHash,
11 ApplicationInstallationReceiptHash, ApplicationLockHash, ApplicationManifestHash,
12 ApplicationPortabilityManifestHash, ApplicationReimportReceiptHash,
13 ApplicationRoleDefinitionHash, ApplicationRoleHash, ApplicationSourceHash, CanonicalInputHash,
14 CanonicalValueHash, CapabilityTokenDigest, ColumnarDefinitionSemanticsHashV1,
15 ColumnarProjectionSpecHashV1, ConflictKeyHash, ContractBundleHash, ContractMigrationInputHash,
16 ContractMigrationJournalHash, ContractMigrationValidationDigest, ContractPlanRootHash,
17 DigestKey, DigestKeyId, EntityKeyHash, EntityRecordHash, EntityTransitionHash,
18 EventConsumerIdentityHash, EventHash, GeneratedArtifactHash, MigrationBundleHash,
19 MigrationSourceHash, OfflineMaintenanceInputHash, PartitionKeyHash,
20 PartitionSetObservationHash, PlanHash, ProjectionApplyHash, ProjectionPlanHash,
21 ProjectionProviderDescriptorHash, QueryModuleHash, QueryParameterHash, QueryPlanHash,
22 QuerySourceHash, ReactiveModuleHash, ReactiveOperationHash, ReactiveSourceHash,
23 ScheduledAttemptHash, SchemaHash, SourceHash,
24};
25
26pub const DIGEST_SCHEME_V1: u8 = 0x01;
28
29const HASH_PREFIX: &[u8] = b"RIFFDB-HASH\0";
30const HMAC_PREFIX: &[u8] = b"RIFFDB-HMAC\0";
31
32#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub enum HashDomain {
35 CanonicalValue,
37 Source,
39 MigrationSource,
41 ContractBundle,
43 MigrationBundle,
45 Plan,
47 ProjectionPlan,
49 ProjectionProviderDescriptor,
51 QueryPlan,
53 PartitionSetObservation,
55 QueryModule,
57 ReactiveSource,
59 ReactiveOperation,
61 ReactiveModule,
63 EventConsumerIdentity,
65 ApplicationManifest,
67 ApplicationSource,
69 ApplicationLock,
71 GeneratedArtifact,
73 ApplicationRoleDefinition,
75 ApplicationRole,
77 QuerySource,
79 QueryParameters,
81 ContractPlanRoot,
83 CommandInput,
85 Event,
87 EntityRecord,
89 EntityTransition,
91 EntityKey,
93 ConflictKey,
95 PartitionKey,
97 Schema,
99 ProjectionApply,
101 OfflineMaintenanceInput,
103 ContractMigrationInput,
105 ContractMigrationJournal,
107 ContractMigrationValidation,
109 CommandBatch,
111 ContextualReaction,
113 ReactiveWakeup,
115 ScheduledAttempt,
117 ApplicationInstallationPlan,
119 ApplicationInstallationReceipt,
121 AdapterConformanceManifest,
123 ApplicationExportPage,
125 ApplicationExportManifest,
127 ApplicationExportReceipt,
129 ApplicationExportCursor,
131 ApplicationExportLedger,
133 ApplicationPortabilityManifest,
135 ApplicationReimportReceipt,
137 ExactResultCheckpoint,
139 ColumnarProjectionSpec,
141 ProjectionSourceRows,
143 ProjectionSourceDescriptor,
145 ProjectionSourceOrigin,
147 ProjectionSourceCopyProgress,
149}
150
151impl HashDomain {
152 pub const ALL: [Self; 57] = [
154 Self::CanonicalValue,
155 Self::Source,
156 Self::MigrationSource,
157 Self::ContractBundle,
158 Self::MigrationBundle,
159 Self::Plan,
160 Self::ProjectionPlan,
161 Self::ProjectionProviderDescriptor,
162 Self::QueryPlan,
163 Self::PartitionSetObservation,
164 Self::QueryModule,
165 Self::ReactiveSource,
166 Self::ReactiveOperation,
167 Self::ReactiveModule,
168 Self::EventConsumerIdentity,
169 Self::ApplicationManifest,
170 Self::ApplicationSource,
171 Self::ApplicationLock,
172 Self::GeneratedArtifact,
173 Self::ApplicationRoleDefinition,
174 Self::ApplicationRole,
175 Self::QuerySource,
176 Self::QueryParameters,
177 Self::ContractPlanRoot,
178 Self::CommandInput,
179 Self::Event,
180 Self::EntityRecord,
181 Self::EntityTransition,
182 Self::EntityKey,
183 Self::ConflictKey,
184 Self::PartitionKey,
185 Self::Schema,
186 Self::ProjectionApply,
187 Self::OfflineMaintenanceInput,
188 Self::ContractMigrationInput,
189 Self::ContractMigrationJournal,
190 Self::ContractMigrationValidation,
191 Self::CommandBatch,
192 Self::ContextualReaction,
193 Self::ReactiveWakeup,
194 Self::ScheduledAttempt,
195 Self::ApplicationInstallationPlan,
196 Self::ApplicationInstallationReceipt,
197 Self::AdapterConformanceManifest,
198 Self::ApplicationExportPage,
199 Self::ApplicationExportManifest,
200 Self::ApplicationExportReceipt,
201 Self::ApplicationExportCursor,
202 Self::ApplicationExportLedger,
203 Self::ApplicationPortabilityManifest,
204 Self::ApplicationReimportReceipt,
205 Self::ExactResultCheckpoint,
206 Self::ColumnarProjectionSpec,
207 Self::ProjectionSourceRows,
208 Self::ProjectionSourceDescriptor,
209 Self::ProjectionSourceOrigin,
210 Self::ProjectionSourceCopyProgress,
211 ];
212
213 pub const fn label(self) -> &'static str {
215 match self {
216 Self::CanonicalValue => "riffdb.canonical-value/v1",
217 Self::Source => "riffdb.source/v1",
218 Self::MigrationSource => "riffdb.migration-source/v1",
219 Self::ContractBundle => "riffdb.contract-bundle/v1",
220 Self::MigrationBundle => "riffdb.migration-bundle/v1",
221 Self::Plan => "riffdb.plan/v1",
222 Self::ProjectionPlan => "riffdb.projection-plan/v1",
223 Self::ProjectionProviderDescriptor => "riffdb.projection-provider-descriptor/v1",
224 Self::QueryPlan => "riffdb.query-plan/v1",
225 Self::PartitionSetObservation => "riffdb.partition-set-observation/v1",
226 Self::QueryModule => "riffdb.query-module/v1",
227 Self::ReactiveSource => "riffdb.reactive-source/v1",
228 Self::ReactiveOperation => "riffdb.reactive-operation/v1",
229 Self::ReactiveModule => "riffdb.reactive-module/v1",
230 Self::EventConsumerIdentity => "riffdb.event-consumer-identity/v1",
231 Self::ApplicationManifest => "riffdb.application-manifest/v1",
232 Self::ApplicationSource => "riffdb.application-source/v1",
233 Self::ApplicationLock => "riffdb.application-lock/v1",
234 Self::GeneratedArtifact => "riffdb.generated-artifact/v1",
235 Self::ApplicationRoleDefinition => "riffdb.application-role-definition/v1",
236 Self::ApplicationRole => "riffdb.application-role/v1",
237 Self::QuerySource => "riffdb.query-source/v1",
238 Self::QueryParameters => "riffdb.query-parameters/v1",
239 Self::ContractPlanRoot => "riffdb.contract-plan-root/v1",
240 Self::CommandInput => "riffdb.command-input/v1",
241 Self::Event => "riffdb.event/v1",
242 Self::EntityRecord => "riffdb.entity-record/v1",
243 Self::EntityTransition => "riffdb.entity-transition/v1",
244 Self::EntityKey => "riffdb.entity-key/v1",
245 Self::ConflictKey => "riffdb.conflict-key/v1",
246 Self::PartitionKey => "riffdb.partition-key/v1",
247 Self::Schema => "riffdb.schema/v1",
248 Self::ProjectionApply => "riffdb.projection-apply/v1",
249 Self::OfflineMaintenanceInput => "riffdb.offline-maintenance-input/v1",
250 Self::ContractMigrationInput => "riffdb.contract-migration-input/v1",
251 Self::ContractMigrationJournal => "riffdb.contract-migration-journal/v1",
252 Self::ContractMigrationValidation => "riffdb.contract-migration-validation/v1",
253 Self::CommandBatch => "riffdb.command-batch/v1",
254 Self::ContextualReaction => "riffdb.contextual-reaction/v1",
255 Self::ReactiveWakeup => "riffdb.reactive-wakeup/v1",
256 Self::ScheduledAttempt => "riffdb.scheduled-attempt/v1",
257 Self::ApplicationInstallationPlan => "riffdb.application-installation-plan/v1",
258 Self::ApplicationInstallationReceipt => "riffdb.application-installation-receipt/v1",
259 Self::AdapterConformanceManifest => "riffdb.adapter-conformance-manifest/v1",
260 Self::ApplicationExportPage => "riffdb.application-export-page/v1",
261 Self::ApplicationExportManifest => "riffdb.application-export-manifest/v1",
262 Self::ApplicationExportReceipt => "riffdb.application-export-receipt/v1",
263 Self::ApplicationExportCursor => "riffdb.application-export-cursor/v1",
264 Self::ApplicationExportLedger => "riffdb.application-export-ledger/v1",
265 Self::ApplicationPortabilityManifest => "riffdb.application-portability-manifest/v1",
266 Self::ApplicationReimportReceipt => "riffdb.application-reimport-receipt/v1",
267 Self::ExactResultCheckpoint => "riffdb.exact-result-checkpoint/v1",
268 Self::ColumnarProjectionSpec => "riffdb.columnar-projection-spec/v1",
269 Self::ProjectionSourceRows => "riffdb.projection-source-rows/v1",
270 Self::ProjectionSourceDescriptor => "riffdb.projection-source-descriptor/v1",
271 Self::ProjectionSourceOrigin => "riffdb.projection-source-origin/v1",
272 Self::ProjectionSourceCopyProgress => "riffdb.projection-source-copy-progress/v1",
273 }
274 }
275}
276
277#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
279pub enum KeyedHashDomain {
280 IdempotencyKey,
282 CapabilityToken,
284 ContextualCausation,
286}
287
288impl KeyedHashDomain {
289 pub const ALL: [Self; 3] = [
291 Self::IdempotencyKey,
292 Self::CapabilityToken,
293 Self::ContextualCausation,
294 ];
295
296 pub const fn label(self) -> &'static str {
298 match self {
299 Self::IdempotencyKey => "riffdb.idempotency-key/v1",
300 Self::CapabilityToken => "riffdb.capability-token/v1",
301 Self::ContextualCausation => "riffdb.contextual-causation/v1",
302 }
303 }
304}
305
306#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
308pub struct ContentDigest {
309 scheme: u8,
310 domain: HashDomain,
311 bytes: [u8; 32],
312}
313
314impl ContentDigest {
315 const fn new(domain: HashDomain, bytes: [u8; 32]) -> Self {
316 Self {
317 scheme: DIGEST_SCHEME_V1,
318 domain,
319 bytes,
320 }
321 }
322
323 #[must_use]
329 pub const fn from_stored_parts(
330 scheme: u8,
331 domain: HashDomain,
332 bytes: [u8; 32],
333 ) -> Option<Self> {
334 if scheme != DIGEST_SCHEME_V1 {
335 return None;
336 }
337 Some(Self::new(domain, bytes))
338 }
339
340 pub const fn scheme(self) -> u8 {
342 self.scheme
343 }
344
345 pub const fn domain(self) -> HashDomain {
347 self.domain
348 }
349
350 pub const fn as_bytes(&self) -> &[u8; 32] {
352 &self.bytes
353 }
354}
355
356impl fmt::Debug for ContentDigest {
357 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
358 formatter
359 .debug_struct("ContentDigest")
360 .field("scheme", &self.scheme)
361 .field("domain", &self.domain)
362 .field("bytes", &HexDigest(&self.bytes))
363 .finish()
364 }
365}
366
367#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
370pub struct KeyedDigest {
371 scheme: u8,
372 key_id: DigestKeyId,
373 bytes: [u8; 32],
374}
375
376impl KeyedDigest {
377 pub const fn new(key_id: DigestKeyId, bytes: [u8; 32]) -> Self {
379 Self {
380 scheme: DIGEST_SCHEME_V1,
381 key_id,
382 bytes,
383 }
384 }
385
386 pub const fn scheme(self) -> u8 {
388 self.scheme
389 }
390
391 pub const fn key_id(self) -> DigestKeyId {
393 self.key_id
394 }
395
396 pub const fn as_bytes(&self) -> &[u8; 32] {
398 &self.bytes
399 }
400}
401
402impl fmt::Debug for KeyedDigest {
403 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
404 formatter
405 .debug_struct("KeyedDigest")
406 .field("scheme", &self.scheme)
407 .field("key_id", &self.key_id)
408 .field("bytes", &"[REDACTED]")
409 .finish()
410 }
411}
412
413pub fn hash(domain: HashDomain, payload: &[u8]) -> ContentDigest {
415 let mut hasher = Sha256::new();
416 write_frame(&mut hasher, HASH_PREFIX, domain.label(), payload);
417 ContentDigest::new(domain, hasher.finalize().into())
418}
419
420#[derive(Clone, Copy, Debug, Eq, PartialEq)]
422pub struct HashLengthMismatch;
423
424impl fmt::Display for HashLengthMismatch {
425 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
426 formatter.write_str("content hash payload length mismatch")
427 }
428}
429
430impl std::error::Error for HashLengthMismatch {}
431
432pub struct ContentHasher {
438 hasher: Sha256,
439 domain: HashDomain,
440 remaining: Option<u64>,
441}
442
443impl fmt::Debug for ContentHasher {
444 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
445 formatter.write_str("ContentHasher([REDACTED])")
446 }
447}
448
449impl ContentHasher {
450 #[must_use]
452 pub fn new(domain: HashDomain, payload_length: u64) -> Self {
453 let mut hasher = Sha256::new();
454 write_frame_header(&mut hasher, HASH_PREFIX, domain.label(), payload_length);
455 Self {
456 hasher,
457 domain,
458 remaining: Some(payload_length),
459 }
460 }
461
462 pub fn update(&mut self, payload: &[u8]) -> Result<(), HashLengthMismatch> {
464 self.remaining = self.remaining.and_then(|remaining| {
465 u64::try_from(payload.len())
466 .ok()
467 .and_then(|length| remaining.checked_sub(length))
468 });
469 if self.remaining.is_none() {
470 return Err(HashLengthMismatch);
471 }
472 Digest::update(&mut self.hasher, payload);
473 Ok(())
474 }
475
476 pub fn finish(self) -> Result<ContentDigest, HashLengthMismatch> {
478 if self.remaining != Some(0) {
479 return Err(HashLengthMismatch);
480 }
481 Ok(ContentDigest::new(
482 self.domain,
483 self.hasher.finalize().into(),
484 ))
485 }
486}
487
488#[must_use]
490pub fn hash_command_batch_document(payload: &[u8]) -> ContentDigest {
491 hash(HashDomain::CommandBatch, payload)
492}
493
494macro_rules! typed_hash_function {
495 ($(#[$meta:meta])* $name:ident, $domain:ident, $output:ident) => {
496 $(#[$meta])*
497 #[must_use]
498 pub fn $name(payload: &[u8]) -> $output {
499 $output::from_bytes(*hash(HashDomain::$domain, payload).as_bytes())
500 }
501 };
502}
503
504typed_hash_function!(
505 hash_canonical_value,
507 CanonicalValue,
508 CanonicalValueHash
509);
510
511typed_hash_function!(
512 hash_columnar_definition_semantics,
514 ColumnarProjectionSpec,
515 ColumnarDefinitionSemanticsHashV1
516);
517typed_hash_function!(
518 hash_columnar_projection_spec,
520 ColumnarProjectionSpec,
521 ColumnarProjectionSpecHashV1
522);
523typed_hash_function!(
524 hash_source,
526 Source,
527 SourceHash
528);
529typed_hash_function!(
530 hash_migration_source,
532 MigrationSource,
533 MigrationSourceHash
534);
535typed_hash_function!(
536 hash_contract_bundle,
538 ContractBundle,
539 ContractBundleHash
540);
541typed_hash_function!(
542 hash_migration_bundle,
544 MigrationBundle,
545 MigrationBundleHash
546);
547typed_hash_function!(
548 hash_plan,
550 Plan,
551 PlanHash
552);
553typed_hash_function!(
554 hash_projection_plan,
556 ProjectionPlan,
557 ProjectionPlanHash
558);
559typed_hash_function!(
560 hash_projection_provider_descriptor,
562 ProjectionProviderDescriptor,
563 ProjectionProviderDescriptorHash
564);
565typed_hash_function!(
566 hash_query_plan,
568 QueryPlan,
569 QueryPlanHash
570);
571typed_hash_function!(
572 hash_partition_set_observation,
574 PartitionSetObservation,
575 PartitionSetObservationHash
576);
577typed_hash_function!(
578 hash_query_module,
580 QueryModule,
581 QueryModuleHash
582);
583typed_hash_function!(
584 hash_reactive_source,
586 ReactiveSource,
587 ReactiveSourceHash
588);
589typed_hash_function!(
590 hash_reactive_operation,
592 ReactiveOperation,
593 ReactiveOperationHash
594);
595typed_hash_function!(
596 hash_reactive_module,
598 ReactiveModule,
599 ReactiveModuleHash
600);
601typed_hash_function!(
602 hash_event_consumer_identity,
604 EventConsumerIdentity,
605 EventConsumerIdentityHash
606);
607typed_hash_function!(
608 hash_scheduled_attempt,
610 ScheduledAttempt,
611 ScheduledAttemptHash
612);
613typed_hash_function!(
614 hash_application_installation_plan,
616 ApplicationInstallationPlan,
617 ApplicationInstallationPlanHash
618);
619typed_hash_function!(
620 hash_application_installation_receipt,
622 ApplicationInstallationReceipt,
623 ApplicationInstallationReceiptHash
624);
625typed_hash_function!(
626 hash_adapter_conformance_manifest,
628 AdapterConformanceManifest,
629 AdapterConformanceManifestHash
630);
631typed_hash_function!(
632 hash_application_export_page,
634 ApplicationExportPage,
635 ApplicationExportPageHash
636);
637typed_hash_function!(
638 hash_application_export_manifest,
640 ApplicationExportManifest,
641 ApplicationExportManifestHash
642);
643typed_hash_function!(
644 hash_application_export_receipt,
646 ApplicationExportReceipt,
647 ApplicationExportReceiptHash
648);
649typed_hash_function!(
650 hash_application_portability_manifest,
652 ApplicationPortabilityManifest,
653 ApplicationPortabilityManifestHash
654);
655typed_hash_function!(
656 hash_application_reimport_receipt,
658 ApplicationReimportReceipt,
659 ApplicationReimportReceiptHash
660);
661typed_hash_function!(
662 hash_application_manifest,
664 ApplicationManifest,
665 ApplicationManifestHash
666);
667typed_hash_function!(
668 hash_application_source,
670 ApplicationSource,
671 ApplicationSourceHash
672);
673typed_hash_function!(
674 hash_application_lock,
676 ApplicationLock,
677 ApplicationLockHash
678);
679typed_hash_function!(
680 hash_generated_artifact,
682 GeneratedArtifact,
683 GeneratedArtifactHash
684);
685typed_hash_function!(
686 hash_application_role_definition,
688 ApplicationRoleDefinition,
689 ApplicationRoleDefinitionHash
690);
691typed_hash_function!(
692 hash_application_role,
694 ApplicationRole,
695 ApplicationRoleHash
696);
697typed_hash_function!(
698 hash_query_source,
700 QuerySource,
701 QuerySourceHash
702);
703typed_hash_function!(
704 hash_query_parameters,
706 QueryParameters,
707 QueryParameterHash
708);
709typed_hash_function!(
710 hash_contract_plan_root,
712 ContractPlanRoot,
713 ContractPlanRootHash
714);
715typed_hash_function!(
716 hash_command_input,
718 CommandInput,
719 CanonicalInputHash
720);
721typed_hash_function!(
722 hash_event,
724 Event,
725 EventHash
726);
727typed_hash_function!(
728 hash_entity_record,
730 EntityRecord,
731 EntityRecordHash
732);
733typed_hash_function!(
734 hash_entity_transition,
736 EntityTransition,
737 EntityTransitionHash
738);
739typed_hash_function!(
740 hash_entity_key,
742 EntityKey,
743 EntityKeyHash
744);
745typed_hash_function!(
746 hash_conflict_key,
748 ConflictKey,
749 ConflictKeyHash
750);
751typed_hash_function!(
752 hash_partition_key,
754 PartitionKey,
755 PartitionKeyHash
756);
757typed_hash_function!(
758 hash_schema,
760 Schema,
761 SchemaHash
762);
763typed_hash_function!(
764 hash_projection_apply,
769 ProjectionApply,
770 ProjectionApplyHash
771);
772typed_hash_function!(
773 hash_offline_maintenance_input,
775 OfflineMaintenanceInput,
776 OfflineMaintenanceInputHash
777);
778typed_hash_function!(
779 hash_contract_migration_input,
781 ContractMigrationInput,
782 ContractMigrationInputHash
783);
784typed_hash_function!(
785 hash_contract_migration_journal,
787 ContractMigrationJournal,
788 ContractMigrationJournalHash
789);
790typed_hash_function!(
791 hash_contract_migration_validation,
793 ContractMigrationValidation,
794 ContractMigrationValidationDigest
795);
796
797pub fn keyed_hash(
799 domain: KeyedHashDomain,
800 key_id: DigestKeyId,
801 key: &DigestKey,
802 payload: &[u8],
803) -> KeyedDigest {
804 keyed_hash_secret(domain, key_id, key.expose_secret(), payload)
805}
806
807pub fn keyed_hash_secret(
810 domain: KeyedHashDomain,
811 key_id: DigestKeyId,
812 key_bytes: &[u8; 32],
813 payload: &[u8],
814) -> KeyedDigest {
815 let mut mac = Hmac::<Sha256>::new_from_slice(key_bytes)
816 .expect("HMAC-SHA-256 accepts keys of every length");
817 write_frame(&mut mac, HMAC_PREFIX, domain.label(), payload);
818 KeyedDigest::new(key_id, mac.finalize().into_bytes().into())
819}
820
821#[must_use]
824pub fn hash_capability_token(
825 key_id: DigestKeyId,
826 key: &DigestKey,
827 raw_token: &[u8; 32],
828) -> CapabilityTokenDigest {
829 hash_capability_token_secret(key_id, key.expose_secret(), raw_token)
830}
831
832#[must_use]
835pub fn hash_capability_token_secret(
836 key_id: DigestKeyId,
837 key_bytes: &[u8; 32],
838 raw_token: &[u8; 32],
839) -> CapabilityTokenDigest {
840 let digest = keyed_hash_secret(
841 KeyedHashDomain::CapabilityToken,
842 key_id,
843 key_bytes,
844 raw_token,
845 );
846 CapabilityTokenDigest::from_hmac_bytes(key_id, *digest.as_bytes())
847}
848
849fn write_frame<T: sha2::digest::Update>(
850 target: &mut T,
851 prefix: &[u8],
852 domain: &str,
853 payload: &[u8],
854) {
855 write_frame_header(target, prefix, domain, payload.len() as u64);
856 target.update(payload);
857}
858
859fn write_frame_header<T: sha2::digest::Update>(
860 target: &mut T,
861 prefix: &[u8],
862 domain: &str,
863 payload_length: u64,
864) {
865 let domain = domain.as_bytes();
866 debug_assert!(u16::try_from(domain.len()).is_ok());
867 let domain_length = domain.len() as u16;
868 target.update(prefix);
869 target.update(&[DIGEST_SCHEME_V1]);
870 target.update(&domain_length.to_be_bytes());
871 target.update(domain);
872 target.update(&payload_length.to_be_bytes());
873}
874
875struct HexDigest<'a>(&'a [u8; 32]);
876
877impl fmt::Debug for HexDigest<'_> {
878 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
879 for byte in self.0 {
880 write!(formatter, "{byte:02x}")?;
881 }
882 Ok(())
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use std::collections::BTreeSet;
889
890 use super::*;
891
892 #[test]
893 fn streaming_hash_preserves_v1_framing_across_chunk_boundaries() {
895 let payload: Vec<u8> = (0..=255).cycle().take(1025).collect();
896 for domain in HashDomain::ALL {
897 for length in [0, 1, 55, 56, 63, 64, 65, 1025] {
898 for chunk_size in [1, 7, 64, 129] {
899 let mut streamed = ContentHasher::new(domain, length as u64);
900 for chunk in payload[..length].chunks(chunk_size) {
901 streamed.update(chunk).expect("within declared length");
902 }
903 assert_eq!(
904 streamed.finish().expect("complete"),
905 hash(domain, &payload[..length])
906 );
907 }
908 }
909 }
910 }
911
912 #[test]
913 fn streaming_hash_refuses_short_extra_and_poisoned_input() {
915 let mut short = ContentHasher::new(HashDomain::Schema, 2);
916 short.update(b"a").expect("prefix");
917 assert_eq!(short.finish(), Err(HashLengthMismatch));
918 let mut extra = ContentHasher::new(HashDomain::Schema, 1);
919 assert_eq!(extra.update(b"secret"), Err(HashLengthMismatch));
920 assert_eq!(extra.update(b"a"), Err(HashLengthMismatch));
921 assert_eq!(extra.finish(), Err(HashLengthMismatch));
922 let mut complete = ContentHasher::new(HashDomain::Schema, 1);
923 complete.update(b"a").expect("complete");
924 assert_eq!(complete.update(b"b"), Err(HashLengthMismatch));
925 assert_eq!(complete.finish(), Err(HashLengthMismatch));
926 assert_eq!(
927 ContentHasher::new(HashDomain::Schema, u64::MAX).finish(),
928 Err(HashLengthMismatch)
929 );
930 }
931
932 #[test]
933 fn domain_registry_has_no_duplicate_labels() {
934 let mut labels = BTreeSet::new();
935 for domain in HashDomain::ALL {
936 assert!(labels.insert(domain.label()));
937 }
938 for domain in KeyedHashDomain::ALL {
939 assert!(labels.insert(domain.label()));
940 }
941 }
942
943 #[test]
944 fn identical_payloads_are_separated_by_domain() {
945 let values = HashDomain::ALL
946 .map(|domain| hash(domain, b"same payload"))
947 .into_iter()
948 .collect::<BTreeSet<_>>();
949 assert_eq!(values.len(), HashDomain::ALL.len());
950 }
951
952 #[test]
953 fn content_digest_retains_its_domain() {
954 for domain in HashDomain::ALL {
955 assert_eq!(hash(domain, b"payload").domain(), domain);
956 }
957 }
958}